UNPKG

@aws-amplify/auth

Version:
273 lines (240 loc) • 6.61 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AMPLIFY_SYMBOL, AmplifyUrl, USER_AGENT_HEADER, urlSafeDecode, } from '@aws-amplify/core/internals/utils'; import { Hub, decodeJWT } from '@aws-amplify/core'; import { cacheCognitoTokens } from '../../tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../dispatchSignedInHubEvent'; import { tokenOrchestrator } from '../../tokenProvider'; import { createOAuthError } from './createOAuthError'; import { resolveAndClearInflightPromises } from './inflightPromise'; import { validateState } from './validateState'; import { oAuthStore } from './oAuthStore'; export const completeOAuthFlow = async ({ currentUrl, userAgentValue, clientId, redirectUri, responseType, domain, preferPrivateSession, }: { currentUrl: string; userAgentValue: string; clientId: string; redirectUri: string; responseType: string; domain: string; preferPrivateSession?: boolean; }): Promise<void> => { const urlParams = new AmplifyUrl(currentUrl); const error = urlParams.searchParams.get('error'); const errorMessage = urlParams.searchParams.get('error_description'); if (error) { throw createOAuthError(errorMessage ?? error); } if (responseType === 'code') { return handleCodeFlow({ currentUrl, userAgentValue, clientId, redirectUri, domain, preferPrivateSession, }); } return handleImplicitFlow({ currentUrl, redirectUri, preferPrivateSession, }); }; const handleCodeFlow = async ({ currentUrl, userAgentValue, clientId, redirectUri, domain, preferPrivateSession, }: { currentUrl: string; userAgentValue: string; clientId: string; redirectUri: string; domain: string; preferPrivateSession?: boolean; }) => { /* Convert URL into an object with parameters as keys { redirect_uri: 'http://localhost:3000/', response_type: 'code', ...} */ const url = new AmplifyUrl(currentUrl); const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); // if `code` or `state` is not presented in the redirect url, most likely // that the end user cancelled the inflight oauth flow by: // 1. clicking the back button of browser // 2. closing the provider hosted UI page and coming back to the app if (!code || !state) { throw createOAuthError('User cancelled OAuth flow.'); } // may throw error is being caught in attemptCompleteOAuthFlow.ts const validatedState = await validateState(state); const oAuthTokenEndpoint = 'https://' + domain + '/oauth2/token'; // TODO(v6): check hub events // dispatchAuthEvent( // 'codeFlow', // {}, // `Retrieving tokens from ${oAuthTokenEndpoint}` // ); const codeVerifier = await oAuthStore.loadPKCE(); const oAuthTokenBody = { grant_type: 'authorization_code', code, client_id: clientId, redirect_uri: redirectUri, ...(codeVerifier ? { code_verifier: codeVerifier } : {}), }; const body = Object.entries(oAuthTokenBody) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); const { access_token, refresh_token: refreshToken, id_token, error, error_message: errorMessage, token_type, expires_in, } = await ( await fetch(oAuthTokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', [USER_AGENT_HEADER]: userAgentValue, }, body, }) ).json(); if (error) { // error is being caught in attemptCompleteOAuthFlow.ts throw createOAuthError(errorMessage ?? error); } const username = (access_token && decodeJWT(access_token).payload.username) ?? 'username'; await cacheCognitoTokens({ username, AccessToken: access_token, IdToken: id_token, RefreshToken: refreshToken, TokenType: token_type, ExpiresIn: expires_in, }); return completeFlow({ redirectUri, state: validatedState, preferPrivateSession, }); }; const handleImplicitFlow = async ({ currentUrl, redirectUri, preferPrivateSession, }: { currentUrl: string; redirectUri: string; preferPrivateSession?: boolean; }) => { // hash is `null` if `#` doesn't exist on URL const url = new AmplifyUrl(currentUrl); const { id_token, access_token, state, token_type, expires_in, error_description, error, } = (url.hash ?? '#') .substring(1) // Remove # from returned code .split('&') .map(pairings => pairings.split('=')) .reduce((accum, [k, v]) => ({ ...accum, [k]: v }), { id_token: undefined, access_token: undefined, state: undefined, token_type: undefined, expires_in: undefined, error_description: undefined, error: undefined, }); if (error) { throw createOAuthError(error_description ?? error); } if (!access_token) { // error is being caught in attemptCompleteOAuthFlow.ts throw createOAuthError('No access token returned from OAuth flow.'); } const validatedState = await validateState(state); const username = (access_token && decodeJWT(access_token).payload.username) ?? 'username'; await cacheCognitoTokens({ username, AccessToken: access_token, IdToken: id_token, TokenType: token_type, ExpiresIn: expires_in, }); return completeFlow({ redirectUri, state: validatedState, preferPrivateSession, }); }; const completeFlow = async ({ redirectUri, state, preferPrivateSession, }: { preferPrivateSession?: boolean; redirectUri: string; state: string; }) => { await tokenOrchestrator.setOAuthMetadata({ oauthSignIn: true, }); await oAuthStore.clearOAuthData(); await oAuthStore.storeOAuthSignIn(true, preferPrivateSession); // this should be called before any call that involves `fetchAuthSession` // e.g. `getCurrentUser()` below, so it allows every inflight async calls to // `fetchAuthSession` can be resolved resolveAndClearInflightPromises(); // clear history before sending out final Hub events clearHistory(redirectUri); if (isCustomState(state)) { Hub.dispatch( 'auth', { event: 'customOAuthState', data: urlSafeDecode(getCustomState(state)), }, 'Auth', AMPLIFY_SYMBOL, ); } Hub.dispatch('auth', { event: 'signInWithRedirect' }, 'Auth', AMPLIFY_SYMBOL); await dispatchSignedInHubEvent(); }; const isCustomState = (state: string): boolean => { return /-/.test(state); }; const getCustomState = (state: string): string => { return state.split('-').splice(1).join('-'); }; const clearHistory = (redirectUri: string) => { if (typeof window !== 'undefined' && typeof window.history !== 'undefined') { window.history.replaceState(window.history.state, '', redirectUri); } };