mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: explain a refused token exchange on a two-factor sign-in
This commit is contained in:
@@ -62,7 +62,9 @@ import {
|
||||
resolveLoginClient,
|
||||
resetLoginClients,
|
||||
stalwartLoginBody,
|
||||
TotpLoginError,
|
||||
} from '../../lib/totp-login';
|
||||
import { describeLoginError } from '../../lib/login-errors';
|
||||
import { discoverOAuthMetadata, loginWithPkce, pkceScopes, revokeRefreshToken } from '../../lib/oauth-native';
|
||||
import {
|
||||
HANDOFF_REDIRECT_URI,
|
||||
@@ -346,6 +348,47 @@ live('live Stalwart sign-in', () => {
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
it('a refused token exchange is not reported as a wrong password, on both', async () => {
|
||||
// A code the server issued, redeemed with the wrong PKCE verifier: the
|
||||
// token endpoint refuses it after the credentials were accepted.
|
||||
const client = await resolveLoginClient(SERVER);
|
||||
const state = 'exchange-state';
|
||||
const res = await fetch(`${SERVER}/api/auth`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(stalwartLoginBody({
|
||||
username: USER,
|
||||
password: PASS,
|
||||
clientId: client.clientId,
|
||||
redirectUri: client.redirectUri,
|
||||
codeChallenge: generateCodeChallenge(generateCodeVerifier()),
|
||||
state,
|
||||
})),
|
||||
});
|
||||
const outcome = readStalwartLoginResponse(await res.json(), state);
|
||||
expect(outcome.kind).toBe('code');
|
||||
if (outcome.kind !== 'code') return;
|
||||
const token = await fetch(`${SERVER}/auth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: outcome.code,
|
||||
client_id: client.clientId,
|
||||
redirect_uri: client.redirectUri,
|
||||
code_verifier: generateCodeVerifier(),
|
||||
}).toString(),
|
||||
});
|
||||
expect(token.ok).toBe(false);
|
||||
// What exchangePasswordForTokens throws for that answer, and what the
|
||||
// sign-in screen makes of it.
|
||||
const copy = describeLoginError(
|
||||
new TotpLoginError('token_exchange_failed', `Token exchange failed: ${token.status}`),
|
||||
{ serverUrl: SERVER },
|
||||
);
|
||||
expect(copy.detail).toMatch(/password and code were accepted/);
|
||||
}, 30_000);
|
||||
|
||||
it('account security with the password (Basic) works on both', async () => {
|
||||
const client = new JMAPClient();
|
||||
await client.connect(SERVER, USER, PASS);
|
||||
|
||||
@@ -24,6 +24,13 @@ describe('describeLoginError', () => {
|
||||
expect(describeLoginError(named('AuthenticationError', 'Invalid credentials')).tone).not.toBe('info');
|
||||
});
|
||||
|
||||
it('explains a refused token exchange after the password and code were accepted', () => {
|
||||
const err = Object.assign(named('TotpLoginError', 'Token exchange failed: 400'), { code: 'token_exchange_failed' });
|
||||
const copy = describeLoginError(err, { serverUrl: 'https://mail.example.com' });
|
||||
expect(copy.title).toBe("mail.example.com didn't start a session");
|
||||
expect(copy.detail).toMatch(/password and code were accepted/);
|
||||
});
|
||||
|
||||
it('blames a refused access token on the token, not on a password', () => {
|
||||
for (const err of [named('InvalidTokenError', 'Invalid access token'), 'Invalid access token']) {
|
||||
const copy = describeLoginError(err);
|
||||
|
||||
@@ -141,6 +141,15 @@ export function describeLoginError(err: unknown, context: LoginErrorContext = {}
|
||||
};
|
||||
}
|
||||
|
||||
// Password and code were fine; the token endpoint then refused the app
|
||||
// (webmail TOKEN_EXCHANGE_FAILED copy).
|
||||
if (name === 'TotpLoginError' && (err as { code?: string }).code === 'token_exchange_failed') {
|
||||
return {
|
||||
title: t('login.mobile.err_token_exchange_title', "{host} didn't start a session", { host }),
|
||||
detail: t('login.error.token_exchange_failed', 'Your password and code were accepted, but the mail server refused to start a session for this app. Ask your administrator to check its OAuth client settings.'),
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'TotpLoginError' && lower.includes('invalid')) {
|
||||
return {
|
||||
title: t('login.mobile.err_bad_title', "That didn't work"),
|
||||
|
||||
Reference in New Issue
Block a user