1import { AuthRequest } from '../AuthRequest';
2import { CodeChallengeMethod, Prompt } from '../AuthRequest.types';
3import { buildQueryString, getQueryParams } from '../QueryParams';
4
5jest.mock('expo-random', () => ({
6  getRandomBytes: jest.fn(() => ''),
7  getRandomBase64StringAsync: jest.fn(async () => ''),
8}));
9jest.mock('expo-crypto', () => ({
10  digestStringAsync: jest.fn(async () => ''),
11  CryptoDigestAlgorithm: { SHA256: 'SHA256' },
12  CryptoEncoding: { BASE64: 'BASE64' },
13}));
14
15const mockDiscovery = {
16  authorizationEndpoint: 'https://demo.io',
17  tokenEndpoint: 'https://demo.io',
18};
19
20// The spec dictates that Plain should only be used on devices that don't support S256.
21// Expo doesn't run on any device like this.
22it(`does not allow plain code challenge method`, () => {
23  expect(
24    () =>
25      // @ts-ignore
26      new AuthRequest({ redirectUri: 'com://auth', codeChallengeMethod: CodeChallengeMethod.Plain })
27  ).toThrow(/does not support `CodeChallengeMethod.Plain`/);
28});
29
30it(`does not allow an empty redirectUri`, () => {
31  expect(
32    () =>
33      // @ts-ignore
34      new AuthRequest({ redirectUri: '' })
35  ).toThrow(/requires a valid `redirectUri`/);
36});
37
38it(`loads a code verifier with PKCE enabled`, async () => {
39  const request = new AuthRequest({
40    redirectUri: 'foo://bar',
41    clientId: '...',
42    scopes: [],
43    usePKCE: true,
44  });
45  const config = await request.getAuthRequestConfigAsync();
46  expect(config.codeChallenge).toBeDefined();
47  expect(request.codeVerifier).toBeDefined();
48});
49
50it(`skips loading a code verifier with PKCE disabled`, async () => {
51  const request = new AuthRequest({
52    redirectUri: 'foo://bar',
53    clientId: '...',
54    scopes: [],
55    usePKCE: false,
56  });
57  const config = await request.getAuthRequestConfigAsync();
58  expect(config.codeChallenge).not.toBeDefined();
59  expect(request.codeVerifier).not.toBeDefined();
60});
61
62// Ensure a promise isn't accidentally returned
63it(`always returns a state when loaded`, async () => {
64  const request = new AuthRequest({
65    redirectUri: 'foo://bar',
66    clientId: '...',
67    scopes: [],
68  });
69  const config = await request.getAuthRequestConfigAsync();
70  expect(typeof config.state).toBe('string');
71});
72
73it(`parses a valid return url`, () => {
74  const request = new AuthRequest({
75    redirectUri: 'foo://bar',
76    clientId: '...',
77    scopes: [],
78    // Ensure the state matches
79    state: 'testvalue',
80  });
81
82  const results = request.parseReturnUrl(
83    'https://demo.identityserver.io/connect/authorize?code_challenge=qOMOVEqLn0PTVkRFSSD4H5T-AQyZeZaFcr3fmu5gRKU&code_challenge_method=S256&client_secret=foobar&redirect_uri=bareexpo%3A%2F%2Foauthredirect&client_id=native.code&response_type=code&state=testvalue&scope=openid%20profile%20email%20offline_access'
84  );
85  expect(results.type).toBe('success');
86});
87
88it(`returns an error when the state doesn't match`, () => {
89  const request = new AuthRequest({
90    redirectUri: 'foo://bar',
91    clientId: '...',
92    scopes: [],
93    // Use a non-matching state
94    state: 'wrongstate',
95  });
96
97  const results = request.parseReturnUrl(
98    'https://demo.identityserver.io/connect/authorize?code_challenge=qOMOVEqLn0PTVkRFSSD4H5T-AQyZeZaFcr3fmu5gRKU&code_challenge_method=S256&client_secret=foobar&redirect_uri=bareexpo%3A%2F%2Foauthredirect&client_id=native.code&response_type=code&state=testvalue&scope=openid%20profile%20email%20offline_access'
99  );
100  if (results.type !== 'error' || !results.error) throw new Error('Invalid type for test');
101  expect(results.error.code).toBe('state_mismatch');
102});
103
104it(`parses the server error into an AuthError`, () => {
105  const request = new AuthRequest({
106    redirectUri: 'foo://bar',
107    clientId: '...',
108    scopes: [],
109    // Use a non-matching state
110    state: 'somn',
111  });
112
113  const queryString = buildQueryString({ state: 'somn', error: 'invalid_request' });
114  const results = request.parseReturnUrl(`https://demo.io?${queryString}`);
115  if (results.type !== 'error' || !results.error) throw new Error('Invalid type for test');
116
117  expect(results.error.code).toBe('invalid_request');
118  // Ensure the extra message is added
119  expect(results.error.message).toMatch(/The request is missing a required parameter/);
120});
121
122it(`can override the code_challenge with extraParams`, async () => {
123  const request = new AuthRequest({
124    clientId: '',
125    scopes: [],
126    redirectUri: 'foo://bar',
127    usePKCE: true,
128    extraParams: { code_challenge: 'custom-value' },
129  });
130
131  const url = await request.makeAuthUrlAsync(mockDiscovery);
132  expect(getQueryParams(url).params.code_challenge).toBe('custom-value');
133});
134
135// Tests `useAuthRequest`
136// Ensure that when a mobile browser prompts, the URL is already loaded.
137it(`loads an auth request`, async () => {
138  const request = new AuthRequest({
139    clientId: '',
140    scopes: ['openid'],
141    redirectUri: 'foo://bar',
142    usePKCE: true,
143    prompt: Prompt.SelectAccount,
144    extraParams: { code_challenge: 'custom-value' },
145  });
146  await request.makeAuthUrlAsync(mockDiscovery);
147
148  expect(request.url).toMatch(/https:\/\/demo.io/);
149  expect(request.url).toContain('prompt=select_account');
150  expect(request.url).toContain('scope=openid');
151  // Test that usePKCE adds the code_challenge
152  expect(request.url).toContain('code_challenge=');
153  expect(request.codeVerifier).toBeDefined();
154  expect(typeof request.state).toBe('string');
155});
156
157it(`loads an auth request without pkce`, async () => {
158  const request = new AuthRequest({
159    clientId: '',
160    scopes: [],
161    redirectUri: 'foo://bar',
162    usePKCE: false,
163  });
164  await request.makeAuthUrlAsync(mockDiscovery);
165
166  expect(request.url).toMatch(/https:\/\/demo.io/);
167  // Test that empty scopes omits the scope param.
168  expect(request.url).not.toContain('scope=');
169  // Test that usePKCE adds the code_challenge.
170  // This is required for dropbox auth.
171  expect(request.url).not.toContain('code_challenge=');
172  expect(request.url).not.toContain('code_challenge_method=');
173  expect(request.codeVerifier).not.toBeDefined();
174
175  expect(typeof request.state).toBe('string');
176});
177