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
33export async function startAsync(options: AuthSessionOptions): Promise<AuthSessionResult> {
34  const authUrl = options.authUrl;
35  // Prevent accidentally starting to an empty url
36  if (!authUrl) {
37    throw new Error(
38      'No authUrl provided to AuthSession.startAsync. An authUrl is required -- it points to the page where the user will be able to sign in.'
39    );
40  }
41  // Prevent multiple sessions from running at the same time, WebBrowser doesn't
42  // support it this makes the behavior predictable.
43  if (_authLock) {
44    if (__DEV__) {
45      console.warn(
46        'Attempted to call AuthSession.startAsync multiple times while already active. Only one AuthSession can be active at any given time.'
47      );
48    }
49
50    return { type: 'locked' };
51  }
52
53  const returnUrl = options.returnUrl || sessionUrlProvider.getDefaultReturnUrl();
54  const startUrl = sessionUrlProvider.getStartUrl(authUrl, returnUrl);
55  const showInRecents = options.showInRecents || false;
56
57  // About to start session, set lock
58  _authLock = true;
59
60  let result;
61  try {
62    result = await _openWebBrowserAsync(startUrl, returnUrl, showInRecents);
63  } finally {
64    // WebBrowser session complete, unset lock
65    _authLock = false;
66  }
67
68  // Handle failures
69  if (!result) {
70    throw new Error('Unexpected missing AuthSession result');
71  }
72  if (!result.url) {
73    if (result.type) {
74      return result;
75    } else {
76      throw new Error('Unexpected AuthSession result with missing type');
77    }
78  }
79
80  const { params, errorCode } = getQueryParams(result.url);
81
82  return {
83    type: errorCode ? 'error' : 'success',
84    params,
85    errorCode,
86    authentication: null,
87    url: result.url,
88  };
89}
90
91export function dismiss() {
92  dismissAuthSession();
93}
94
95export const getDefaultReturnUrl = sessionUrlProvider.getDefaultReturnUrl;
96
97/**
98 * @deprecated Use `makeRedirectUri({ path, useProxy })` instead.
99 *
100 * @param path
101 */
102export function getRedirectUrl(path?: string): string {
103  return sessionUrlProvider.getRedirectUrl(path);
104}
105
106/**
107 * Create a redirect url for the current platform.
108 *
109 * - **Web:** Generates a path based on the current \`window.location\`. For production web apps you should hard code the URL.
110 * - **Managed:** Uses the `scheme` property of your `app.config.js` or `app.json`.
111 *   - **Proxy:** Uses auth.expo.io as the base URL for the path. This only works in Expo client and standalone environments.
112 * - **Bare workflow:** Provide either the `scheme` or a manual `native` property to use.
113 *
114 * @param options Additional options for configuring the path.
115 *
116 * @example
117 * ```ts
118 * const redirectUri = makeRedirectUri({
119 *   scheme: 'my-scheme',
120 *   path: 'redirect'
121 * });
122 * // Custom app: my-scheme://redirect
123 * // Expo Go: exp://127.0.0.1:19000/--/redirect
124 * // Web dev: https://localhost:19006/redirect
125 * // Web prod: https://yourwebsite.com/redirect
126 *
127 * const redirectUri2 = makeRedirectUri({
128 *   scheme: 'scheme2',
129 *   preferLocalhost: true,
130 *   isTripleSlashed: true,
131 * });
132 * // Custom app: scheme2:///
133 * // Expo Go: exp://localhost:19000
134 * // Web dev: https://localhost:19006
135 * // Web prod: https://yourwebsite.com
136 * ```
137 *
138 * const redirectUri3 = makeRedirectUri({
139 *   useProxy: true,
140 * });
141 * // Custom app: https://auth.expo.io/@username/slug
142 * // Expo Go: https://auth.expo.io/@username/slug
143 * // Web dev: https://localhost:19006
144 * // Web prod: https://yourwebsite.com
145 * ```
146 */
147export function makeRedirectUri({
148  native,
149  scheme,
150  isTripleSlashed,
151  queryParams,
152  path,
153  preferLocalhost,
154  useProxy,
155}: AuthSessionRedirectUriOptions = {}): string {
156  if (
157    Platform.OS !== 'web' &&
158    native &&
159    [ExecutionEnvironment.Standalone, ExecutionEnvironment.Bare].includes(
160      Constants.executionEnvironment
161    )
162  ) {
163    // Should use the user-defined native scheme in standalone builds
164    return native;
165  }
166  if (!useProxy || Platform.OS === 'web') {
167    const url = Linking.createURL(path || '', {
168      isTripleSlashed,
169      scheme,
170      queryParams,
171    });
172
173    if (preferLocalhost) {
174      const ipAddress = url.match(
175        /\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/
176      );
177      // Only replace if an IP address exists
178      if (ipAddress?.length) {
179        const [protocol, path] = url.split(ipAddress[0]);
180        return `${protocol}localhost${path}`;
181      }
182    }
183
184    return url;
185  }
186  // Attempt to use the proxy
187  return sessionUrlProvider.getRedirectUrl(path);
188}
189
190/**
191 * Build an `AuthRequest` and load it before returning.
192 *
193 * @param config
194 * @param issuerOrDiscovery
195 */
196export async function loadAsync(
197  config: AuthRequestConfig,
198  issuerOrDiscovery: IssuerOrDiscovery
199): Promise<AuthRequest> {
200  const request = new AuthRequest(config);
201  const discovery = await resolveDiscoveryAsync(issuerOrDiscovery);
202  await request.makeAuthUrlAsync(discovery);
203  return request;
204}
205
206async function _openWebBrowserAsync(startUrl: string, returnUrl: string, showInRecents: boolean) {
207  // $FlowIssue: Flow thinks the awaited result can be a promise
208  const result = await openAuthSessionAsync(startUrl, returnUrl, { showInRecents });
209  if (result.type === 'cancel' || result.type === 'dismiss') {
210    return { type: result.type };
211  }
212
213  return result;
214}
215
216export { useAutoDiscovery, useAuthRequest } from './AuthRequestHooks';
217export { AuthError, TokenError } from './Errors';
218
219export {
220  AuthSessionOptions,
221  AuthSessionRedirectUriOptions,
222  AuthSessionResult,
223  AuthRequest,
224  AuthRequestConfig,
225  AuthRequestPromptOptions,
226  CodeChallengeMethod,
227  DiscoveryDocument,
228  Issuer,
229  IssuerOrDiscovery,
230  Prompt,
231  ProviderMetadata,
232  ResponseType,
233  resolveDiscoveryAsync,
234  fetchDiscoveryAsync,
235  generateHexStringAsync,
236};
237
238export {
239  // Token classes
240  TokenResponse,
241  AccessTokenRequest,
242  RefreshTokenRequest,
243  RevokeTokenRequest,
244  // Token methods
245  revokeAsync,
246  refreshAsync,
247  exchangeCodeAsync,
248  fetchUserInfoAsync,
249} from './TokenRequest';
250
251// Token types
252export * from './TokenRequest.types';
253