fix: stop sending the webmail's OAuth client secret to user-chosen mail servers

With custom JMAP endpoints allowed, the password+TOTP login posted the
admin's client secret to `/auth/token` on whatever server the user
named. The secret now only goes to admin-configured servers.
This commit is contained in:
Linus Rath
2026-09-27 04:12:47 +02:00
parent ce5f177d53
commit 32b2afee3b
2 changed files with 41 additions and 1 deletions
@@ -65,3 +65,41 @@ describe('totp-token-exchange error detail', () => {
expect((await res.json()).detail).toBe(SECRET_BODY);
});
});
describe('totp-token-exchange client secret', () => {
// The webmail's OAuth client secret belongs to the admin's own server. A
// user-chosen server (custom JMAP endpoints) must never receive it.
function stalwart() {
const bodies: string[] = [];
const handler = vi.fn(async (url: unknown, init?: { body?: unknown }) => {
bodies.push(String(init?.body ?? ''));
return String(url).endsWith('/api/auth')
? Response.json({ type: 'authenticated', client_code: 'code-1' })
: Response.json({ access_token: 'at', refresh_token: 'rt', expires_in: 3600 });
});
return { bodies, handler };
}
it('does not send the client secret to a user-chosen server', async () => {
config.allowCustomJmapEndpoint = true;
config.oauthClientSecret = 'webmail-client-secret';
const { bodies, handler } = stalwart();
upstream.mockImplementation(handler);
const { POST } = await import('@/app/api/auth/totp-token-exchange/route');
const res = await POST(login('https://somewhere.example'));
expect(res.status).toBe(200);
expect(bodies).toHaveLength(2);
expect(bodies.join(' ')).not.toContain('webmail-client-secret');
});
it('still sends it to the configured server', async () => {
config.jmapServerUrl = 'https://mail.example.org';
config.oauthClientSecret = 'webmail-client-secret';
const { bodies, handler } = stalwart();
vi.stubGlobal('fetch', handler);
const { POST } = await import('@/app/api/auth/totp-token-exchange/route');
const res = await POST(login('https://mail.example.org'));
expect(res.status).toBe(200);
expect(new URLSearchParams(bodies[1]).get('client_secret')).toBe('webmail-client-secret');
});
});
+3 -1
View File
@@ -166,7 +166,9 @@ export async function stalwartPasswordLogin(
code_verifier: verifier,
});
// Confidential clients still send their secret; harmless for public clients.
if (opts.clientSecret) tokenParams.set('client_secret', opts.clientSecret);
// The secret is the admin's, for the admin's server: a user-chosen server
// (allowCustomJmapEndpoint) never receives it.
if (opts.clientSecret && opts.trusted) tokenParams.set('client_secret', opts.clientSecret);
let tokens: { access_token?: string; expires_in?: number; refresh_token?: string };
try {