@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.
- package/README.md +140 -0
- package/dist/client/index.d.ts +141 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +192 -0
- package/dist/client/index.js.map +1 -0
- package/dist/jwks/index.d.ts +58 -0
- package/dist/jwks/index.d.ts.map +1 -0
- package/dist/jwks/index.js +84 -0
- package/dist/jwks/index.js.map +1 -0
- package/dist/registration/index.d.ts +7 -0
- package/dist/registration/index.d.ts.map +1 -0
- package/dist/registration/index.js +7 -0
- package/dist/registration/index.js.map +1 -0
- package/dist/registration/manifest.d.ts +70 -0
- package/dist/registration/manifest.d.ts.map +1 -0
- package/dist/registration/manifest.js +105 -0
- package/dist/registration/manifest.js.map +1 -0
- package/dist/registration/provision.d.ts +68 -0
- package/dist/registration/provision.d.ts.map +1 -0
- package/dist/registration/provision.js +132 -0
- package/dist/registration/provision.js.map +1 -0
- package/dist/registration/schema.d.ts +65 -0
- package/dist/registration/schema.d.ts.map +1 -0
- package/dist/registration/schema.js +59 -0
- package/dist/registration/schema.js.map +1 -0
- package/dist/resource-server/index.d.ts +73 -0
- package/dist/resource-server/index.d.ts.map +1 -0
- package/dist/resource-server/index.js +199 -0
- package/dist/resource-server/index.js.map +1 -0
- package/dist/testkit/index.d.ts +91 -0
- package/dist/testkit/index.d.ts.map +1 -0
- package/dist/testkit/index.js +108 -0
- package/dist/testkit/index.js.map +1 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# `@opendatalabs/service-auth`
|
|
2
|
+
|
|
3
|
+
OAuth 2.0 service-to-service authentication helper built around the
|
|
4
|
+
`private_key_jwt` client authentication method. A caller signs a JWT
|
|
5
|
+
assertion (RFC 7521 / 7523), exchanges it at the OAuth token endpoint
|
|
6
|
+
(Hydra in Vana's deployment) for an RFC 9068 JWT access token, and
|
|
7
|
+
forwards that token as `Authorization: Bearer <jwt>` to the resource
|
|
8
|
+
server. The resource server verifies the JWT locally against the
|
|
9
|
+
issuer's JWKS, with no back-channel introspect on the hot path.
|
|
10
|
+
|
|
11
|
+
The package wraps [`jose`](https://github.com/panva/jose) and ships three
|
|
12
|
+
subpaths consumers care about (`./client`, `./jwks`, `./resource-server`)
|
|
13
|
+
plus a `./testkit` for verifying integrations.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @opendatalabs/service-auth jose
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Caller (`./client`)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createServiceAuthClient } from "@opendatalabs/service-auth/client";
|
|
25
|
+
|
|
26
|
+
const auth = createServiceAuthClient({
|
|
27
|
+
clientId: process.env.SERVICE_CLIENT_ID!,
|
|
28
|
+
privateKeyJwk: JSON.parse(process.env.SERVICE_PRIVATE_JWK!),
|
|
29
|
+
tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT!,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { token } = await auth.getServiceToken({
|
|
33
|
+
audience: "vana-account-introspect",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await fetch("https://account.vana.org/api/oauth/introspect", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${token}`,
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ token: someUserToken }),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Tokens are cached in-process by `(audience, scope)` until 30s before
|
|
47
|
+
expiry. No on-disk cache, no shared cache, no refresh-token flow. A cold
|
|
48
|
+
start re-fetches.
|
|
49
|
+
|
|
50
|
+
## Publishing JWKS (`./jwks`)
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// app/.well-known/jwks.json/route.ts
|
|
54
|
+
import { createJwksHandler } from "@opendatalabs/service-auth/jwks";
|
|
55
|
+
|
|
56
|
+
const handler = createJwksHandler({
|
|
57
|
+
keys: [
|
|
58
|
+
JSON.parse(process.env.SERVICE_PUBLIC_JWK_CURRENT!),
|
|
59
|
+
// During rotation, include the previous public key too:
|
|
60
|
+
JSON.parse(process.env.SERVICE_PUBLIC_JWK_PREVIOUS ?? "null"),
|
|
61
|
+
].filter(Boolean),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export function GET(request: Request) {
|
|
65
|
+
return handler(request);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The helper strips all private fields. If you accidentally pass a private
|
|
70
|
+
JWK, the response still only exposes the public half.
|
|
71
|
+
|
|
72
|
+
## Resource server (`./resource-server`)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
|
|
76
|
+
|
|
77
|
+
const validator = createServiceAuthValidator({
|
|
78
|
+
jwksUri: `${process.env.OAUTH_PUBLIC_URL}/.well-known/jwks.json`,
|
|
79
|
+
issuer: process.env.OAUTH_ISSUER!,
|
|
80
|
+
audience: "vana-account-introspect",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export async function POST(request: Request) {
|
|
84
|
+
const auth = await validator.requireJwt()(request);
|
|
85
|
+
if (!auth.ok) return auth.response;
|
|
86
|
+
// auth.result.clientId is the verified caller.
|
|
87
|
+
// ...do your work.
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rejection shapes:
|
|
92
|
+
|
|
93
|
+
- **401** with `WWW-Authenticate: Bearer error="invalid_token"` for
|
|
94
|
+
expired / wrong-iss / wrong-aud / bad-signature / not-a-bearer.
|
|
95
|
+
- **403** with `WWW-Authenticate: Bearer error="insufficient_scope"` when
|
|
96
|
+
`requiredScopes` is not satisfied.
|
|
97
|
+
|
|
98
|
+
## Testing (`./testkit`)
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createFixtures } from "@opendatalabs/service-auth/testkit";
|
|
102
|
+
import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
|
|
103
|
+
|
|
104
|
+
const f = await createFixtures({ audience: "my-resource" });
|
|
105
|
+
const validator = createServiceAuthValidator({
|
|
106
|
+
issuer: f.issuer.issuer,
|
|
107
|
+
audience: "my-resource",
|
|
108
|
+
jwks: f.issuer.jwks,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## About this package
|
|
113
|
+
|
|
114
|
+
This package implements [RFC 7521](https://www.rfc-editor.org/rfc/rfc7521)
|
|
115
|
+
(assertion framework), [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)
|
|
116
|
+
(JWT bearer assertions for `private_key_jwt`), and
|
|
117
|
+
[RFC 9068](https://www.rfc-editor.org/rfc/rfc9068) (JWT access tokens).
|
|
118
|
+
|
|
119
|
+
It is the public companion to Vana's internal service-auth setup. The
|
|
120
|
+
`./registration` subpath (manifest schema + Hydra admin reconciliation)
|
|
121
|
+
is Vana-operator-only and not part of the public surface; it is exported
|
|
122
|
+
so the Vana account app can consume it as a workspace package.
|
|
123
|
+
|
|
124
|
+
## Versioning
|
|
125
|
+
|
|
126
|
+
Released via [semantic-release](https://github.com/semantic-release/semantic-release)
|
|
127
|
+
from the `vana-com/unity-surfaces` repository. Pushes to `main` publish
|
|
128
|
+
the stable channel (dist-tag `latest`). Pushes to `dev` publish the
|
|
129
|
+
prerelease channel (dist-tag `next`, e.g. `1.0.0-next.1`). Use
|
|
130
|
+
[conventional commits](https://www.conventionalcommits.org/) for commit
|
|
131
|
+
messages.
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npm install @opendatalabs/service-auth # latest
|
|
135
|
+
npm install @opendatalabs/service-auth@next # prerelease
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-to-service caller. INTERNAL Vana package, not a public SDK.
|
|
3
|
+
*
|
|
4
|
+
* What this does
|
|
5
|
+
* --------------
|
|
6
|
+
*
|
|
7
|
+
* Given a Vana service that wants to call another Vana service:
|
|
8
|
+
*
|
|
9
|
+
* 1. Signs a `client_assertion` JWT per RFC 7523 with the caller's
|
|
10
|
+
* private key.
|
|
11
|
+
* 2. POSTs `grant_type=client_credentials` + the assertion to Hydra's
|
|
12
|
+
* token endpoint to obtain a JWT-shaped access token (RFC 9068).
|
|
13
|
+
* 3. Caches the access token by (audience, scope) until 30s before
|
|
14
|
+
* `exp` and returns it to the caller for forwarding as
|
|
15
|
+
* `Authorization: Bearer <jwt>` on the inter-service hop.
|
|
16
|
+
*
|
|
17
|
+
* What this does NOT do
|
|
18
|
+
* ---------------------
|
|
19
|
+
*
|
|
20
|
+
* - It does NOT manage your private key. Hand `privateKeyJwk` in from
|
|
21
|
+
* workload secrets (Doppler, Vercel env, GCP Secret Manager). The
|
|
22
|
+
* key never touches `process.env` of this package.
|
|
23
|
+
* - It does NOT host your JWKS. Use the `jwks` subpath for that.
|
|
24
|
+
* - It does NOT speak Hydra's admin API. Use the `registration`
|
|
25
|
+
* subpath for provisioning.
|
|
26
|
+
*
|
|
27
|
+
* RFC references
|
|
28
|
+
* --------------
|
|
29
|
+
*
|
|
30
|
+
* - RFC 7521: Assertion Framework for OAuth 2.0.
|
|
31
|
+
* - RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and
|
|
32
|
+
* Authorization Grants (the `private_key_jwt` pattern).
|
|
33
|
+
* - RFC 9068: JWT Profile for OAuth 2.0 Access Tokens (the shape of
|
|
34
|
+
* the token Hydra returns to us).
|
|
35
|
+
*/
|
|
36
|
+
import { type JWK } from "jose";
|
|
37
|
+
/**
|
|
38
|
+
* Construction options. All fields are required and have no defaults
|
|
39
|
+
* because every value is environment- and service-specific.
|
|
40
|
+
*/
|
|
41
|
+
export interface ServiceAuthClientOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Hydra OAuth client id provisioned for this service. Matches the
|
|
44
|
+
* `hydra_client_id` in the service-auth.yaml manifest.
|
|
45
|
+
*/
|
|
46
|
+
clientId: string;
|
|
47
|
+
/**
|
|
48
|
+
* Private JWK whose public counterpart is published at the service's
|
|
49
|
+
* JWKS URI and registered with Hydra under `jwks_uri`. Must have
|
|
50
|
+
* `kid` and `alg` set.
|
|
51
|
+
*
|
|
52
|
+
* The JWK is never serialized or logged by this package.
|
|
53
|
+
*/
|
|
54
|
+
privateKeyJwk: JWK;
|
|
55
|
+
/**
|
|
56
|
+
* Hydra token endpoint URL, e.g.
|
|
57
|
+
* `https://oauth-dev.vana.org/oauth2/token`. Used both as the POST
|
|
58
|
+
* target and as the `aud` claim on the client assertion.
|
|
59
|
+
*/
|
|
60
|
+
tokenEndpoint: string;
|
|
61
|
+
/**
|
|
62
|
+
* Hydra issuer URL, used as the `iss` claim on the client assertion.
|
|
63
|
+
* Distinct from `tokenEndpoint` so deployments with a separate
|
|
64
|
+
* issuer hostname (e.g. proxied via a public domain) still work.
|
|
65
|
+
*
|
|
66
|
+
* RFC 7523 §3 only requires `iss` to be a value the auth server can
|
|
67
|
+
* use to identify the client; for Hydra `private_key_jwt`, the
|
|
68
|
+
* caller's `client_id` is the canonical value, so we default the
|
|
69
|
+
* assertion `iss` to `clientId`. This field is kept for forensics
|
|
70
|
+
* and future flexibility.
|
|
71
|
+
*/
|
|
72
|
+
issuer?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional override of the wall clock (seconds since epoch). Tests
|
|
75
|
+
* pass a deterministic value; production omits.
|
|
76
|
+
*/
|
|
77
|
+
now?: () => number;
|
|
78
|
+
/** Optional fetch override (tests). */
|
|
79
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
80
|
+
/**
|
|
81
|
+
* Cache eviction watermark in seconds. Tokens are refreshed this
|
|
82
|
+
* many seconds BEFORE their `exp`. Default 30s.
|
|
83
|
+
*/
|
|
84
|
+
refreshSkewSec?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Assertion `exp` lifetime in seconds. Hydra rejects assertions
|
|
87
|
+
* with exp too far in the future; 60s is the well-trodden value.
|
|
88
|
+
*/
|
|
89
|
+
assertionLifetimeSec?: number;
|
|
90
|
+
}
|
|
91
|
+
export interface GetServiceTokenInput {
|
|
92
|
+
/**
|
|
93
|
+
* The resource audience (e.g. `"vana-account-introspect"`). MUST be
|
|
94
|
+
* present in the manifest's `allowed_audiences`. Sent to Hydra as
|
|
95
|
+
* the `audience` form parameter; Hydra mints an access token whose
|
|
96
|
+
* `aud` includes this value.
|
|
97
|
+
*/
|
|
98
|
+
audience: string;
|
|
99
|
+
/**
|
|
100
|
+
* Optional space-separated scopes. Empty/undefined means "no
|
|
101
|
+
* scopes" (service tokens are usually audience-scoped, not
|
|
102
|
+
* permission-scoped).
|
|
103
|
+
*/
|
|
104
|
+
scope?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface ServiceToken {
|
|
107
|
+
/** The raw JWT-shaped access token. */
|
|
108
|
+
token: string;
|
|
109
|
+
/** Absolute unix-seconds expiry from the Hydra response. */
|
|
110
|
+
expiresAt: number;
|
|
111
|
+
}
|
|
112
|
+
export interface SignRequestAssertionInput {
|
|
113
|
+
/**
|
|
114
|
+
* Optional override of the assertion `aud`. Defaults to the token
|
|
115
|
+
* endpoint, which is what RFC 7523 §3 prescribes for the
|
|
116
|
+
* `private_key_jwt` use case.
|
|
117
|
+
*/
|
|
118
|
+
audience?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface ServiceAuthClient {
|
|
121
|
+
/**
|
|
122
|
+
* Fetch (or return cached) a service-to-service access token for
|
|
123
|
+
* the given audience+scope. Refreshes when within `refreshSkewSec`
|
|
124
|
+
* of expiry. Throws on token-endpoint errors.
|
|
125
|
+
*/
|
|
126
|
+
getServiceToken(input: GetServiceTokenInput): Promise<ServiceToken>;
|
|
127
|
+
/**
|
|
128
|
+
* Mint a single RFC 7523 client assertion. Advanced use only;
|
|
129
|
+
* `getServiceToken` already does this internally.
|
|
130
|
+
*/
|
|
131
|
+
signRequestAssertion(input?: SignRequestAssertionInput): Promise<string>;
|
|
132
|
+
/** Test-only: drop the in-memory token cache. */
|
|
133
|
+
clearCache(): void;
|
|
134
|
+
}
|
|
135
|
+
export declare class ServiceAuthTokenError extends Error {
|
|
136
|
+
readonly status: number;
|
|
137
|
+
readonly body: unknown;
|
|
138
|
+
constructor(status: number, body: unknown);
|
|
139
|
+
}
|
|
140
|
+
export declare function createServiceAuthClient(options: ServiceAuthClientOptions): ServiceAuthClient;
|
|
141
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAEL,KAAK,GAAG,EAGT,MAAM,MAAM,CAAC;AAKd;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,GAAG,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,uCAAuC;IACvC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpE;;;OAGG;IACH,oBAAoB,CAClB,KAAK,CAAC,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,iDAAiD;IACjD,UAAU,IAAI,IAAI,CAAC;CACpB;AAqDD,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBACX,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAgB1C;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,iBAAiB,CAuHnB"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-to-service caller. INTERNAL Vana package, not a public SDK.
|
|
3
|
+
*
|
|
4
|
+
* What this does
|
|
5
|
+
* --------------
|
|
6
|
+
*
|
|
7
|
+
* Given a Vana service that wants to call another Vana service:
|
|
8
|
+
*
|
|
9
|
+
* 1. Signs a `client_assertion` JWT per RFC 7523 with the caller's
|
|
10
|
+
* private key.
|
|
11
|
+
* 2. POSTs `grant_type=client_credentials` + the assertion to Hydra's
|
|
12
|
+
* token endpoint to obtain a JWT-shaped access token (RFC 9068).
|
|
13
|
+
* 3. Caches the access token by (audience, scope) until 30s before
|
|
14
|
+
* `exp` and returns it to the caller for forwarding as
|
|
15
|
+
* `Authorization: Bearer <jwt>` on the inter-service hop.
|
|
16
|
+
*
|
|
17
|
+
* What this does NOT do
|
|
18
|
+
* ---------------------
|
|
19
|
+
*
|
|
20
|
+
* - It does NOT manage your private key. Hand `privateKeyJwk` in from
|
|
21
|
+
* workload secrets (Doppler, Vercel env, GCP Secret Manager). The
|
|
22
|
+
* key never touches `process.env` of this package.
|
|
23
|
+
* - It does NOT host your JWKS. Use the `jwks` subpath for that.
|
|
24
|
+
* - It does NOT speak Hydra's admin API. Use the `registration`
|
|
25
|
+
* subpath for provisioning.
|
|
26
|
+
*
|
|
27
|
+
* RFC references
|
|
28
|
+
* --------------
|
|
29
|
+
*
|
|
30
|
+
* - RFC 7521: Assertion Framework for OAuth 2.0.
|
|
31
|
+
* - RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and
|
|
32
|
+
* Authorization Grants (the `private_key_jwt` pattern).
|
|
33
|
+
* - RFC 9068: JWT Profile for OAuth 2.0 Access Tokens (the shape of
|
|
34
|
+
* the token Hydra returns to us).
|
|
35
|
+
*/
|
|
36
|
+
import { importJWK, SignJWT, } from "jose";
|
|
37
|
+
const ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
38
|
+
function defaultNow() {
|
|
39
|
+
return Math.floor(Date.now() / 1000);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a stable cache key. Scope tokens are sorted so callers that
|
|
43
|
+
* pass `"a b"` and `"b a"` share a cache slot.
|
|
44
|
+
*/
|
|
45
|
+
function cacheKey(audience, scope) {
|
|
46
|
+
const sorted = (scope ?? "")
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.sort()
|
|
50
|
+
.join(" ");
|
|
51
|
+
return `${audience}::${sorted}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generate a 128-bit random `jti`. Hydra requires uniqueness across
|
|
55
|
+
* the validity window of the assertion.
|
|
56
|
+
*/
|
|
57
|
+
function randomJti() {
|
|
58
|
+
const bytes = new Uint8Array(16);
|
|
59
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
60
|
+
let hex = "";
|
|
61
|
+
for (const b of bytes) {
|
|
62
|
+
hex += b.toString(16).padStart(2, "0");
|
|
63
|
+
}
|
|
64
|
+
return hex;
|
|
65
|
+
}
|
|
66
|
+
export class ServiceAuthTokenError extends Error {
|
|
67
|
+
status;
|
|
68
|
+
body;
|
|
69
|
+
constructor(status, body) {
|
|
70
|
+
let message = `service-auth: token endpoint returned ${status}`;
|
|
71
|
+
if (body &&
|
|
72
|
+
typeof body === "object" &&
|
|
73
|
+
"error" in body) {
|
|
74
|
+
const err = body.error;
|
|
75
|
+
const desc = body.error_description;
|
|
76
|
+
message += ` (${err}${desc ? `: ${desc}` : ""})`;
|
|
77
|
+
}
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "ServiceAuthTokenError";
|
|
80
|
+
this.status = status;
|
|
81
|
+
this.body = body;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function createServiceAuthClient(options) {
|
|
85
|
+
assertNonEmpty(options.clientId, "clientId");
|
|
86
|
+
assertNonEmpty(options.tokenEndpoint, "tokenEndpoint");
|
|
87
|
+
if (!options.privateKeyJwk?.kid) {
|
|
88
|
+
throw new Error("service-auth: privateKeyJwk.kid is required (must match the public key registered in JWKS).");
|
|
89
|
+
}
|
|
90
|
+
if (!options.privateKeyJwk?.alg) {
|
|
91
|
+
throw new Error("service-auth: privateKeyJwk.alg is required (e.g. 'ES256', 'RS256').");
|
|
92
|
+
}
|
|
93
|
+
const now = options.now ?? defaultNow;
|
|
94
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
95
|
+
const refreshSkewSec = options.refreshSkewSec ?? 30;
|
|
96
|
+
const assertionLifetimeSec = options.assertionLifetimeSec ?? 60;
|
|
97
|
+
const issuer = options.issuer ?? options.clientId;
|
|
98
|
+
const cache = new Map();
|
|
99
|
+
// jose.importJWK returns either a CryptoKey (web) or KeyObject
|
|
100
|
+
// (Node). Both satisfy the SignJWT.sign() signature.
|
|
101
|
+
let privateKeyPromise = null;
|
|
102
|
+
function getPrivateKey() {
|
|
103
|
+
if (!privateKeyPromise) {
|
|
104
|
+
privateKeyPromise = importJWK(options.privateKeyJwk, options.privateKeyJwk.alg);
|
|
105
|
+
}
|
|
106
|
+
return privateKeyPromise;
|
|
107
|
+
}
|
|
108
|
+
async function signRequestAssertion(input) {
|
|
109
|
+
const privateKey = await getPrivateKey();
|
|
110
|
+
const issuedAt = now();
|
|
111
|
+
return new SignJWT({})
|
|
112
|
+
.setProtectedHeader({
|
|
113
|
+
alg: options.privateKeyJwk.alg,
|
|
114
|
+
kid: options.privateKeyJwk.kid,
|
|
115
|
+
typ: "JWT",
|
|
116
|
+
})
|
|
117
|
+
.setIssuer(issuer)
|
|
118
|
+
.setSubject(options.clientId)
|
|
119
|
+
.setAudience(input?.audience ?? options.tokenEndpoint)
|
|
120
|
+
.setIssuedAt(issuedAt)
|
|
121
|
+
.setNotBefore(issuedAt)
|
|
122
|
+
.setExpirationTime(issuedAt + assertionLifetimeSec)
|
|
123
|
+
.setJti(randomJti())
|
|
124
|
+
// Use `any`-shaped key argument for jose 6 cross-runtime types.
|
|
125
|
+
.sign(privateKey);
|
|
126
|
+
}
|
|
127
|
+
async function fetchToken(audience, scope) {
|
|
128
|
+
const assertion = await signRequestAssertion();
|
|
129
|
+
const form = new URLSearchParams({
|
|
130
|
+
grant_type: "client_credentials",
|
|
131
|
+
client_assertion_type: ASSERTION_TYPE,
|
|
132
|
+
client_assertion: assertion,
|
|
133
|
+
audience,
|
|
134
|
+
});
|
|
135
|
+
if (scope && scope.trim()) {
|
|
136
|
+
form.set("scope", scope.trim());
|
|
137
|
+
}
|
|
138
|
+
const response = await fetchImpl(options.tokenEndpoint, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
142
|
+
accept: "application/json",
|
|
143
|
+
},
|
|
144
|
+
body: form.toString(),
|
|
145
|
+
});
|
|
146
|
+
const text = await response.text();
|
|
147
|
+
let parsed = {};
|
|
148
|
+
if (text) {
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(text);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
parsed = { raw: text };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new ServiceAuthTokenError(response.status, parsed);
|
|
158
|
+
}
|
|
159
|
+
const ok = parsed;
|
|
160
|
+
if (typeof ok.access_token !== "string" || typeof ok.expires_in !== "number") {
|
|
161
|
+
throw new ServiceAuthTokenError(response.status, parsed);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
token: ok.access_token,
|
|
165
|
+
expiresAt: now() + ok.expires_in,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
async getServiceToken(input) {
|
|
170
|
+
assertNonEmpty(input.audience, "audience");
|
|
171
|
+
const key = cacheKey(input.audience, input.scope);
|
|
172
|
+
const hit = cache.get(key);
|
|
173
|
+
const t = now();
|
|
174
|
+
if (hit && hit.expiresAt - refreshSkewSec > t) {
|
|
175
|
+
return { token: hit.token, expiresAt: hit.expiresAt };
|
|
176
|
+
}
|
|
177
|
+
const fresh = await fetchToken(input.audience, input.scope);
|
|
178
|
+
cache.set(key, fresh);
|
|
179
|
+
return fresh;
|
|
180
|
+
},
|
|
181
|
+
signRequestAssertion,
|
|
182
|
+
clearCache() {
|
|
183
|
+
cache.clear();
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function assertNonEmpty(value, name) {
|
|
188
|
+
if (!value || typeof value !== "string") {
|
|
189
|
+
throw new Error(`service-auth: ${name} is required`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWKS publishing helper. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* A service that authenticates to Hydra with `private_key_jwt` must
|
|
5
|
+
* publish the *public* counterpart of its signing key at a stable
|
|
6
|
+
* URI that Hydra fetches. This helper produces:
|
|
7
|
+
*
|
|
8
|
+
* - `buildJwksDocument({ keys })` -- returns the JSON shape suitable
|
|
9
|
+
* for `/.well-known/jwks.json`.
|
|
10
|
+
* - `createJwksHandler({ keys })` -- returns a function
|
|
11
|
+
* `(request: Request) => Response` that a Next.js / Hono / plain
|
|
12
|
+
* Node handler can call.
|
|
13
|
+
*
|
|
14
|
+
* Key rotation
|
|
15
|
+
* ------------
|
|
16
|
+
*
|
|
17
|
+
* `keys` is an array; publish the current and previous keys
|
|
18
|
+
* simultaneously during rotation so callers that cached an older
|
|
19
|
+
* `kid` still verify until they refresh. Hydra's
|
|
20
|
+
* `createRemoteJWKSet` and ours both pick the right key by `kid`.
|
|
21
|
+
*
|
|
22
|
+
* Safety
|
|
23
|
+
* ------
|
|
24
|
+
*
|
|
25
|
+
* Each entry is stripped of any private-key material via an
|
|
26
|
+
* allowlist of public-only members. This package will NEVER serialize
|
|
27
|
+
* `d`, `p`, `q`, `dp`, `dq`, or `qi`.
|
|
28
|
+
*/
|
|
29
|
+
import type { JWK } from "jose";
|
|
30
|
+
export interface PublicJwk {
|
|
31
|
+
kty: string;
|
|
32
|
+
kid: string;
|
|
33
|
+
alg: string;
|
|
34
|
+
use?: string;
|
|
35
|
+
n?: string;
|
|
36
|
+
e?: string;
|
|
37
|
+
crv?: string;
|
|
38
|
+
x?: string;
|
|
39
|
+
y?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface JwksDocument {
|
|
42
|
+
keys: PublicJwk[];
|
|
43
|
+
}
|
|
44
|
+
export interface CreateJwksOptions {
|
|
45
|
+
/**
|
|
46
|
+
* One or more JWKs. Each must have a stable `kid` and `alg`.
|
|
47
|
+
* Private-key fields are stripped before publication.
|
|
48
|
+
*/
|
|
49
|
+
keys: JWK[];
|
|
50
|
+
}
|
|
51
|
+
export declare function buildJwksDocument(options: CreateJwksOptions): JwksDocument;
|
|
52
|
+
export type JwksHandler = (request: Request) => Response;
|
|
53
|
+
/**
|
|
54
|
+
* Build the JWKS Response with the standard headers. Pre-computed at
|
|
55
|
+
* handler-creation time because the keyset is immutable per process.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createJwksHandler(options: CreateJwksOptions): JwksHandler;
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jwks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhC,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;CAEZ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAwBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAiB1E;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CAYzE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWKS publishing helper. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* A service that authenticates to Hydra with `private_key_jwt` must
|
|
5
|
+
* publish the *public* counterpart of its signing key at a stable
|
|
6
|
+
* URI that Hydra fetches. This helper produces:
|
|
7
|
+
*
|
|
8
|
+
* - `buildJwksDocument({ keys })` -- returns the JSON shape suitable
|
|
9
|
+
* for `/.well-known/jwks.json`.
|
|
10
|
+
* - `createJwksHandler({ keys })` -- returns a function
|
|
11
|
+
* `(request: Request) => Response` that a Next.js / Hono / plain
|
|
12
|
+
* Node handler can call.
|
|
13
|
+
*
|
|
14
|
+
* Key rotation
|
|
15
|
+
* ------------
|
|
16
|
+
*
|
|
17
|
+
* `keys` is an array; publish the current and previous keys
|
|
18
|
+
* simultaneously during rotation so callers that cached an older
|
|
19
|
+
* `kid` still verify until they refresh. Hydra's
|
|
20
|
+
* `createRemoteJWKSet` and ours both pick the right key by `kid`.
|
|
21
|
+
*
|
|
22
|
+
* Safety
|
|
23
|
+
* ------
|
|
24
|
+
*
|
|
25
|
+
* Each entry is stripped of any private-key material via an
|
|
26
|
+
* allowlist of public-only members. This package will NEVER serialize
|
|
27
|
+
* `d`, `p`, `q`, `dp`, `dq`, or `qi`.
|
|
28
|
+
*/
|
|
29
|
+
const PRIVATE_FIELDS = new Set(["d", "p", "q", "dp", "dq", "qi", "k"]);
|
|
30
|
+
function toPublicJwk(jwk) {
|
|
31
|
+
if (!jwk.kid) {
|
|
32
|
+
throw new Error("service-auth/jwks: every key must have a stable kid");
|
|
33
|
+
}
|
|
34
|
+
if (!jwk.alg) {
|
|
35
|
+
throw new Error("service-auth/jwks: every key must declare alg");
|
|
36
|
+
}
|
|
37
|
+
if (!jwk.kty) {
|
|
38
|
+
throw new Error("service-auth/jwks: every key must declare kty");
|
|
39
|
+
}
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [k, v] of Object.entries(jwk)) {
|
|
42
|
+
if (PRIVATE_FIELDS.has(k))
|
|
43
|
+
continue;
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
out[k] = v;
|
|
47
|
+
}
|
|
48
|
+
if (!out.use)
|
|
49
|
+
out.use = "sig";
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function buildJwksDocument(options) {
|
|
53
|
+
if (!Array.isArray(options.keys) || options.keys.length === 0) {
|
|
54
|
+
throw new Error("service-auth/jwks: at least one key is required");
|
|
55
|
+
}
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const k of options.keys) {
|
|
59
|
+
const pub = toPublicJwk(k);
|
|
60
|
+
if (seen.has(pub.kid)) {
|
|
61
|
+
throw new Error(`service-auth/jwks: duplicate kid '${pub.kid}' in keyset`);
|
|
62
|
+
}
|
|
63
|
+
seen.add(pub.kid);
|
|
64
|
+
out.push(pub);
|
|
65
|
+
}
|
|
66
|
+
return { keys: out };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the JWKS Response with the standard headers. Pre-computed at
|
|
70
|
+
* handler-creation time because the keyset is immutable per process.
|
|
71
|
+
*/
|
|
72
|
+
export function createJwksHandler(options) {
|
|
73
|
+
const doc = buildJwksDocument(options);
|
|
74
|
+
const body = JSON.stringify(doc);
|
|
75
|
+
return (_request) => new Response(body, {
|
|
76
|
+
status: 200,
|
|
77
|
+
headers: {
|
|
78
|
+
"content-type": "application/jwk-set+json",
|
|
79
|
+
// 5 minutes; mirrors Hydra's own JWKS cache cadence.
|
|
80
|
+
"cache-control": "public, max-age=300, must-revalidate",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/jwks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAEvE,SAAS,WAAW,CAAC,GAAQ;IAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QACpC,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACb,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG;QAAE,GAAG,CAAC,GAAG,GAAG,KAAK,CAAC;IAC9B,OAAO,GAA2B,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,qCAAqC,GAAG,CAAC,GAAG,aAAa,CAC1D,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACvB,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,CAAC,QAAiB,EAAE,EAAE,CAC3B,IAAI,QAAQ,CAAC,IAAI,EAAE;QACjB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACP,cAAc,EAAE,0BAA0B;YAC1C,qDAAqD;YACrD,eAAe,EAAE,sCAAsC;SACxD;KACF,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration: manifest schema + Hydra provisioning. INTERNAL Vana
|
|
3
|
+
* package; see ./manifest.ts and ./provision.ts.
|
|
4
|
+
*/
|
|
5
|
+
export { diffHydraClient, type HydraClientBody, loadManifest, ManifestValidationError, manifestToHydraClient, parseManifestText, serviceAuthManifestSchema, type ServiceAuthManifest, } from "./manifest";
|
|
6
|
+
export { HydraAdminApiError, provisionServiceClient, type ProvisionAction, type ProvisionFetch, type ProvisionOptions, type ProvisionResult, } from "./provision";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registration/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,YAAY,EACZ,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration: manifest schema + Hydra provisioning. INTERNAL Vana
|
|
3
|
+
* package; see ./manifest.ts and ./provision.ts.
|
|
4
|
+
*/
|
|
5
|
+
export { diffHydraClient, loadManifest, ManifestValidationError, manifestToHydraClient, parseManifestText, serviceAuthManifestSchema, } from "./manifest";
|
|
6
|
+
export { HydraAdminApiError, provisionServiceClient, } from "./provision";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/registration/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EAEf,YAAY,EACZ,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,GAE1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GAKvB,MAAM,aAAa,CAAC"}
|