authentication-strategy.e2e-spec.ts 16 KB

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