npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alien-id/sso-agent-id

v2.1.1

Published

Verify Alien Agent ID tokens — Ed25519 token verification for Node.js services

Readme

@alien-id/sso-agent-id

Verify inbound RFC 9449 (OAuth 2.0 DPoP) requests from Alien-bound agents in any Node.js service. Zero runtime dependencies.

The Alien agent sends two HTTP headers to your service:

Authorization: DPoP <access_token>      ← Alien at+jwt, signed by SSO
DPoP:          <proof JWT>              ← EdDSA, signed by the agent's own key

This library walks the RFC 9449 §4.3 verification checklist, the RFC 7800 §3.1 / RFC 9449 §6.1 cnf.jkt proof-of-possession binding, and the RFC 9068 §4 access-token claim checks. On success, you can trust the sub (the human owner) and jkt (the agent's DPoP key thumbprint) — both transitively signed by the SSO and the agent respectively.

Install

npm install @alien-id/sso-agent-id

Requires Node.js 18+ (uses built-in crypto, URL).

Quick start

import { fetchAlienJWKS, verifyDPoPRequest } from '@alien-id/sso-agent-id';

// Fetch the SSO's JWKS once at startup. Cache it; refresh every few hours.
const jwks = await fetchAlienJWKS();

// In your request handler:
const result = verifyDPoPRequest(req, { jwks });

if (!result.ok) {
  return res.status(401)
    .set('WWW-Authenticate', `DPoP error="${result.code}"`)
    .json({ error: result.error });
}

// result.sub  — human owner's AlienID address (signed by SSO)
// result.jkt  — agent's Ed25519 key thumbprint (RFC 7638)
// result.accessTokenClaims, result.proofClaims — raw decoded JWT payloads

That's the full integration for a service that wants to accept any agent on the Alien network. expectedIssuer defaults to https://sso.alien-api.com and expectedAudience defaults to that same value — see Federated audience for the rationale and when to override.

The verifier needs the full request: method, URL, and headers. It uses these to compare the proof's htm and htu claims against the actual request (RFC 9449 §4.3 step 8–9).

Federated audience

The Alien SSO mints every access token with aud = [client_id, issuer]. By default the verifier checks that aud contains expectedIssuer — i.e. the token was minted by the Alien SSO at all. This is the "federated audience" pattern: every Alien-aware service treats the SSO issuer URL as its own audience identifier, so one agent identity works against the whole network with no per-service configuration.

The default is also defense-in-depth on top of the existing typ == at+jwt check: an id+jwt from the same SSO has aud = client_id only (no issuer), so even if the typ check were ever weakened the federated-audience default would reject it.

Override expectedAudience only when you specifically want to narrow acceptance:

// Scope to agents bound to your own OAuth client only:
verifyDPoPRequest(req, { jwks, expectedAudience: 'YOUR_CLIENT_ID' });

// Scope to an RFC 8707 resource indicator (when SSO/agent support lands):
verifyDPoPRequest(req, { jwks, expectedAudience: 'https://your.service/api' });

// Skip the audience check entirely (test fixtures only):
verifyDPoPRequest(req, { jwks, expectedAudience: false });

See docs/RS-INTEGRATION.md for the full resource-server integration checklist.

API

verifyDPoPRequest(req, opts)

| Parameter | Type | Description | | --- | --- | --- | | req.method | string | HTTP method, e.g. "GET". Must match the proof's htm (case-sensitive). | | req.url | string | Full request URL including scheme/host/path. Compared to the proof's htu after both sides strip query and fragment. | | req.headers | Record<string, string \| string[] \| undefined> | Must include exactly one authorization: DPoP <at> and exactly one dpop: <proof>. | | opts.jwks | JWKS | Pre-fetched JWKS from the SSO (see fetchAlienJWKS). | | opts.expectedIssuer | string | Defaults to https://sso.alien-api.com. Override for staging/self-hosted SSO. | | opts.expectedAudience | string \| false | Defaults to expectedIssuer (federated audience). Pass a string to scope to a specific OAuth client_id or RFC 8707 resource. Pass false to skip (test fixtures only). | | opts.proofMaxAgeSec | number | DPoP proof freshness window, in seconds. Default 30. | | opts.clockSkewSec | number | Clock skew applied to access-token exp, in seconds. Default 30. | | opts.jtiStore | DPoPJtiStore | Replay-protection store for the proof's jti claim. Default: in-memory Map. Inject a shared store (e.g. Redis-backed) for multi-instance deployments. |

Returns VerifyDPoPSuccess:

{
  ok: true,
  sub: string,                          // owner sub (from at+jwt)
  jkt: string,                          // RFC 7638 thumbprint of the agent's DPoP key
  accessTokenClaims: Record<string, unknown>,
  proofClaims: Record<string, unknown>,
}

Returns VerifyDPoPFailure:

{
  ok: false,
  code: string,    // machine-readable, e.g. "jkt_mismatch"
  error: string,   // human-readable
}

fetchAlienJWKS(ssoBaseUrl?)

Fetch the JWKS from the Alien SSO server. Callers should cache the result.

| Parameter | Type | Description | | --- | --- | --- | | ssoBaseUrl | string | Default: https://sso.alien-api.com. |

Returns Promise<JWKS>.

DPoPJtiStore

Pluggable interface for proof-replay protection (RFC 9449 §11.1):

interface DPoPJtiStore {
  has(jti: string): boolean;
  add(jti: string, iat: number): void;
}

The default in-memory store is single-process and capped at 10,000 entries. For multi-instance deployments, back it with Redis/Memcached so a captured proof can't be replayed against a different worker.

Framework examples

Express

import express from 'express';
import { fetchAlienJWKS, verifyDPoPRequest } from '@alien-id/sso-agent-id';

const app = express();
const jwks = await fetchAlienJWKS();

function requireAgent(req, res, next) {
  // Express's req.url is the path; reconstruct the absolute URL the agent saw.
  const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
  const result = verifyDPoPRequest(
    { method: req.method, url: fullUrl, headers: req.headers },
    { jwks },
  );
  if (!result.ok) {
    res.set('WWW-Authenticate', `DPoP error="${result.code}"`);
    return res.status(401).json({ error: result.error });
  }
  req.agent = { sub: result.sub, jkt: result.jkt };
  next();
}

app.get('/api/data', requireAgent, (req, res) => {
  res.json({ data: 'secret', owner: req.agent.sub });
});

If you sit behind a reverse proxy (ALB, Cloudflare, nginx), trust X-Forwarded-Proto and X-Forwarded-Host to reconstruct the URL the agent actually addressed — otherwise htu comparison will fail.

Fastify

import Fastify from 'fastify';
import { fetchAlienJWKS, verifyDPoPRequest } from '@alien-id/sso-agent-id';

const app = Fastify();
const jwks = await fetchAlienJWKS();

app.addHook('preHandler', async (request, reply) => {
  const fullUrl = `${request.protocol}://${request.hostname}${request.url}`;
  const result = verifyDPoPRequest(
    { method: request.method, url: fullUrl, headers: request.headers },
    { jwks },
  );
  if (!result.ok) {
    reply.header('WWW-Authenticate', `DPoP error="${result.code}"`);
    return reply.code(401).send({ error: result.error });
  }
  request.agent = { sub: result.sub, jkt: result.jkt };
});

Next.js (App Router)

import { NextRequest, NextResponse } from 'next/server';
import { fetchAlienJWKS, verifyDPoPRequest } from '@alien-id/sso-agent-id';

const jwks = await fetchAlienJWKS();

export async function GET(req: NextRequest) {
  // NextRequest's headers are a Headers instance — flatten for the verifier.
  const headers: Record<string, string | undefined> = {};
  req.headers.forEach((v, k) => { headers[k] = v; });

  const result = verifyDPoPRequest(
    { method: req.method, url: req.url, headers },
    { jwks },
  );
  if (!result.ok) {
    return NextResponse.json({ error: result.error }, {
      status: 401,
      headers: { 'WWW-Authenticate': `DPoP error="${result.code}"` },
    });
  }
  return NextResponse.json({ owner: result.sub, agent_jkt: result.jkt });
}

Access control patterns

Any owner-bound agent

if (!result.ok) return res.status(401).json({ error: result.error });
// result.sub is guaranteed by the SSO's signature.

Allow-list by agent key

const ALLOWED_JKTS = new Set(['wEf6o2ux8sBAUG4oQYhP284gfpZwUJMTxXDPH5XxthY', ...]);
if (!ALLOWED_JKTS.has(result.jkt)) {
  return res.status(403).json({ error: 'Agent not authorized' });
}

Allow-list by owner

const ALLOWED_OWNERS = new Set(['00000003...', '00000003...']);
if (!ALLOWED_OWNERS.has(result.sub)) {
  return res.status(403).json({ error: 'Owner not authorized' });
}

How it works

sequenceDiagram
  participant Agent
  participant Service
  participant SSO as Alien SSO

  Note over Agent,SSO: 1) Agent obtains an at+jwt access token (DPoP-bound to its key)
  Agent->>SSO: /oauth/token (with DPoP)
  SSO-->>Agent: access_token{cnf.jkt = thumbprint(agent.jwk)}

  Note over Agent,Service: 2) Agent calls your service with a per-request proof
  Agent->>Service: Authorization: DPoP <at> + DPoP: <proof JWT>
  Service->>Service: verify proof sig, htm, htu, iat, jti, ath
  Service->>SSO: (cached) JWKS
  Service->>Service: verify AT sig, iss, aud, exp, sub
  Service->>Service: cnf.jkt == thumbprint(proof.jwk)
  Service-->>Agent: 200 OK

Every fact the service trusts is signed either by the SSO (over standard RFC 9068 access-token claims) or by the agent (over the per-request RFC 9449 DPoP proof). There is no parallel envelope: no ownerBinding, no idTokenHash, no agent-issued attestation of sub. The cnf-binding ties the SSO-attested owner to the per-request proof-of-possession.

Error codes

result.code values map to RFC 9449 / RFC 9068 / RFC 6750 categories. Stable across releases; new values may be added.

| Code | RFC | Meaning | | --- | --- | --- | | missing_authorization | RFC 9449 §4.3 step 1 | Missing or duplicate Authorization header | | invalid_scheme | RFC 9449 §7.1 | Not Authorization: DPoP <token> | | missing_dpop | RFC 9449 §4.3 step 1 | Missing or duplicate DPoP header | | malformed_proof | §4.3 step 2 | DPoP value is not a well-formed JWS | | bad_proof_typ | §4.3 step 4 | typdpop+jwt | | bad_proof_alg | §4.3 step 5 | algEdDSA (Alien agent keys are Ed25519) | | missing_proof_jwk / bad_proof_jwk | §4.3 step 6 | Header jwk missing or not OKP/Ed25519 | | private_in_proof_jwk | §4.3 step 6 | Proof leaks the private d member | | bad_proof_signature | §4.3 step 7 | Signature does not verify with the embedded jwk | | bad_proof_htm | §4.3 step 8 | htm ≠ request method | | bad_proof_htu | §4.3 step 9 | htu ≠ request URL (query/fragment stripped) | | bad_proof_iat / stale_proof / future_proof | §4.3 step 11 | Proof iat is malformed or outside the freshness window | | missing_proof_jti / replayed_proof_jti | §4.3 step 12 + §11.1 | Proof lacks jti or it's been seen before | | bad_proof_ath | §4.3 step 10 | ath ≠ SHA-256(access_token) | | malformed_access_token | RFC 9068 §4 | Access-token is not a well-formed JWS | | bad_access_token_typ | RFC 9068 §4 | Access-token typat+jwt | | bad_access_token_alg | RFC 9068 §4 | Access-token algRS256 | | unknown_access_token_kid | RFC 7515 | Access-token's kid not in the JWKS | | bad_access_token_signature / access_token_sig_error | RFC 7515 | Access-token signature fails verification | | bad_access_token_iss | RFC 7519 §4.1.1 | issexpectedIssuer | | bad_access_token_aud | RFC 7519 §4.1.3 | aud does not include expectedAudience (defaults to expectedIssuer) | | expired_access_token | RFC 7519 §4.1.4 | Access-token exp is in the past | | missing_access_token_sub | RFC 7519 §4.1.2 | Access-token has no sub claim | | missing_cnf_jkt | RFC 7800 §3.1 | Access-token has no cnf.jkt (not DPoP-bound) | | jkt_mismatch | RFC 9449 §6.1 | cnf.jkt ≠ thumbprint of the proof's jwk |

Caveats

  • Pre-fetched JWKS. fetchAlienJWKS() does not cache. Call it at startup, hold the result, refresh every few hours. The SSO rotates signing keys infrequently.
  • Reverse proxies. If your service runs behind a load balancer or CDN, reconstruct the URL the agent actually addressed (using X-Forwarded-Proto / X-Forwarded-Host) — otherwise the htu comparison will reject every request.
  • jti replay store. The default in-memory store is single-process. Inject a shared jtiStore for multi-instance deployments so a captured proof can't be replayed against a different worker.
  • Clock sync. The 30-second default freshness window assumes loosely synchronized clocks. Tighten via proofMaxAgeSec / clockSkewSec if you have stricter NTP, widen if you have flakier clocks.

Additional resources