FoxAuth
dev.foxauthPublished by foxauth.devOfficial publisherv1.5.0Updated Nov 5, 2026
Administer a FoxAuth OAuth 2.1 / OpenID Connect server: projects, apps, users, keys and settings.
Overview
Administer a FoxAuth OAuth 2.1 / OpenID Connect server over MCP: projects, apps, users, signing keys and settings.
- What you can do
- Manage projects, teams, OAuth clients and APIs, administrators, end-users and groups
- Manage upstream identity providers, SCIM provisioning connections, SMTP and server settings
- Generate, promote and retire signing keys without a restart
- Read the append-only audit trail and error store
- Work with the same management API the admin console uses, as an OAuth 2.1 protected resource
- When to use it
- Useful when an assistant should operate a FoxAuth identity deployment directly — creating or adjusting OAuth clients, managing users and groups, rotating signing keys or changing settings — instead of doing it by hand in the admin console.
- Requirements
- A remote streamable HTTP endpoint at auth.foxauth.dev, reached with OAuth authentication (required). The server's MCP control plane is off by default and must be enabled in the console under Settings → Administration. The assistant acts with the authority of the administrator who authorized it.
Installation
In SourceWeft
- Open FoxAuth in the dashboard and add it to a workspace.
- Connect the required credentials. They are stored privately and never shown on public pages.
- Enable the server for the chats that should use its tools.
Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.
Use with an agent
Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.
Read https://sourceweft.com/mcp/dev.foxauth%2Fadmin/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.Other MCP clients
Add this to your client's mcpServers config. The client completes OAuth sign-in on first connect.
{
"mcpServers": {
"foxauth": {
"type": "http",
"url": "https://auth.foxauth.dev/mcp"
}
}
}README
OAuth-server.ts
Source-available OAuth 2.0 and OpenID Connect authorization server — built with Bun and TypeScript.
[CI status] [Coverage status] [Security scanning status] [OpenSSF Scorecard]
Website · Features · Docs · Compare · OAuth 2.0 · OpenID Connect · Bun · FSL-1.1-ALv2 License
OAuth-server.ts is a source-available (FSL-1.1-ALv2), standards-compliant authorization server written in TypeScript and powered by Bun and Elysia. It implements OAuth 2.0 and OpenID Connect from the ground up, giving you complete control over your identity infrastructure — with no vendor lock-in.
Features
Most protocol capabilities are governed by a named feature flag, and a flag that is off means the capability's endpoint is not served at all. So this list is split by what you get on a default install versus what you switch on deliberately — the distinction is load-bearing, not editorial.
Out of the box
- Authorization Code flow with PKCE — PKCE is mandatory for every client, public or confidential, per OAuth 2.1
- OpenID Connect — OIDC Core 1.0 with ID tokens, UserInfo endpoint, and discovery
- Authorization-response issuer identification —
isson every authorization response (RFC 9207) - Resource Indicators — audience-restricted tokens via
resource(RFC 8707) - Refresh Token grant — with rotation and reuse detection; offered while
offline_accessis among the supportedscopes, which it is by default - RP-initiated logout — by GET or form POST, with a confirmation step
- JWT tokens — access and ID tokens signed with the algorithms your key store holds;
bun run db:setupprovisions an initial RS256 key, and ES256 and EdDSA keys are supported. Keys live in the database, are generated on first run, and are published at/jwks— and each project with an address of its own publishes its own at its ownjwks_uri - Database-backed clients — clients live in this server's own store and are created through the admin API, dynamic registration, or
bun run db:setup. There is no static client configuration file - Administration console — projects, teams, OAuth clients and APIs, administrators, end-users and their groups, upstream identity providers, SCIM provisioning connections, settings, SMTP, signing keys and an append-only audit trail
- End-user self-service — email verification and password reset, each with per-identity attempt and cooldown caps
- TOTP second factor — per-project, optionally enforced for the admin console (RFC 6238)
- Pairwise subject identifiers — per-sector
subvalues, salted from the database - Per-origin rate limiting — tiered by route class, on by default
- Sign-in brute-force throttle — persisted per-address failure counters with escalating lockouts, and no way to tell a throttled refusal from a wrong password
- Security headers — HSTS,
Permissions-Policy, framing and content-type protections - CORS closed by data — an origin is readable only if it is listed on the project owning the calling client
- Pluggable storage — adapter architecture with MongoDB, PostgreSQL and in-memory implementations
- Consent & login UI — built-in authentication and consent screens (React + Ant Design)
- Scope-based access control — fine-grained, per-client scope enforcement
- Extensible — 31 named behaviour seams (account lookup, interaction policy, refresh-token rotation, resource resolution, pairwise identifiers, RAR handling and more) replaced at call time via
addons.override()
Opt-in capabilities
Implemented, and off until the flag is set. Set them in the console under Settings or on
ApplicationConfig; a change saved in the console applies at once.
Quick Start
With Docker Compose
Prerequisites: Docker with Compose v2.
That starts MongoDB, provisions the schema and the admin panel (an idempotent one-shot step), and
serves the server at http://localhost:3000. Open http://localhost:3000/admin — the first visit
shows a one-time setup screen that creates the initial super administrator. Discovery is at
http://localhost:3000/.well-known/openid-configuration. Set ISSUER in docker-compose.yml to its
final public URL before this first docker compose up: the seeded admin client's redirect URI is
derived from it once, and a later change is not repaired by re-running setup.
Images are published to ghcr.io/redfox-soft/oauth-server-ts on every release, tagged with the
version and latest. To upgrade, pull and run docker compose up again; the setup step re-runs and
is safe to repeat.
From source
Prerequisites: Bun v1.3+ and a running MongoDB or PostgreSQL instance.
Schema provisioning creates the initial signing key, so no key configuration is required. If the server ever starts against an empty key store it also generates and persists one automatically.
Admin panel provisioning
bun run db:setup also provisions the admin panel: the reserved "Administrators" project and the
first-party admin-panel OAuth client. The seed is idempotent — re-run bun run db:setup after
upgrading an existing install so the admin client and project exist.
On first visit to /admin, a one-time setup screen creates the first administrator and makes them a
member of Super administrators; the setup route is closed once that team has an active member. The admin panel requires a database-backed deployment, MongoDB or PostgreSQL
(the in-memory adapter does not persist seeded data across restarts).
Configuration
Keyring
Secrets the server must read back (application secrets, sign-in provider credentials, the SMTP password,
private signing keys, authenticator secrets) are stored encrypted with the instance keyring. Supply it as
KEYRING or a keyring file; without one, first-run setup generates it, writing it to keyring/ only when
that directory exists, and shows it once. Keep it in a secrets manager and back it up apart from the
database: a backup cannot be read without it. bun run keyring add and bun run keyring write <kid> rotate
it in two steps.
Signing keys
Signing keys are stored in the database, not in an environment variable — every issuer's in the
bucketKeys area, the instance's included. The initial RS256 signing key is created when you
provision the schema (bun run db:setup); the server also generates and persists one automatically
if it starts against an empty store. Rotation needs no restart: generate a key (it is published
first), promote it to sign once every instance serves it, and retire the old one, which keeps
verifying for a day — in the console, the admin API, or over MCP (jwks_generate, jwks_promote,
jwks_retire). One key signs per algorithm, so RS256 and PS256 can sign side by side.
Rate limiting
Requests are counted per calling origin and refused with 429 once an origin exceeds its allowance
inside a window. The refusal happens before the endpoint does any work, and carries Retry-After
with the seconds remaining; the body is the standard OAuth error shape, the admin plane's own shape,
or an HTML page, depending on which surface was addressed. No RateLimit-* headers are emitted —
they would tell a caller probing for the threshold exactly where it is.
Allowances are tiered by route class rather than being one blanket number, because a limit tight enough to protect the token endpoint would refuse the console's asset burst:
All of it is settable from the admin console under Settings → Rate limiting, or directly on
ApplicationConfig. A change saved in the console applies at once, like every other server setting.
The server refuses to start if any of these is not a positive integer, rather than serving with limiting silently absent or silently total.
Who a request is counted against is decided once, by clientAddress.source: a proxy header is believed only
on a connection from a proxy the choice trusts, because a caller can write any header. automatic (the default)
fits a server reached directly and one behind nginx, an Ingress or a load balancer on a private network, and reads
X-Forwarded-For from the right; fly reads Fly's own Fly-Client-IP (the shipped fly.toml needs it);
cloudflare and listed add Cloudflare's ranges or your own proxy addresses; direct believes no header. The
console card under the setting shows which address the server sees for you. See
Behind a reverse proxy.
Two properties to know before relying on this. Counting is per instance and held in memory, so with N machines serving concurrently the effective allowance is N times the configured value, and an instance restart clears every counter. This is a resource protection — it bounds what one source can cost the server — and not a security boundary. The limits that must hold absolutely are the per-identity throttles: verification codes cap at five attempts, resends and password-reset requests are cooldown- and daily-capped, and the password sign-in door has the brute-force throttle below — all independently of anything here.
Tuning. Raise strict.max if a legitimate server-to-server integration behind one address, or
many users behind one corporate NAT, start seeing refusals. Raise public.max if the admin console
stutters while loading. Lower maxTrackedOrigins under memory pressure. Set rateLimit.enabled to
false to switch the whole thing off during an incident; it applies on save.
Sign-in brute-force throttle
Failed password attempts are counted per project and address. Once an address reaches the cap, the sign-in door is shut for that address and refuses every further attempt — including one carrying the correct password — until the window ends. Each further exhaustion shuts it for longer, doubling to the ceiling: 15 → 30 → 60 minutes by default, which holds a sustained attack to roughly 120 guesses a day. The counter is forgotten after 24 hours without a failure.
The refusal is the ordinary "invalid username or password" page. It never says that a throttle exists, how many attempts remain, or when the door reopens, because saying any of it would tell an attacker that the address they are guessing is real. For the same reason attempts are counted for addresses that have no account at all — a counter's existence means somebody typed that address, nothing more. And because the refusal happens before the account lookup, a refused attempt costs no password hashing: a flood against one address stops being a CPU cost as well as stopping being a guessing opportunity.
Unlike the per-origin limiter above, these counters are persisted, so the limit holds across restarts and across every machine serving concurrently. That is what makes this a security boundary rather than a resource protection — and why there is deliberately no switch to turn it off.
Two counters are cleared immediately rather than waiting out the window: a password that verifies, and a completed password reset. The second is the escape hatch — consuming the emailed secret proves control of the address, which is exactly what an attacker guessing passwords does not have — so a locked-out user is never stuck waiting. Merely requesting a reset clears nothing.
A project that requires a one-time code stays at the first window however often it is tripped: a guessed password there does not sign anyone in, so the deeper lockouts buy little. This matters most for the reserved Administrators project, whose operators sign in through this same door and have no self-service reset to escape with — their worst case is a 15-minute wait, and the way to make that door properly hard is to require the second factor on it (Admins → Require an authenticator app).
Docker
Published image:
Run bun run db:setup against the same database once before the first start (the Compose file
above does this for you).
Every published image is signed over its digest, and carries an SBOM and a build provenance attestation. The signing is keyless, so verification names the workflow that is allowed to have produced it rather than a key:
Reading the SBOM, the build record and the signed SLSA provenance is three more commands.
To build locally instead:
Endpoints
Most endpoints are governed by a feature flag, and a flag that is off means the endpoint is not
served at all — not merely that discovery stops advertising it. The refusal is deliberately
indistinguishable from a path the server does not have, so if a request below returns 404, check the
flag before suspecting a defect. Flags are set in the admin console under Settings, or directly on
ApplicationConfig, and a change saved in the console applies at once.
Always available
Enabled by default
Flag-governed, but on unless you turn them off.
Opt-in
Absent from a default install until the flag is set.
Reading your own registration follows registration.enabled rather than
registrationManagement.enabled: the registration response hands the client that URI, so refusing the
read while registration is on would break the capability that issued it. Only the mutating methods
need the management flag.
Route families
Reached by redirect or through the console rather than called directly, so these are described as families rather than enumerated.
The interaction and self-service families are unconditional by design — a user locked out of their
account cannot be asked to wait for a capability toggle. The console is unconditional too, even though
/mcp is gated: switching MCP off must not take the console with it.
Administration
The server administers itself. GET /admin is a console for projects, teams, OAuth clients,
administrators, end-users, upstream identity providers, settings, signing keys and an
append-only audit trail. Administrators sign in through an OpenID Connect flow against this server's
own issuer, so there is no second password store.
[The administration console: a project's OAuth clients]
Administration from an AI agent (MCP)
The same management API is available to an AI agent over the Model Context Protocol, as an OAuth 2.1 protected resource of this server. It is off by default — the capability hands an agent the authority of the administrator who authorized it, so a deployment should switch it on deliberately:
- In the console, Settings → Administration → Enable the administrative MCP control plane (it applies on save).
- Point your MCP client at
https://your-server/mcpwith client idadmin-mcpand resourcehttps://your-server/mcp.
An agent then acts as the administrator who signed in, with exactly that account's permissions. Every change it makes runs through the same routes, the same checks and the same audit trail as the console's, and each audit entry names both the operator and the agent.
A few things it deliberately cannot do: delete a project or a team, purge the fault store, and change which client identities may administer the instance. The deletions destroy a container with nothing left afterwards to inspect, and the last is a privilege-escalation path, so they stay console-only. Everything else destructive takes two steps — the agent describes what would change and you confirm that specific operation.
Which client identity works here. The reserved
client_idabove always works and needs no setup beyond the toggle. An agent host that cannot be told aclient_idcan use this surface too, provided it identifies itself with an OAuth Client ID Metadata Document — an HTTPS URL naming a document that describes it — and a super administrator has named that identity under Agent access in the console. A client that registered itself through Dynamic Client Registration cannot administer the instance under any configuration: its identity is minted on demand by whoever asked for it, so there is nothing an operator could meaningfully allowlist.
Protecting your own MCP server
The same protocol surface works for an MCP server that is not this one. Declare it as a protected
resource of a project — its canonical URI, the scopes it recognises, how its tokens are verified —
and this server mints tokens whose audience is exactly that resource, with no code change here and no
restart. An agent host discovers it, signs your end-users in, and comes back with a token your MCP
server verifies against the jwks_uri its issuer's metadata names, without asking anything.
Protect your MCP server with OAuth walks it end to end — including what a correct refusal looks like, so you can tell "protected" from "not reached yet".
Implemented Standards
The Flag column names the capability's governing flag; — means always on. See
Features for defaults.
Security
Report a vulnerability to [email protected] or through private vulnerability reporting; SECURITY.md has the scope, the response times and the safe harbour.
The same file says what stands behind the code: a published threat model, CodeQL, dependency and container scanning on every push and every week (what runs and where to read it), signed images with an SBOM and build provenance, and an OpenSSF Scorecard. It also says what does not exist yet — no external audit, no paid bounty, no OpenID Foundation certification — because a security page that only lists the good news is not one you can trust.
Contributing
Questions and ideas go to GitHub Discussions: Q&A for how-do-I, Ideas for proposals.
Contributions are welcome! Please open an issue or pull request. For significant changes, open an issue first to discuss your proposal.
License
OAuth-server.ts is licensed under the Functional Source License, Version 1.1, ALv2 Future
License (FSL-1.1-ALv2) — the same license Sentry uses. In short: you may use, modify,
self-host and redistribute it for any purpose except offering it to others as a competing hosted
service, and each version converts to the Apache License 2.0 two years after its release.
This project began in April 2025 as a fork of node-oidc-provider by Filip Skokan, distributed under the MIT License. Its notice is preserved in NOTICE.
Source: README.md at commit 4af4e42
Tools
0Version history
5- v1.5.0LatestNov 5, 2026
- v1.4.0Nov 3, 2026
- v1.3.0Oct 22, 2026
- v1.2.0Oct 21, 2026
- v1.1.0Oct 19, 2026
Score
75% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 daysRequired25 pts
- Auth declaredRequired10 pts
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts
Declare a license in the repository or the registry entry.