fix: refuse a pairing re-auth the identity provider did not make fresh

The step-up for SSO accounts asks the provider for a new login with
`prompt=login`, but a provider that ignores it answers from its existing
session, so anyone at an unlocked desktop could link a phone. When the
ID token's `auth_time` is older than the step-up, the re-auth is now
refused.
This commit is contained in:
Linus Rath
2026-09-27 04:49:54 +02:00
parent 75bffd35e6
commit 5c1da4efa7
2 changed files with 50 additions and 1 deletions
@@ -314,3 +314,27 @@ describe('reauth/sso/complete', () => {
expect(redeemPairingCode(firstCode.code)).toEqual({ ok: false, reason: 'expired' });
});
});
describe('reauth/sso/complete freshness', () => {
const idToken = (claims: Record<string, unknown>) =>
`h.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.s`;
it('refuses a login the provider did not make fresh (old auth_time)', async () => {
signIn(0);
pending({ created_at: Date.now() });
exchangeCodeForTokens.mockResolvedValue({ ...FRESH, id_token: idToken({ auth_time: Math.floor(Date.now() / 1000) - 3600 }) });
const res = await complete();
expect(res).toEqual({ status: 401, body: { error: 'reauth_not_fresh' } });
expect(readPairReauthFromStore(cookieStore as never)).toBeNull();
});
it('accepts a fresh auth_time, and an ID token without one', async () => {
signIn(0);
pending({ created_at: Date.now() });
exchangeCodeForTokens.mockResolvedValue({ ...FRESH, id_token: idToken({ auth_time: Math.floor(Date.now() / 1000) }) });
expect((await complete()).status).toBe(200);
pending({ created_at: Date.now() });
exchangeCodeForTokens.mockResolvedValue({ ...FRESH, id_token: idToken({ sub: 'x' }) });
expect((await complete()).status).toBe(200);
});
});