Set up OAuth, step by step¶
Authenticate callers tells you which variables to set. This guide covers the other half: how to create the provider config they point at, click by click.
Read it if OAuth isn't your home turf yet. There's a copy-paste prompt at the end if you'd rather hand the whole thing to a coding agent.
We'll use Auth0 as the worked example. It's the most common starting point, and the steps map to any provider (see Other providers).
The mental model¶
Three sentences and you've got it:
- Pontifex is a resource server. It validates a token. It never logs anyone in.
- Your IdP (Auth0) does the login and mints the token, stamped with an audience and a list of permissions.
- The client (Claude Desktop, your agent) gets that token and sends it. Pontifex checks the signature, the audience, and the scopes, then runs the tool.
The setup, then: tell Auth0 about your API, define the permissions, and copy six values into your environment.
Before you start
You need an Auth0 tenant (the free tier is fine) and your Pontifex server's public
URL, call it https://your-server.example. Have your tool scopes in mind, in
namespace:resource:action form (e.g. orders:order:read).
Step 1: create the API¶
In Auth0, an "API" is your resource server.
- Auth0 Dashboard → Applications → APIs → Create API.
- Name: anything (e.g.
Pontifex). - Identifier: a URL-like string that becomes your audience. Use your server URL:
https://your-server.example. It's an identifier, so it doesn't have to resolve. - Leave the signing algorithm as RS256. Pontifex accepts asymmetric signatures only, and RS256 is one.
The Identifier you typed is your AUTH_AUDIENCE. Keep it handy.
Step 2: define your permissions¶
These are your scopes. They have to match what your tools require, exactly.
- Open the API → Permissions tab.
- Add one row per scope, in
namespace:resource:actionform:
| Permission | Description |
|---|---|
orders:order:read |
Read a single order |
orders:*:read |
Read anything in orders |
Add every scope your tools check. (Recap of the scope model: Errors & scopes.)
Step 3: put the permissions in the token¶
By default Auth0 won't include permissions in the access token. Turn that on.
- API → Settings tab.
- Enable RBAC.
- Enable Add Permissions in the Access Token.
- Save.
Now every token Auth0 mints carries a permissions array, the claim Pontifex reads.
That's why AUTH_SCOPES_CLAIM=permissions for Auth0.
Step 4: register the client¶
The client is the app the human logs into: Claude Desktop, or your own agent.
- Auth0 → Applications → Applications → Create Application.
- Pick Native for a desktop client like Claude Desktop (or Single-Page for a browser app).
- In the application's settings, note the Client ID. That's what you give the client to connect.
Why pre-register?
Some MCP clients can self-register via Dynamic Client Registration. Auth0 restricts self-registered clients from custom APIs, so the reliable path is this one pre-registered app. Pontifex works with either. It validates the resulting token. (Background: authenticating callers.)
Step 5: grant permissions to users¶
A user only gets a scope in their token if they're assigned it.
The quick way: User Management → Roles → Create Role (e.g. orders-reader), add
the permissions from Step 2, then assign the role to your user. For a first test you can
assign permissions to a single user directly.
Step 6: fill in the environment¶
You now have every value. Each one comes from a step above:
# Replace your-tenant and the region to match your Auth0 namespace.
AUTH_ISSUER=https://your-tenant.us.auth0.com/ # trailing slash matters
AUTH_JWKS_URL=https://your-tenant.us.auth0.com/.well-known/jwks.json
AUTH_AUDIENCE=https://your-server.example # the API Identifier from Step 1
AUTH_SCOPES_CLAIM=permissions # Auth0 puts scopes here
AUTH_AUTHORIZATION_SERVER=https://your-tenant.us.auth0.com/
PUBLIC_BASE_URL=https://your-server.example # your server's real public URL
Setting the AUTH_* group is what turns the OAuth path on. Restart the server.
Verify it works¶
Three checks. The first two need no client.
1. Discovery is advertised.
You should see your authorization_servers pointing at the Auth0 namespace.
2. Unauthenticated calls are challenged. A request with no token returns 401 with
a WWW-Authenticate: Bearer … header naming that discovery URL. MCP clients follow that
breadcrumb.
3. A real token carries the right claims. Grab an access token from Auth0's API → Test tab and decode it at jwt.io. Confirm:
audincludes yourAUTH_AUDIENCEpermissionslists the scopes you granted
If both are right, a scoped call will pass the scope check. If permissions is empty,
revisit Steps 3 and 5.
Hand it to a coding agent¶
To skip the clicking, paste this to a coding agent that has access to your Auth0 tenant (the Auth0 CLI or the Management API). Fill in the three placeholders first.
Set up Auth0 as the OAuth provider for my Pontifex MCP server. Use the Auth0 CLI
(or Management API) against my current tenant. Goal end-state:
1. An Auth0 API (resource server) named "Pontifex" with
Identifier = "https://your-server.example", signing alg RS256.
2. These permissions defined on that API: orders:order:read, orders:*:read
(replace with my actual tool scopes).
3. On that API, enable RBAC and "Add Permissions in the Access Token"
(token_dialect = access_token_authz) so tokens include a `permissions` array.
4. A Native application named "Pontifex MCP Client"; report its Client ID.
5. A role "orders-reader" holding those permissions, assigned to user
<my-email@example.com>.
Then output a .env block with exactly these six variables, filled in from the
tenant: AUTH_ISSUER, AUTH_JWKS_URL, AUTH_AUDIENCE, AUTH_SCOPES_CLAIM (=permissions),
AUTH_AUTHORIZATION_SERVER, PUBLIC_BASE_URL (=https://your-server.example).
Don't print any client secret. Confirm each step as you complete it.
The prompt states the end-state, not brittle flags, so a capable agent picks the right commands and you stay provider-correct.
Other providers¶
The same six variables work everywhere. Only three values change shape:
| Auth0 | Microsoft Entra | Keycloak | |
|---|---|---|---|
AUTH_ISSUER |
https://TENANT.auth0.com/ |
https://login.microsoftonline.com/TENANT/v2.0 |
https://HOST/realms/REALM |
AUTH_JWKS_URL |
…/.well-known/jwks.json |
…/discovery/v2.0/keys |
…/protocol/openid-connect/certs |
AUTH_SCOPES_CLAIM |
permissions |
scp or roles |
a claim you map (e.g. roles) |
The rest of the flow is the same: create an API/app registration, define scopes as
namespace:resource:action, make sure they land in the configured claim, and assign them
to users. Switching providers is a config change, not a code change.
Next¶
- Issuing
sk_…keys for scripts and CI: Authenticate callers. - What every variable means: Configuration.
- Why the strict validation matters: Security model.