diff --git a/src/backend/services/auth/AuthService.test.ts b/src/backend/services/auth/AuthService.test.ts index 25d24ad78..67f9f03e6 100644 --- a/src/backend/services/auth/AuthService.test.ts +++ b/src/backend/services/auth/AuthService.test.ts @@ -2296,6 +2296,34 @@ describe('AuthService (integration)', () => { expect(rows).toHaveLength(0); }); + // Only a full-access token is admitted to a socket, so only its revoke + // is worth a cluster-wide eviction broadcast. + it('announces a revoked full-access token, and no other kind', async () => { + const user = await makeUser(); + const actor = makeActor({ + user: { id: user.id, uuid: user.uuid, username: user.username }, + }); + const emit = vi.spyOn(server.clients.event, 'emit'); + const announced = () => + emit.mock.calls.filter( + (c) => c[0] === 'auth.access-token.revoked', + ).length; + + const scoped = await authService.createAccessToken(actor, [ + [`user:${user.uuid}:email:read`], + ]); + await authService.revokeAccessToken(actor, scoped); + expect(announced()).toBe(0); + + const full = await authService.createAccessToken(actor, [ + [FULL_API_ACCESS], + ]); + await authService.revokeAccessToken(actor, full); + expect(announced()).toBe(1); + + emit.mockRestore(); + }); + it('revokeAccessToken rejects with 404 when the token belongs to another user', async () => { const u1 = await makeUser(); const u2 = await makeUser(); diff --git a/src/backend/services/auth/AuthService.ts b/src/backend/services/auth/AuthService.ts index 310d41466..a5220a01f 100644 --- a/src/backend/services/auth/AuthService.ts +++ b/src/backend/services/auth/AuthService.ts @@ -1885,6 +1885,8 @@ export class AuthService extends PuterService { let tokenUid: string; let issuerUuidFromJwt: string | undefined; let sessionUidFromJwt: string | undefined; + // A uuid says nothing about the token; assume it held one. + let couldHoldSocket = true; const isJwt = /^[\w-]+\.[\w-]+\.[\w-]+$/.test(tokenOrUuid.trim()); if (isJwt) { const decoded = this.services.token.verify( @@ -1899,6 +1901,7 @@ export class AuthService extends PuterService { tokenUid = decoded.token_uid; issuerUuidFromJwt = decoded.user_uid; sessionUidFromJwt = decoded.session_uid; + couldHoldSocket = !decoded.app_uid && decoded.full_access === true; } else { tokenUid = tokenOrUuid; } @@ -1935,6 +1938,7 @@ export class AuthService extends PuterService { tokenUid, sessionUidFromJwt, sessionRow, + { couldHoldSocket }, ); } @@ -2003,10 +2007,12 @@ export class AuthService extends PuterService { ); } + // Full access is refused above, so nothing here ever held a socket. await this.#revokeAccessTokenTail( decoded.token_uid, decoded.session_uid, null, + { couldHoldSocket: false }, ); } @@ -2052,6 +2058,7 @@ export class AuthService extends PuterService { tokenUid: string, sessionUidFromJwt: string | undefined, sessionRow: SessionRow | null, + opts: { couldHoldSocket?: boolean } = {}, ): Promise { await this.#dropAccessTokenGrants(tokenUid); @@ -2068,12 +2075,14 @@ export class AuthService extends PuterService { if (row) await this.stores.session.removeByUuid(row.uuid); } - // `revoked_at` is otherwise only read at the next handshake. - this.clients.event?.emit( - 'auth.access-token.revoked', - { token_uid: tokenUid }, - {}, - ); + // Only a token the handshake admits has a socket to drop. + if (opts.couldHoldSocket !== false) { + this.clients.event?.emit( + 'auth.access-token.revoked', + { token_uid: tokenUid }, + {}, + ); + } } // -- Internals ---------------------------------------------------