@opendatalabs/service-auth 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Resource-server middleware. INTERNAL Vana package.
3
+ *
4
+ * Validates an inbound `Authorization: Bearer <jwt>` header against
5
+ * Hydra's published JWKS, enforces `iss`/`aud`/`exp`/`nbf`/`alg`, and
6
+ * returns a structured success or failure. Two surfaces:
7
+ *
8
+ * - `validateAccessToken(authHeader, opts?)` -- pure function. Use
9
+ * this when you control the response shape.
10
+ * - `requireJwt({ requiredScopes? })` -- Next.js-style middleware
11
+ * wrapper that returns a `Response` on rejection.
12
+ *
13
+ * Compliance
14
+ * ----------
15
+ *
16
+ * - RFC 9068 access token shape. We do NOT invent Vana-specific
17
+ * claim names; we read `aud`, `iss`, `client_id`, `scope`, `sub`.
18
+ * - Algorithm allowlist defaults to `["RS256", "ES256"]`. Hydra's
19
+ * defaults are `ES256` for access tokens; allowing both lets
20
+ * services bring their own key shape without our code knowing.
21
+ */
22
+ import { createLocalJWKSet, createRemoteJWKSet, errors as joseErrors, jwtVerify, } from "jose";
23
+ const DEFAULT_ALLOWED_ALGORITHMS = ["RS256", "ES256"];
24
+ // Per-jwksUri resolver cache. jose.createRemoteJWKSet already handles
25
+ // in-flight coalescing and rotation, so we just want one resolver per
26
+ // URL across all validators in a process.
27
+ const remoteJwksCache = new Map();
28
+ function getRemoteJwks(uri) {
29
+ let resolver = remoteJwksCache.get(uri);
30
+ if (!resolver) {
31
+ resolver = createRemoteJWKSet(new URL(uri));
32
+ remoteJwksCache.set(uri, resolver);
33
+ }
34
+ return resolver;
35
+ }
36
+ /** Test-only: drop cached resolvers between tests. */
37
+ export function _resetJwksCacheForTests() {
38
+ remoteJwksCache.clear();
39
+ }
40
+ function looksLikeJwt(token) {
41
+ if (token.length < 16)
42
+ return false;
43
+ const parts = token.split(".");
44
+ if (parts.length !== 3)
45
+ return false;
46
+ const b64u = /^[A-Za-z0-9_-]+$/;
47
+ return parts.every((p) => p.length > 0 && b64u.test(p));
48
+ }
49
+ function extractBearer(header) {
50
+ if (!header)
51
+ return null;
52
+ const trimmed = header.trim();
53
+ if (trimmed.length < 8)
54
+ return null;
55
+ if (trimmed.slice(0, 7).toLowerCase() !== "bearer ")
56
+ return null;
57
+ return trimmed.slice(7).trim() || null;
58
+ }
59
+ function audienceIncludes(claim, expected) {
60
+ if (typeof claim === "string")
61
+ return claim === expected;
62
+ if (Array.isArray(claim))
63
+ return claim.includes(expected);
64
+ return false;
65
+ }
66
+ function scopeIncludes(claim, required) {
67
+ if (required.length === 0)
68
+ return true;
69
+ const present = typeof claim === "string"
70
+ ? new Set(claim.split(/\s+/).filter(Boolean))
71
+ : null;
72
+ if (!present)
73
+ return false;
74
+ for (const r of required) {
75
+ if (!present.has(r))
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+ export function createServiceAuthValidator(options) {
81
+ if (!options.issuer)
82
+ throw new Error("service-auth: issuer is required");
83
+ if (!options.audience)
84
+ throw new Error("service-auth: audience is required");
85
+ if (!(options.jwksUri || options.jwks)) {
86
+ throw new Error("service-auth: jwksUri or jwks must be provided");
87
+ }
88
+ const allowedAlgorithms = (options.allowedAlgorithms ??
89
+ DEFAULT_ALLOWED_ALGORITHMS);
90
+ const clockTolerance = options.clockToleranceSec ?? 30;
91
+ const keyResolver = options.jwks
92
+ ? createLocalJWKSet(options.jwks)
93
+ : getRemoteJwks(options.jwksUri);
94
+ async function validateAccessToken(header, input) {
95
+ const presented = extractBearer(header);
96
+ if (!presented)
97
+ return { valid: false, reason: "not_bearer" };
98
+ if (!looksLikeJwt(presented))
99
+ return { valid: false, reason: "not_a_jwt" };
100
+ let payload;
101
+ try {
102
+ const verified = await jwtVerify(presented, keyResolver, {
103
+ issuer: options.issuer,
104
+ algorithms: allowedAlgorithms,
105
+ clockTolerance,
106
+ });
107
+ payload = verified.payload;
108
+ }
109
+ catch (err) {
110
+ return classifyJoseError(err);
111
+ }
112
+ if (!audienceIncludes(payload.aud, options.audience)) {
113
+ return { valid: false, reason: "wrong_audience" };
114
+ }
115
+ if (!scopeIncludes(payload.scope, input?.requiredScopes ?? [])) {
116
+ return { valid: false, reason: "insufficient_scope" };
117
+ }
118
+ let clientId = null;
119
+ if (typeof payload.client_id === "string") {
120
+ clientId = payload.client_id;
121
+ }
122
+ else if (typeof payload.sub === "string") {
123
+ clientId = payload.sub;
124
+ }
125
+ if (!clientId) {
126
+ return {
127
+ valid: false,
128
+ reason: "missing_claims",
129
+ detail: "no client_id / sub",
130
+ };
131
+ }
132
+ return {
133
+ valid: true,
134
+ claims: payload,
135
+ clientId,
136
+ };
137
+ }
138
+ function requireJwt(input) {
139
+ return async (request) => {
140
+ const result = await validateAccessToken(request.headers.get("authorization"), input);
141
+ if (result.valid) {
142
+ return { ok: true, result };
143
+ }
144
+ return {
145
+ ok: false,
146
+ response: rejectionResponse(result.reason),
147
+ };
148
+ };
149
+ }
150
+ return { validateAccessToken, requireJwt };
151
+ }
152
+ function classifyJoseError(err) {
153
+ if (err instanceof joseErrors.JWTExpired) {
154
+ return { valid: false, reason: "expired" };
155
+ }
156
+ if (err instanceof joseErrors.JWSSignatureVerificationFailed) {
157
+ return { valid: false, reason: "bad_signature" };
158
+ }
159
+ if (err instanceof joseErrors.JOSEAlgNotAllowed) {
160
+ return { valid: false, reason: "wrong_algorithm" };
161
+ }
162
+ if (err instanceof joseErrors.JWTClaimValidationFailed) {
163
+ const claim = err.claim;
164
+ if (claim === "iss")
165
+ return { valid: false, reason: "wrong_issuer" };
166
+ return {
167
+ valid: false,
168
+ reason: "missing_claims",
169
+ detail: `claim=${claim ?? "unknown"}`,
170
+ };
171
+ }
172
+ if (err instanceof joseErrors.JWSInvalid ||
173
+ err instanceof joseErrors.JWTInvalid) {
174
+ return { valid: false, reason: "not_a_jwt" };
175
+ }
176
+ return {
177
+ valid: false,
178
+ reason: "verify_error",
179
+ detail: err instanceof Error ? err.message : String(err),
180
+ };
181
+ }
182
+ function rejectionResponse(reason) {
183
+ // RFC 6750 §3 "WWW-Authenticate: Bearer" semantics: 401 for
184
+ // authentication failures, 403 for insufficient scope.
185
+ const status = reason === "insufficient_scope" ? 403 : 401;
186
+ const error = reason === "expired"
187
+ ? "invalid_token"
188
+ : reason === "insufficient_scope"
189
+ ? "insufficient_scope"
190
+ : "invalid_token";
191
+ return new Response(JSON.stringify({ error, reason }), {
192
+ status,
193
+ headers: {
194
+ "content-type": "application/json",
195
+ "www-authenticate": `Bearer error="${error}"`,
196
+ },
197
+ });
198
+ }
199
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resource-server/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Test fixtures for service-auth consumers. INTERNAL Vana package.
3
+ *
4
+ * Use this from a service's own tests to:
5
+ *
6
+ * - Spin up a fake issuer with a fresh ES256 keypair.
7
+ * - Mint valid access tokens for happy-path tests.
8
+ * - Get pre-made bad tokens (expired, wrong-aud, wrong-iss,
9
+ * unsigned, malformed) for negative tests.
10
+ *
11
+ * NOT a public SDK. The shapes here are tuned for our own tests; they
12
+ * may change without notice.
13
+ */
14
+ import { type JWK } from "jose";
15
+ export interface FakeIssuerOptions {
16
+ /**
17
+ * The fake issuer URL to use in `iss` claims. Defaults to
18
+ * `"https://fake-issuer.test"`.
19
+ */
20
+ issuer?: string;
21
+ /**
22
+ * Algorithm. Defaults to `"ES256"` (matches Hydra's default for
23
+ * `access_token_strategy: jwt`).
24
+ */
25
+ algorithm?: "ES256" | "RS256";
26
+ /** Stable kid. Defaults to `"fake-kid-1"`. */
27
+ kid?: string;
28
+ }
29
+ export interface SignAccessTokenInput {
30
+ audience: string | string[];
31
+ subject?: string;
32
+ clientId?: string;
33
+ scope?: string;
34
+ expiresInSec?: number;
35
+ notBeforeSec?: number;
36
+ issuedAtSec?: number;
37
+ /** Override the issuer for negative-path tests. */
38
+ issuerOverride?: string;
39
+ /** Extra arbitrary claims. */
40
+ extraClaims?: Record<string, unknown>;
41
+ }
42
+ export interface SignAssertionInput {
43
+ /** Endpoint URL; becomes the `aud` claim per RFC 7523. */
44
+ tokenEndpoint: string;
45
+ clientId: string;
46
+ expiresInSec?: number;
47
+ }
48
+ export interface FakeIssuer {
49
+ issuer: string;
50
+ jwks: {
51
+ keys: JWK[];
52
+ };
53
+ signAccessToken(input: SignAccessTokenInput): Promise<string>;
54
+ signAssertion(input: SignAssertionInput): Promise<string>;
55
+ }
56
+ export declare function createFakeIssuer(options?: FakeIssuerOptions): Promise<FakeIssuer>;
57
+ /**
58
+ * Pre-rolled fixtures. Builds tokens against a transient issuer; the
59
+ * returned object includes the issuer + jwks so a consumer's
60
+ * validator can be wired up:
61
+ *
62
+ * const f = await createFixtures({ audience: "vana-account-introspect" });
63
+ * const v = createServiceAuthValidator({
64
+ * issuer: f.issuer.issuer,
65
+ * audience: "vana-account-introspect",
66
+ * jwks: f.issuer.jwks,
67
+ * });
68
+ * await v.validateAccessToken(`Bearer ${f.expiredToken}`);
69
+ */
70
+ export interface CommonFixturesInput {
71
+ audience: string;
72
+ clientId?: string;
73
+ }
74
+ export interface CommonFixtures {
75
+ issuer: FakeIssuer;
76
+ validToken: string;
77
+ expiredToken: string;
78
+ wrongAudienceToken: string;
79
+ wrongIssuerToken: string;
80
+ /**
81
+ * A JWT-shaped string with a valid header+payload but a flipped
82
+ * signature byte. jose rejects with JWSSignatureVerificationFailed.
83
+ */
84
+ unsignedToken: string;
85
+ /**
86
+ * Not a JWT at all (no segments). Hits the `not_a_jwt` reason.
87
+ */
88
+ malformedToken: string;
89
+ }
90
+ export declare function createFixtures(input: CommonFixturesInput): Promise<CommonFixtures>;
91
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testkit/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAIL,KAAK,GAAG,EAET,MAAM,MAAM,CAAC;AAEd,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC;IACtB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3D;AAuBD,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,UAAU,CAAC,CA0CrB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,cAAc,CAAC,CA2CzB"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Test fixtures for service-auth consumers. INTERNAL Vana package.
3
+ *
4
+ * Use this from a service's own tests to:
5
+ *
6
+ * - Spin up a fake issuer with a fresh ES256 keypair.
7
+ * - Mint valid access tokens for happy-path tests.
8
+ * - Get pre-made bad tokens (expired, wrong-aud, wrong-iss,
9
+ * unsigned, malformed) for negative tests.
10
+ *
11
+ * NOT a public SDK. The shapes here are tuned for our own tests; they
12
+ * may change without notice.
13
+ */
14
+ import { exportJWK, generateKeyPair, SignJWT, } from "jose";
15
+ async function buildKey(alg, kid) {
16
+ const { privateKey, publicKey } = await generateKeyPair(alg, {
17
+ extractable: true,
18
+ });
19
+ const publicJwk = await exportJWK(publicKey);
20
+ publicJwk.alg = alg;
21
+ publicJwk.use = "sig";
22
+ publicJwk.kid = kid;
23
+ return { privateKey, publicJwk, alg, kid };
24
+ }
25
+ export async function createFakeIssuer(options = {}) {
26
+ const issuer = options.issuer ?? "https://fake-issuer.test";
27
+ const alg = options.algorithm ?? "ES256";
28
+ const kid = options.kid ?? "fake-kid-1";
29
+ const key = await buildKey(alg, kid);
30
+ return {
31
+ issuer,
32
+ jwks: { keys: [key.publicJwk] },
33
+ async signAccessToken(input) {
34
+ const now = Math.floor(Date.now() / 1000);
35
+ const iat = input.issuedAtSec ?? now;
36
+ const builder = new SignJWT({
37
+ ...(input.clientId ? { client_id: input.clientId } : {}),
38
+ ...(input.scope ? { scope: input.scope } : {}),
39
+ ...(input.extraClaims ?? {}),
40
+ })
41
+ .setProtectedHeader({ alg: key.alg, kid: key.kid, typ: "JWT" })
42
+ .setIssuer(input.issuerOverride ?? issuer)
43
+ .setSubject(input.subject ?? input.clientId ?? "fake-sub")
44
+ .setAudience(input.audience)
45
+ .setIssuedAt(iat)
46
+ .setExpirationTime(iat + (input.expiresInSec ?? 300));
47
+ if (input.notBeforeSec !== undefined) {
48
+ builder.setNotBefore(input.notBeforeSec);
49
+ }
50
+ return builder.sign(key.privateKey);
51
+ },
52
+ async signAssertion(input) {
53
+ const now = Math.floor(Date.now() / 1000);
54
+ return new SignJWT({})
55
+ .setProtectedHeader({ alg: key.alg, kid: key.kid, typ: "JWT" })
56
+ .setIssuer(input.clientId)
57
+ .setSubject(input.clientId)
58
+ .setAudience(input.tokenEndpoint)
59
+ .setIssuedAt(now)
60
+ .setNotBefore(now)
61
+ .setExpirationTime(now + (input.expiresInSec ?? 60))
62
+ .setJti(`fake-${now}-${Math.random().toString(36).slice(2, 10)}`)
63
+ .sign(key.privateKey);
64
+ },
65
+ };
66
+ }
67
+ export async function createFixtures(input) {
68
+ const issuer = await createFakeIssuer();
69
+ const clientId = input.clientId ?? "test-client";
70
+ const validToken = await issuer.signAccessToken({
71
+ audience: input.audience,
72
+ clientId,
73
+ });
74
+ const expiredToken = await issuer.signAccessToken({
75
+ audience: input.audience,
76
+ clientId,
77
+ issuedAtSec: Math.floor(Date.now() / 1000) - 7200,
78
+ expiresInSec: -3600, // exp = iat - 3600 = 2h ago
79
+ });
80
+ const wrongAudienceToken = await issuer.signAccessToken({
81
+ audience: "some-other-service",
82
+ clientId,
83
+ });
84
+ const wrongIssuerToken = await issuer.signAccessToken({
85
+ audience: input.audience,
86
+ clientId,
87
+ issuerOverride: "https://evil.example",
88
+ });
89
+ // Build a definitively-broken signature by replacing the entire
90
+ // signature segment with a same-length string of base64url 'A's.
91
+ // For ES256 this gives the wrong (r, s) pair with probability ~1
92
+ // and jose rejects with JWSSignatureVerificationFailed.
93
+ const unsignedToken = (() => {
94
+ const parts = validToken.split(".");
95
+ const broken = "A".repeat(parts[2].length);
96
+ return [parts[0], parts[1], broken].join(".");
97
+ })();
98
+ return {
99
+ issuer,
100
+ validToken,
101
+ expiredToken,
102
+ wrongAudienceToken,
103
+ wrongIssuerToken,
104
+ unsignedToken,
105
+ malformedToken: "this-is-not-a-jwt",
106
+ };
107
+ }
108
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testkit/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@opendatalabs/service-auth",
3
+ "version": "1.0.0-next.1",
4
+ "private": false,
5
+ "description": "OAuth private_key_jwt service-to-service auth helper (RFC 7521/7523/9068) for Vana.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/vana-com/unity-surfaces.git",
12
+ "directory": "packages/service-auth"
13
+ },
14
+ "homepage": "https://github.com/vana-com/unity-surfaces/tree/main/packages/service-auth#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/vana-com/unity-surfaces/issues"
17
+ },
18
+ "keywords": [
19
+ "oauth",
20
+ "oauth2",
21
+ "private_key_jwt",
22
+ "service-to-service",
23
+ "hydra",
24
+ "jwt",
25
+ "vana"
26
+ ],
27
+ "exports": {
28
+ "./client": "./src/client/index.ts",
29
+ "./resource-server": "./src/resource-server/index.ts",
30
+ "./jwks": "./src/jwks/index.ts",
31
+ "./registration": "./src/registration/index.ts",
32
+ "./testkit": "./src/testkit/index.ts"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "main": "./dist/client/index.js",
41
+ "types": "./dist/client/index.d.ts",
42
+ "exports": {
43
+ "./client": {
44
+ "types": "./dist/client/index.d.ts",
45
+ "default": "./dist/client/index.js"
46
+ },
47
+ "./resource-server": {
48
+ "types": "./dist/resource-server/index.d.ts",
49
+ "default": "./dist/resource-server/index.js"
50
+ },
51
+ "./jwks": {
52
+ "types": "./dist/jwks/index.d.ts",
53
+ "default": "./dist/jwks/index.js"
54
+ },
55
+ "./registration": {
56
+ "types": "./dist/registration/index.d.ts",
57
+ "default": "./dist/registration/index.js"
58
+ },
59
+ "./testkit": {
60
+ "types": "./dist/testkit/index.d.ts",
61
+ "default": "./dist/testkit/index.js"
62
+ }
63
+ }
64
+ },
65
+ "scripts": {
66
+ "typecheck": "tsc --noEmit",
67
+ "test": "vitest run",
68
+ "build": "tsc -p tsconfig.build.json",
69
+ "prepack": "pnpm build",
70
+ "provision": "tsx ./bin/provision.ts"
71
+ },
72
+ "dependencies": {
73
+ "jose": "^6.2.3",
74
+ "yaml": "^2.6.1",
75
+ "zod": "^3.25.76"
76
+ },
77
+ "devDependencies": {
78
+ "@types/node": "^20",
79
+ "tsx": "^4.21.0",
80
+ "typescript": "^5.9.3",
81
+ "vitest": "^4.1.5"
82
+ }
83
+ }