- New auth module verifies Keycloak access tokens offline against the realm's signing keys (issuer, RS256, typ=Bearer, allowed azp); guests are recognised by the guest realm role - Clients send an auth message after connecting and get on_auth or on_auth_error; unauthenticated sockets are closed after 5 seconds and their messages ignored; a socket can refresh its token but not switch user - A user can be connected from several sockets; a late close of an old socket no longer drops the new one - Malformed messages are ignored instead of throwing - Games store player names and guest flags and send them with the connection info - Legacy ?userKey= clients and /register stay available behind LEGACY_USERKEY_AUTH; legacy keys must be UUIDs so they cannot take a Keycloak user's kc: id - TypeScript target raised to es2020 for async/await
62 lines
3.0 KiB
TypeScript
62 lines
3.0 KiB
TypeScript
import { createLocalJWKSet, exportJWK, generateKeyPair, JWK, JWTPayload, KeyLike, SignJWT } from "jose";
|
|
import { InvalidTokenError, KeycloakTokenVerifier } from "../../src/auth";
|
|
|
|
const ISSUER = "http://keycloak.test/realms/mancala";
|
|
|
|
describe("KeycloakTokenVerifier", () => {
|
|
let privateKey: KeyLike;
|
|
let otherPrivateKey: KeyLike;
|
|
let verifier: KeycloakTokenVerifier;
|
|
|
|
beforeAll(async () => {
|
|
const keyPair = await generateKeyPair("RS256");
|
|
privateKey = keyPair.privateKey;
|
|
otherPrivateKey = (await generateKeyPair("RS256")).privateKey;
|
|
const publicJwk: JWK = { ...(await exportJWK(keyPair.publicKey)), kid: "test-key", alg: "RS256" };
|
|
verifier = new KeycloakTokenVerifier({
|
|
issuer: ISSUER,
|
|
allowedClientIds: ["mancala-web"],
|
|
keys: createLocalJWKSet({ keys: [publicJwk] }),
|
|
});
|
|
});
|
|
|
|
const sign = (claims: JWTPayload, options: { key?: KeyLike, expiresIn?: string, issuer?: string } = {}) =>
|
|
new SignJWT({ typ: "Bearer", azp: "mancala-web", preferred_username: "alice", ...claims })
|
|
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
|
|
.setSubject("user-1")
|
|
.setIssuer(options.issuer ?? ISSUER)
|
|
.setIssuedAt()
|
|
.setExpirationTime(options.expiresIn ?? "5m")
|
|
.sign(options.key ?? privateKey);
|
|
|
|
it("returns the user of a valid access token", async () => {
|
|
const user = await verifier.verify(await sign({ realm_access: { roles: ["offline_access"] } }));
|
|
expect(user).toEqual({ id: "kc:user-1", name: "alice", isGuest: false });
|
|
});
|
|
|
|
it("marks users with the guest role as guests", async () => {
|
|
const user = await verifier.verify(await sign({ preferred_username: "guest-1a2b3c4d", realm_access: { roles: ["guest"] } }));
|
|
expect(user).toEqual({ id: "kc:user-1", name: "guest-1a2b3c4d", isGuest: true });
|
|
});
|
|
|
|
it.each([
|
|
["signed with another key", () => sign({}, { key: otherPrivateKey })],
|
|
["from another issuer", () => sign({}, { issuer: "http://evil.test/realms/mancala" })],
|
|
["expired", () => sign({}, { expiresIn: "-1m" })],
|
|
["an ID token", () => sign({ typ: "ID" })],
|
|
["a refresh token", () => sign({ typ: "Refresh" })],
|
|
["issued to another client", () => sign({ azp: "some-other-app" })],
|
|
["not a JWT", async () => "not-a-token"],
|
|
])("rejects a token %s", async (_, makeToken) => {
|
|
await expect(verifier.verify(await makeToken())).rejects.toBeInstanceOf(InvalidTokenError);
|
|
});
|
|
|
|
it("rejects unsigned tokens", async () => {
|
|
const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url");
|
|
const payload = Buffer.from(JSON.stringify({
|
|
iss: ISSUER, sub: "user-1", typ: "Bearer", azp: "mancala-web", exp: Math.floor(Date.now() / 1000) + 300,
|
|
})).toString("base64url");
|
|
await expect(verifier.verify(`${header}.${payload}.`)).rejects.toBeInstanceOf(InvalidTokenError);
|
|
});
|
|
});
|