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