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