authentication-strategy.e2e-spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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 { TEST_SETUP_TIMEOUT_MS, testConfig } 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 * as Codegen from './graphql/generated-e2e-admin-types';
  20. import {
  21. AttemptLoginDocument,
  22. CurrentUserFragment,
  23. CustomerFragment,
  24. HistoryEntryType,
  25. } 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/vendurehq/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 } =
  184. await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  185. expect(customers1.items).toEqual(EXPECTED_CUSTOMERS);
  186. const { authenticate: auth1 } = await shopClient.query<Codegen.AuthenticateMutation>(
  187. AUTHENTICATE,
  188. {
  189. input: {
  190. test_strategy2: {
  191. token: VALID_AUTH_TOKEN,
  192. email: userData.email,
  193. },
  194. },
  195. },
  196. );
  197. currentUserGuard.assertSuccess(auth1);
  198. expect(auth1.identifier).toBe(userData.email);
  199. const { customers: customers2 } =
  200. await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  201. expect(customers2.items).toEqual(EXPECTED_CUSTOMERS);
  202. await shopClient.asAnonymousUser();
  203. const { authenticate: auth2 } = await shopClient.query<Codegen.AuthenticateMutation>(
  204. AUTHENTICATE,
  205. {
  206. input: {
  207. test_strategy: {
  208. token: VALID_AUTH_TOKEN,
  209. userData,
  210. },
  211. },
  212. },
  213. );
  214. currentUserGuard.assertSuccess(auth2);
  215. expect(auth2.identifier).toBe(userData.email);
  216. const { customers: customers3 } =
  217. await adminClient.query<Codegen.GetCustomersQuery>(GET_CUSTOMERS);
  218. expect(customers3.items).toEqual(EXPECTED_CUSTOMERS);
  219. });
  220. it('registerCustomerAccount with external email', async () => {
  221. const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard(
  222. input => input.success != null,
  223. );
  224. const { registerCustomerAccount } = await shopClient.query<
  225. RegisterMutation,
  226. RegisterMutationVariables
  227. >(REGISTER_ACCOUNT, {
  228. input: {
  229. emailAddress: userData.email,
  230. },
  231. });
  232. successErrorGuard.assertSuccess(registerCustomerAccount);
  233. expect(registerCustomerAccount.success).toBe(true);
  234. const { customer } = await adminClient.query<
  235. Codegen.GetCustomerUserAuthQuery,
  236. Codegen.GetCustomerUserAuthQueryVariables
  237. >(GET_CUSTOMER_USER_AUTH, {
  238. id: newCustomerId,
  239. });
  240. expect(customer?.user?.authenticationMethods.length).toBe(3);
  241. expect(customer?.user?.authenticationMethods.map(m => m.strategy)).toEqual([
  242. 'test_strategy',
  243. 'test_strategy2',
  244. 'native',
  245. ]);
  246. const { customer: customer2 } = await adminClient.query<
  247. Codegen.GetCustomerHistoryQuery,
  248. Codegen.GetCustomerHistoryQueryVariables
  249. >(GET_CUSTOMER_HISTORY, {
  250. id: newCustomerId,
  251. options: {
  252. skip: 4,
  253. },
  254. });
  255. expect(customer2?.history.items.map(pick(['type', 'data']))).toEqual([
  256. {
  257. type: HistoryEntryType.CUSTOMER_REGISTERED,
  258. data: {
  259. strategy: 'native',
  260. },
  261. },
  262. ]);
  263. });
  264. // https://github.com/vendurehq/vendure/issues/926
  265. it('Customer and Admin external auth does not reuse same User for different strategies', async () => {
  266. const emailAddress = 'hello@test-domain.com';
  267. await adminClient.asAnonymousUser();
  268. const { authenticate: adminAuth } = await adminClient.query<Codegen.AuthenticateMutation>(
  269. AUTHENTICATE,
  270. {
  271. input: {
  272. test_sso_strategy_admin: {
  273. email: emailAddress,
  274. },
  275. },
  276. },
  277. );
  278. currentUserGuard.assertSuccess(adminAuth);
  279. const { authenticate: shopAuth } = await shopClient.query<Codegen.AuthenticateMutation>(
  280. AUTHENTICATE,
  281. {
  282. input: {
  283. test_sso_strategy_shop: {
  284. email: emailAddress,
  285. },
  286. },
  287. },
  288. );
  289. currentUserGuard.assertSuccess(shopAuth);
  290. expect(adminAuth.id).not.toBe(shopAuth.id);
  291. });
  292. });
  293. describe('native auth', () => {
  294. const testEmailAddress = 'test-person@testdomain.com';
  295. // https://github.com/vendurehq/vendure/issues/486#issuecomment-704991768
  296. it('allows login for an email address which is shared by a previously-deleted Customer', async () => {
  297. const { createCustomer: result1 } = await adminClient.query<
  298. Codegen.CreateCustomerMutation,
  299. Codegen.CreateCustomerMutationVariables
  300. >(CREATE_CUSTOMER, {
  301. input: {
  302. firstName: 'Test',
  303. lastName: 'Person',
  304. emailAddress: testEmailAddress,
  305. },
  306. password: 'password1',
  307. });
  308. customerGuard.assertSuccess(result1);
  309. await adminClient.query<Codegen.DeleteCustomerMutation, Codegen.DeleteCustomerMutationVariables>(
  310. DELETE_CUSTOMER,
  311. {
  312. id: result1.id,
  313. },
  314. );
  315. const { createCustomer: result2 } = await adminClient.query<
  316. Codegen.CreateCustomerMutation,
  317. Codegen.CreateCustomerMutationVariables
  318. >(CREATE_CUSTOMER, {
  319. input: {
  320. firstName: 'Test',
  321. lastName: 'Person',
  322. emailAddress: testEmailAddress,
  323. },
  324. password: 'password2',
  325. });
  326. customerGuard.assertSuccess(result2);
  327. const { authenticate } = await shopClient.query(AUTHENTICATE, {
  328. input: {
  329. native: {
  330. username: testEmailAddress,
  331. password: 'password2',
  332. },
  333. },
  334. });
  335. currentUserGuard.assertSuccess(authenticate);
  336. expect(pick(authenticate, ['id', 'identifier'])).toEqual({
  337. id: result2.user!.id,
  338. identifier: result2.emailAddress,
  339. });
  340. });
  341. });
  342. });
  343. describe('No NativeAuthStrategy on Shop API', () => {
  344. const { server, adminClient, shopClient } = createTestEnvironment(
  345. mergeConfig(testConfig(), {
  346. authOptions: {
  347. shopAuthenticationStrategy: [new TestAuthenticationStrategy()],
  348. },
  349. }),
  350. );
  351. beforeAll(async () => {
  352. await server.init({
  353. initialData,
  354. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  355. customerCount: 1,
  356. });
  357. await adminClient.asSuperAdmin();
  358. }, TEST_SETUP_TIMEOUT_MS);
  359. afterAll(async () => {
  360. await server.destroy();
  361. });
  362. // https://github.com/vendurehq/vendure/issues/2282
  363. it('can log in to Admin API', async () => {
  364. const { login } = await adminClient.query(AttemptLoginDocument, {
  365. username: 'superadmin',
  366. password: 'superadmin',
  367. });
  368. currentUserGuard.assertSuccess(login);
  369. expect(login.identifier).toBe('superadmin');
  370. });
  371. });
  372. const AUTHENTICATE = gql`
  373. mutation Authenticate($input: AuthenticationInput!) {
  374. authenticate(input: $input) {
  375. ...CurrentUser
  376. ... on InvalidCredentialsError {
  377. authenticationError
  378. errorCode
  379. message
  380. }
  381. }
  382. }
  383. ${CURRENT_USER_FRAGMENT}
  384. `;
  385. const GET_CUSTOMERS = gql`
  386. query GetCustomers {
  387. customers {
  388. totalItems
  389. items {
  390. id
  391. emailAddress
  392. }
  393. }
  394. }
  395. `;
  396. const GET_CUSTOMER_USER_AUTH = gql`
  397. query GetCustomerUserAuth($id: ID!) {
  398. customer(id: $id) {
  399. id
  400. user {
  401. id
  402. verified
  403. authenticationMethods {
  404. id
  405. strategy
  406. }
  407. }
  408. }
  409. }
  410. `;