UNPKG

expo-auth-session

Version:

Expo module for browser-based authentication

226 lines 8.87 kB
import * as Application from 'expo-application'; import { useEffect, useMemo, useState } from 'react'; import { Platform } from 'react-native'; import { applyRequiredScopes, invariantClientId } from './ProviderUtils'; import { AuthRequest } from '../AuthRequest'; import { Prompt, ResponseType, } from '../AuthRequest.types'; import { useAuthRequestResult, useLoadedAuthRequest } from '../AuthRequestHooks'; import { makeRedirectUri } from '../AuthSession'; import { generateHexStringAsync } from '../PKCE'; import { AccessTokenRequest } from '../TokenRequest'; const settings = { windowFeatures: { width: 515, height: 680 }, minimumScopes: [ 'openid', 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email', ], }; export const discovery = { authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', tokenEndpoint: 'https://oauth2.googleapis.com/token', revocationEndpoint: 'https://oauth2.googleapis.com/revoke', userInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo', }; // @needsAudit /** * Extends [`AuthRequest`](#authrequest) and accepts [`GoogleAuthRequestConfig`](#googleauthrequestconfig) in the constructor. */ class GoogleAuthRequest extends AuthRequest { nonce; constructor({ language, loginHint, selectAccount, extraParams = {}, clientSecret, ...config }) { const inputParams = { ...extraParams, }; if (language) inputParams.hl = language; if (loginHint) inputParams.login_hint = loginHint; if (selectAccount) inputParams.prompt = Prompt.SelectAccount; // Apply the default scopes const scopes = applyRequiredScopes(config.scopes, settings.minimumScopes); const isImplicit = config.responseType === ResponseType.Token || config.responseType === ResponseType.IdToken; if (isImplicit) { // PKCE must be disabled in implicit mode. config.usePKCE = false; } let inputClientSecret; // Google will throw if you attempt to use the client secret if (config.responseType && config.responseType !== ResponseType.Code) { // TODO: maybe warn that you shouldn't store the client secret on the client inputClientSecret = clientSecret; } super({ ...config, clientSecret: inputClientSecret, scopes, extraParams: inputParams, }); } /** * Load and return a valid auth request based on the input config. */ async getAuthRequestConfigAsync() { const { extraParams = {}, ...config } = await super.getAuthRequestConfigAsync(); if (config.responseType === ResponseType.IdToken && !extraParams.nonce && !this.nonce) { if (!this.nonce) { this.nonce = await generateHexStringAsync(16); } extraParams.nonce = this.nonce; } return { ...config, extraParams, }; } } /** * Load an authorization request with an ID Token for authentication with Firebase. * * Returns a loaded request, a response, and a prompt method. * When the prompt method completes then the response will be fulfilled. * * The id token can be retrieved with `response.params.id_token`. * * - [Get Started](https://docs.expo.dev/guides/authentication/#google) * * @param config * @param redirectUriOptions */ export function useIdTokenAuthRequest(config, redirectUriOptions = {}) { const isWebAuth = Platform.OS === 'web'; return useAuthRequest({ ...config, responseType: // If the client secret is provided then code can be used !config.clientSecret && // When web auth is used, we can request the `id_token` directly without exchanging a code. isWebAuth ? ResponseType.IdToken : undefined, }, { ...redirectUriOptions }); } /** * Load an authorization request. * Returns a loaded request, a response, and a prompt method. * When the prompt method completes, then the response will be fulfilled. * * - [Get Started](https://docs.expo.dev/guides/authentication/#google) * * @param config * @param redirectUriOptions */ export function useAuthRequest(config = {}, redirectUriOptions = {}) { const clientId = useMemo(() => { const propertyName = Platform.select({ ios: 'iosClientId', android: 'androidClientId', default: 'webClientId', }); const clientId = config[propertyName] ?? config.clientId; invariantClientId(propertyName, clientId, 'Google'); return clientId; }, [config.iosClientId, config.androidClientId, config.webClientId, config.clientId]); const responseType = useMemo(() => { // Allow overrides. if (typeof config.responseType !== 'undefined') { return config.responseType; } // You can only use `response_token=code` on installed apps (iOS, Android without proxy). // Installed apps can auto exchange without a client secret and get the token and id-token (Firebase). const isInstalledApp = Platform.OS !== 'web'; // If the user provided the client secret (they shouldn't!) then use code exchange by default. if (config.clientSecret || isInstalledApp) { return ResponseType.Code; } // This seems the most pragmatic option since it can result in a full authentication on web and proxy platforms as expected. return ResponseType.Token; }, [config.responseType, config.clientSecret]); const redirectUri = useMemo(() => { if (typeof config.redirectUri !== 'undefined') { return config.redirectUri; } return makeRedirectUri({ native: `${Application.applicationId}:/oauthredirect`, ...redirectUriOptions, // native: `com.googleusercontent.apps.${guid}:/oauthredirect`, }); }, [config.redirectUri, redirectUriOptions]); const extraParams = useMemo(() => { const output = config.extraParams ? { ...config.extraParams } : {}; if (config.language) { output.hl = output.language; } if (config.loginHint) { output.login_hint = output.loginHint; } if (config.selectAccount) { output.prompt = Prompt.SelectAccount; } return output; }, [config.extraParams, config.language, config.loginHint, config.selectAccount]); const request = useLoadedAuthRequest({ ...config, responseType, extraParams, clientId, redirectUri, }, discovery, GoogleAuthRequest); const [result, promptAsync] = useAuthRequestResult(request, discovery, { windowFeatures: settings.windowFeatures, }); const [fullResult, setFullResult] = useState(null); const shouldAutoExchangeCode = useMemo(() => { // allow overrides if (typeof config.shouldAutoExchangeCode !== 'undefined') { return config.shouldAutoExchangeCode; } // has a code to exchange and doesn't have an authentication yet. return result?.type === 'success' && result.params.code && !result.authentication; }, [config.shouldAutoExchangeCode, result?.type]); useEffect(() => { let isMounted = true; if (shouldAutoExchangeCode && result?.type === 'success') { const exchangeRequest = new AccessTokenRequest({ clientId, clientSecret: config.clientSecret, redirectUri, scopes: config.scopes, code: result.params.code, extraParams: { code_verifier: request?.codeVerifier || '', }, }); exchangeRequest.performAsync(discovery).then((authentication) => { if (isMounted) { setFullResult({ ...result, params: { id_token: authentication?.idToken || '', access_token: authentication.accessToken, ...result.params, }, authentication, }); } }); } else { setFullResult(result); } return () => { isMounted = false; }; }, [ clientId, redirectUri, shouldAutoExchangeCode, config.clientSecret, config.scopes?.join(','), request?.codeVerifier, result, ]); return [request, fullResult, promptAsync]; } //# sourceMappingURL=Google.js.map