mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(auth): require a hashed poll secret for desktop handoff (#2357)
This commit is contained in:
1 parent
b82681b77a
commit
f703969e80
45 files changed
+382
-188
No files matched your search
@@ -9,10 +9,12 @@ import {
|
||||
AuthTokenWithUserIdResponse,
|
||||
EmailRevertRequest,
|
||||
ForgotPasswordRequest,
|
||||
HandoffCancelRequest,
|
||||
HandoffCodeParam,
|
||||
HandoffCompleteRequest,
|
||||
HandoffInfoResponse,
|
||||
HandoffInitiateResponse,
|
||||
HandoffStatusRequest,
|
||||
HandoffStatusResponse,
|
||||
IpAuthorizationPollQuery,
|
||||
IpAuthorizationPollResponse,
|
||||
@@ -632,10 +634,39 @@ export function AuthController(app: HonoApp) {
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
'/auth/handoff/:code/status',
|
||||
RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_STATUS),
|
||||
Validator('param', HandoffCodeParam),
|
||||
Validator('json', HandoffStatusRequest),
|
||||
OpenAPI({
|
||||
operationId: 'get_handoff_status_with_secret',
|
||||
summary: 'Get handoff status with secret',
|
||||
responseSchema: HandoffStatusResponse,
|
||||
statusCode: 200,
|
||||
security: [],
|
||||
tags: ['Auth'],
|
||||
description:
|
||||
'Check the status of a handoff session using the poll secret from initiation. Returns the authentication token once the handoff is complete and the presented secret matches.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const clientIp = requireClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const response = await ctx.get('authRequestService').getHandoffStatus({
|
||||
code: ctx.req.valid('param').code,
|
||||
clientIp,
|
||||
pollSecret: ctx.req.valid('json').poll_secret,
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
'/auth/handoff/:code',
|
||||
RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_CANCEL),
|
||||
Validator('param', HandoffCodeParam),
|
||||
Validator('json', HandoffCancelRequest),
|
||||
OpenAPI({
|
||||
operationId: 'cancel_handoff',
|
||||
summary: 'Cancel handoff',
|
||||
@@ -646,7 +677,10 @@ export function AuthController(app: HonoApp) {
|
||||
description: 'Cancel an ongoing handoff session. The handoff code will no longer be valid for authentication.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('authRequestService').cancelHandoff({code: ctx.req.valid('param').code});
|
||||
await ctx.get('authRequestService').cancelHandoff({
|
||||
code: ctx.req.valid('param').code,
|
||||
pollSecret: ctx.req.valid('json').poll_secret,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -133,6 +133,12 @@ interface AuthHandoffInfoRequest {
|
||||
interface AuthHandoffStatusRequest {
|
||||
code: string;
|
||||
clientIp: string;
|
||||
pollSecret?: string;
|
||||
}
|
||||
|
||||
interface AuthHandoffCancelRequest {
|
||||
code: string;
|
||||
pollSecret: string;
|
||||
}
|
||||
|
||||
export class AuthRequestService {
|
||||
@@ -305,6 +311,7 @@ export class AuthRequestService {
|
||||
return {
|
||||
code: result.code,
|
||||
expires_at: result.expiresAt.toISOString(),
|
||||
poll_secret: result.pollSecret,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,8 +359,8 @@ export class AuthRequestService {
|
||||
);
|
||||
}
|
||||
|
||||
async getHandoffStatus({code, clientIp}: AuthHandoffStatusRequest): Promise<HandoffStatusResponse> {
|
||||
const result = await this.desktopHandoffService.getHandoffStatus(code, clientIp);
|
||||
async getHandoffStatus({code, clientIp, pollSecret}: AuthHandoffStatusRequest): Promise<HandoffStatusResponse> {
|
||||
const result = await this.desktopHandoffService.getHandoffStatus(code, clientIp, pollSecret);
|
||||
return {
|
||||
status: result.status,
|
||||
token: result.token,
|
||||
@@ -362,8 +369,8 @@ export class AuthRequestService {
|
||||
};
|
||||
}
|
||||
|
||||
async cancelHandoff({code}: {code: string}): Promise<void> {
|
||||
await this.desktopHandoffService.cancelHandoff(code);
|
||||
async cancelHandoff({code, pollSecret}: AuthHandoffCancelRequest): Promise<void> {
|
||||
await this.desktopHandoffService.cancelHandoff(code, pollSecret);
|
||||
}
|
||||
|
||||
private async getUserPartial(userId: string): Promise<UserPartialResponse> {
|
||||
|
||||
@@ -1,60 +1,84 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {randomBytes} from 'node:crypto';
|
||||
import {createHash, randomBytes, timingSafeEqual} from 'node:crypto';
|
||||
import {HandoffCodeExpiredError} from '@fluxer/errors/src/domains/auth/HandoffCodeExpiredError';
|
||||
import {InvalidHandoffCodeError} from '@fluxer/errors/src/domains/auth/InvalidHandoffCodeError';
|
||||
import {
|
||||
DESKTOP_HANDOFF_CODE_ALPHABET,
|
||||
DESKTOP_HANDOFF_CODE_LENGTH,
|
||||
formatDesktopHandoffCode,
|
||||
parseDesktopHandoffCode,
|
||||
} from '@fluxer/schema/src/domains/auth/DesktopHandoffCode';
|
||||
import {ms, seconds} from 'itty-time';
|
||||
import type {ApiContext} from '../../ApiContext';
|
||||
import type {SessionOrigin} from '../AuthSession';
|
||||
|
||||
const HANDOFF_CODE_PREFIX = 'desktop-handoff-v2:';
|
||||
const HANDOFF_TOKEN_PREFIX = 'desktop-handoff-token:';
|
||||
const CODE_CHARACTERS = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
||||
const CODE_LENGTH = 12;
|
||||
const NORMALIZED_CODE_REGEX = /^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{12}$/;
|
||||
const HANDOFF_ATTEMPT_PREFIX = 'desktop-handoff-attempts:';
|
||||
const HANDOFF_APPROVER_PREFIX = 'desktop-handoff-approver:';
|
||||
const MAX_FAILED_ATTEMPTS = 5;
|
||||
const ATTEMPT_TTL_SECONDS = 900;
|
||||
const MAX_INFO_LOOKUPS = 3;
|
||||
const POLL_SECRET_BYTES = 32;
|
||||
|
||||
interface HandoffData {
|
||||
createdAt: number;
|
||||
origin: SessionOrigin;
|
||||
infoLookupCount: number;
|
||||
pollSecretHash: string;
|
||||
}
|
||||
|
||||
interface HandoffTokenData {
|
||||
token: string;
|
||||
userId: string;
|
||||
pollSecretHash: string;
|
||||
}
|
||||
|
||||
interface HandoffApproverData {
|
||||
approvedAt: number;
|
||||
}
|
||||
|
||||
function generateHandoffCode(): string {
|
||||
const maxUnbiased = 256 - (256 % CODE_CHARACTERS.length);
|
||||
function generateNormalizedHandoffCode(): string {
|
||||
const maxUnbiased = 256 - (256 % DESKTOP_HANDOFF_CODE_ALPHABET.length);
|
||||
let code = '';
|
||||
while (code.length < CODE_LENGTH) {
|
||||
const bytes = randomBytes(CODE_LENGTH - code.length);
|
||||
for (let i = 0; i < bytes.length && code.length < CODE_LENGTH; i++) {
|
||||
while (code.length < DESKTOP_HANDOFF_CODE_LENGTH) {
|
||||
const bytes = randomBytes(DESKTOP_HANDOFF_CODE_LENGTH - code.length);
|
||||
for (let i = 0; i < bytes.length && code.length < DESKTOP_HANDOFF_CODE_LENGTH; i++) {
|
||||
if (bytes[i] < maxUnbiased) {
|
||||
code += CODE_CHARACTERS[bytes[i] % CODE_CHARACTERS.length];
|
||||
code += DESKTOP_HANDOFF_CODE_ALPHABET[bytes[i] % DESKTOP_HANDOFF_CODE_ALPHABET.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${code.slice(0, 6)}-${code.slice(6, 12)}`;
|
||||
return code;
|
||||
}
|
||||
|
||||
function normalizeHandoffCode(code: string): string {
|
||||
return code.replace(/[-\s]/g, '').toUpperCase();
|
||||
}
|
||||
|
||||
function assertValidHandoffCode(code: string): void {
|
||||
if (!NORMALIZED_CODE_REGEX.test(code)) {
|
||||
function requireNormalizedHandoffCode(code: string): string {
|
||||
const normalized = parseDesktopHandoffCode(code);
|
||||
if (normalized == null) {
|
||||
throw new InvalidHandoffCodeError();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function generatePollSecret(): string {
|
||||
return randomBytes(POLL_SECRET_BYTES).toString('base64url');
|
||||
}
|
||||
|
||||
function hashPollSecret(secret: string): string {
|
||||
return createHash('sha256').update(secret).digest('hex');
|
||||
}
|
||||
|
||||
function pollSecretMatches(presented: string | undefined, storedHash: string | undefined): boolean {
|
||||
if (!presented || !storedHash) {
|
||||
return false;
|
||||
}
|
||||
const presentedHash = Buffer.from(hashPollSecret(presented), 'hex');
|
||||
const stored = Buffer.from(storedHash, 'hex');
|
||||
if (presentedHash.length !== stored.length) {
|
||||
return false;
|
||||
}
|
||||
return timingSafeEqual(presentedHash, stored);
|
||||
}
|
||||
|
||||
export class DesktopHandoffService {
|
||||
@@ -63,19 +87,21 @@ export class DesktopHandoffService {
|
||||
async initiateHandoff(args: {origin: SessionOrigin}): Promise<{
|
||||
code: string;
|
||||
expiresAt: Date;
|
||||
pollSecret: string;
|
||||
}> {
|
||||
const {cache} = this.apiContext.services;
|
||||
const code = generateHandoffCode();
|
||||
const normalizedCode = normalizeHandoffCode(code);
|
||||
const normalizedCode = generateNormalizedHandoffCode();
|
||||
const pollSecret = generatePollSecret();
|
||||
const handoffData: HandoffData = {
|
||||
createdAt: Date.now(),
|
||||
origin: args.origin,
|
||||
infoLookupCount: 0,
|
||||
pollSecretHash: hashPollSecret(pollSecret),
|
||||
};
|
||||
const expirySeconds = seconds('5 minutes');
|
||||
await cache.set(`${HANDOFF_CODE_PREFIX}${normalizedCode}`, handoffData, expirySeconds);
|
||||
const expiresAt = new Date(Date.now() + ms('5 minutes'));
|
||||
return {code, expiresAt};
|
||||
return {code: formatDesktopHandoffCode(normalizedCode), expiresAt, pollSecret};
|
||||
}
|
||||
|
||||
async completeHandoff(
|
||||
@@ -84,8 +110,7 @@ export class DesktopHandoffService {
|
||||
approverIp: string,
|
||||
): Promise<void> {
|
||||
const {cache} = this.apiContext.services;
|
||||
const normalizedCode = normalizeHandoffCode(code);
|
||||
assertValidHandoffCode(normalizedCode);
|
||||
const normalizedCode = requireNormalizedHandoffCode(code);
|
||||
await this.checkAttemptLimit(approverIp);
|
||||
const storedApprover = await cache.get<HandoffApproverData>(`${HANDOFF_APPROVER_PREFIX}${normalizedCode}`);
|
||||
if (!storedApprover) {
|
||||
@@ -108,6 +133,7 @@ export class DesktopHandoffService {
|
||||
const tokenData: HandoffTokenData = {
|
||||
token,
|
||||
userId,
|
||||
pollSecretHash: handoffData.pollSecretHash,
|
||||
};
|
||||
await cache.set(`${HANDOFF_TOKEN_PREFIX}${normalizedCode}`, tokenData, remainingSeconds);
|
||||
await cache.delete(`${HANDOFF_CODE_PREFIX}${normalizedCode}`);
|
||||
@@ -122,8 +148,7 @@ export class DesktopHandoffService {
|
||||
origin?: SessionOrigin;
|
||||
}> {
|
||||
const {cache} = this.apiContext.services;
|
||||
const normalizedCode = normalizeHandoffCode(code);
|
||||
assertValidHandoffCode(normalizedCode);
|
||||
const normalizedCode = requireNormalizedHandoffCode(code);
|
||||
await this.checkAttemptLimit(approverIp);
|
||||
const codeKey = `${HANDOFF_CODE_PREFIX}${normalizedCode}`;
|
||||
const handoffData = await cache.get<HandoffData>(codeKey);
|
||||
@@ -149,18 +174,23 @@ export class DesktopHandoffService {
|
||||
|
||||
async getHandoffStatus(
|
||||
code: string,
|
||||
_pollerIp: string,
|
||||
pollerIp: string,
|
||||
pollSecret: string | undefined,
|
||||
): Promise<{
|
||||
status: 'pending' | 'completed' | 'expired';
|
||||
token?: string;
|
||||
userId?: string;
|
||||
}> {
|
||||
const {cache} = this.apiContext.services;
|
||||
const normalizedCode = normalizeHandoffCode(code);
|
||||
assertValidHandoffCode(normalizedCode);
|
||||
const normalizedCode = requireNormalizedHandoffCode(code);
|
||||
await this.checkAttemptLimit(pollerIp);
|
||||
const tokenKey = `${HANDOFF_TOKEN_PREFIX}${normalizedCode}`;
|
||||
const tokenData = await cache.get<HandoffTokenData>(tokenKey);
|
||||
if (tokenData) {
|
||||
if (!pollSecretMatches(pollSecret, tokenData.pollSecretHash)) {
|
||||
await this.recordFailedAttempt(pollerIp);
|
||||
return {status: 'pending'};
|
||||
}
|
||||
await cache.delete(tokenKey);
|
||||
return {
|
||||
status: 'completed',
|
||||
@@ -175,12 +205,19 @@ export class DesktopHandoffService {
|
||||
return {status: 'expired'};
|
||||
}
|
||||
|
||||
async cancelHandoff(code: string): Promise<void> {
|
||||
async cancelHandoff(code: string, pollSecret: string): Promise<void> {
|
||||
const {cache} = this.apiContext.services;
|
||||
const normalizedCode = normalizeHandoffCode(code);
|
||||
assertValidHandoffCode(normalizedCode);
|
||||
await cache.delete(`${HANDOFF_CODE_PREFIX}${normalizedCode}`);
|
||||
await cache.delete(`${HANDOFF_TOKEN_PREFIX}${normalizedCode}`);
|
||||
const normalizedCode = requireNormalizedHandoffCode(code);
|
||||
const codeKey = `${HANDOFF_CODE_PREFIX}${normalizedCode}`;
|
||||
const tokenKey = `${HANDOFF_TOKEN_PREFIX}${normalizedCode}`;
|
||||
const handoffData = await cache.get<HandoffData>(codeKey);
|
||||
const tokenData = await cache.get<HandoffTokenData>(tokenKey);
|
||||
const storedHash = handoffData?.pollSecretHash ?? tokenData?.pollSecretHash;
|
||||
if (!pollSecretMatches(pollSecret, storedHash)) {
|
||||
throw new InvalidHandoffCodeError();
|
||||
}
|
||||
await cache.delete(codeKey);
|
||||
await cache.delete(tokenKey);
|
||||
await cache.delete(`${HANDOFF_APPROVER_PREFIX}${normalizedCode}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user