1import Constants, { ExecutionEnvironment } from 'expo-constants';
2import * as Linking from 'expo-linking';
3import { Platform } from 'expo-modules-core';
4import { dismissAuthSession, openAuthSessionAsync } from 'expo-web-browser';
5
6import { AuthRequest } from './AuthRequest';
7import {
8  AuthRequestConfig,
9  AuthRequestPromptOptions,
10  CodeChallengeMethod,
11  Prompt,
12  ResponseType,
13} from './AuthRequest.types';
14import {
15  AuthSessionOptions,
16  AuthSessionRedirectUriOptions,
17  AuthSessionResult,
18} from './AuthSession.types';
19import {
20  DiscoveryDocument,
21  fetchDiscoveryAsync,
22  Issuer,
23  IssuerOrDiscovery,
24  ProviderMetadata,
25  resolveDiscoveryAsync,
26} from './Discovery';
27import { generateHexStringAsync } from './PKCE';
28import { getQueryParams } from './QueryParams';
29import sessionUrlProvider from './SessionUrlProvider';
30
31let _authLock = false;
32
33// @needsAudit
34/**
35 * Initiate an authentication session with the given options. Only one `AuthSession` can be active at any given time in your application.
36 * If you attempt to open a second session while one is still in progress, the second session will return a value to indicate that `AuthSession` is locked.
37 *
38 * @param options An object of type `AuthSessionOptions`.
39 * @return Returns a Promise that resolves to an `AuthSessionResult` object.
40 */
41export async function startAsync(options: AuthSessionOptions): Promise<AuthSessionResult> {
42  const authUrl = options.authUrl;
43  // Prevent accidentally starting to an empty url
44  if (!authUrl) {
45    throw new Error(
46      'No authUrl provided to AuthSession.startAsync. An authUrl is required -- it points to the page where the user will be able to sign in.'
47    );
48  }
49  // Prevent multiple sessions from running at the same time, WebBrowser doesn't
50  // support it this makes the behavior predictable.
51  if (_authLock) {
52    if (__DEV__) {
53      console.warn(
54        'Attempted to call AuthSession.startAsync multiple times while already active. Only one AuthSession can be active at any given time.'
55      );
56    }
57
58    return { type: 'locked' };
59  }
60
61  const returnUrl = options.returnUrl || sessionUrlProvider.getDefaultReturnUrl();
62  const startUrl = sessionUrlProvider.getStartUrl(authUrl, returnUrl);
63  const showInRecents = options.showInRecents || false;
64
65  // About to start session, set lock
66  _authLock = true;
67
68  let result;
69  try {
70    result = await _openWebBrowserAsync(startUrl, returnUrl, showInRecents);
71  } finally {
72    // WebBrowser session complete, unset lock
73    _authLock = false;
74  }
75
76  // Handle failures
77  if (!result) {
78    throw new Error('Unexpected missing AuthSession result');
79  }
80  if (!result.url) {
81    if (result.type) {
82      return result;
83    } else {
84      throw new Error('Unexpected AuthSession result with missing type');
85    }
86  }
87
88  const { params, errorCode } = getQueryParams(result.url);
89
90  return {
91    type: errorCode ? 'error' : 'success',
92    params,
93    errorCode,
94    authentication: null,
95    url: result.url,
96  };
97}
98
99// @needsAudit
100/**
101 * Cancels an active `AuthSession` if there is one. No return value, but if there is an active `AuthSession`
102 * then the Promise returned by the `AuthSession.startAsync()` that initiated it resolves to `{ type: 'dismiss' }`.
103 */
104export function dismiss() {
105  dismissAuthSession();
106}
107
108export const getDefaultReturnUrl = sessionUrlProvider.getDefaultReturnUrl;
109
110// @needsAudit @docsMissing
111/**
112 * Get the URL that your authentication provider needs to redirect to. For example: `https://auth.expo.io/@your-username/your-app-slug`. You can pass an additional path component to be appended to the default redirect URL.
113 * > **Note** This method will throw an exception if you're using the bare workflow on native.
114 *
115 * @param path
116 * @return
117 *
118 * @example
119 * ```ts
120 * const url = AuthSession.getRedirectUrl('redirect');
121 *
122 * // Managed: https://auth.expo.io/@your-username/your-app-slug/redirect
123 * // Web: https://localhost:19006/redirect
124 * ```
125 *
126 * @deprecated Use `makeRedirectUri({ path, useProxy })` instead.
127 */
128export function getRedirectUrl(path?: string): string {
129  return sessionUrlProvider.getRedirectUrl(path);
130}
131
132// @needsAudit
133/**
134 * Create a redirect url for the current platform and environment. You need to manually define the redirect that will be used in
135 * a bare workflow React Native app, or an Expo standalone app, this is because it cannot be inferred automatically.
136 * - **Web:** Generates a path based on the current `window.location`. For production web apps, you should hard code the URL as well.
137 * - **Managed workflow:** Uses the `scheme` property of your `app.config.js` or `app.json`.
138 *   - **Proxy:** Uses `auth.expo.io` as the base URL for the path. This only works in Expo Go and standalone environments.
139 * - **Bare workflow:** Will fallback to using the `native` option for bare workflow React Native apps.
140 *
141 * @param options Additional options for configuring the path.
142 * @return The `redirectUri` to use in an authentication request.
143 *
144 * @example
145 * ```ts
146 * const redirectUri = makeRedirectUri({
147 *   scheme: 'my-scheme',
148 *   path: 'redirect'
149 * });
150 * // Custom app: my-scheme://redirect
151 * // Expo Go: exp://127.0.0.1:19000/--/redirect
152 * // Web dev: https://localhost:19006/redirect
153 * // Web prod: https://yourwebsite.com/redirect
154 *
155 * const redirectUri2 = makeRedirectUri({
156 *   scheme: 'scheme2',
157 *   preferLocalhost: true,
158 *   isTripleSlashed: true,
159 * });
160 * // Custom app: scheme2:///
161 * // Expo Go: exp://localhost:19000
162 * // Web dev: https://localhost:19006
163 * // Web prod: https://yourwebsite.com
164 *
165 * const redirectUri3 = makeRedirectUri({
166 *   useProxy: true,
167 * });
168 * // Custom app: https://auth.expo.io/@username/slug
169 * // Expo Go: https://auth.expo.io/@username/slug
170 * // Web dev: https://localhost:19006
171 * // Web prod: https://yourwebsite.com
172 * ```
173 */
174export function makeRedirectUri({
175  native,
176  scheme,
177  isTripleSlashed,
178  queryParams,
179  path,
180  preferLocalhost,
181  useProxy,
182}: AuthSessionRedirectUriOptions = {}): string {
183  if (
184    Platform.OS !== 'web' &&
185    native &&
186    [ExecutionEnvironment.Standalone, ExecutionEnvironment.Bare].includes(
187      Constants.executionEnvironment
188    )
189  ) {
190    // Should use the user-defined native scheme in standalone builds
191    return native;
192  }
193  if (!useProxy || Platform.OS === 'web') {
194    const url = Linking.createURL(path || '', {
195      isTripleSlashed,
196      scheme,
197      queryParams,
198    });
199
200    if (preferLocalhost) {
201      const ipAddress = url.match(
202        /\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/
203      );
204      // Only replace if an IP address exists
205      if (ipAddress?.length) {
206        const [protocol, path] = url.split(ipAddress[0]);
207        return `${protocol}localhost${path}`;
208      }
209    }
210
211    return url;
212  }
213  // Attempt to use the proxy
214  return sessionUrlProvider.getRedirectUrl(path);
215}
216
217// @needsAudit
218/**
219 * Build an `AuthRequest` and load it before returning.
220 *
221 * @param config A valid [`AuthRequestConfig`](#authrequestconfig) that specifies what provider to use.
222 * @param issuerOrDiscovery A loaded [`DiscoveryDocument`](#discoverydocument) or issuer URL.
223 * (Only `authorizationEndpoint` is required for requesting an authorization code).
224 * @return Returns an instance of `AuthRequest` that can be used to prompt the user for authorization.
225 */
226export async function loadAsync(
227  config: AuthRequestConfig,
228  issuerOrDiscovery: IssuerOrDiscovery
229): Promise<AuthRequest> {
230  const request = new AuthRequest(config);
231  const discovery = await resolveDiscoveryAsync(issuerOrDiscovery);
232  await request.makeAuthUrlAsync(discovery);
233  return request;
234}
235
236async function _openWebBrowserAsync(startUrl: string, returnUrl: string, showInRecents: boolean) {
237  // $FlowIssue: Flow thinks the awaited result can be a promise
238  const result = await openAuthSessionAsync(startUrl, returnUrl, { showInRecents });
239  if (result.type === 'cancel' || result.type === 'dismiss') {
240    return { type: result.type };
241  }
242
243  return result;
244}
245
246export { useAutoDiscovery, useAuthRequest } from './AuthRequestHooks';
247export { AuthError, TokenError } from './Errors';
248
249export {
250  AuthSessionOptions,
251  AuthSessionRedirectUriOptions,
252  AuthSessionResult,
253  AuthRequest,
254  AuthRequestConfig,
255  AuthRequestPromptOptions,
256  CodeChallengeMethod,
257  DiscoveryDocument,
258  Issuer,
259  IssuerOrDiscovery,
260  Prompt,
261  ProviderMetadata,
262  ResponseType,
263  resolveDiscoveryAsync,
264  fetchDiscoveryAsync,
265  generateHexStringAsync,
266};
267
268export {
269  // Token classes
270  TokenResponse,
271  AccessTokenRequest,
272  RefreshTokenRequest,
273  RevokeTokenRequest,
274  // Token methods
275  revokeAsync,
276  refreshAsync,
277  exchangeCodeAsync,
278  fetchUserInfoAsync,
279} from './TokenRequest';
280
281// Token types
282export * from './TokenRequest.types';
283
284// Provider specific types
285export { GoogleAuthRequestConfig } from './providers/Google';
286export { FacebookAuthRequestConfig } from './providers/Facebook';
287