1import * as WebBrowser from 'expo-web-browser'; 2import invariant from 'invariant'; 3import { Platform } from 'react-native'; 4import { CodeChallengeMethod, ResponseType, } from './AuthRequest.types'; 5import { AuthError } from './Errors'; 6import * as PKCE from './PKCE'; 7import * as QueryParams from './QueryParams'; 8import { TokenResponse } from './TokenRequest'; 9let _authLock = false; 10// @needsAudit @docsMissing 11/** 12 * Used to manage an authorization request according to the OAuth spec: [Section 4.1.1](https://tools.ietf.org/html/rfc6749#section-4.1.1). 13 * You can use this class directly for more info around the authorization. 14 * 15 * **Common use-cases:** 16 * 17 * - Parse a URL returned from the authorization server with `parseReturnUrlAsync()`. 18 * - Get the built authorization URL with `makeAuthUrlAsync()`. 19 * - Get a loaded JSON representation of the auth request with crypto state loaded with `getAuthRequestConfigAsync()`. 20 * 21 * @example 22 * ```ts 23 * // Create a request. 24 * const request = new AuthRequest({ ... }); 25 * 26 * // Prompt for an auth code 27 * const result = await request.promptAsync(discovery); 28 * 29 * // Get the URL to invoke 30 * const url = await request.makeAuthUrlAsync(discovery); 31 * 32 * // Get the URL to invoke 33 * const parsed = await request.parseReturnUrlAsync("<URL From Server>"); 34 * ``` 35 */ 36export class AuthRequest { 37 /** 38 * Used for protection against [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12). 39 */ 40 state; 41 url = null; 42 codeVerifier; 43 codeChallenge; 44 responseType; 45 clientId; 46 extraParams; 47 usePKCE; 48 codeChallengeMethod; 49 redirectUri; 50 scopes; 51 clientSecret; 52 prompt; 53 constructor(request) { 54 this.responseType = request.responseType ?? ResponseType.Code; 55 this.clientId = request.clientId; 56 this.redirectUri = request.redirectUri; 57 this.scopes = request.scopes; 58 this.clientSecret = request.clientSecret; 59 this.prompt = request.prompt; 60 this.state = request.state ?? PKCE.generateRandom(10); 61 this.extraParams = request.extraParams ?? {}; 62 this.codeChallengeMethod = request.codeChallengeMethod ?? CodeChallengeMethod.S256; 63 // PKCE defaults to true 64 this.usePKCE = request.usePKCE ?? true; 65 // Some warnings in development about potential confusing application code 66 if (__DEV__) { 67 if (this.prompt && this.extraParams.prompt) { 68 console.warn(`\`AuthRequest\` \`extraParams.prompt\` will be overwritten by \`prompt\`.`); 69 } 70 if (this.clientSecret && this.extraParams.client_secret) { 71 console.warn(`\`AuthRequest\` \`extraParams.client_secret\` will be overwritten by \`clientSecret\`.`); 72 } 73 if (this.codeChallengeMethod && this.extraParams.code_challenge_method) { 74 console.warn(`\`AuthRequest\` \`extraParams.code_challenge_method\` will be overwritten by \`codeChallengeMethod\`.`); 75 } 76 } 77 invariant(this.codeChallengeMethod !== CodeChallengeMethod.Plain, `\`AuthRequest\` does not support \`CodeChallengeMethod.Plain\` as it's not secure.`); 78 invariant(this.redirectUri, `\`AuthRequest\` requires a valid \`redirectUri\`. Ex: ${Platform.select({ 79 web: 'https://yourwebsite.com/', 80 default: 'com.your.app:/oauthredirect', 81 })}`); 82 } 83 /** 84 * Load and return a valid auth request based on the input config. 85 */ 86 async getAuthRequestConfigAsync() { 87 if (this.usePKCE) { 88 await this.ensureCodeIsSetupAsync(); 89 } 90 return { 91 responseType: this.responseType, 92 clientId: this.clientId, 93 redirectUri: this.redirectUri, 94 scopes: this.scopes, 95 clientSecret: this.clientSecret, 96 codeChallenge: this.codeChallenge, 97 codeChallengeMethod: this.codeChallengeMethod, 98 prompt: this.prompt, 99 state: this.state, 100 extraParams: this.extraParams, 101 usePKCE: this.usePKCE, 102 }; 103 } 104 /** 105 * Prompt a user to authorize for a code. 106 * 107 * @param discovery 108 * @param promptOptions 109 */ 110 async promptAsync(discovery, { url, ...options } = {}) { 111 if (!url) { 112 if (!this.url) { 113 // Generate a new url 114 return this.promptAsync(discovery, { 115 ...options, 116 url: await this.makeAuthUrlAsync(discovery), 117 }); 118 } 119 // Reuse the preloaded url 120 url = this.url; 121 } 122 // Prevent accidentally starting to an empty url 123 invariant(url, 'No authUrl provided to AuthSession.startAsync. An authUrl is required -- it points to the page where the user will be able to sign in.'); 124 const startUrl = url; 125 const returnUrl = this.redirectUri; 126 // Prevent multiple sessions from running at the same time, WebBrowser doesn't 127 // support it this makes the behavior predictable. 128 if (_authLock) { 129 if (__DEV__) { 130 console.warn('Attempted to call AuthSession.startAsync multiple times while already active. Only one AuthSession can be active at any given time.'); 131 } 132 return { type: 'locked' }; 133 } 134 // About to start session, set lock 135 _authLock = true; 136 let result; 137 try { 138 result = await WebBrowser.openAuthSessionAsync(startUrl, returnUrl, options); 139 } 140 finally { 141 _authLock = false; 142 } 143 if (result.type === 'opened') { 144 // This should never happen 145 throw new Error('An unexpected error occurred'); 146 } 147 if (result.type !== 'success') { 148 return { type: result.type }; 149 } 150 return this.parseReturnUrl(result.url); 151 } 152 parseReturnUrl(url) { 153 const { params, errorCode } = QueryParams.getQueryParams(url); 154 const { state, error = errorCode } = params; 155 let parsedError = null; 156 let authentication = null; 157 if (state !== this.state) { 158 // This is a non-standard error 159 parsedError = new AuthError({ 160 error: 'state_mismatch', 161 error_description: 'Cross-Site request verification failed. Cached state and returned state do not match.', 162 }); 163 } 164 else if (error) { 165 parsedError = new AuthError({ error, ...params }); 166 } 167 if (params.access_token) { 168 authentication = TokenResponse.fromQueryParams(params); 169 } 170 return { 171 type: parsedError ? 'error' : 'success', 172 error: parsedError, 173 url, 174 params, 175 authentication, 176 // Return errorCode for legacy 177 errorCode, 178 }; 179 } 180 /** 181 * Create the URL for authorization. 182 * 183 * @param discovery 184 */ 185 async makeAuthUrlAsync(discovery) { 186 const request = await this.getAuthRequestConfigAsync(); 187 if (!request.state) 188 throw new Error('Cannot make request URL without a valid `state` loaded'); 189 // Create a query string 190 const params = {}; 191 if (request.codeChallenge) { 192 params.code_challenge = request.codeChallenge; 193 } 194 // copy over extra params 195 for (const extra in request.extraParams) { 196 if (extra in request.extraParams) { 197 params[extra] = request.extraParams[extra]; 198 } 199 } 200 if (request.usePKCE && request.codeChallengeMethod) { 201 params.code_challenge_method = request.codeChallengeMethod; 202 } 203 if (request.clientSecret) { 204 params.client_secret = request.clientSecret; 205 } 206 if (request.prompt) { 207 params.prompt = request.prompt; 208 } 209 // These overwrite any extra params 210 params.redirect_uri = request.redirectUri; 211 params.client_id = request.clientId; 212 params.response_type = request.responseType; 213 params.state = request.state; 214 if (request.scopes?.length) { 215 params.scope = request.scopes.join(' '); 216 } 217 const query = QueryParams.buildQueryString(params); 218 // Store the URL for later 219 this.url = `${discovery.authorizationEndpoint}?${query}`; 220 return this.url; 221 } 222 async ensureCodeIsSetupAsync() { 223 if (this.codeVerifier) { 224 return; 225 } 226 // This method needs to be resolved like all other native methods. 227 const { codeVerifier, codeChallenge } = await PKCE.buildCodeAsync(); 228 this.codeVerifier = codeVerifier; 229 this.codeChallenge = codeChallenge; 230 } 231} 232//# sourceMappingURL=AuthRequest.js.map