fix: announce only the revokes that can have a socket to drop

Every access-token revoke broadcast an eviction cluster-wide, including the
read-URL tokens `revokeReadUrl` retires constantly. A scoped token is refused at
the handshake, so none of those broadcasts could ever reach a connection.

The announcement now goes out only for a token the handshake would have
admitted: full access, no app in the chain. `revokeOwnAccessToken` refuses
full-access tokens outright, so that whole path is silent; a revoke by raw uuid
says nothing about the token and still announces, which costs a no-op broadcast
rather than leaving a revoked socket up.

Fails without it: the test revokes a scoped token and a full-access one through
the same entry point and counts the announcements.
This commit is contained in:
Juan Castro committed 2026-10-02 15:07:15 -04:00
1 parent 5b9c43c58e
commit 751f9316ae
2 files changed
+43 -6

No files matched your search

@@ -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();
+15 -6
View File
@@ -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<AccessTokenPayload>(
@@ -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<void> {
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 ---------------------------------------------------