@happyvertical/auth
v0.111.6
Published
Unified authentication for Keycloak, Cognito, Nostr, and local ChatGPT sessions with OAuth2/OIDC and public key identity
Readme
@happyvertical/auth
Unified authentication interface supporting multiple providers.
Providers
- Keycloak - Full OIDC/OAuth2 with admin capabilities
- AWS Cognito - OAuth2 with hosted UI
- Nostr - Decentralized public key identity
Installation
npm install @happyvertical/authClaude Code Context
Install Claude Code context files for AI-assisted development:
npx have-auth-contextThis copies the package's AGENT.md documentation and metadata.json metadata to your project's .claude/ directory, enabling Claude to provide better assistance when working with this package.
Quick Start
import { getAuth } from '@happyvertical/auth';
// Keycloak
const auth = await getAuth({
type: 'keycloak',
serverUrl: 'https://auth.example.com',
realm: 'my-realm',
clientId: 'my-app'
});
// Cognito
const auth = await getAuth({
type: 'cognito',
region: 'us-east-1',
userPoolId: 'us-east-1_xxx',
clientId: 'xxx'
});
// Nostr
const auth = await getAuth({
type: 'nostr',
relays: ['wss://relay.damus.io']
});
// Authenticate
const result = await auth.authenticate({ username, password });
console.log(result.accessToken);
// Validate token
const claims = await auth.validateToken(token);
// Check role
if (await auth.hasRole(token, 'admin')) {
// Admin access
}Documentation
See AGENT.md for complete API documentation.
Authorization server
@happyvertical/auth/server provides a framework-independent OAuth 2.0
authorization-code issuer. It intentionally does not authenticate browser
sessions or render consent: an application validates its current session and
permissions, then calls parseAuthorizationRequest() and approve() with an
application-owned OAuthConsentContext.
import { createAuthorizationServer } from '@happyvertical/auth/server';
const request = await server.parseAuthorizationRequest(url.searchParams);
const approved = await server.approve(request, {
subject: session.userId,
tenantId: session.activeTenantId,
});
return Response.redirect(approved.redirectUri, 302);Supply a durable OAuthAuthorizationStorage implementation. Its
consumeAuthorizationCode and rotateRefreshGrant operations must be atomic
database transactions (or compare-and-swap operations); the exported in-memory
store is for tests only. The server enforces S256 PKCE, exact redirect URI and
resource binding, JWT signing/JWKS, refresh-token rotation with family replay
revocation, and live authorization checks at both code exchange and refresh
through OAuthIdentityProvider.revalidateConsent (the legacy refreshConsent
hook is a fallback for both). Return the unchanged subject and tenant, optional
replacement application claims, and optional scopes that are a subset of the
grant. Return null to deny; changing identity, tenant, or expanding scope fails
closed. Omitting claims removes previous application claims. JWT protocol claims
are reserved. Consent supplied to approve may also narrow the requested scopes.
Stores must implement narrowRefreshGrant when refresh tokens are issued (the constructor rejects stores without it): atomically
validate the replacement token hash and active grant, then persist only a subset
of its current scopes. Throw on any failure. The server revokes the family on
failed live checks or narrowing persistence; it never returns the replacement.
Storage failures must reject; never return success before durable commit. A
connection failure after commit can leave an unreachable hash-only grant, but
must not expose any token. Concrete SQL adapters and dialect tests belong to the
host application.
Path issuers such as https://host/oauth advertise endpoints under /oauth and
RFC 8414 discovery at /.well-known/oauth-authorization-server/oauth. Public
DCR accepts only code responses, authorization-code/refresh grants, allowed
scopes, and HTTPS redirects. HTTP loopback redirects require explicit
allowLoopbackRedirects: true for native/development clients. Resource servers must pass their own audience to verifyAccessToken(token, audience). Access-token
verification requires typ: at+jwt, expiry, issued-at, subject, audience, token
ID, client ID and scope with valid claim types. Registration bodies are limited to 16 KiB while streaming, including when
Content-Length is absent or understated. Metadata permits at most 10 redirect
URIs of 2,048 characters each, a 256-character client name, a 2,048-character
scope string, two grant types and one response type. Direct register() calls
apply the same field bounds. Oversized bodies return 413 before persistence;
invalid metadata returns 400. Unknown extension metadata is ignored within the
body limit. Revocation returns success for unknown or invalid tokens, but
storage outages return a generic 500 server_error so callers can retry after
recovery. CIMD is not implemented. The signing API supports one active key;
overlapping signing-key rotation is not implemented. Hosts must supply a public
JWK matching the private signing key and keep private material out of JWKS.
Local Sign in with ChatGPT
Node-only local applications can import @happyvertical/auth/local to manage
their Sign in with ChatGPT session. The manager uses a 127.0.0.1 loopback
callback, PKCE and OIDC validation, and writes account-scoped renewable
credentials atomically with owner-only permissions. Pass a configuration path
outside the project directory and open the returned authorization URL in the
system browser. It never exposes credentials from sessions().
Use accessToken(clientId) before an authorized request; it serializes token
refreshes. logout(clientId) always clears local credentials and reports
whether remote revocation was confirmed. This local runtime does not make any
inference request or use browser storage.
License
MIT
Refresh storage adapters must implement narrowRefreshGrant to atomically persist both the narrowed scopes and the current claims on the exact replacement token before it is returned. An explicit undefined claims value clears previous application claims. Token and revocation form bodies share the 16 KiB streaming limit with registration. Refresh requests may request a subset of the original grant scope; expansions are rejected.
