fix(auth): require a hashed poll secret for desktop handoff (#2357)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-02 17:25:54 +02:00
1 parent b82681b77a
commit f703969e80
45 files changed
+382 -188

No files matched your search

@@ -7,6 +7,7 @@ import {createAuthHarness, createTestAccount, loginAccount} from './AuthTestUtil
interface HandoffInitiateResponse {
code: string;
poll_secret: string;
}
interface HandoffInfoResponse {
@@ -59,7 +60,8 @@ describe('Auth desktop handoff code normalization', () => {
.expect(204)
.execute();
const status2 = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(status2.status).toBe('completed');
expect(status2.token).toBeTruthy();
@@ -12,6 +12,7 @@ import {createAuthHarness, createTestAccount, fetchMe, loginAccount} from './Aut
interface HandoffInitiateResponse {
code: string;
expires_at: string;
poll_secret: string;
}
interface HandoffInfoResponse {
@@ -93,7 +94,8 @@ describe('Auth desktop handoff flow', () => {
.expect(204)
.execute();
const completed = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
const sessions = await createBuilder<Array<AuthSessionsResponseItem>>(harness, completed.token!)
.get('/auth/sessions')
@@ -116,13 +118,15 @@ describe('Auth desktop handoff flow', () => {
expect(initResp.code).toBeTruthy();
expect(validateHandoffCodeFormat(initResp.code)).toBe(true);
expect(initResp.expires_at).toBeTruthy();
expect(initResp.poll_secret).toBeTruthy();
const info = await createBuilderWithoutAuth<HandoffInfoResponse>(harness)
.get(`/auth/handoff/${initResp.code}/info`)
.execute();
expect(info.status).toBe('pending');
expect(info.client_info).toBeTruthy();
const pending = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(pending.status).toBe('pending');
await createBuilderWithoutAuth(harness)
@@ -135,7 +139,8 @@ describe('Auth desktop handoff flow', () => {
.expect(204)
.execute();
const completed = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(completed.status).toBe('completed');
expect(completed.token).toBeTruthy();
@@ -150,7 +155,8 @@ describe('Auth desktop handoff flow', () => {
const handoffUser = handoffSession.json as UserMeResponse;
expect(handoffUser.id).toBe(login.userId);
const retrieved = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(retrieved.status).toBe('expired');
});
@@ -159,7 +165,11 @@ describe('Auth desktop handoff flow', () => {
.post('/auth/handoff/initiate')
.body(null)
.execute();
await createBuilderWithoutAuth(harness).delete(`/auth/handoff/${initResp.code}`).expect(204).execute();
await createBuilderWithoutAuth(harness)
.delete(`/auth/handoff/${initResp.code}`)
.body({poll_secret: initResp.poll_secret})
.expect(204)
.execute();
const cancelled = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.execute();
@@ -184,7 +194,8 @@ describe('Auth desktop handoff flow', () => {
.expect(204)
.execute();
const completed = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(completed.status).toBe('completed');
expect(completed.token).toBeTruthy();
@@ -233,8 +244,9 @@ describe('Auth desktop handoff flow', () => {
.expect(204)
.execute();
const completed = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.header('x-forwarded-for', pollerIp)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(completed.status).toBe('completed');
expect(completed.user_id).toBe(login.userId);
@@ -267,8 +279,9 @@ describe('Auth desktop handoff flow', () => {
.expect(204)
.execute();
const completed = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.post(`/auth/handoff/${initResp.code}/status`)
.header('x-forwarded-for', pollerIp)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(completed.status).toBe('completed');
expect(completed.user_id).toBe(login.userId);
@@ -72,6 +72,7 @@ describe('Auth desktop handoff negative paths', () => {
it('handles cancel for unknown handoff code gracefully', async () => {
await createBuilderWithoutAuth(harness)
.delete('/auth/handoff/unknown-code')
.body({poll_secret: 'not-the-secret'})
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_HANDOFF_CODE)
.execute();
});
@@ -7,6 +7,7 @@ import {createAuthHarness, createTestAccount, loginAccount} from './AuthTestUtil
interface HandoffInitiateResponse {
code: string;
poll_secret?: string;
}
interface HandoffStatusResponse {
@@ -56,8 +57,38 @@ describe('Auth desktop handoff complete single use', () => {
const status = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.execute();
expect(status.status).toBe('completed');
expect(status.token).toBeTruthy();
expect(status.token).not.toBe(login.token);
expect(status.status).toBe('pending');
expect(status.token).toBeUndefined();
});
it('releases the token only to a poller presenting the correct secret', async () => {
const account = await createTestAccount(harness);
const login = await loginAccount(harness, account);
const initResp = await createBuilderWithoutAuth<HandoffInitiateResponse>(harness)
.post('/auth/handoff/initiate')
.body(null)
.execute();
expect(initResp.poll_secret).toBeTruthy();
await createBuilderWithoutAuth(harness).get(`/auth/handoff/${initResp.code}/info`).execute();
await createBuilderWithoutAuth(harness)
.post('/auth/handoff/complete')
.header('Authorization', login.token)
.body({
code: initResp.code,
user_id: login.userId,
})
.expect(204)
.execute();
const withoutSecret = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.get(`/auth/handoff/${initResp.code}/status`)
.execute();
expect(withoutSecret.status).toBe('pending');
expect(withoutSecret.token).toBeUndefined();
const withSecret = await createBuilderWithoutAuth<HandoffStatusResponse>(harness)
.post(`/auth/handoff/${initResp.code}/status`)
.body({poll_secret: initResp.poll_secret})
.execute();
expect(withSecret.status).toBe('completed');
expect(withSecret.token).toBeTruthy();
expect(withSecret.token).not.toBe(login.token);
});
});