diff --git a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts index 4a46f2dc454..45dc82cbeea 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts @@ -165,7 +165,7 @@ describe('signInWithRedirect', () => { const [oauthUrl, redirectSignIn, preferPrivateSession] = mockOpenAuthSession.mock.calls[0]; expect(oauthUrl).toStrictEqual( - 'https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=Google&scope=phone%20email%20openid%20profile%20aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256', + 'https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=Google&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256', ); expect(redirectSignIn).toEqual( mockAuthConfigWithOAuth.Auth.Cognito.loginWith.oauth.redirectSignIn, @@ -178,7 +178,7 @@ describe('signInWithRedirect', () => { await signInWithRedirect(); const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; expect(oauthUrl).toStrictEqual( - `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedDefaultProvider}&scope=phone%20email%20openid%20profile%20aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, + `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedDefaultProvider}&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, ); }); @@ -187,7 +187,7 @@ describe('signInWithRedirect', () => { await signInWithRedirect({ provider: { custom: expectedCustomProvider } }); const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; expect(oauthUrl).toStrictEqual( - `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedCustomProvider}&scope=phone%20email%20openid%20profile%20aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, + `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedCustomProvider}&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, ); }); @@ -207,7 +207,7 @@ describe('signInWithRedirect', () => { const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; const cognitoPrompt = prompt.toLowerCase(); expect(oauthUrl).toStrictEqual( - `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedCustomProvider}&scope=phone%20email%20openid%20profile%20aws.cognito.signin.user.admin&prompt=${cognitoPrompt}&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, + `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=${expectedCustomProvider}&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&prompt=${cognitoPrompt}&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, ); mockOpenAuthSession.mockClear(); } @@ -233,6 +233,29 @@ describe('signInWithRedirect', () => { mockAssertUserNotAuthenticated.mockClear(); }); + it('calls default openAuthSession if no override specified', async () => { + const mockAuthSessionOpener = jest.fn(); + await signInWithRedirect({ + provider: 'Google', + }); + + expect(mockOpenAuthSession).toHaveBeenCalled(); + expect(mockAuthSessionOpener).not.toHaveBeenCalled(); + }); + + it('allows to override openAuthSession if specified', async () => { + const mockAuthSessionOpener = jest.fn(); + await signInWithRedirect({ + provider: 'Google', + options: { + authSessionOpener: mockAuthSessionOpener, + }, + }); + + expect(mockOpenAuthSession).not.toHaveBeenCalled(); + expect(mockAuthSessionOpener).toHaveBeenCalled(); + }); + describe('specifications on Web', () => { describe('side effect', () => { it('attaches oauth listener to the Amplify singleton', async () => { @@ -368,7 +391,7 @@ describe('signInWithRedirect', () => { mockOpenAuthSession.mock.calls[0]; expect(oauthUrl).toStrictEqual( - 'https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=Google&scope=phone%20email%20openid%20profile%20aws.cognito.signin.user.admin&login_hint=someone%40gmail.com&lang=en&nonce=88388838883&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256', + 'https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&identity_provider=Google&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&login_hint=someone%40gmail.com&lang=en&nonce=88388838883&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256', ); expect(redirectSignIn).toEqual( mockAuthConfigWithOAuth.Auth.Cognito.loginWith.oauth.redirectSignIn, diff --git a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts index b2ca355274c..b5f4be4a1f9 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts @@ -12,7 +12,10 @@ import { import '../utils/oauth/enableOAuthListener'; import { cognitoHostedUIIdentityProviderMap } from '../types/models'; -import { getAuthUserAgentValue, openAuthSession } from '../../../utils'; +import { + openAuthSession as _openAuthSession, + getAuthUserAgentValue, +} from '../../../utils'; import { assertUserNotAuthenticated } from '../utils/signInHelpers'; import { SignInWithRedirectInput } from '../types'; import { @@ -25,6 +28,7 @@ import { } from '../utils/oauth'; import { createOAuthError } from '../utils/oauth/createOAuthError'; import { listenForOAuthFlowCancellation } from '../utils/oauth/cancelOAuthFlow'; +import { OpenAuthSession } from '../../../utils/types'; /** * Signs in a user with OAuth. Redirects the application to an Identity Provider. @@ -66,6 +70,7 @@ export async function signInWithRedirect( nonce: input?.options?.nonce, prompt: input?.options?.prompt, }, + authSessionOpener: input?.options?.authSessionOpener, }); } @@ -76,6 +81,7 @@ const oauthSignIn = async ({ customState, preferPrivateSession, options, + authSessionOpener, }: { oauthConfig: OAuthConfig; provider: string; @@ -83,10 +89,12 @@ const oauthSignIn = async ({ customState?: string; preferPrivateSession?: boolean; options?: SignInWithRedirectInput['options']; + authSessionOpener?: OpenAuthSession; }) => { const { domain, redirectSignIn, responseType, scopes } = oauthConfig; const { loginHint, lang, nonce, prompt } = options ?? {}; const randomState = generateState(); + const openAuthSession = authSessionOpener || _openAuthSession; /* encodeURIComponent is not URL safe, use urlSafeEncode instead. Cognito single-encodes/decodes url on first sign in and double-encodes/decodes url @@ -105,28 +113,25 @@ const oauthSignIn = async ({ oAuthStore.storeOAuthState(state); oAuthStore.storePKCE(value); - const queryString = Object.entries({ - redirect_uri: redirectUri, - response_type: responseType, - client_id: clientId, - identity_provider: provider, - scope: scopes.join(' '), - // eslint-disable-next-line camelcase - ...(loginHint && { login_hint: loginHint }), - ...(lang && { lang }), - ...(nonce && { nonce }), - ...(prompt && { prompt: prompt.toLowerCase() }), // Cognito expects lowercase prompt values - state, - ...(responseType === 'code' && { - code_challenge: toCodeChallenge(), - code_challenge_method: method, - }), - }) - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join('&'); - - // TODO(v6): use URL object instead - const oAuthUrl = `https://${domain}/oauth2/authorize?${queryString}`; + const params = new URLSearchParams([ + ['redirect_uri', redirectUri], + ['response_type', responseType], + ['client_id', clientId], + ['identity_provider', provider], + ['scope', scopes.join(' ')], + ]); + + loginHint && params.append('login_hint', loginHint); + lang && params.append('lang', lang); + nonce && params.append('nonce', nonce); + prompt && params.append('prompt', prompt.toLowerCase()); + params.append('state', state); + if (responseType === 'code') { + params.append('code_challenge', toCodeChallenge()); + params.append('code_challenge_method', method); + } + const oAuthUrl = new URL('/oauth2/authorize', `https://${domain}/`); + oAuthUrl.search = params.toString(); // this will only take effect in the following scenarios: // 1. the user cancels the OAuth flow on web via back button, and @@ -135,8 +140,11 @@ const oauthSignIn = async ({ // the following is effective only in react-native as openAuthSession resolves only in react-native const { type, error, url } = - (await openAuthSession(oAuthUrl, redirectSignIn, preferPrivateSession)) ?? - {}; + (await openAuthSession( + oAuthUrl.href, + redirectSignIn, + preferPrivateSession, + )) ?? {}; try { if (type === 'error') { diff --git a/packages/auth/src/types/inputs.ts b/packages/auth/src/types/inputs.ts index 441a5043208..3f23e784312 100644 --- a/packages/auth/src/types/inputs.ts +++ b/packages/auth/src/types/inputs.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { OpenAuthSession } from '../utils/types'; + import { AuthDevice, AuthUserAttribute, @@ -70,6 +72,12 @@ export interface AuthSignInWithRedirectInput { provider?: AuthProvider | { custom: string }; customState?: string; options?: { + /** + * on various mobile frameworks which allow js usage for app development (e.g. cordova) + * in-app or webview redirects are discouraged or not allowed by the OS. + * this gives an option to adjust the behaviour to the framework + */ + authSessionOpener?: OpenAuthSession; /** * On iOS devices, setting this to true requests that the browser not share cookies or other browsing data between * the authentication session and the user’s normal browser session. This will bypass the permissions dialog that