authentication-strategy.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. /* tslint:disable:no-non-null-assertion */
  2. import { ErrorCode } from '@vendure/common/lib/generated-shop-types';
  3. import { pick } from '@vendure/common/lib/pick';
  4. import { mergeConfig, NativeAuthenticationStrategy } from '@vendure/core';
  5. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  6. import gql from 'graphql-tag';
  7. import path from 'path';
  8. import { initialData } from '../../../e2e-common/e2e-initial-data';
  9. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  10. import {
  11. TestAuthenticationStrategy,
  12. TestAuthenticationStrategy2,
  13. TestSSOStrategyAdmin,
  14. TestSSOStrategyShop,
  15. VALID_AUTH_TOKEN,
  16. } from './fixtures/test-authentication-strategies';
  17. import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
  18. import { CurrentUserFragment, CustomerFragment, HistoryEntryType } from './graphql/generated-e2e-admin-types';
  19. import * as Codegen from './graphql/generated-e2e-admin-types';
  20. import { RegisterMutation, RegisterMutationVariables } from './graphql/generated-e2e-shop-types';
  21. import { CREATE_CUSTOMER, DELETE_CUSTOMER, GET_CUSTOMER_HISTORY, ME } from './graphql/shared-definitions';
  22. import { REGISTER_ACCOUNT } from './graphql/shop-definitions';
  23. describe('AuthenticationStrategy', () => {
  24. const { server, adminClient, shopClient } = createTestEnvironment(
  25. mergeConfig(testConfig(), {
  26. authOptions: {
  27. shopAuthenticationStrategy: [
  28. new NativeAuthenticationStrategy(),
  29. new TestAuthenticationStrategy(),
  30. new TestAuthenticationStrategy2(),
  31. new TestSSOStrategyShop(),
  32. ],
  33. adminAuthenticationStrategy: [new NativeAuthenticationStrategy(), new TestSSOStrategyAdmin()],
  34. },
  35. }),
  36. );
  37. beforeAll(async () => {
  38. await server.init({
  39. initialData,
  40. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  41. customerCount: 1,
  42. });
  43. await adminClient.asSuperAdmin();
  44. }, TEST_SETUP_TIMEOUT_MS);
  45. afterAll(async () => {
  46. await server.destroy();
  47. });
  48. const currentUserGuard: ErrorResultGuard<CurrentUserFragment> = createErrorResultGuard(
  49. input => input.identifier != null,
  50. );
  51. const customerGuard: ErrorResultGuard<CustomerFragment> = createErrorResultGuard(
  52. input => input.emailAddress != null,
  53. );
  54. describe('external auth', () => {
  55. const userData = {
  56. email: 'test@email.com',
  57. firstName: 'Cixin',
  58. lastName: 'Liu',
  59. };
  60. let newCustomerId: string;
  61. it('fails with a bad token', async () => {
  62. const { authenticate } = await shopClient.query(AUTHENTICATE, {
  63. input: {
  64. test_strategy: {
  65. token: 'bad-token',
  66. },
  67. },
  68. });
  69. expect(authenticate.message).toBe('The provided credentials are invalid');
  70. expect(authenticate.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  71. expect(authenticate.authenticationError).toBe('');
  72. });
  73. it('fails with an expired token', async () => {
  74. const { authenticate } = await shopClient.query(AUTHENTICATE, {
  75. input: {
  76. test_strategy: {
  77. token: 'expired-token',
  78. },
  79. },
  80. });
  81. expect(authenticate.message).toBe('The provided credentials are invalid');
  82. expect(authenticate.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  83. expect(authenticate.authenticationError).toBe('Expired token');
  84. });
  85. it('creates a new Customer with valid token', async () => {
  86. const { customers: before } = await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  87. expect(before.totalItems).toBe(1);
  88. const { authenticate } = await shopClient.query<Codegen.AuthenticateMutation>(AUTHENTICATE, {
  89. input: {
  90. test_strategy: {
  91. token: VALID_AUTH_TOKEN,
  92. userData,
  93. },
  94. },
  95. });
  96. currentUserGuard.assertSuccess(authenticate);
  97. expect(authenticate.identifier).toEqual(userData.email);
  98. const { customers: after } = await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  99. expect(after.totalItems).toBe(2);
  100. expect(after.items.map(i => i.emailAddress)).toEqual([
  101. 'hayden.zieme12@hotmail.com',
  102. userData.email,
  103. ]);
  104. newCustomerId = after.items[1].id;
  105. });
  106. it('creates customer history entry', async () => {
  107. const { customer } = await adminClient.query<
  108. Codegen.GetCustomerHistoryQuery,
  109. Codegen.GetCustomerHistoryQueryVariables
  110. >(GET_CUSTOMER_HISTORY, {
  111. id: newCustomerId,
  112. });
  113. expect(
  114. customer?.history.items.sort((a, b) => (a.id > b.id ? 1 : -1)).map(pick(['type', 'data'])),
  115. ).toEqual([
  116. {
  117. type: HistoryEntryType.CUSTOMER_REGISTERED,
  118. data: {
  119. strategy: 'test_strategy',
  120. },
  121. },
  122. {
  123. type: HistoryEntryType.CUSTOMER_VERIFIED,
  124. data: {
  125. strategy: 'test_strategy',
  126. },
  127. },
  128. ]);
  129. });
  130. it('user authenticationMethod populated', async () => {
  131. const { customer } = await adminClient.query<
  132. Codegen.GetCustomerUserAuthQuery,
  133. Codegen.GetCustomerUserAuthQueryVariables
  134. >(GET_CUSTOMER_USER_AUTH, {
  135. id: newCustomerId,
  136. });
  137. expect(customer?.user?.authenticationMethods.length).toBe(1);
  138. expect(customer?.user?.authenticationMethods[0].strategy).toBe('test_strategy');
  139. });
  140. it('creates authenticated session', async () => {
  141. const { me } = await shopClient.query<Codegen.MeQuery>(ME);
  142. expect(me?.identifier).toBe(userData.email);
  143. });
  144. it('log out', async () => {
  145. await shopClient.asAnonymousUser();
  146. });
  147. it('logging in again re-uses created User & Customer', async () => {
  148. const { authenticate } = await shopClient.query<Codegen.AuthenticateMutation>(AUTHENTICATE, {
  149. input: {
  150. test_strategy: {
  151. token: VALID_AUTH_TOKEN,
  152. userData,
  153. },
  154. },
  155. });
  156. currentUserGuard.assertSuccess(authenticate);
  157. expect(authenticate.identifier).toEqual(userData.email);
  158. const { customers: after } = await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  159. expect(after.totalItems).toBe(2);
  160. expect(after.items.map(i => i.emailAddress)).toEqual([
  161. 'hayden.zieme12@hotmail.com',
  162. userData.email,
  163. ]);
  164. });
  165. // https://github.com/vendure-ecommerce/vendure/issues/695
  166. it('multiple external auth strategies to not interfere with one another', async () => {
  167. const EXPECTED_CUSTOMERS = [
  168. {
  169. emailAddress: 'hayden.zieme12@hotmail.com',
  170. id: 'T_1',
  171. },
  172. {
  173. emailAddress: 'test@email.com',
  174. id: 'T_2',
  175. },
  176. ];
  177. const { customers: customers1 } = await adminClient.query<Codegen.GetCustomersQuery>(
  178. GET_CUSTOMERS,
  179. );
  180. expect(customers1.items).toEqual(EXPECTED_CUSTOMERS);
  181. const { authenticate: auth1 } = await shopClient.query<Codegen.AuthenticateMutation>(
  182. AUTHENTICATE,
  183. {
  184. input: {
  185. test_strategy2: {
  186. token: VALID_AUTH_TOKEN,
  187. email: userData.email,
  188. },
  189. },
  190. },
  191. );
  192. currentUserGuard.assertSuccess(auth1);
  193. expect(auth1.identifier).toBe(userData.email);
  194. const { customers: customers2 } = await adminClient.query<Codegen.GetCustomersQuery>(
  195. GET_CUSTOMERS,
  196. );
  197. expect(customers2.items).toEqual(EXPECTED_CUSTOMERS);
  198. await shopClient.asAnonymousUser();
  199. const { authenticate: auth2 } = await shopClient.query<Codegen.AuthenticateMutation>(
  200. AUTHENTICATE,
  201. {
  202. input: {
  203. test_strategy: {
  204. token: VALID_AUTH_TOKEN,
  205. userData,
  206. },
  207. },
  208. },
  209. );
  210. currentUserGuard.assertSuccess(auth2);
  211. expect(auth2.identifier).toBe(userData.email);
  212. const { customers: customers3 } = await adminClient.query<Codegen.GetCustomersQuery>(
  213. GET_CUSTOMERS,
  214. );
  215. expect(customers3.items).toEqual(EXPECTED_CUSTOMERS);
  216. });
  217. it('registerCustomerAccount with external email', async () => {
  218. const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard(
  219. input => input.success != null,
  220. );
  221. const { registerCustomerAccount } = await shopClient.query<
  222. RegisterMutation,
  223. RegisterMutationVariables
  224. >(REGISTER_ACCOUNT, {
  225. input: {
  226. emailAddress: userData.email,
  227. },
  228. });
  229. successErrorGuard.assertSuccess(registerCustomerAccount);
  230. expect(registerCustomerAccount.success).toBe(true);
  231. const { customer } = await adminClient.query<
  232. Codegen.GetCustomerUserAuthQuery,
  233. Codegen.GetCustomerUserAuthQueryVariables
  234. >(GET_CUSTOMER_USER_AUTH, {
  235. id: newCustomerId,
  236. });
  237. expect(customer?.user?.authenticationMethods.length).toBe(3);
  238. expect(customer?.user?.authenticationMethods.map(m => m.strategy)).toEqual([
  239. 'test_strategy',
  240. 'test_strategy2',
  241. 'native',
  242. ]);
  243. const { customer: customer2 } = await adminClient.query<
  244. Codegen.GetCustomerHistoryQuery,
  245. Codegen.GetCustomerHistoryQueryVariables
  246. >(GET_CUSTOMER_HISTORY, {
  247. id: newCustomerId,
  248. options: {
  249. skip: 4,
  250. },
  251. });
  252. expect(customer2?.history.items.map(pick(['type', 'data']))).toEqual([
  253. {
  254. type: HistoryEntryType.CUSTOMER_REGISTERED,
  255. data: {
  256. strategy: 'native',
  257. },
  258. },
  259. ]);
  260. });
  261. // https://github.com/vendure-ecommerce/vendure/issues/926
  262. it('Customer and Admin external auth does not reuse same User for different strategies', async () => {
  263. const emailAddress = 'hello@test-domain.com';
  264. await adminClient.asAnonymousUser();
  265. const { authenticate: adminAuth } = await adminClient.query<Codegen.AuthenticateMutation>(
  266. AUTHENTICATE,
  267. {
  268. input: {
  269. test_sso_strategy_admin: {
  270. email: emailAddress,
  271. },
  272. },
  273. },
  274. );
  275. currentUserGuard.assertSuccess(adminAuth);
  276. const { authenticate: shopAuth } = await shopClient.query<Codegen.AuthenticateMutation>(
  277. AUTHENTICATE,
  278. {
  279. input: {
  280. test_sso_strategy_shop: {
  281. email: emailAddress,
  282. },
  283. },
  284. },
  285. );
  286. currentUserGuard.assertSuccess(shopAuth);
  287. expect(adminAuth.id).not.toBe(shopAuth.id);
  288. });
  289. });
  290. describe('native auth', () => {
  291. const testEmailAddress = 'test-person@testdomain.com';
  292. // https://github.com/vendure-ecommerce/vendure/issues/486#issuecomment-704991768
  293. it('allows login for an email address which is shared by a previously-deleted Customer', async () => {
  294. const { createCustomer: result1 } = await adminClient.query<
  295. Codegen.CreateCustomerMutation,
  296. Codegen.CreateCustomerMutationVariables
  297. >(CREATE_CUSTOMER, {
  298. input: {
  299. firstName: 'Test',
  300. lastName: 'Person',
  301. emailAddress: testEmailAddress,
  302. },
  303. password: 'password1',
  304. });
  305. customerGuard.assertSuccess(result1);
  306. await adminClient.query<Codegen.DeleteCustomerMutation, Codegen.DeleteCustomerMutationVariables>(
  307. DELETE_CUSTOMER,
  308. {
  309. id: result1.id,
  310. },
  311. );
  312. const { createCustomer: result2 } = await adminClient.query<
  313. Codegen.CreateCustomerMutation,
  314. Codegen.CreateCustomerMutationVariables
  315. >(CREATE_CUSTOMER, {
  316. input: {
  317. firstName: 'Test',
  318. lastName: 'Person',
  319. emailAddress: testEmailAddress,
  320. },
  321. password: 'password2',
  322. });
  323. customerGuard.assertSuccess(result2);
  324. const { authenticate } = await shopClient.query(AUTHENTICATE, {
  325. input: {
  326. native: {
  327. username: testEmailAddress,
  328. password: 'password2',
  329. },
  330. },
  331. });
  332. currentUserGuard.assertSuccess(authenticate);
  333. expect(pick(authenticate, ['id', 'identifier'])).toEqual({
  334. id: result2.user!.id,
  335. identifier: result2.emailAddress,
  336. });
  337. });
  338. });
  339. });
  340. const AUTHENTICATE = gql`
  341. mutation Authenticate($input: AuthenticationInput!) {
  342. authenticate(input: $input) {
  343. ...CurrentUser
  344. ... on InvalidCredentialsError {
  345. authenticationError
  346. errorCode
  347. message
  348. }
  349. }
  350. }
  351. ${CURRENT_USER_FRAGMENT}
  352. `;
  353. const GET_CUSTOMERS = gql`
  354. query GetCustomers {
  355. customers {
  356. totalItems
  357. items {
  358. id
  359. emailAddress
  360. }
  361. }
  362. }
  363. `;
  364. const GET_CUSTOMER_USER_AUTH = gql`
  365. query GetCustomerUserAuth($id: ID!) {
  366. customer(id: $id) {
  367. id
  368. user {
  369. id
  370. verified
  371. authenticationMethods {
  372. id
  373. strategy
  374. }
  375. }
  376. }
  377. }
  378. `;