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

@di-framework/auth

v5.3.7

Published

Authentication for di-framework — sessions, JWT/JWS bearer, OAuth2/OIDC, and WebAuthn passkeys on WebCrypto with zero runtime dependencies

Readme

@di-framework/auth

Authentication for di-framework — server sessions, JWT/JWS bearer tokens, OAuth2/OIDC sign-in, and WebAuthn passkeys. Built entirely on the Web Cryptography API with zero runtime dependencies, so the same code runs on Bun, Node 20+, Deno, and Cloudflare Workers.

Domain services stay on @di-framework/core; this package is the authentication and authorization integration layer. It provides a policy-neutral manager boundary, while applications or the optional @di-framework/authz companion own the policy model.

Features

  • Password + server sessions: NIST SP 800-63B password policy, PBKDF2-HMAC-SHA-256 hashing, opaque session tokens stored hashed, __Host- cookies, absolute and inactivity timeouts, regeneration on login, signed double-submit CSRF.
  • JWT / JWS bearer tokens: compact JWS over WebCrypto with a mandatory algorithm allowlist, kid and JWKS publishing, overlapping key rotation, and opaque refresh tokens with rotation and reuse detection.
  • OAuth2 / OIDC relying party: Authorization Code with mandatory PKCE S256, discovery, state + nonce binding, ID-token validation, and presets for Google, Microsoft Entra, GitHub, and any compliant OIDC provider.
  • WebAuthn passkeys: W3C WebAuthn Level 3 registration and authentication, including a CTAP2-canonical CBOR decoder and COSE key handling, with no dependencies.
  • Provider pattern throughout: strategies and storage are plain interfaces with factory-function implementations. In-memory stores ship for development; a bridge over @di-framework/repo's StorageAdapter covers real backends.
  • First-class HTTP and GraphQL guards: a typed req.principal for @di-framework/http, and an @Authenticated() decorator plus protectSchema() for @di-framework/graphql.
  • Pluggable authorization: an AuthorizationManager hook with opaque policy metadata, requireAuthz() for HTTP, and @Authorize() for GraphQL. Policies remain application-owned and can live in OPA, SpiceDB, SQL, or an in-process manager.

Installation

bun add @di-framework/auth @di-framework/core
# optional integrations
bun add @di-framework/http @di-framework/graphql @di-framework/repo
npm install @di-framework/auth @di-framework/core

Decorators need TypeScript 5 and experimentalDecorators. emitDecoratorMetadata is not required.

Quick Start

import { registerAuth } from '@di-framework/auth';
import { requireAuth, withAuthErrors, withAuthRoutes } from '@di-framework/auth/http';
import { TypedRouter, json } from '@di-framework/http';

const auth = registerAuth({
  // At least 32 bytes. HKDF-expanded into the CSRF and cookie keys.
  secret: process.env.AUTH_SECRET!,
  jwt: { issuer: 'https://api.example.com', audience: 'api' },
});

await auth.passwords.createUser({
  identifier: '[email protected]',
  password: 'correct horse battery staple',
});

const router = TypedRouter({ catch: withAuthErrors() });
const secure = withAuthRoutes(router);

router.get('/health', () => json({ ok: true }));

// `req.principal` is typed as Principal, not any.
secure.get('/me', (req) => json({ sub: req.principal.sub }));

Core Concepts

Strategies

An AuthStrategy answers one question: does this request carry a credential of my kind, and is it valid? Strategies are factory functions returning object literals, the same shape @di-framework/events uses for transports.

import { authenticated, authFailed, chain, createPrincipal, noCredential } from '@di-framework/auth';

function headerStrategy(users: UserStore): AuthStrategy {
  return {
    name: 'x-user',
    async authenticate({ request }) {
      const id = request.headers.get('x-user-id');
      if (!id) return noCredential();                 // not mine — try the next
      const user = await users.findById(id);
      if (!user) return authFailed('invalid_credentials', `No user '${id}'`);  // mine, and bad — stop
      return authenticated(createPrincipal({ sub: user.id, method: 'api-key' }));
    },
  };
}

const strategy = chain([sessionStrategy, bearerStrategy, headerStrategy(users)]);

The three-state result is deliberate. no-credential means try the next strategy; failed halts the chain. Without that distinction, a forged bearer token falls through and the request ends up anonymous rather than rejected.

Principal

interface Principal {
  sub: string;              // stable subject id
  method: AuthMethod;       // how they proved it on this request
  amr?: readonly string[];  // RFC 8176 methods, e.g. ['hwk', 'user', 'mfa']
  acr?: string;
  authTime: number;         // when the original authentication happened
  scope?: readonly string[];
  sessionId?: string;
  claims?: Record<string, unknown>;
}

Authorization managers

AuthorizationManager is a policy decision point, not a role model. It receives the authenticated Principal (or undefined for an explicitly anonymous policy) and a transport context containing opaque metadata. Return { allowed: true } or { allowed: false, reason }; denial reasons are retained for logs and never sent to clients.

import type { AuthorizationManager } from '@di-framework/auth';

const authorization: AuthorizationManager = {
  async authorize(principal, context) {
    const response = await fetch('https://opa.example/v1/data/app/allow', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        input: { subject: principal?.sub, metadata: context.metadata },
      }),
    });
    const decision = await response.json() as { result?: boolean };
    return decision.result === true
      ? { allowed: true }
      : { allowed: false, reason: 'remote policy denied the request' };
  },
};

registerAuth({ secret, authorization });

Pass manager to a guard or protectSchema() for a per-call override. Without an explicit manager, both integrations resolve auth.AuthorizationManager from DI.

HTTP

import { TypedRouter, json } from '@di-framework/http';
import {
  applyAuthHeaders, createAuthRoutes, mountAuthRoutes,
  requireAuth, requireAuthz, secured, securitySchemesFor, withAuthErrors, withAuthRoutes,
} from '@di-framework/auth/http';

const router = TypedRouter({
  before: [requireAuth()],          // or guard per route, below
  catch: withAuthErrors(),
  finally: [applyAuthHeaders],      // applies cookies a guard queued
});

Per-route protection, which types the principal:

const secure = withAuthRoutes(router);

@Controller()
export class MeController {
  @Endpoint({ summary: 'Current principal', security: secured('bearerAuth') })
  static get = secure.get('/me', async (req) => {
    const controller = useContainer().resolve(MeController);
    return json(await controller.load(req.principal.sub));
  });
}

Escape hatches: { auth: false } for a public route, { auth: { mode: 'optional' } } to attach a principal when present.

Add authorization after authentication with a protected-router option:

secure.get('/admin', (req) => json({ subject: req.principal.sub }), {
  authorization: { metadata: { resource: 'admin', action: 'read' } },
});

For RouteOptions.use, preserve the same order explicitly:

router.get('/admin', handler, {
  use: [requireAuth(), requireAuthz({ metadata: { action: 'admin:read' } })],
});

By default, requireAuthz() rejects a missing principal with 401. Set allowAnonymous: true only when the manager intentionally evaluates public-policy requests. A policy denial is always a generic 403 { error: "Access denied", code: "access_denied" } on the wire.

Mountable auth routes

const authRouter = createAuthRoutes({ oauth: { google: googleClient } });
mountAuthRoutes(router, authRouter, '/auth');

| Method | Path | Notes | | --- | --- | --- | | POST | /auth/register | Creates a user and signs them in. | | POST | /auth/login | Regenerates the session id — the fixation defence. | | POST | /auth/logout | Idempotent; clears cookies server-side and on the client. | | POST | /auth/refresh | Rotates the refresh token; reuse revokes the family. | | GET | /auth/session | Whoami. Never echoes raw claims. | | GET | /auth/csrf | A token bound to the current session. | | POST | /auth/webauthn/{register,login}/{options,verify} | Challenge stored server-side, single-use. | | GET | /auth/oauth/:provider/{start,callback} | PKCE and single-use state. |

OpenAPI

const spec = generateOpenAPI({
  title: 'API',
  securitySchemes: securitySchemesFor([auth.strategy]),
  security: secured('bearerAuth'),   // document-level default
});

Mark an operation public with @Endpoint({ security: publicEndpoint }).

Why there is no @CurrentUser()

Handlers are static class properties invoked with itty's positional (req, ...args), not through a DI-constructed call. With emitDecoratorMetadata: false and no AsyncLocalStorage, a parameter decorator has nothing to hook into — implementing one would need a module-level "current request", which is unsound the moment two requests are in flight. Read req.principal, or getPrincipal(req) where the static type is not narrowed.

GraphQL

import { buildSemanticSchema, createGraphQLHandler } from '@di-framework/graphql';
import { Authenticated, Authorize, createAuthContext, protectSchema, requireSubject } from '@di-framework/auth/graphql';

@Portal()
class Library {
  @Field(() => Book)
  catalogue(): Book[] { return books; }        // public

  @Authenticated()
  @Authorize({ resource: 'loan', action: 'create' })
  @Action(() => Loan)
  borrow(@Arg('id') id: string, @Ctx() ctx: AuthGraphQLContext): Loan {
    return lend(id, requireSubject(ctx));
  }
}

const api = protectSchema(buildSemanticSchema());
const handler = createGraphQLHandler(api, {
  context: createAuthContext({ strategy: auth.strategy }),
});

@Authenticated({ amr: ['mfa'], maxAge: 300 }) requires a stronger or more recent authentication — still authentication, not authorization.

@Authorize(metadata) applies to types, fields, actions, and subscriptions. It invokes the registered manager after any @Authenticated() check. Use @Authorize(metadata, { allowAnonymous: true }) only for a manager that deliberately makes decisions about anonymous callers. Policy denials use extensions.code === 'FORBIDDEN'; internal reasons remain redacted.

Authentication rejections surface as errors[0].extensions.code === 'UNAUTHENTICATED', and the message on the wire is always the generic public text.

Note: printSDL renders from the type graph rather than the executable schema, so @authenticated does not appear in the printed SDL. The SDL describes the shape; the executable schema enforces access.

Storage providers

import { inMemoryAuthStores } from '@di-framework/auth';
import { repoSessionStore, repoUserStore } from '@di-framework/auth/repo';

registerAuth({
  secret,
  stores: {
    users: repoUserStore({ adapter: userAdapter }),
    sessions: repoSessionStore({ adapter: sessionAdapter }),
  },
});

Interfaces: UserStore, SessionStore, CredentialStore, StateStore, RefreshTokenStore, KeyStore, LoginThrottle.

Three methods must be a compare-and-swap: StateStore.consume, RefreshTokenStore.rotate, and CredentialStore.updateSignCount. They are the replay defences for OAuth state, refresh tokens, and WebAuthn sign counters. StorageAdapter has no conditional write, so repoStateStore and repoRefreshTokenStore require an explicit atomic hook and throw at construction without one rather than shipping a defence that silently does nothing.

Security notes

Choosing a password hasher. The default is PBKDF2-HMAC-SHA-256 at 600,000 iterations (NIST SP 800-132; OWASP's 2024 figure), because it is the only password KDF the Web Cryptography API offers and this package carries no dependencies. PBKDF2 is memory-cheap, so a GPU attacker gets better value against it than against Argon2id. If you run on Bun or Node, supply a stronger hasher:

registerAuth({ secret, hasher: bunPasswordHasher() });   // Argon2id via Bun.password

Stored hashes record their own parameters, and login re-hashes transparently when they are below the current setting — so upgrading is a one-line change with no migration.

Login throttling is on by default and is not optional in spirit: SP 800-63B §5.2.2 requires it, and PBKDF2 at 600,000 iterations is expensive enough to be a denial-of-service vector without it. The in-memory throttle is per-process; use a shared one behind a load balancer.

Cookies. Defaults are __Host- prefixed: Secure, HttpOnly, Path=/, no Domain, SameSite=Lax. Setting a Domain downgrades the name to __Secure- and warns, because browsers reject a __Host- cookie carrying a Domain outright. A __Secure- cookie is writable by every subdomain, so if you need cross-subdomain SSO, pair it with strict CSRF checking.

Cloudflare Workers. Session and bearer verification are cheap and run fine at the edge. PBKDF2 at 600,000 iterations may exceed the Workers CPU limit — verify passwords at your origin.

Timing. Login reports the same error and burns the same work whether the user is missing, has no password, or typed the wrong one.

API Reference

| Export | Description | | --- | --- | | registerAuth(options) | Build and register the runtime with the DI container. | | Auth(options) | Class-decorator sugar over registerAuth. | | chain(strategies) | Compose strategies, first match wins. | | sessionCookieStrategy / bearerTokenStrategy / apiKeyStrategy | Built-in strategies. | | sessionManager(options) | Session issuance, resolution, regeneration, revocation. | | passwordService(options) | NIST-compliant password policy, login, and rehashing. | | csrfGuard(options) | Session-bound signed double-submit tokens. | | signJwt / verifyJwt / signJws / verifyJws | Token primitives with a required algorithm allowlist. | | keyService(options) / remoteJwks(uri) | Key rotation and JWKS publishing/consumption. | | refreshService(options) | Rotation with reuse detection. | | webAuthnService(options) | @di-framework/auth/webauthn — the two ceremonies. | | oauthClient(provider, deps) | @di-framework/auth/oauth — the relying party. | | inMemoryAuthStores() | Development and test storage. | | repo*Store(options) | @di-framework/auth/repoStorageAdapter bridge. | | withAuthRoutes / protect / requireAuth | @di-framework/auth/http — guards. | | AuthorizationManager / AUTHORIZATION_MANAGER | Application-owned policy decision point and its DI token. | | requireAuthz / authorize | @di-framework/auth/http — authorization guards. | | createAuthRoutes(options) | @di-framework/auth/http — mountable protocol endpoints. | | protectSchema / Authenticated / Authorize / createAuthContext | @di-framework/auth/graphql. |

Non-goals (v1)

  1. An authorization policy model. The package supplies the AuthorizationManager extension point and transport plumbing, but no roles, permissions, RBAC/ABAC DSL, SpEL expressions, or @Roles(). Applications and remote policy agents define the metadata vocabulary and make every allow/deny decision.
  2. Full WebAuthn attestation verification. none and self-attested packed are verified; anything else needs FIDO Metadata Service integration. The extension point is WebAuthnConfig.verifyAttestation. Known gap: fido-u2f, which older security keys still emit.
  3. Being an authorization server. This is a relying party — no /authorize, no /token, no consent screen, no client registration.
  4. Multi-factor orchestration (TOTP, SMS, magic links, step-up state machines). amr and acr are recorded so you can build it.
  5. Redis, KV, and SQL stores. Interfaces, in-memory implementations, and the StorageAdapter bridge only.
  6. Account recovery and email delivery. Tokens are provided; sending them is yours.
  7. An @authenticated directive in the printed SDL.
  8. SAML, LDAP, Kerberos, and multi-tenant modelling.

Example

https://github.com/di-framework/di-framework/tree/main/examples/packages/auth

License

Licensed under either MIT or Apache-2.0, at your option.