feat: enforce realtime delegation audience on the message socket
The socket used to accept any valid token, so a full REST/actor token could open the live stream. Now it can require a narrowly-scoped delegated token (aud=iios-message), so a leaked socket token can't drive privileged REST, and vice-versa. - MessagePrincipal gains `audience`, surfaced from both verify paths (OIDC aud, and the app-token `aud` claim). - message.gateway: when IIOS_REALTIME_AUDIENCE is set, handleConnection accepts only a token whose aud matches (opt-in, like IIOS_REQUIRE_ATTESTATION; unset = no change). - spec: verifier surfaces aud; gateway accepts iios-message, rejects iios-core / no-aud when enforcing, passes through when off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,14 @@ export class MessageGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||
try {
|
||||
const token = String(client.handshake.auth?.token ?? '');
|
||||
const principal = this.session.verify(token);
|
||||
// Realtime delegation (least privilege): when IIOS_REALTIME_AUDIENCE is set, the socket
|
||||
// accepts ONLY a token minted for that audience (a short-lived `iios-message` delegate) —
|
||||
// a full REST/actor token (`iios-core`) or an un-scoped token cannot open the stream. So a
|
||||
// leaked socket token can't drive privileged REST, and vice-versa. Unset → no enforcement.
|
||||
const required = process.env.IIOS_REALTIME_AUDIENCE?.trim();
|
||||
if (required && principal.audience !== required) {
|
||||
throw new Error(`realtime audience "${principal.audience ?? '(none)'}" != required "${required}"`);
|
||||
}
|
||||
client.data = { principal } satisfies SocketState;
|
||||
} catch (err) {
|
||||
this.logger.warn(`rejecting socket: ${(err as Error).message}`);
|
||||
|
||||
Reference in New Issue
Block a user