Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive guide for building production-ready MCP servers with tools, resources, prompts, and React widgets using mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/authentication/custom.md
1# Custom & OAuth Proxy Authentication23Two factories cover everything that isn't a built-in provider:45- **`oauthCustomProvider`** — for identity providers that support **Dynamic Client Registration (DCR)** and advertise a `registration_endpoint` in their OAuth metadata. MCP clients register themselves directly with the upstream; your server only verifies tokens.6- **`oauthProxy`** — for providers **without DCR** (Google, GitHub, Okta, Azure AD, standard Auth0 apps, etc.). You register an app in the provider's dashboard, pass the fixed `clientId` / `clientSecret` here, and your server mediates the token exchange.78> Picking between them: if your provider lists a `registration_endpoint` at `https://<provider>/.well-known/oauth-authorization-server`, use `oauthCustomProvider`. Otherwise use `oauthProxy`.910---1112## `oauthCustomProvider` (DCR)1314Use this when your identity provider supports DCR and advertises a `registration_endpoint`. Clients discover the endpoints and register themselves against the upstream.1516```typescript17import { MCPServer, oauthCustomProvider, object } from "mcp-use/server";18import { jwtVerify, createRemoteJWKSet } from "jose";1920const JWKS = createRemoteJWKSet(21new URL("https://auth.example.com/.well-known/jwks.json")22);2324const server = new MCPServer({25name: "my-server",26version: "1.0.0",27oauth: oauthCustomProvider({28issuer: "https://auth.example.com",29authEndpoint: "https://auth.example.com/oauth/authorize",30tokenEndpoint: "https://auth.example.com/oauth/token",3132async verifyToken(token) {33const result = await jwtVerify(token, JWKS, {34issuer: "https://auth.example.com",35audience: "your-audience",36});37return { payload: result.payload as Record<string, unknown> };38},3940getUserInfo(payload) {41return {42userId: payload.sub as string,43email: payload.email as string | undefined,44name: payload.name as string | undefined,45roles: (payload.roles as string[]) || [],46};47},48}),49});5051server.tool(52{ name: "whoami", description: "Get authenticated user info" },53async (_args, ctx) =>54object({55userId: ctx.auth.user.userId,56email: ctx.auth.user.email,57})58);5960server.listen();61```6263### Configuration Options6465```typescript66oauthCustomProvider({67// Required: OAuth endpoints68issuer: "https://auth.example.com",69authEndpoint: "https://auth.example.com/oauth/authorize",70tokenEndpoint: "https://auth.example.com/oauth/token",7172// Required: must return { payload: Record<string, unknown> } or throw73async verifyToken(token) {74const { payload } = await jwtVerify(token, JWKS, { issuer: "..." });75return { payload: payload as Record<string, unknown> };76},7778// Optional79jwksUrl: "https://auth.example.com/.well-known/jwks.json", // advertised in discovery metadata80userInfoEndpoint: "https://auth.example.com/userinfo",81scopesSupported: ["openid", "profile", "email"],82grantTypesSupported: ["authorization_code", "refresh_token"],83audience: "your-api-identifier",84getUserInfo: (payload) => ({85userId: payload.sub as string,86email: payload.email as string | undefined,87name: payload.name as string | undefined,88}),89})90```9192| Option | Type | Required | Description |93|--------|------|----------|-------------|94| `issuer` | `string` | Yes | OAuth issuer URL |95| `authEndpoint` | `string` | Yes | Authorization endpoint |96| `tokenEndpoint` | `string` | Yes | Token endpoint |97| `verifyToken` | `(token: string) => Promise<{ payload: Record<string, unknown> }>` | Yes | Token verification function |98| `jwksUrl` | `string?` | No | JWKS endpoint advertised in discovery metadata |99| `userInfoEndpoint` | `string?` | No | User info endpoint URL |100| `scopesSupported` | `string[]?` | No | Default: `["openid", "profile", "email"]` |101| `grantTypesSupported` | `string[]?` | No | Default: `["authorization_code", "refresh_token"]` |102| `audience` | `string?` | No | Audience for JWT verification |103| `getUserInfo` | `(payload) => UserInfo` | No | Custom user info extraction |104105### Default Claim Extraction106107Without `getUserInfo`, the provider extracts standard OIDC claims automatically:108109| Field | Extracted From |110|-------|---------------|111| `userId` | `sub`, `user_id`, or `id` |112| `email` | `email` |113| `name` | `name` |114| `username` | `username` or `preferred_username` |115| `nickname` | `nickname` |116| `picture` | `picture` or `avatar_url` |117| `roles` | `roles` (if array) |118| `permissions` | `permissions` (if array) |119| `scopes` | Parsed from `scope` string |120121Override if your provider uses non-standard claim names:122123```typescript124getUserInfo: (payload) => ({125userId: payload.user_id as string,126email: payload.mail as string,127name: payload.display_name as string,128roles: (payload.groups as string[]) || [],129})130```131132---133134## `oauthProxy` (non-DCR providers)135136Use this for providers that don't support DCR — Google, GitHub, Okta, Azure AD, standard Auth0 Regular Web Apps, and anything else where you register an app in a dashboard and receive a fixed `clientId` / `clientSecret`.137138The proxy flow:139140```141Client → /register → MCP server returns the pre-registered client_id142Client → /authorize → MCP server redirects to upstream /authorize143Upstream → redirect → authorization code returned to the client144Client → /token → MCP server injects clientId + clientSecret, forwards to upstream145Upstream → token → returned to the client146Client → /mcp/... → MCP server verifies bearer via verifyToken()147```148149### `jwksVerifier` helper150151Use `jwksVerifier` to build a standard JWT+JWKS `verifyToken`. It handles signature verification, issuer checking, and optional audience validation. Pair it with `oauthProxy` for any JWT-based provider.152153```typescript154import { oauthProxy, jwksVerifier } from "mcp-use/server";155156oauthProxy({157// ...158verifyToken: jwksVerifier({159jwksUrl: "https://<provider>/.well-known/jwks.json",160issuer: "https://<provider>/",161audience: "your-audience", // optional — enforces `aud` claim162}),163})164```165166> `verifyToken` — whether from `jwksVerifier` or handwritten — **must resolve to `{ payload: Record<string, unknown> }`** or throw. The proxy surfaces `payload` to `getUserInfo` and to `ctx.auth.payload`.167168For non-JWT providers (GitHub opaque tokens), write your own `verifyToken` that calls the provider's API — see [GitHub](#github-opaque-tokens) below.169170### `oauthProxy` Options171172| Option | Type | Required | Description |173|--------|------|----------|-------------|174| `authEndpoint` | `string` | Yes | Upstream authorization endpoint |175| `tokenEndpoint` | `string` | Yes | Upstream token endpoint |176| `issuer` | `string` | Yes | Token issuer (used in metadata and enforced by `jwksVerifier`) |177| `clientId` | `string` | Yes | Pre-registered OAuth client ID |178| `clientSecret` | `string?` | No | Client secret (omit for public clients) |179| `verifyToken` | `VerifyToken` | Yes | Token verification — use `jwksVerifier()` or a custom function |180| `scopes` | `string[]?` | No | Scopes to request. Default: `["openid", "email", "profile"]` |181| `grantTypes` | `string[]?` | No | Default: `["authorization_code", "refresh_token"]` |182| `extraAuthorizeParams` | `Record<string, string>?` | No | Extra query params on `/authorize` (e.g. `access_type`, `audience`, `prompt`) |183| `getUserInfo` | `(payload) => UserInfo` | No | Custom user info extraction from the verified payload |184185---186187## Provider Examples (`oauthProxy`)188189190191```typescript192oauth: oauthProxy({193authEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",194tokenEndpoint: "https://oauth2.googleapis.com/token",195issuer: "https://accounts.google.com",196clientId: process.env.GOOGLE_CLIENT_ID!,197clientSecret: process.env.GOOGLE_CLIENT_SECRET!,198scopes: ["openid", "email", "profile"],199extraAuthorizeParams: { access_type: "offline" },200verifyToken: jwksVerifier({201jwksUrl: "https://www.googleapis.com/oauth2/v3/certs",202issuer: "https://accounts.google.com",203audience: process.env.GOOGLE_CLIENT_ID!,204}),205})206```207208### Okta209210```typescript211const oktaDomain = process.env.OKTA_DOMAIN!; // e.g. "https://dev-123.okta.com"212213oauth: oauthProxy({214authEndpoint: `${oktaDomain}/oauth2/default/v1/authorize`,215tokenEndpoint: `${oktaDomain}/oauth2/default/v1/token`,216issuer: `${oktaDomain}/oauth2/default`,217clientId: process.env.OKTA_CLIENT_ID!,218clientSecret: process.env.OKTA_CLIENT_SECRET,219scopes: ["openid", "email", "profile"],220verifyToken: jwksVerifier({221jwksUrl: `${oktaDomain}/oauth2/default/v1/keys`,222issuer: `${oktaDomain}/oauth2/default`,223}),224})225```226227### Azure AD (Microsoft Entra ID)228229```typescript230const tenantId = process.env.AZURE_TENANT_ID!;231const base = `https://login.microsoftonline.com/${tenantId}/v2.0`;232233oauth: oauthProxy({234authEndpoint: `${base}/oauth2/v2.0/authorize`,235tokenEndpoint: `${base}/oauth2/v2.0/token`,236issuer: base,237clientId: process.env.AZURE_CLIENT_ID!,238clientSecret: process.env.AZURE_CLIENT_SECRET,239scopes: ["openid", "profile", "email"],240verifyToken: jwksVerifier({241jwksUrl: "https://login.microsoftonline.com/common/discovery/v2.0/keys",242issuer: base,243audience: process.env.AZURE_CLIENT_ID!,244}),245})246```247248### Auth0 (Regular Web App, no Early Access)249250For Auth0 with a standard Regular Web App (no DCR Early Access), use the proxy. See [auth0.md](auth0.md) for the full guide.251252```typescript253const domain = process.env.AUTH0_DOMAIN!;254const audience = process.env.AUTH0_AUDIENCE ?? "";255256oauth: oauthProxy({257authEndpoint: `https://${domain}/authorize`,258tokenEndpoint: `https://${domain}/oauth/token`,259issuer: `https://${domain}/`,260clientId: process.env.AUTH0_CLIENT_ID!,261clientSecret: process.env.AUTH0_CLIENT_SECRET,262scopes: ["openid", "email", "profile"],263extraAuthorizeParams: { audience },264verifyToken: jwksVerifier({265jwksUrl: `https://${domain}/.well-known/jwks.json`,266issuer: `https://${domain}/`,267audience,268}),269})270```271272### GitHub (opaque tokens)273274GitHub uses non-JWT opaque tokens. Use a custom `verifyToken` that calls the GitHub API instead of `jwksVerifier`:275276```typescript277oauth: oauthProxy({278authEndpoint: "https://github.com/login/oauth/authorize",279tokenEndpoint: "https://github.com/login/oauth/access_token",280issuer: "https://github.com",281clientId: process.env.GITHUB_CLIENT_ID!,282clientSecret: process.env.GITHUB_CLIENT_SECRET!,283scopes: ["read:user", "user:email"],284285// GitHub uses opaque tokens — validate by calling the API286async verifyToken(token) {287const res = await fetch("https://api.github.com/user", {288headers: {289Authorization: `Bearer ${token}`,290"User-Agent": "my-mcp-server",291},292});293if (!res.ok) throw new Error("Invalid GitHub token");294const user = await res.json();295return { payload: { sub: String(user.id), ...user } };296},297298getUserInfo(payload) {299return {300userId: payload.sub as string,301username: payload.login as string | undefined,302name: payload.name as string | undefined,303email: payload.email as string | undefined,304picture: payload.avatar_url as string | undefined,305};306},307})308```309310---311312## Accessing user info in tools313314```typescript315server.tool(316{ name: "get-user-info", description: "Get authenticated user info" },317async (_args, ctx) =>318object({319userId: ctx.auth.user.userId,320email: ctx.auth.user.email,321name: ctx.auth.user.name,322scopes: ctx.auth.scopes,323})324);325```326327---328329## Common Mistakes330331- **Using `oauthCustomProvider` for Google / GitHub / Okta / Azure AD** — those don't support DCR. Use `oauthProxy` instead.332- **`verifyToken` returning the wrong shape** — must resolve to `{ payload: Record<string, unknown> }` or throw. Returning `jwtVerify`'s raw result doesn't satisfy this in TypeScript — cast explicitly: `return { payload: result.payload as Record<string, unknown> }`.333- **Forgetting `audience` for JWT verification** — most providers issue per-client tokens; without `audience` in `jwksVerifier`, tokens from other clients could be accepted.334- **Missing `extraAuthorizeParams`** — Google needs `access_type: "offline"` for refresh tokens; Auth0 needs `audience` to issue JWT access tokens instead of opaque ones.335336---337338## Resources339340- [`jose` library](https://github.com/panva/jose) — JWT verification primitives (already a dependency of `mcp-use`)341- [OAuth 2.1 Specification](https://oauth.net/2.1/)342- [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)343- [Runnable Auth0 proxy example](https://github.com/mcp-use/mcp-use/tree/main/libraries/typescript/packages/mcp-use/examples/server/oauth/auth0-proxy)344345---346347## Next Steps348349- **Auth overview** → [overview.md](overview.md)350- **Auth0 setup** → [auth0.md](auth0.md)351- **WorkOS setup** → [workos.md](workos.md)352- **Supabase setup** → [supabase.md](supabase.md)353- **Keycloak setup** → [keycloak.md](keycloak.md)354- **Build tools** → [../server/tools.md](../server/tools.md)355