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