1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import { describe, expect, it } from "@effect/vitest";
3import * as Clock from "effect/Clock";
4import * as Effect from "effect/Effect";
5import * as Jwk from "effect/jose/Jwk";
6import * as Jws from "effect/jose/Jws";
7import * as Redacted from "effect/Redacted";
8import * as Schema from "effect/Schema";
9import * as TestClock from "effect/testing/TestClock";
10
11import { decodeRelayJwt, RelayJwtError, signRelayJwt, verifyRelayJwt } from "./relayJwt.ts";
12
13const Claims = Schema.Struct({ sub: Schema.String });
14
15const keyPair = Effect.gen(function* () {
16 const pair = yield* Jwk.generateKeyPair({ algorithm: "EdDSA" });
17 const publicKey = yield* Jwk.importPublic(pair.publicKey, "EdDSA").pipe(
18 Effect.flatMap((key) => Jwk.exportSpki(key, { format: "pem" })),
19 );
20 const privateKey = yield* Jwk.importPrivate(pair.privateKey, "EdDSA").pipe(
21 Effect.flatMap((key) => Jwk.exportPkcs8(key, { format: "pem" })),
22 );
23 return { publicKey, privateKey: Redacted.value(privateKey) };
24});
25
26describe("relayJwt", () => {
27 it.effect("verifies against the ambient clock and reports claim diagnostics", () =>
28 Effect.gen(function* () {
29 yield* TestClock.setTime(1_000_000);
30 const keys = yield* keyPair;
31 const token = yield* signRelayJwt({
32 privateKey: keys.privateKey,
33 typ: "test+jwt",
34 payload: { sub: "user", iss: "relay", aud: "env", iat: 1_000, exp: 1_100 },
35 });
36 const verify = verifyRelayJwt(Claims, {
37 publicKey: keys.publicKey,
38 token,
39 typ: "test+jwt",
40 issuer: "relay",
41 audience: "env",
42 });
43
44 expect(yield* verify).toEqual({ sub: "user" });
45 expect((yield* decodeRelayJwt(token)).sub).toBe("user");
46
47 yield* TestClock.adjust("161 seconds");
48 const error = yield* Effect.flip(verify);
49 expect(RelayJwtError.diagnosticCode(error)).toBe("ClaimValidationFailed.exp.expired");
50 expect(error.message).toBe('Failed to verify relay JWT of type "test+jwt".');
51
52 const es256 = yield* Jwk.generateKeyPair({ algorithm: "ES256" });
53 const es256Private = yield* Jwk.importPrivate(es256.privateKey, "ES256");
54 const signWrong = yield* Effect.flip(
55 signRelayJwt({ privateKey: es256Private, typ: "test+jwt", payload: { sub: "user" } }),
56 );
57 expect(RelayJwtError.diagnosticCode(signWrong)).toBe("AlgorithmNotAllowed");
58 const es256Token = yield* Jws.sign(
59 new TextEncoder().encode(
60 JSON.stringify({
61 sub: "user",
62 iss: "relay",
63 aud: "env",
64 iat: Math.floor((yield* Clock.currentTimeMillis) / 1_000),
65 }),
66 ),
67 es256Private,
68 { typ: "test+jwt" },
69 );
70 const verifyWrong = yield* Effect.flip(
71 verifyRelayJwt(Claims, {
72 publicKey: yield* Jwk.importPublic(es256.publicKey, "ES256"),
73 token: Redacted.value(es256Token),
74 typ: "test+jwt",
75 issuer: "relay",
76 audience: "env",
77 }),
78 );
79 expect(RelayJwtError.diagnosticCode(verifyWrong)).toBe("AlgorithmNotAllowed");
80 }).pipe(Effect.provide(NodeCrypto.layerSignature)),
81 );
82});