authentication-strategy.e2e-spec.ts 15 KB

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