@opendatalabs/service-auth
v1.26.0
Published
OAuth private_key_jwt service-to-service auth helper (RFC 7521/7523/9068) for Vana.
Readme
@opendatalabs/service-auth
OAuth 2.0 service-to-service authentication helper built around the
private_key_jwt client authentication method. A caller signs a JWT
assertion (RFC 7521 / 7523), exchanges it at the OAuth token endpoint
(Hydra in Vana's deployment) for an RFC 9068 JWT access token, and
forwards that token as Authorization: Bearer <jwt> to the resource
server. The resource server verifies the JWT locally against the
issuer's JWKS, with no back-channel introspect on the hot path.
The package wraps jose and ships three
subpaths consumers care about (./client, ./jwks, ./resource-server)
plus a ./testkit for verifying integrations.
Install
npm install @opendatalabs/service-auth joseCaller (./client)
import { createServiceAuthClient } from "@opendatalabs/service-auth/client";
const auth = createServiceAuthClient({
clientId: process.env.SERVICE_CLIENT_ID!,
privateKeyJwk: JSON.parse(process.env.SERVICE_PRIVATE_JWK!),
tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT!,
});
const { token } = await auth.getServiceToken({
audience: "vana-account-introspect",
});
await fetch("https://account.vana.org/api/oauth/introspect", {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify({ token: someUserToken }),
});Tokens are cached in-process by (audience, scope) until 30s before
expiry. No on-disk cache, no shared cache, no refresh-token flow. A cold
start re-fetches.
Publishing JWKS (./jwks)
// app/.well-known/jwks.json/route.ts
import { createJwksHandler } from "@opendatalabs/service-auth/jwks";
const handler = createJwksHandler({
keys: [
JSON.parse(process.env.SERVICE_PUBLIC_JWK_CURRENT!),
// During rotation, include the previous public key too:
JSON.parse(process.env.SERVICE_PUBLIC_JWK_PREVIOUS ?? "null"),
].filter(Boolean),
});
export function GET(request: Request) {
return handler(request);
}The helper strips all private fields. If you accidentally pass a private JWK, the response still only exposes the public half.
Resource server (./resource-server)
import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
const validator = createServiceAuthValidator({
jwksUri: `${process.env.OAUTH_PUBLIC_URL}/.well-known/jwks.json`,
issuer: process.env.OAUTH_ISSUER!,
audience: "vana-account-introspect",
});
export async function POST(request: Request) {
const auth = await validator.requireJwt()(request);
if (!auth.ok) return auth.response;
// auth.result.clientId is the verified caller.
// ...do your work.
}Rejection shapes:
- 401 with
WWW-Authenticate: Bearer error="invalid_token"for expired / wrong-iss / wrong-aud / bad-signature / not-a-bearer. - 403 with
WWW-Authenticate: Bearer error="insufficient_scope"whenrequiredScopesis not satisfied.
Testing (./testkit)
import { createFixtures } from "@opendatalabs/service-auth/testkit";
import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
const f = await createFixtures({ audience: "my-resource" });
const validator = createServiceAuthValidator({
issuer: f.issuer.issuer,
audience: "my-resource",
jwks: f.issuer.jwks,
});About this package
This package implements RFC 7521
(assertion framework), RFC 7523
(JWT bearer assertions for private_key_jwt), and
RFC 9068 (JWT access tokens).
It is the public companion to Vana's internal service-auth setup. The
./registration subpath (manifest schema + Hydra admin reconciliation)
is Vana-operator-only and not part of the public surface; it is exported
so the Vana account app can consume it as a workspace package.
Versioning
Released via semantic-release
from the vana-com/unity-surfaces repository. Pushes to main publish
the stable channel (dist-tag latest). Pushes to dev publish the
prerelease channel (dist-tag next, e.g. 1.0.0-next.1). Use
conventional commits for commit
messages.
npm install @opendatalabs/service-auth # latest
npm install @opendatalabs/service-auth@next # prereleaseLicense
MIT.
