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

@spinajs/authn-keys

v3.0.25

Published

Key registry, JWS and HTTP signature authentication for SpinaJS

Readme

@spinajs/authn-keys

Public-key authentication for SpinaJS: a key registry, strict JWS (the canvas C8 rules), JWS client assertions, RFC 9421 HTTP signatures and a challenge-response login.

Keys

  • Ed25519 ({ "kty": "OKP", "crv": "Ed25519", "x" }, RFC 8037) and P-256 ({ "kty": "EC", "crv": "P-256", "x", "y" }) public JWKs only. The algorithm comes from the key type (EdDSA, ES256), never from a token.
  • Refused: private parts (d), other key types and curves, values that are not strict unpadded base64url of 32 bytes, Ed25519 points that are not canonical or have small order, P-256 points off the curve.
  • kid: 1–64 of [A-Za-z0-9._-]; default: the RFC 7638 thumbprint.
  • Owners: a user, a service or an agent. A user's key needs a proof of possession (below); services and agents are registered by an admin route or the CLI.
  • Table authn_key (migration AuthnKeys_2026_10_02_00_00_00).

DefaultKeyRegistry implements KeyRegistry over a KeyStore (OrmKeyStore by default, MemoryKeyStore for tests) and a SecretStore. The registry and the strategies get their dependencies through DI. Tests build them with new, passing the dependencies in one object, e.g. new DefaultKeyRegistry({ store, secrets, throttle, options }) or new HttpSignatureStrategy({ registry, nonces, options }). OrmKeyStore injects the Orm, so the authn:key-* commands bring the database up on their own.

JwsAssertionStrategy and HttpSignatureStrategy claim each jti and signature nonce in the core NonceStore (@spinajs/authn/contracts). Only @spinajs/authn-orm registers one (DbNonceStore), so import it, or register your own NonceStore; without one the strategies fail at boot with an error naming @spinajs/authn-orm. MemoryNonceStore from @spinajs/authn/nonces is for tests and single-process apps, and is not registered for you.

Challenges

KeyRegistry.challenge answers { id, nonce, expiresAt }. The client signs the UTF-8 bytes of

spinajs-key-<purpose>:<nonce>:<audience>

where <purpose> is register (proof of possession when a user adds a key) or login (challenge-response login), <nonce> is the nonce exactly as received (base64url, so it holds no :), and <audience> is the service's authn.keys.challenge.audience (default: its authn.keys.assertion.audience). The client knows the audience from its own configuration of the service, never from the challenge answer: a service that relays another one's challenge then gets a signature the other one refuses. It sends the raw signature as unpadded base64url: 64 bytes for Ed25519, and for P-256 an ECDSA SHA-256 signature as IEEE P1363 r || s (64 bytes, not DER). It names the challenge by its id, unchanged: the id carries the nonce back to the server, so treat it as opaque.

  • Registration: { owner, jwk, proof: { challengeId: id, signature } }.
  • Login: credentials { kind: 'key-challenge', keyId, challengeId: id, signature }.

A challenge is single use, lives authn.keys.challenge.ttlSeconds (default 120 s), and is redeemed only after the signature holds. A login challenge counts against the login flow's IP throttle key.

With OpenSSL 3, for an Ed25519 key in key.pem:

printf 'spinajs-key-login:%s:%s' "$NONCE" "$AUDIENCE" > input.bin
openssl pkeyutl -sign -inkey key.pem -rawin -in input.bin | basenc --base64url | tr -d '=\n'

From Node: signChallenge(challenge, 'login', audience, privateKey, 'EdDSA').

JWS

StrictJwsVerifier implements JwsVerifier under the C8 D4 rules. Checks run in this order and the first failure gives the code: malformed, bad_header (exactly { alg, kid, typ }), unknown_key, bad_signature (with the key's own algorithm), invalid_claims (JSON read as serde_json reads it; integer iat < exp, lifetime within the limit), wrong_issuer, wrong_audience, expired (now ≥ exp + skew), not_yet_valid (iat > now + skew).

LocalJwsSigner implements JwsSigner over private JWKs the app loads: new LocalJwsSigner(privateJwks, { current: kid }). It writes the header as {"alg","kid","typ"} and the claims in the caller's order, so the canvas C8 golden tickets come out byte for byte.

JWS client assertions (keys-jws)

Authorization: Bearer <jws> → credentials { kind: 'jws-assertion', jws }.

  • Header: { "alg": "EdDSA" | "ES256", "kid": "<kid>", "typ": "spinajs-client+jwt" }.
  • Claims: iss is the key owner as <type>:<id> (user:<uuid>, service:runner-1), aud is authn.keys.assertion.audience (required), integer iat and exp with exp − iat ≤ 300 (or the configured maxLifetimeSeconds), and a jti of 1–128 characters.
  • A jti is accepted once per key until exp plus the skew (authn_nonce or memory).

HTTP signatures (keys-httpsig)

An RFC 9421 subset with exactly one signature:

Signature-Input: sig1=("@method" "@target-uri" "content-digest");created=1790000000;expires=1790000300;nonce="<random>";keyid="<kid>"
Signature: sig1=:<standard base64 of the raw signature>:
Content-Digest: sha-256=:<standard base64 of SHA-256(body)>:
  • Any label; the Signature-Input member must be in canonical RFC 8941 form (single spaces, no parameters on components).
  • Covered: @method and @target-uri always; content-digest whenever the request has a body (RFC 9530, sha-256 or sha-512, checked against the body); also each name in authn.keys.httpSignature.requiredComponents. @authority, @path, @query and present request headers may be covered too.
  • Parameters: created, expires, nonce, keyid required; alg optional (ed25519 or ecdsa-p256-sha256, and then it must name the key's algorithm); nothing else.
  • expires − created ≤ maxAgeSeconds (300), created not ahead of now by more than the skew, now < expires; the nonce is accepted once per key.
  • The signature base is RFC 9421 §2.5; its @signature-params line is the Signature-Input member value as sent.

Strategies

| Name | Credentials kind | Session | |---|---|---| | keys-jws (JwsAssertionStrategy) | jws-assertion | no | | keys-httpsig (HttpSignatureStrategy) | http-signature | no | | keys-challenge (KeyChallengeStrategy) | key-challenge | yes |

Each refuses credentials over 8 KiB before parsing them, verifies only usable keys (not revoked, not expired), answers an unknown key with the same work and result as a bad signature, throttles by key:<kid>, and returns the key owner as the identity with the key's scope as the ceiling.

Configuration (authn.keys)

| Key | Default | |---|---| | algorithms | ['EdDSA', 'ES256'] | | challenge.ttlSeconds | 120 | | challenge.audience | null (then assertion.audience; challenges need one) | | assertion.typ | 'spinajs-client+jwt' | | assertion.audience | null (required for keys-jws) | | assertion.maxLifetimeSeconds | 300 (at most 300) | | assertion.skewSeconds | 30 | | httpSignature.maxAgeSeconds | 300 (at most 300) | | httpSignature.skewSeconds | 30 | | httpSignature.requiredComponents | ['@method', '@target-uri', 'content-digest'] |

authn_nonce and the NonceStore that fills it (DbNonceStore) come with @spinajs/authn-orm; this package has no nonce table or model of its own.

List the strategies an app uses in authn.strategies; service is the class name:

authn: {
  strategies: [
    { name: 'keys-jws', service: 'JwsAssertionStrategy' },
    { name: 'keys-httpsig', service: 'HttpSignatureStrategy' },
    { name: 'keys-challenge', service: 'KeyChallengeStrategy' },
  ],
  keys: { assertion: { audience: 'https://api.example.com' } },
}

Dates in authn_key and authn_nonce are UTC instants; the process time zone does not matter.