From cca5a076b93cbc4ca790dc786b57c1d357df7095 Mon Sep 17 00:00:00 2001 From: Michael Sober Date: Thu, 30 Jul 2026 11:45:20 +0000 Subject: [PATCH] fix(auth): allow prompt=none silent SSO to resume federated sessions --- .changeset/fluffy-donkeys-jump.md | 11 ++++++ .../cognito/signInWithRedirect.test.ts | 37 +++++++++++++++++++ .../cognito/apis/signInWithRedirect.ts | 13 +++++-- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 .changeset/fluffy-donkeys-jump.md diff --git a/.changeset/fluffy-donkeys-jump.md b/.changeset/fluffy-donkeys-jump.md new file mode 100644 index 00000000000..e69bfd535f4 --- /dev/null +++ b/.changeset/fluffy-donkeys-jump.md @@ -0,0 +1,11 @@ +--- +'@aws-amplify/auth': patch +--- + +fix(auth): allow prompt=none silent SSO to resume federated sessions + +`signInWithRedirect` always appended `identity_provider=COGNITO` to the `/oauth2/authorize` request when no `provider` or `idpIdentifier` was supplied. Cognito treats `identity_provider` as a provider selector, so pinning it to `COGNITO` while requesting a silent sign in with `options.prompt: 'NONE'` restricted the attempt to native Cognito sessions. Users whose live hosted UI session originated from a federated IdP (for example Google or a SAML provider) were rejected with `error=login_required` instead of having their session resumed. + +`identity_provider` is now omitted only when `prompt` is `'NONE'` and neither `provider` nor `idpIdentifier` is specified, which lets Cognito resume whichever session is already active. All other behavior is unchanged: an explicit `provider` still sends `identity_provider`, an `idpIdentifier` still sends `idp_identifier`, and the interactive no-argument call still defaults to `identity_provider=COGNITO`. + +Fixes https://github.com/aws-amplify/amplify-js/issues/14897 diff --git a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts index f1a7b596f7d..625b057f1a4 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts @@ -203,6 +203,43 @@ describe('signInWithRedirect', () => { ); }); + it('omits identity_provider when prompt is NONE and no provider is specified', async () => { + await signInWithRedirect({ options: { prompt: 'NONE' } }); + const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; + expect(oauthUrl).not.toContain('identity_provider'); + expect(oauthUrl).toStrictEqual( + `https://oauth.domain.com/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=userPoolClientId&scope=phone+email+openid+profile+aws.cognito.signin.user.admin&prompt=none&state=oauth_state&code_challenge=code_challenge&code_challenge_method=S256`, + ); + }); + + it('keeps identity_provider when prompt is NONE and a provider is specified', async () => { + await signInWithRedirect({ + provider: 'Google', + options: { prompt: 'NONE' }, + }); + const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; + expect(oauthUrl).toContain('identity_provider=Google'); + }); + + it('keeps idp_identifier only when prompt is NONE and an idpIdentifier is specified', async () => { + await signInWithRedirect({ + provider: { idpIdentifier: 'example.com' }, + options: { prompt: 'NONE' }, + }); + const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; + expect(oauthUrl).toContain('idp_identifier=example.com'); + expect(oauthUrl).not.toContain('identity_provider'); + }); + + it('keeps the default identity_provider for prompt values other than NONE', async () => { + for (const prompt of promptTypes.filter(value => value !== 'NONE')) { + await signInWithRedirect({ options: { prompt } }); + const [oauthUrl] = mockOpenAuthSession.mock.calls[0]; + expect(oauthUrl).toContain('identity_provider=COGNITO'); + mockOpenAuthSession.mockClear(); + } + }); + it('uses custom state if specified', async () => { const expectedCustomState = 'verify_me'; await signInWithRedirect({ customState: expectedCustomState }); diff --git a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts index c630be74298..c72c77db91a 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts @@ -50,7 +50,7 @@ export async function signInWithRedirect( await assertUserNotAuthenticated(); } - let provider = 'COGNITO'; // Default + let provider: string | undefined = 'COGNITO'; // Default let idpIdentifier: string | undefined; if (typeof input?.provider === 'string') { @@ -59,6 +59,13 @@ export async function signInWithRedirect( provider = input.provider.custom; } else if (input?.provider?.idpIdentifier) { ({ idpIdentifier } = input.provider); + } else if (input?.options?.prompt === 'NONE') { + // `identity_provider` acts as a provider selector, so pinning it to the + // default `COGNITO` would restrict a silent `prompt=none` attempt to + // native Cognito sessions and fail with `login_required` for users whose + // existing session came from a federated IdP. Omitting it lets Cognito + // resume whichever session is already active. + provider = undefined; } return oauthSignIn({ @@ -89,7 +96,7 @@ const oauthSignIn = async ({ authSessionOpener, }: { oauthConfig: OAuthConfig; - provider: string; + provider?: string; idpIdentifier?: string; clientId: string; customState?: string; @@ -127,7 +134,7 @@ const oauthSignIn = async ({ // Add either identity_provider or idp_identifier, but not both if (idpIdentifier) { params.append('idp_identifier', idpIdentifier); - } else { + } else if (provider) { params.append('identity_provider', provider); }