English | 日本語
auth-provider is a self-hosted OAuth 2.0 authorization server and OpenID Connect (OIDC) provider for Node.js, written in TypeScript. Generate an app with the features you need, then run it on Node.js or build its Docker image.
The generator adds the modules those features depend on. At startup the app checks its configuration and lists every problem it finds, naming the setting or the module.
0.18 rewrites 0.17 under new package names, and it is still in prerelease. Until 0.18.0 is final, install it with the npm next tag. Without the tag, npm installs 0.17. To move an existing 0.17 deployment, read the migration guide first: there is no in-place upgrade, the configuration must be rewritten, and stored state is not migrated.
The provider signs users in, manages their sessions and issues tokens to your applications. It manages the signing keys and gives you endpoints to check and revoke tokens.
| Feature | Specification | Module |
|---|---|---|
| Authorization code flow, with PKCE (S256) required for every client | RFC 6749, RFC 7636 | authorize, code-grant |
| Refresh tokens, rotated on every use, with reuse detection | RFC 6749 | refresh |
| Client credentials grant | RFC 6749 | token-endpoint |
| ID tokens and UserInfo | OpenID Connect Core 1.0 | oidc, userinfo |
| Discovery documents and public signing keys (JWKS) | OpenID Connect Discovery 1.0, RFC 8414 | discovery, keys |
| RP-initiated (client-initiated), front-channel and back-channel logout | OpenID Connect logout specifications | rp-logout |
| Token introspection and revocation | RFC 7662, RFC 7009 | token-control |
Client secrets and private_key_jwt; public clients without client authentication |
RFC 6749, RFC 7523 | clients, client-assertion |
| Client ID Metadata Documents (a URL as the client ID) | clients-cimd | |
| JWT bearer grant | RFC 7523 | jwt-bearer |
| Device Authorization Grant | RFC 8628 | device-grant |
| Token Exchange | RFC 8693 | token-exchange |
| Tokens bound to a client-held key (DPoP) | RFC 9449 | dpop |
| Certificate-bound tokens (mTLS) | RFC 8705 | mtls |
| Resource indicators | RFC 8707 | resource-indicators |
| Passkey login and registration | WebAuthn | webauthn-login, passkey-registration |
| Second factors: authenticator apps (TOTP), email codes, recovery codes and security keys | RFC 6238, WebAuthn | mfa and the mfa-* modules |
| Sign-in through an upstream OpenID Connect provider, Google, GitHub or Apple | OpenID Connect; GitHub OAuth | federation and the idp-* modules |
Passkey login is a custom grant at the token endpoint and does not open a browser session. The mTLS module binds tokens to a certificate; clients still authenticate with another method.
Other modules cover consent, step-up authentication, keeping upstream tokens for your clients, and ending a user's sessions when your user store revokes them. The owner map lists every module and what it is responsible for.
- A login page, and a consent page if some clients need consent. The provider redirects the browser to your login page with a
redirect_toparameter. Your page gets a CSRF token fromGET /session/csrf, posts the username, password, token andredirect_totoPOST /session/login, then sends the browser to theredirect_toit gets back. Serve the page from the provider's public origin (for example, behind the same reverse proxy): a login posted from another origin is refused. The login page guide gives each request and answer; the contracts are in login-password and csrf. - User accounts, either listed in the configuration or in your own HTTP service that the provider calls, which the docs call a user ledger (users-http). Sign-up and password recovery are not included.
- A reverse proxy that terminates TLS. The app itself serves plain HTTP.
- Authorization inside your services. Your services, or a separate policy engine, decide what a token allows; see Related projects.
- Node.js 24 or later.
- pnpm, at the version in the generated project's
packageManagerfield.corepack enableinstalls that version for you; Node.js 25 and later ship without Corepack, so runnpm install -g corepackfirst. - Redis 7.0 or later, if you use the Redis adapter for persistent state or several instances. See Before production. The settings the server must have, such as an append-only file and no eviction, are in the runbook's Redis section; the app checks them at startup.
- Docker, if you want to build and run the generated image.
Some patterns also need an HTTP user ledger, an audit collector, an SMTP relay or upstream identity providers. simple-web needs none of them.
Start with simple-web. It keeps users in the configuration and state in memory, and gives you password login for web apps (authorization code flow, OIDC, refresh tokens) plus client credentials for services.
npx @o3co/create-auth-provider my-provider --pattern simple-web
cd my-provider
pnpm install
cp .env.example .env
cp config/local.conf.example config/local.confThe generated README.md explains the pattern and its settings. In both example files every line is commented out, so copying them sets nothing. Uncomment the settings you need and replace the example values.
In .env, set:
ISSUER_URLandHTTP_PUBLIC_ORIGIN: the URL clients see, with no trailing slash, and its scheme, host and port. The two must have the same origin.- The public origin must be https by default. To try the provider over plain http on your machine, set both to
http://127.0.0.1:8080andHTTP_INSECURE_ORIGIN=truein.env.
- The public origin must be https by default. To try the provider over plain http on your machine, set both to
KEYS_SIGNING_KIDandKEYS_SIGNING_ALG. The example uses EdDSA, for an Ed25519 key.AUTHORIZE_LOGIN_PAGE: the URL of your login page, on the provider's own origin, such ashttp://127.0.0.1:8080/loginbehind a proxy that serves the page and passes the rest to the app. The app does not serve this page. A page on another origin lets the app start, but every login from it is refused403 csrf.CONSENT_PAGEis the URL of your consent page; leave it unset if all your clients are first-party.KERNEL_DEPLOYMENT=single,STORE_ACCEPT_VOLATILE=true,MAIL_ACCEPT_UNSENT=trueandTOKENS_REVOCATION=denylist, as in the example. They state that this deployment runs one instance, keeps its state in memory (lost on restart) and sends no mail.TOKENS_REVOCATION=denylistkeeps revoked access tokens in a denylist and refuses them;unsupportedanswers revocation as unsupported.
Generate a signing key:
node -e 'const {generateKeyPairSync}=require("node:crypto");process.stdout.write(generateKeyPairSync("ed25519").privateKey.export({type:"pkcs8",format:"pem"}))' > signing.pemMake a password hash for a test user. The provider accepts bcrypt or scrypt hashes, never plain passwords. This prints an scrypt hash:
node -e 'const c=require("node:crypto");const s=c.randomBytes(16);const k=c.scryptSync(process.argv[1],s,32,{N:2**15,r:8,p:1,maxmem:64*1024*1024});const b=x=>x.toString("base64").replace(/=+$/,"");console.log(`$scrypt$ln=15,r=8,p=1$${b(s)}$${b(k)}`)' 'your-password'Then write config/local.conf. It holds the key (leave KEYS_SIGNING_PRIVATE_KEY unset in .env), the audit receiver, your users and your clients. users.registered is not in the example file, so add it yourself:
keys.signing.private-key = """
<paste the contents of signing.pem>
"""
audit.receivers = [log]
users.registered = [
{ identifier = "alice", password-hash = "<the hash printed above>" }
]
clients.registered = [
{
id = "my-web-app"
kind = "public"
method = "none"
sections {
authorize { redirect-uris = ["http://127.0.0.1:3000/callback"] }
client-allowance {
grants = ["authorization_code", "refresh_token"]
scopes = ["openid", "profile"]
default-scopes = ["openid"]
}
consent { first-party = true }
}
}
]A client is allowed no grants and no scopes until you list them in client-allowance. A code-flow client also needs its redirect-uris, and first-party = true skips the consent page. A confidential client uses kind = "confidential", a method such as client_secret_basic, and a hash of its secret in sections.client-secret.hash. The configuration reference lists every client section.
Start the app:
APP_CONFIG=config/local.conf,config/application.conf pnpm startIf a setting is missing or invalid, the app prints one refused <setting or module>: <reason> line for each problem on standard error and exits with status 78. Fix them and start again; fixing one round can bring the next round's refusals.
With the default port and the local issuer above, check that it is up:
curl http://127.0.0.1:8080/readyz
curl http://127.0.0.1:8080/.well-known/openid-configurationIf your issuer URL has a path, the discovery document is under that path.
A browser sign-in also needs your login page. To check a sign-in without a browser, walk the steps a login page and a client take: a CSRF token, the login, the authorization request with PKCE, and the code redeemed at the token endpoint. With the user and client above:
V=$(node -e 'console.log(require("crypto").randomBytes(32).toString("base64url"))')
C=$(node -e 'console.log(require("crypto").createHash("sha256").update(process.argv[1]).digest("base64url"))' "$V")
AUTH="http://127.0.0.1:8080/oauth/authorize?response_type=code&client_id=my-web-app&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback&scope=openid&state=xyz&code_challenge=$C&code_challenge_method=S256"
TOKEN=$(curl -s -c jar -b jar http://127.0.0.1:8080/session/csrf | node -pe 'JSON.parse(require("fs").readFileSync(0)).csrf_token')
curl -s -c jar -b jar -H "X-CSRF-Token: $TOKEN" --data-urlencode username=alice --data-urlencode password=your-password --data-urlencode "redirect_to=$AUTH" http://127.0.0.1:8080/session/login
curl -s -o /dev/null -w '%{redirect_url}\n' -b jar "$AUTH"
curl -s http://127.0.0.1:8080/oauth/token -d grant_type=authorization_code -d client_id=my-web-app --data-urlencode redirect_uri=http://127.0.0.1:3000/callback -d code=<code> -d code_verifier="$V"The third request prints the redirect to http://127.0.0.1:3000/callback with the code; put it in place of <code> in the last one, which answers the tokens. The login page guide gives each request and answer. A recipe goes on to a second factor (TOTP), with every request and answer recorded from a running app.
A pattern is a ready-made selection of modules for one kind of deployment. Choose one with --pattern; the generated README lists what it needs. Each pattern has a short card in modules/scaffold/create-app/src/patterns.
If you leave out --pattern, you get full. Use simple-web to start without outside services.
Add features with --add, using module names of the form @mod/<name>, where <name> is the module's directory under modules/:
npx @o3co/create-auth-provider my-provider-dpop --pattern simple-web --add @mod/dpopRepeat --add for each feature. To keep simple-web's store in Redis, add --axis @mod/store=@mod/redis. You can also replace a pattern's login methods, grants or other feature groups. Axis and group options need a pattern other than full. Run npx @o3co/create-auth-provider --help for all options.
You can also list modules yourself instead of choosing a pattern. The generator then adds what they need to connect, and you write the configuration. See app-output for what each kind of project contains.
To change an existing app's selection, generate a new project into an empty directory and copy your configuration into it. The generator never overwrites a directory that has files in it.
Configuration uses HOCON, with one section per module, named after it: for example issuer.url, keys.signing.alg and tokens.access-token-lifetime. Unknown keys, sections for modules you have not loaded, and invalid values stop the app from starting. Error messages name the setting and never print its value. Times and sizes need a unit, such as 30s or 5KiB.
A project generated from a pattern reads two local files. Both are left out of git and out of the Docker build context, so keep your secrets there.
.envsupplies the environment variables thatconfig/application.confreads.config/local.confholds lists, multi-line values and any other setting you write directly in HOCON.
A variable's name is the setting's path in upper case, with . and - replaced by _: keys.signing.private-key becomes KEYS_SIGNING_PRIVATE_KEY. A variable only affects a setting that config/application.conf reads from it; it is not a general override.
APP_CONFIG lists the configuration files, separated by commas. Earlier files win over later ones. Without it, the app reads config/application.conf.
Replace every example value before you start. A secret's placeholder starts with REPLACE- and is refused if you leave it in.
The configuration reference lists every setting with its type, default and environment variable. Each module directory in this repository also has a generated reference.conf with its defaults, for reading only.
Run pnpm install first so that pnpm-lock.yaml exists, then:
docker build -t auth-provider .
docker run --env-file .env \
-e HTTP_HOST=0.0.0.0 \
-e HTTP_PORT=8080 \
-e APP_CONFIG=config/local.conf,config/application.conf \
-v "$PWD/config/local.conf:/app/config/local.conf:ro" \
-p 8080:8080 \
auth-providerDocker env files take one line per variable, so keep multi-line values such as PEM keys in config/local.conf.
The image uses Node 24, installs production dependencies only and runs as a non-root user. Its health check calls /_healthcheck.
Replace the in-memory store with Redis, or with your own adapter that meets the store contract. The Redis adapter needs Redis 7.0 or later as a single primary; cluster mode is refused. The app checks the server's settings at startup. Read the Redis requirements for persistence, replication and managed services.
Set kernel.deployment to single or multi. With multi, the app refuses to start with modules that cannot run across instances, such as the in-memory store and the logging-only mailer.
Put the app behind a TLS-terminating proxy and set http.trusted-proxies. The proxy must remove any forwarded or client-certificate headers that clients send before it sets its own. Read the deployment requirements and the security model before you expose the service.
The route reference lists the methods, request bodies and policies of every route in the full template with all optional features. Your app serves only the routes of the modules it loads.
Common endpoints:
/.well-known/openid-configurationand/.well-known/oauth-authorization-server: discovery, when the issuer has no path;/.well-known/jwks.json: the public signing keys;/oauth/authorizeand/oauth/token.
Your deployment's discovery document lists the endpoints it serves.
Use GET /_healthcheck for liveness and GET /readyz for readiness. Readiness returns 503 while a dependency such as Redis is down. Take that instance out of traffic until the dependency recovers; restarting it does not help.
When it is loaded, the Prometheus publisher serves /metrics (change the path with metrics-prometheus.path). Restrict access to it at your proxy. Logs are JSON lines on standard output; set the level with kernel.logging.level.
Choose audit receivers with audit.receivers. log is built in; stdout and http need their receiver modules (audit-stdout, audit-http). Delivery is best effort, so watch auth_audit_events_lost_total. The audit event reference lists every event and its attributes.
SIGTERM and SIGINT shut the app down gracefully within kernel.stop-deadline (30s by default). Give your orchestrator a longer grace period.
The operations runbook covers deployment, rotating the signing and sealing keys, upgrades, revoking users and alerts.
Find the contract you need in the owner map, then read that module's README.
Define your module with defineModule from @o3co/auth-provider-foundation/kernel, and add it to the module list that the generated src/main.ts passes to assemble. The recipe for a grant of your own walks through one, as a package of its own, built against the published packages. The kernel README describes the module API.
To replace an implementation, such as the store or the user ledger, implement its contract and run the contract tests published at its testing subpath, for example @o3co/auth-provider-foundation/store/testing.
For modules inside this repository, the combinations README explains how a module's manifest declares adapters and the modules that connect it to others.
Assembling without the generator. If you keep your own composition root instead of a generated project:
- Install
@o3co/auth-provider-scaffoldand get the module names from@o3co/auth-provider-scaffold/combinations:composedfor a selection, orneededfor the modules you list. Both refuse a name they do not know. - Pass the names to
published. For each, it gives the package to depend on and the path to import the module from (<entry>/module). - Add those packages to your
package.jsonat the scaffold bundle's version (every bundle of a release shares it), with the one holding@mod/kernel(published(["@mod/kernel"])) forassemble. - If the names hold no log sink, add one, such as
@mod/log-stdout; without one, log lines go nowhere. - Boot it. Where boot is refused for a setting only you can decide, such as
tokens.subject-check = nonewhen nothing checks subjects, set it in your configuration.
The generator picks the packages your app needs. Published packages are named @o3co/auth-provider-<concern>, share one version and expose each module as a subpath. The migration guide maps the 0.17 packages to their 0.18 replacements.
Before 1.0, a minor release may break compatibility, and so may a new release candidate. Read the migration guide and the changelog before you upgrade.
Security fixes are released for the latest 0.18.x only; a fix found in a release candidate lands in the next one. Report a vulnerability privately with Report a vulnerability on this repository's Security tab, as SECURITY.md describes. Please don't open a public issue.
Use Node.js 24 or later and the pnpm version in the repository's package.json (corepack enable installs it):
pnpm install
pnpm checkpnpm check runs type checking, lint, unit tests, the guard and the distribution check. pnpm test:integration runs the tests that need Docker.
Modules live under modules/<group>/<name>, and each module's README is its contract. Find the module responsible for your change in the owner map. The guard checks dependency direction, each package's public surface and module size.
Read CONTRIBUTING.md before you open a pull request, and target develop. Maintainers cut releases as docs/releasing.md describes.
- auth: the umbrella repository, with the architecture and end-to-end tests across components.
- auth.policy-verifier: the policy engine that makes authorization decisions.
- auth.proxy: an optional reverse proxy that validates tokens in front of services.
- protobuf.interceptors: authorization inside gRPC and ConnectRPC services, using this provider's introspection and the policy verifier's decisions.
Copyright 2026 1o1 Co. Ltd. Licensed under the Apache License, Version 2.0.
