Authentication and authorization are a core part of nearly every web application, and it is also one of the areas that changes most often. Developers regularly update token expiry settings, callback URLs, or API permissions for new features. The changes are usually small, but the workflow around them is not. You end up bouncing between your editor, the Auth0 dashboard, and the docs just to push through a minor update. Increasingly, that editor is a coding agent like Claude Code, which raises the question: what if you could handle those updates without ever leaving Claude CLI?
Claude Code is a terminal-based AI agent (also available as a VS code extension) that you can pair with the Auth0 MCP Server, which exposes Auth0's Management API as native third-party tools. This pairing gives you an assistant that can scaffold, refactor, and update authentication integrations with full context across your codebase and Auth0 tenant. But giving an AI agent access to sensitive authentication infrastructure raises a fair question: "How do you do that without opening up new security risks?"
In this tutorial, you will set up Claude Code with the Auth0 MCP Server, create a scoped machine-to-machine credential flow and set up security guardrails that let an AI agent interact safely with your authentication layer.
How Claude Code and Auth0 MCP Work Together
Based on your goal, Claude Code works through multiple steps (like reading/writing code, running shell commands etc.) autonomously, and asks for confirmation at decision points that warrant human review.
The Auth0 MCP Server implements the Model Context Protocol (MCP) to expose Auth0's Management API as a set of tools that any compatible AI agent can invoke directly. Instead of constructing raw HTTP requests or relying on potentially stale training data about APIs, Claude Code calls well-defined tools with typed inputs and outputs. The MCP Server handles calls to the Management API, and the agent operates strictly within the permissions you grant. If you run it in the read-only mode, the coding agent will not be able to make any changes in your Auth0 tenant.
Together, they bring authentication workflows closer to your code. Claude Code can understand both your application and your Auth0 tenant, then work across them in a single session.
Setting Up the Auth0 MCP Server with Scoped Access
In addition to Claude Desktop, Cursor, and Windsurf, Auth0 MCP server also works with Claude seamlessly.
Prerequisites
You will need the following to complete this tutorial:
- Node.js v18 or higher (
node --versionto check) - Claude Code CLI installed
- An active Auth0 account with administrative permissions on the target tenant
- Your tenant domain handy (for example,
dev-xxxx.us.auth0.com) - Demo code from this GitHub repo for your reference (optional).
Install and initialize
Create a dedicated working directory:
mkdir Claude-Code-With-Auth0-MCP && cd Claude-Code-With-Auth0-MCP
Then initialize the Auth0 MCP Server:
npx @auth0/auth0-mcp-server init
While this tutorial selected all MCP scopes for the demo, you should only use the scopes you actually need. After install and initialization, it kicks off a browser-based Auth0 authentication flow as shown below:

Confirm the code displayed in the browser is same as the one shown in your terminal:

Grant requested permissions:

Once authorized, credentials are stored securely in your OS keychain, never in plain text or project files.
Why use device authorization flow instead of static secrets
The Auth0 MCP Server uses the OAuth 2.0 Device Authorization Flow (RFC 8628) rather than a static API key or client secret. Static secrets have to live somewhere (usually a config or .env file), which risks exposing them in source control. OAuth access tokens sidestep this: they represent a delegated authorization, carry restricted scopes, and expire automatically. The Device Authorization Flow adds a human login step to token issuance, giving you an audit trail tied to an interactive session and instant revocation from the Auth0 dashboard. The MCP server stores the resulting token in your OS keychain, so it never touches source control.
The one trade-off is that the flow requires a browser-based login step, so it is unsuitable for CI pipelines. For non-interactive access, a scoped M2M application with client credentials is more suitable.
Scoping the MCP server
The Auth0 MCP Server grants no scopes by default. You request them explicitly at init time using the --scopes flag. For this tutorial demo, all scopes were selected during init. For a more targeted setup, you can specify exactly what you need::
# Grant all read permissions npx @auth0/auth0-mcp-server init --scopes 'read:*' # Grant a targeted mix of permissions npx @auth0/auth0-mcp-server init --scopes create:clients,update:actions'
Scopes map directly to Management API operations. For example, read:clients lets the agent call auth0_get_application, while create:actions lets it call auth0_create_action. Some scopes carry significant implications: update:actions can push custom code into production, and read:logs exposes detailed user activity and authentication events. See the scopes reference for more detail.
For this tutorial, all scopes were granted during init for the demo. However, while coding your applications, you must follow the principle of least privilege and grant only what your workflow actually needs.
Claude Code and Auth0 integration
To integrate the Auth0 MCP Server with Claude Code, run the following command from within your Claude-Code-With-Auth0-MCP directory:
$ claude mcp add auth0 -- npx -y @auth0/auth0-mcp-server run
You should see output similar to the following:
Added stdio MCP server auth0 with command: npx -y @auth0/auth0-mcp-server run to local config File modified: /home/<user-name>/.claude.json [project: /path-to/Claude-Code-With-Auth0-MCP]
This adds the Auth0 MCP Server configuration block to your ~/.claude.json:
"mcpServers": { "auth0": { "type": "stdio", "command": "npx", "args": ["-y", "@auth0/auth0-mcp-server", "run"], "env": {} } }
Alternatively, you can create a configuration JSON manually and register it with claude mcp add-json.
Credentials are not stored in this JSON. The MCP server reads your token from the OS keychain at startup. To enable debug logging, add "DEBUG": "auth0-mcp" to the env block.
Verify your integration
From your Claude-Code-With-Auth0-MCP directory, launch claude CLI and run /mcp to confirm auth0 is listed with a tool count. Then try this read-only prompt:
Show me all applications in my Auth0 tenant.
On the first Auth0 tool call, Claude Code will ask for your permission. You can approve it once or allow it permanently for the project.

Giving Claude Code Project Context with CLAUDE.md
CLAUDE.md is a markdown file at your project root that Claude Code reads at the start of every session. It tells the agent what kind of project it is working in, what constraints apply, and where the sensitive files are. Without it, Claude Code might make technically correct changes that are contextually wrong, like updating a callback URL shared across environments or granting a broader scope than your security policy allows.
A good CLAUDE.md for an Auth0 project should cover:
## Auth0 Context - Active tenant: dev-xxxx.us.auth0.com (development only) - Auth-sensitive files: src/auth/config.ts, .env.local - Existing applications: one SPA (authorization code flow with PKCE), one M2M (client credentials) - Permitted scopes: read:users, update:users - Hard constraints: never modify production callback URLs without explicit confirmation
With this in place, Claude Code operates with the same guardrails a human teammate would naturally apply. See the demo project's CLAUDE.md for a practical example.
Refactoring an Auth0 Integration from Claude CLI
To demonstrate this workflow in practice, this example uses a minimal Flask app with a single protected endpoint (/api/protected). It validates Auth0 JWT tokens using python-jose. It works, but it has several problems common in real codebases: JWKS fetched once at startup and never refreshed, no kid matching when selecting a signing key, a bare except that swallows all validation errors identically, and Auth0 config stored as module-level globals.
# JWKS fetched once at startup — never refreshed JWKS = requests.get(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json").json() def validate_token(token): # No kid matching — always uses the first key regardless # of which key signed the token rsa_key = { "kty": JWKS["keys"][0]["kty"], "kid": JWKS["keys"][0]["kid"], "use": JWKS["keys"][0]["use"], "n": JWKS["keys"][0]["n"], "e": JWKS["keys"][0]["e"], } # No clock skew tolerance, no error differentiation return jwt.decode( token, rsa_key, algorithms=ALGORITHMS, audience=API_AUDIENCE, issuer=f"https://{AUTH0_DOMAIN}/" )
You can view this full (suboptimal/naive) implementation on GitHub that we will fix and improve iteratively with Claude Code and Auth0 MCP.
Auth0 setup and protected API
On the Auth0 side, you need one API (resource server) registered with an identifier (https://mh-test-api.example.com. This identifier URI should be unique in your app, but it does not need to resolve).

When you register an API, Auth0 automatically creates a test M2M application named "[Your API Name] (Test Application)," already authorized against your API. You will find it under Applications.

That is the minimum setup needed for this demo for Claude Code to audit and provision against something real.
Obtaining an Auth0 token
Use this cURL command to obtain your Auth0 token:
curl --request POST --url "https://YOUR_AUTH0_DOMAIN.us.auth0.com/oauth/token" --header 'content-type: application/json' --data '{ "client_id":"<client-ID>", "client_secret":"<client-secret>", "audience":"https://mh-test-api.example.com", "scope":"", "grant_type":"client_credentials" }' {"access_token":"eyJhbGc...","expires_in":86400,"token_type":"Bearer"}
Calling your protected API
Download the starter code into your Claude-Code-With-Auth0-MCP directory, create a virtual environment (python3 -m venv ./.venv), populate .env file, install dependencies (pip install -r requirements.txt).
Run the Flask server:
python app.py
Then call the protected endpoint with your Auth0 token:
TOKEN="eyJhbGc..." curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/protected
You should see a response like this:
{ "message": "Access granted to a protected resource", "user": "UFIv...@clients" }
Running the audit prompt
The naive implementation appears to work at this point, but we can double-check it with Claude Code to ensure there are no errors and nothing crucial is missing.
Inside the claude session connected to Auth0 MCP, this prompt was run:
Claude "Audit" Prompt: ----------------------- Review app.py and audit the Auth0 JWT validation setup. Cross-reference it against our actual tenant configuration using the Auth0 MCP tools. Identify any mismatches between what the tenant is configured to issue and what the app is validating against, and list all security or reliability problems you find. Do not make any changes yet.
Claude Code reviewed the codebase, asked for relevant permissions (human in the loop) and showed this:

Using the Auth0 MCP Server, Claude Code pulled live tenant data and cross-referenced it against app.py directly for this audit. The most critical finding would never surface in static analysis: the tenant is currently publishing two rotating RS256 signing keys, while the app validates every token against only the first one. A linter can flag the missing kid matching logic, but a live auth0_list_applications call reveals that key rotation is already in progress and valid tokens are failing right now. The remaining findings cover robustness and hardening gaps, but this one mismatch makes the case for Auth0 MCP integration clearly: live tenant access surfaces the subtle bugs that static code review would miss.
Here is Claude Code audit summary:
Claude "Audit" Response: ------------------------- ※ recap: Audited app.py's Auth0 JWT validation against the live tenant and found the key bug: the app validates every token against only the first JWKS key while the tenant publishes two, plus reliability issues. No changes made yet; next action is drafting fixes if you want them. (disable recaps in /config)
Importantly, Claude Code made no changes during this step as instructed.
Running the provision prompt
Once the audit was complete, the provision prompt asked Claude CLI to create a new M2M application ("Claude-Auth0 MCP API Client") and a read:data scoped client grant in a single instruction:
Claude "Provision" Prompt: --------------------------- Create a new M2M application in our Auth0 tenant called "Claude-Auth0 MCP API Client". Set the token lifetime to 3600 seconds. Then create a client grant authorizing it against our API audience (https://mh-test-api.example.com) with `read:data` scope only. Use the credentials returned to update the `.env` file. Do not touch app.py yet.
Before proceeding, Claude Code flagged a real Auth0 misconception worth noting: token lifetime is a resource server setting that applies to all clients of an API, not a per-application property. Rather than applying a silent global change, it surfaced this as a clarifying question with explicit options. That pushback, grounded in live tenant data, is exactly what makes Auth0 MCP integration useful.

After confirming the token lifetime should remain unchanged and approving the MCP tool calls, Claude Code invoked auth0_create_application and auth0_create_client_grant in sequence, updated .env with the returned credentials (client ID and secret), and left app.py untouched as instructed.

The Auth0 dashboard confirmed the new Claude-Auth0 MCP API Client application was added:

And the API confirmed the read:data permission was in place:

Here is how Claude Code summarized this session:
Claude "Provision" Response: --------------------------- ※ recap: Goal: secure the Auth0 JWT setup. Audited app.py (key bug: ignores kid, breaks on rotation) and created the M2M app, read:data scope, grant, plus .env credentials. Next action: fix app.py's validation issues when you're ready. (disable recaps in /config)
Running the implementation prompt
This prompt gave Claude Code a single instruction to fix everything surfaced in the audit:
Claude "Refactor" Prompt: --------------------------- Now refactor `app.py` to fix all the issues found in the audit. Use the credentials and tenant configuration we just provisioned: fetch the JWKS URI, issuer, and audience dynamically from the tenant rather than hardcoding them. Fix the kid matching, add proper error handling, and make sure the app stays in sync with what the tenant is configured to issue.
With the audit findings and provisioned credentials already in context, Claude Code asked for confirmation as specified in CLAUDE.md, then rewrote app.py without needing to be walked through each fix individually.

The refactored code introduced an OIDCProvider class that discovers the issuer and JWKS URI dynamically from the tenant's OIDC discovery document, caches the JWKS with a configurable TTL, and handles key rotation by retrying with a forced refresh on an unknown kid. The minimal except was replaced with typed exception handling that differentiates expired tokens, claim mismatches, and signature failures. Scope enforcement was added via a require_scope decorator (see refactored code: app.py):
def get_signing_key(self, kid): """Return the JWK matching ``kid`` (audit F1), refreshing if stale/rotated.""" stale = (self._jwks is None or time.monotonic() - self._jwks_fetched_at > self._jwks_ttl) if stale: self._refresh_jwks() key = self._find_key(kid) if key is None and not stale: # kid unknown against a cache we didn't just refresh: keys may have # rotated. Refresh once and retry before giving up. self._refresh_jwks() key = self._find_key(kid) return key def validate_token(token): # ... try: return jwt.decode( token, rsa_key, algorithms=ALGORITHMS, audience=API_AUDIENCE, issuer=oidc.issuer, options={"leeway": LEEWAY_SECONDS}, ) except ExpiredSignatureError as e: raise AuthError("token_expired", 401) from e except JWTClaimsError as e: raise AuthError("invalid_claims", 401) from e except JWTError as e: raise AuthError("invalid_token", 401) from e def require_scope(required_scope): """Authenticate the request and enforce a scope (audit F8).""" def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): payload = validate_token(_get_bearer_token()) granted = payload.get("scope", "").split() if required_scope not in granted: raise AuthError("insufficient_scope", 403) g.jwt_payload = payload return fn(*args, **kwargs) return wrapper return decorator
A separate get_token.py utility was also produced for minting test tokens using the client credentials flow, correctly kept separate from app.py since a resource server should not hold a client secret.
This refactored demo can be run with:
TOKEN=$(python get_token.py) curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/protected
Which produces the expected response:
{"message":"Access granted to a protected resource","sub":"l5rDOCiobYJWeRiYEImEbA8FzFTHkqGc@clients"}
Throughout the refactor, Claude Code used the credentials it had just provisioned via the Auth0 MCP Server rather than asking for them again, keeping the codebase and tenant configuration in sync from the start.

Here is how Claude Code summarized the session:
Claude "Refactor" Response: ----------------------------- ※ recap: Goal: harden the Auth0 JWT validation in this Flask app. Done: audited app.py, provisioned the M2M client plus read:data grant, and refactored app.py to fix all findings, verified live. Next action: optionally migrate from python-jose to PyJWT, or stop here. (disable recaps in /config)
Observations: Claude Code with Auth0 MCP server
A few things stood out running this end-to-end. Audit findings were grounded in live tenant state, not static code analysis alone. The two-key rotation mismatch only surfaced because the Auth0 MCP Server pulled the actual JWKS from the tenant and compared it against what app.py was doing. Without that, the finding would have been "no kid matching logic" rather than "key rotation is already in progress and valid tokens are failing right now." That distinction matters in production.
The token lifetime clarification was similarly useful. Seeing token_lifetime: 86400 as a tenant-wide setting let Claude Code correctly flag that changing it would affect all clients, not just the new M2M application. A code-only agent would have missed that entirely.
The refactored code also went further than the prompt asked. The OIDCProvider, scope enforcement, and separation of get_token.py all emerged from the audit findings without individual prompting. Throughout, Claude Code asked for permission before invoking any Auth0 MCP tools, creating applications, or running bash commands.
That last point addresses the question raised in the introduction. Scoped tools, a CLAUDE.md that encodes your constraints, and consistent human-in-the-loop confirmation keep the agent working within a well-defined boundary. It has live tenant access, but only as far as you explicitly allow.
You can compare the naive implementation with the refactored implementation directly.
Security Guardrails
Giving an AI agent access to your Auth0 tenant is similar to giving a new team member API access. The same principles apply:
Keep the MCP server scoped to development tenants only. A practical way to enforce this is maintaining separate
CLAUDE.mdfiles per environment, each pointing to the right tenant domain. That way there is no path from a local Claude Code session to production credentials by default. If a session does need to touch production, make it a deliberate, separate setup with explicit sign-off.Treat the device auth token like any other API credential. After finishing a focused session, revoke it from the Auth0 dashboard rather than leaving it active. In fact, using a short access token lifetime helps reduce security risks. If your team uses Auth0 log streaming, pipe those logs to your monitoring tool of choice so MCP-driven Management API calls show up in the same audit trail as everything else.
Client secrets belong in
.env, not inCLAUDE.mdor inline prompts. It is easy to paste a secret into a prompt for convenience and forget it now lives in your shell history and potentially in Claude Code's session context. Let the MCP server handle authentication; your prompts should never carry credentials.
Code and Auth, in Sync
In this tutorial, you connected Claude Code to Auth0's Management API via the Auth0 MCP Server, built a scoped M2M credential flow, and refactored a suboptimal JWT validation setup to correctly handle key rotation, typed errors, and scope enforcement. The security principles applied throughout were practical: least-privilege scopes, device auth over static secrets, environment-specific configs, and keeping agentic changes in the normal PR review flow.
The entire workflow, from auditing a live tenant to provisioning an application to rewriting the validation code, happened in a single terminal session. The Auth0 dashboard was only needed to verify the changes Claude Code had made. Which answers the question we started with. You can give an AI agent access to sensitive authentication infrastructure securely with Auth0 MCP: secure access, scoped tools, and human-in-the-loop confirmation keep the agent within well-defined boundaries, without opening up new security risks.
Auth0's Management API exposes the full surface area of your tenant as programmable, auditable operations to any AI agent or automation you want to build on top of it. If you want to extend what you built here, the Auth0 MCP Server tools reference is a good next stop.
About the author

Manish Hatwalne
Architect
