@stevesouth/expo-auth-session
Version:
Expo module for browser-based authentication
253 lines (229 loc) • 6.96 kB
text/typescript
import Constants, { ExecutionEnvironment } from 'expo-constants';
import * as Linking from 'expo-linking';
import { Platform } from 'expo-modules-core';
import { dismissAuthSession, openAuthSessionAsync } from 'expo-web-browser';
import { AuthRequest } from './AuthRequest';
import {
AuthRequestConfig,
AuthRequestPromptOptions,
CodeChallengeMethod,
Prompt,
ResponseType,
} from './AuthRequest.types';
import {
AuthSessionOptions,
AuthSessionRedirectUriOptions,
AuthSessionResult,
} from './AuthSession.types';
import {
DiscoveryDocument,
fetchDiscoveryAsync,
Issuer,
IssuerOrDiscovery,
ProviderMetadata,
resolveDiscoveryAsync,
} from './Discovery';
import { generateHexStringAsync } from './PKCE';
import { getQueryParams } from './QueryParams';
import sessionUrlProvider from './SessionUrlProvider';
let _authLock = false;
export async function startAsync(options: AuthSessionOptions): Promise<AuthSessionResult> {
const authUrl = options.authUrl;
// Prevent accidentally starting to an empty url
if (!authUrl) {
throw new Error(
'No authUrl provided to AuthSession.startAsync. An authUrl is required -- it points to the page where the user will be able to sign in.'
);
}
// Prevent multiple sessions from running at the same time, WebBrowser doesn't
// support it this makes the behavior predictable.
if (_authLock) {
if (__DEV__) {
console.warn(
'Attempted to call AuthSession.startAsync multiple times while already active. Only one AuthSession can be active at any given time.'
);
}
return { type: 'locked' };
}
const returnUrl = options.returnUrl || sessionUrlProvider.getDefaultReturnUrl();
const startUrl = sessionUrlProvider.getStartUrl(authUrl, returnUrl);
const showInRecents = options.showInRecents || false;
// About to start session, set lock
_authLock = true;
let result;
try {
result = await _openWebBrowserAsync(startUrl, returnUrl, showInRecents);
} finally {
// WebBrowser session complete, unset lock
_authLock = false;
}
// Handle failures
if (!result) {
throw new Error('Unexpected missing AuthSession result');
}
if (!result.url) {
if (result.type) {
return result;
} else {
throw new Error('Unexpected AuthSession result with missing type');
}
}
const { params, errorCode } = getQueryParams(result.url);
return {
type: errorCode ? 'error' : 'success',
params,
errorCode,
authentication: null,
url: result.url,
};
}
export function dismiss() {
dismissAuthSession();
}
export const getDefaultReturnUrl = sessionUrlProvider.getDefaultReturnUrl;
/**
* @deprecated Use `makeRedirectUri({ path, useProxy })` instead.
*
* @param path
*/
export function getRedirectUrl(path?: string): string {
return sessionUrlProvider.getRedirectUrl(path);
}
/**
* Create a redirect url for the current platform.
*
* - **Web:** Generates a path based on the current \`window.location\`. For production web apps you should hard code the URL.
* - **Managed:** Uses the `scheme` property of your `app.config.js` or `app.json`.
* - **Proxy:** Uses auth.expo.io as the base URL for the path. This only works in Expo client and standalone environments.
* - **Bare workflow:** Provide either the `scheme` or a manual `native` property to use.
*
* @param options Additional options for configuring the path.
*
* @example
* ```ts
* const redirectUri = makeRedirectUri({
* scheme: 'my-scheme',
* path: 'redirect'
* });
* // Custom app: my-scheme://redirect
* // Expo Go: exp://127.0.0.1:19000/--/redirect
* // Web dev: https://localhost:19006/redirect
* // Web prod: https://yourwebsite.com/redirect
*
* const redirectUri2 = makeRedirectUri({
* scheme: 'scheme2',
* preferLocalhost: true,
* isTripleSlashed: true,
* });
* // Custom app: scheme2:///
* // Expo Go: exp://localhost:19000
* // Web dev: https://localhost:19006
* // Web prod: https://yourwebsite.com
* ```
*
* const redirectUri3 = makeRedirectUri({
* useProxy: true,
* });
* // Custom app: https://auth.expo.io/@username/slug
* // Expo Go: https://auth.expo.io/@username/slug
* // Web dev: https://localhost:19006
* // Web prod: https://yourwebsite.com
* ```
*/
export function makeRedirectUri({
native,
scheme,
isTripleSlashed,
queryParams,
path,
preferLocalhost,
useProxy,
}: AuthSessionRedirectUriOptions = {}): string {
if (
Platform.OS !== 'web' &&
native &&
[ExecutionEnvironment.Standalone, ExecutionEnvironment.Bare].includes(
Constants.executionEnvironment
)
) {
// Should use the user-defined native scheme in standalone builds
return native;
}
if (!useProxy || Platform.OS === 'web') {
const url = Linking.createURL(path || '', {
isTripleSlashed,
scheme,
queryParams,
});
if (preferLocalhost) {
const ipAddress = url.match(
/\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/
);
// Only replace if an IP address exists
if (ipAddress?.length) {
const [protocol, path] = url.split(ipAddress[0]);
return `${protocol}localhost${path}`;
}
}
return url;
}
// Attempt to use the proxy
return sessionUrlProvider.getRedirectUrl(path);
}
/**
* Build an `AuthRequest` and load it before returning.
*
* @param config
* @param issuerOrDiscovery
*/
export async function loadAsync(
config: AuthRequestConfig,
issuerOrDiscovery: IssuerOrDiscovery
): Promise<AuthRequest> {
const request = new AuthRequest(config);
const discovery = await resolveDiscoveryAsync(issuerOrDiscovery);
await request.makeAuthUrlAsync(discovery);
return request;
}
async function _openWebBrowserAsync(startUrl: string, returnUrl: string, showInRecents: boolean) {
// $FlowIssue: Flow thinks the awaited result can be a promise
const result = await openAuthSessionAsync(startUrl, returnUrl, { showInRecents });
if (result.type === 'cancel' || result.type === 'dismiss') {
return { type: result.type };
}
return result;
}
export { useAutoDiscovery, useAuthRequest } from './AuthRequestHooks';
export { AuthError, TokenError } from './Errors';
export {
AuthSessionOptions,
AuthSessionRedirectUriOptions,
AuthSessionResult,
AuthRequest,
AuthRequestConfig,
AuthRequestPromptOptions,
CodeChallengeMethod,
DiscoveryDocument,
Issuer,
IssuerOrDiscovery,
Prompt,
ProviderMetadata,
ResponseType,
resolveDiscoveryAsync,
fetchDiscoveryAsync,
generateHexStringAsync,
};
export {
// Token classes
TokenResponse,
AccessTokenRequest,
RefreshTokenRequest,
RevokeTokenRequest,
// Token methods
revokeAsync,
refreshAsync,
exchangeCodeAsync,
fetchUserInfoAsync,
} from './TokenRequest';
// Token types
export * from './TokenRequest.types';