From c2697f7cba4aa2e7b0cc53c3e8d96ab25c6ebb40 Mon Sep 17 00:00:00 2001 From: Philipp Andreas Paul Date: Thu, 26 Jun 2025 14:34:06 +0200 Subject: [PATCH] feat: make authSessionOpener configurable --- .../cognito/signInWithRedirect.test.ts | 21 ++++++-- .../cognito/apis/signInWithRedirect.ts | 54 +++++++++++-------- packages/auth/src/types/inputs.ts | 8 +++ packages/aws-amplify/package.json | 4 +- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts index 0714c091278..0921f7e4b2f 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts @@ -157,7 +157,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, @@ -170,7 +170,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`, ); }); @@ -179,7 +179,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`, ); }); @@ -189,6 +189,19 @@ describe('signInWithRedirect', () => { expect(mockUrlSafeEncode).toHaveBeenCalledWith(expectedCustomState); }); + 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 () => { @@ -324,7 +337,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 bac92589dc8..f48c28083bc 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. @@ -62,6 +66,7 @@ export async function signInWithRedirect( lang: input?.options?.lang, nonce: input?.options?.nonce, }, + authSessionOpener: input?.options?.authSessionOpener, }); } @@ -72,6 +77,7 @@ const oauthSignIn = async ({ customState, preferPrivateSession, options, + authSessionOpener, }: { oauthConfig: OAuthConfig; provider: string; @@ -79,10 +85,12 @@ const oauthSignIn = async ({ customState?: string; preferPrivateSession?: boolean; options?: SignInWithRedirectInput['options']; + authSessionOpener?: OpenAuthSession; }) => { const { domain, redirectSignIn, responseType, scopes } = oauthConfig; const { loginHint, lang, nonce } = 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 @@ -101,27 +109,24 @@ 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 }), - state, - ...(responseType === 'code' && { - code_challenge: toCodeChallenge(), - code_challenge_method: method, - }), - }) - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join('&'); + const params = new URLSearchParams([ + ['redirect_uri', redirectUri], + ['response_type', responseType], + ['client_id', clientId], + ['identity_provider', provider], + ['scope', scopes.join(' ')], + ]); - // TODO(v6): use URL object instead - const oAuthUrl = `https://${domain}/oauth2/authorize?${queryString}`; + loginHint && params.append('login_hint', loginHint); + lang && params.append('lang', lang); + nonce && params.append('nonce', nonce); + 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 @@ -130,8 +135,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 81ea27e6b88..660ed0b7719 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, @@ -58,6 +60,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 diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 07116ba63c4..18d16bb2ee2 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -451,7 +451,7 @@ "name": "[Auth] signInWithRedirect (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect }", - "limit": "19.37 kB" + "limit": "19.44 kB" }, { "name": "[Auth] fetchUserAttributes (Cognito)", @@ -469,7 +469,7 @@ "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "19.93 kB" + "limit": "20.01 kB" }, { "name": "[Auth] Associate WebAuthN Credential (Cognito)",