Skip to main content
QAuth Labs
Star on GitHub

Developer-friendly · Headless · Apache 2.0

OAuth 2.1 and OpenID Connect, run by you

A standards-first authorization server for humans, services and AI agents, from a side project to a national ID rollout. Headless by design: build your own sign-in screens on the Interaction API (OpenAPI 3.1, typed client), or use the reference pages. PKCE everywhere, RFC 8693 delegation, the MCP authorization profile and hybrid ML-DSA-65 signing behind a flag. TypeScript on Fastify, with PostgreSQL and Redis.

# Needs Docker 23+ with Compose 2, and OpenSSL

❯ git clone https://github.com/qauth-labs/qauth.git && cd qauth

❯ cp .env.docker.example .env

# add JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, KEK and DB_PASSWORD to .env

❯ docker compose up -d

❯ curl http://localhost:3000/ready

How to generate each value: Docker guide.

OAuth 2.1
OpenID Connect 1.0
PKCE
RFC 8693
RFC 8707
RFC 9728
RFC 9449 DPoP
MCP 2026-07-28

What ships today

Capabilities

The full list, and what each switch turns on, is on the status page. Off by default: WALLET_FEDERATION_ENABLED, HYBRID_SIGNING_ENABLED, ID_JAG_ENABLED (enterprise-managed authorization) and DPOP_ENABLED (RFC 9449).

Agent-native authorization

An agent client type, RFC 8693 on-behalf-of token exchange with an act claim, ReadOnly / Admin / Exec scope modes capped per client, step-up before dangerous operations, and an audit trail per agent. For any agent calling any API, not just MCP.

RFC 8693 · Step-up · Audit

MCP authorization profile

Revision 2026-07-28: RFC 9728 protected-resource metadata, Client ID Metadata Documents with RFC 7591 as the fallback, audience binding and step-up challenges. @qauth-labs/resource-guard handles the resource-server side.

RFC 9728 · CIMD · resource-guard

OAuth 2.1 and OpenID Connect core

Authorization code with mandatory PKCE, client_credentials, rotating refresh tokens, discovery, JWKS, ID tokens, userinfo, introspection, revocation, RFC 8707 resource indicators, and RP-initiated and back-channel logout.

OAuth 2.1 · OIDC 1.0

Post-quantum hybrid signing

Ed25519 by default. One flag adds hybrid ML-DSA-65 + Ed25519 signatures and a mixed AKP+OKP JWKS, with the post-quantum component served through introspection.

ML-DSA-65 · FIPS 204 · Opt-in

Self-hosted, multi-tenant

Realms with their own issuer and hosts, a developer portal for clients and API keys, and encrypted signing keys. Structured logs, Prometheus metrics and OpenTelemetry events go to your monitoring; nothing is sent to us.

Docker · Realms · Apache 2.0

Federation

Sign-in through a realm's upstream OpenID Connect providers today. EU Digital Identity Wallet sign-in over OID4VP 1.0 with SD-JWT VC ships behind a flag; it is tested against a mock wallet, and the real-wallet pass is still open.

Upstream OIDC · OID4VP 1.0 · Opt-in

Grants, side by side

Humansauthorization_code + PKCESession + refresh token
Servicesclient_credentialsScoped access token
Any client/.well-known/openid-configurationRegister + discover JWKS

The same server issues tokens for logins, service calls and delegated agents: one identity core, not three bolted together.

Core protocol

One auth server, every kind of client

Before an agent ever delegates anything, QAuth is an OAuth 2.1 and OpenID Connect server: authorization code with mandatory PKCE (S256) for your users, client_credentials for your services, rotating refresh tokens that revoke their family on replay, discovery, JWKS, userinfo, introspection and revocation. No implicit flow and no proprietary SDK required. Agent delegation and MCP support sit on this same core.

PKCE

Mandatory on every authorization code flow

CIMD

Client ID Metadata Documents; RFC 7591 as fallback

Logout

RP-initiated and back-channel logout

OIDC

Standard identity layer, no proprietary claims

Delegation flow

DelegateUserToken Exchange (RFC 8693)JWT { sub: user, act: agent, aud: resource }
Blast radiusReadOnlyAdminExec
Step-upExec + dangerous opFresh human approval

Audience-bound tokens keep an agent scoped to the one resource it was delegated for; nothing else is reachable.

Agent authorization

Agents act. Your auth server answers for them.

Agent auth isn't user auth with extra steps. A token has to say "agent X acting for user Y", so RFC 8693 token exchange mints a delegated token with an act claim. Agents can't file a ticket for a client ID either, so they register at machine speed through Client ID Metadata Documents, with RFC 7591 as the fallback. A development agent never reaches production: the environment is a policy dimension. All of it ships today and works the same in front of an MCP server or a plain REST API.

RFC 8693

On-behalf-of token exchange

3 modes

ReadOnly / Admin / Exec, capped per client

Step-up

Human-in-the-loop before dangerous ops

Audit

A trail per agent, for every delegated action

Integrate

Auth that gets outof your way

Protect your resource servers, MCP or plain REST, with standards-based OAuth 2.1: RFC 9728 discovery, client registration by metadata document, and PKCE. The SDK is optional; any OAuth library works.

1

Run QAuth

Docker Compose builds the images, runs the migrations and starts the auth server with PostgreSQL 18 and Redis 7. The quickstart above has the commands.

2

Protect a resource server

Register @qauth-labs/resource-guard on an MCP server, or verify bearer tokens against the JWKS yourself on a plain REST API. The tokens are audience-bound either way.

3

Let clients sign in

MCP clients such as Claude Code discover your authorization server, register with a Client ID Metadata Document and run authorization_code with PKCE. Services use client_credentials instead.

import Fastify from 'fastify'

import { resourceGuardPlugin } from '@qauth-labs/resource-guard/fastify'


const app = Fastify()


// Protect your MCP resource server: audience-bound tokens only

await app.register(resourceGuardPlugin, {

  resource: 'https://mcp.yourapp.com',

  authorizationServer: 'https://auth.yourapp.com',

  requiredScopes: ['mcp:read'],

})


app.get('/mcp', { preHandler: app.requireBearer }, async (request) => {

  // request.tokenClaims: verified signature, issuer, audience, scope

  return { tools: listTools() }

})


// No token → 401 Unauthorized

// WWW-Authenticate: Bearer resource_metadata=

// "https://mcp.yourapp.com/.well-known/oauth-protected-resource"


// The MCP client (e.g. Claude Code) discovers your auth server from

// that document, registers via CIMD or RFC 7591, then runs

// authorization_code + PKCE for an audience-bound access token.


// Authorization: Bearer <token> → resource-guard verifies it → 200 OK

# Plain OAuth 2.1: no MCP, no agent, just your service

❯ curl -X POST https://auth.yourapp.com/oauth/token -d grant_type=client_credentials -d scope=documents:read

Hybrid signing flow

DefaultEd25519JWT (64 B sig)On by default
Opt-inEd25519ML-DSA-65Hybrid JWTShipped
LaterML-DSA-65PQC-only JWTEvaluation 2028+

The bearer token stays a plain Ed25519 JWS. The 3,309-byte ML-DSA-65 signature travels through RFC 7662 introspection by default, so no header or cookie limit breaks. NIST IR 8547 deprecates EdDSA, ECDSA and RSA signatures after 2030. Harvest now, decrypt later targets key exchange, not signatures: terminate TLS in front of QAuth with X25519MLKEM768.

Post-quantum

Hybrid ML-DSA-65 signing ships today

Signing sits behind algorithm-agnostic interfaces. Set SIGNING_ALGORITHM_MODE=ed25519+ml-dsa-65 and HYBRID_SIGNING_ENABLED=true and QAuth signs with ML-DSA-65 next to Ed25519, publishes a mixed JWKS and serves the post-quantum component through introspection. Verifiers that only know Ed25519 keep working.

Level 3

NIST security category of ML-DSA-65

FIPS 204

The ML-DSA standard, published 2024

Opt-in

HYBRID_SIGNING_ENABLED, off by default

AKP+OKP

Mixed JWKS for hybrid verifiers