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