diff --git a/packages/auth/src/Auth.test.ts b/packages/auth/src/Auth.test.ts index d2db66a19c..c165dd5bc8 100644 --- a/packages/auth/src/Auth.test.ts +++ b/packages/auth/src/Auth.test.ts @@ -504,6 +504,45 @@ describe('Auth', () => { ); }); + it('forwards the email as login_hint for direct email login (PLT-1666)', async () => { + const mockOidcUser = { + id_token: 'token', + access_token: 'access', + refresh_token: 'refresh', + expired: false, + profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' }, + }; + + (decodeJwtPayload as jest.Mock).mockReturnValue({ + username: 'username123', + passport: undefined, + }); + + mockUserManager.signinPopup.mockResolvedValue(mockOidcUser); + + const auth = Object.create(Auth.prototype) as Auth; + (auth as any).userManager = mockUserManager; + (auth as any).config = { + popupOverlayOptions: { disableHeadlessLoginPromptOverlay: true }, + }; + getDetailMock.mockReturnValue('runtime-id-value'); + + await (auth as any).loginWithPopup({ + directLoginMethod: 'email', + email: 'test@example.com', + }); + + expect(mockUserManager.signinPopup).toHaveBeenCalledWith( + expect.objectContaining({ + extraQueryParams: expect.objectContaining({ + direct: 'email', + email: 'test@example.com', + login_hint: 'test@example.com', + }), + }), + ); + }); + it('rejects when signinPopup rejects', async () => { const error = new Error('Authentication failed'); mockUserManager.signinPopup.mockRejectedValue(error); diff --git a/packages/auth/src/Auth.ts b/packages/auth/src/Auth.ts index f4050986e1..0164b90afb 100644 --- a/packages/auth/src/Auth.ts +++ b/packages/auth/src/Auth.ts @@ -454,6 +454,12 @@ export class Auth { if (emailValue) { params.direct = directLoginOptions.directLoginMethod; params.email = emailValue; + // Standard OIDC login_hint so the hosted (Ory/Hydra) login path also + // receives the email: Hydra does not relay the custom `email` param to + // the login UI, but it does relay login_hint onto the login request, so + // the branded page can resume at the OTP screen (PLT-1666). The Auth0 + // path keeps using `email` above and is unaffected. + params.login_hint = emailValue; } } else { params.direct = directLoginOptions.directLoginMethod; diff --git a/packages/auth/src/login/standalone.ts b/packages/auth/src/login/standalone.ts index 01032bb428..531f8555c8 100644 --- a/packages/auth/src/login/standalone.ts +++ b/packages/auth/src/login/standalone.ts @@ -414,6 +414,11 @@ async function buildAuthorizationUrl( if (directLoginOptions.email) { url.searchParams.set('direct', 'email'); url.searchParams.set('email', directLoginOptions.email); + // Standard OIDC login_hint so the hosted (Ory/Hydra) login path receives the + // email too — Hydra relays login_hint onto the login request (the custom + // `email` param is dropped), letting the branded page resume at the OTP + // screen (PLT-1666). The Auth0 path keeps using `email` and is unaffected. + url.searchParams.set('login_hint', directLoginOptions.email); } } else { url.searchParams.set('direct', directLoginOptions.directLoginMethod);