mcp-auth-test-server
v1.0.3
Published
Three servers that demonstrate MCP authentication with GitHub OIDC and Entra ID:
Downloads
104
Readme
MCP Auth Test Server
Three servers that demonstrate MCP authentication with GitHub OIDC and Entra ID:
| Server | Transport | Port | Command |
| ----------------------------------------------------- | --------- | ---- | ---------------------- |
| Local MCP for use w/stand alone service (default) | stdio | — | npm start |
| Stand alone authentication exchange | HTTP | 3124 | npm run start:oidc |
| Remote MCP Server w/auth built-in | HTTP | 3125 | npm run start:remote |
All three servers share the same echo tool and support the same token exchange flows.
Quick Start
npm install
cp .env.example .env # configure environment variables
npm run build
npm start # starts the local stdio MCP serverServers
Local MCP Server (src/mcp-local/server.ts)
A stdio-based MCP server intended for local tool use. Reads an Entra ID access token from the MCP_ACCESS_TOKEN environment variable, verifies it using the same JWKS-based validation as the remote server, then exposes the echo tool over stdio.
MCP_ACCESS_TOKEN=<token> npm startRequires AZURE_TENANT_ID and AZURE_CLIENT_ID to verify the token.
Remote MCP Server (src/mcp/server.ts)
An HTTP-based MCP server secured with OAuth 2.0. Uses the MCP TypeScript SDK's auth router for dynamic client registration, authorization code flow, and bearer auth on the /mcp endpoint. Also supports GitHub OIDC token exchange for machine-to-machine access.
npm run start:remoteOIDC Token Exchange Server (src/oidc-stand-alone/server.ts)
A standalone HTTP token exchange server. Accepts GitHub OIDC tokens and returns real Entra ID access tokens. Does not serve MCP — used as an upstream token provider.
npm run start:oidcSupported Grant Types
All three grant types are supported by both the remote MCP server and the OIDC standalone server via a shared handler architecture. Each grant type extracts a GitHub OIDC JWT from the request, verifies it, and exchanges it for an Entra ID token.
JWT Bearer (urn:ietf:params:oauth:grant-type:jwt-bearer)
RFC 7523. The JWT is sent as the assertion parameter.
POST /token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<github-oidc-jwt>
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=<target-audience>
&scope=<optional>
&resource=<optional>Token Exchange (urn:ietf:params:oauth:grant-type:token-exchange)
RFC 8693. The JWT is sent as subject_token.
POST /token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<github-oidc-jwt>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=<target-audience>
&client_id=<optional>
&scope=<optional>
&resource=<optional>Client Credentials (client_credentials)
OAuth 2.0 client credentials with a JWT assertion (used by Azure AD Workload Identity Federation).
POST /token
grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<github-oidc-jwt>
&client_id=<client-id>
&scope=<optional>The remote MCP server also supports client_credentials with client_id + client_secret for dynamically registered clients.
Token Exchange Flow
All three grant types follow the same flow after extracting the JWT:
GitHub OIDC JWT
│
▼
Verify signature + claims (JWKS)
Extract actor identity
│
├── Actor in sub + stored credentials? ──▶ User-mapped flow
│ Refresh stored Entra token (delegated token)
│
└── Otherwise ──▶ Workload Identity Federation
GitHub JWT as client assertion (app-level token)
│
▼
{ access_token, issued_token_type, token_type, expires_in }User-mapped flow: Returns a user-delegated Entra ID token. Requires a one-time interactive login via GET /map-user?github_login=<username> to link a GitHub user to their Entra ID account.
Workload Identity Federation: Returns an app-level Entra ID token using the GitHub OIDC JWT as a federated client assertion. Requires a federated identity credential on the Entra ID app registration.
Endpoints
Remote MCP Server (port 3125)
| Endpoint | Description |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| GET /.well-known/oauth-authorization-server | OAuth 2.0 server metadata |
| GET /.well-known/oauth-protected-resource | Protected resource metadata (RFC 9728) |
| POST /register | Dynamic client registration |
| GET /authorize | OAuth authorization (auto-approves in demo mode) |
| POST /token | Token endpoint (authorization_code, refresh_token, jwt-bearer, token-exchange, client_credentials) |
| POST /revoke | Token revocation |
| GET /map-user?github_login=<username> | Initiate Entra ID login to link a GitHub user |
| GET /callback | Entra ID OAuth redirect callback |
| POST /mcp | MCP Streamable HTTP endpoint (requires Bearer token) |
| GET /mcp | SSE stream for server-to-client notifications |
| DELETE /mcp | Close an MCP session |
OIDC Token Exchange Server (port 3124)
| Endpoint | Description |
| --------------------------------------- | --------------------------------------------------------------- |
| GET /health | Health check |
| GET /map-user?github_login=<username> | Initiate Entra ID login to link a GitHub user |
| GET /callback | Entra ID OAuth redirect callback |
| POST /token | Token endpoint (jwt-bearer, token-exchange, client_credentials) |
Tools
| Tool | Description | Input |
| ------ | -------------------------------- | ------------------------- |
| echo | Echoes back the provided message | { "message": "string" } |
Available on both the local (stdio) and remote (HTTP) MCP servers.
Configuration
Required Environment Variables
| Variable | Description |
| --------------------- | ------------------------------------------------------------------- |
| GITHUB_OIDC_ISSUER | GitHub OIDC issuer URL(s), comma-separated |
| ENTRA_AUDIENCE | Resource identifier / audience for Entra token requests |
| AZURE_TENANT_ID | Entra ID tenant ID |
| AZURE_CLIENT_ID | Entra ID app registration client ID |
| AZURE_CLIENT_SECRET | Entra ID app registration client secret |
| REDIRECT_URI | OAuth callback URL for the OIDC standalone server |
| MCP_REDIRECT_URI | OAuth callback URL for the remote MCP server |
| ENCRYPTION_KEY | 32-byte hex key for AES-256-GCM encryption of stored refresh tokens |
Optional Environment Variables
| Variable | Description | Default |
| ------------------------- | --------------------------------------- | ----------------------------------------------- |
| SKIP_ENTRA_EXCHANGE | Skip Entra ID exchange (see below) | false |
| FAIL_FIRST_MCP_AUTH | Reject first /mcp request with 401 | false |
| MCP_PORT | Remote MCP server port | 3125 |
| PORT | OIDC standalone server port | 3124 |
| HOST_URL | Public URL for the remote MCP server | http://localhost:<MCP_PORT> |
| MCP_METADATA_ISSUER_URL | OAuth issuer URL in metadata | https://login.microsoftonline.com/common/v2.0 |
| TOKEN_SCOPES | Space-separated Entra ID scopes | <ENTRA_AUDIENCE>/.default offline_access |
| GITHUB_OIDC_JWKS_URL | Override GitHub OIDC JWKS endpoint | Derived from issuer |
| MCP_ACCESS_TOKEN | Entra ID token for the local MCP server | — |
Testing Without Entra ID (SKIP_ENTRA_EXCHANGE)
Set SKIP_ENTRA_EXCHANGE=true to test the token exchange flow without an Entra ID dependency. In this mode:
- GitHub OIDC JWTs are still cryptographically verified against the JWKS endpoint
- The verified JWT is returned directly as the
access_token(no Entra exchange) - Azure environment variables (
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,REDIRECT_URI,ENCRYPTION_KEY) are not required - The
/map-userand/callbackendpoints return 404 (user mapping requires Entra) - The local MCP server verifies the access token as a GitHub OIDC JWT instead of an Entra JWT
expires_inis derived from the GitHub JWT'sexpclaim
Minimal configuration for skip mode:
GITHUB_OIDC_ISSUER=https://token.actions.githubusercontent.com
ENTRA_AUDIENCE=api://your-audience
SKIP_ENTRA_EXCHANGE=trueThis is intended for the OIDC standalone server and local MCP server only. The remote MCP server's built-in OAuth flow (dynamic client registration, authorization code) works independently of this setting.
Testing Re-Authentication (FAIL_FIRST_MCP_AUTH)
Set FAIL_FIRST_MCP_AUTH=true to test that clients correctly handle a 401 on the /mcp endpoint and retry after re-authenticating. When enabled:
- The first authenticated
/mcprequest is rejected with HTTP 401 and aWWW-Authenticate: Bearer resource_metadata="..."header - All subsequent requests proceed normally
- The client must perform a new OIDC flow to obtain a fresh token and retry
This is useful for verifying that MCP clients implement the re-authentication loop correctly.
FAIL_FIRST_MCP_AUTH=true npm run start:remoteGenerating an Encryption Key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Project Structure
src/
├── mcp-local/ # Local MCP server (stdio, default)
│ └── server.ts
├── mcp/ # Remote MCP server (HTTP + OAuth)
│ ├── server.ts # Express app, OAuth metadata, /mcp endpoints
│ ├── auth-provider.ts # In-memory OAuth provider (authorization code, refresh, client_credentials)
│ └── token-exchange.ts # Thin wrapper mounting shared routes with fallthrough
├── oidc-stand-alone/ # OIDC token exchange server (HTTP)
│ └── server.ts
└── shared/ # Shared modules
├── auth/ # Token verification & identity providers
│ ├── entra-auth.ts # Entra ID OAuth client (code, refresh, WIF)
│ ├── entra-token-verifier.ts # Entra ID JWT verification via JWKS
│ ├── github-oidc.ts # GitHub OIDC token verification & claims
│ ├── id-token-parser.ts # Decode Entra identity from id_tokens
│ ├── jwt-verifier.ts # Generic JWKS-backed JWT verifier
│ └── token-store.ts # Encrypted refresh token storage (AES-256-GCM)
├── grants/ # OAuth grant type handling
│ ├── grant-handler.ts # GrantHandler interface + error types
│ ├── grant-exchange.ts # Verify JWT → user-mapped or WIF → Entra token
│ ├── jwt-bearer.ts # JWT Bearer handler (RFC 7523)
│ ├── token-exchange.ts # Token Exchange handler (RFC 8693)
│ ├── client-credentials.ts # Client Credentials handler
│ └── index.ts # Barrel export
├── http/ # Express server infrastructure
│ ├── middleware.ts # Request/response logger + error handler
│ └── token-exchange-routes.ts # Shared router factory (map-user, callback, POST /token)
├── config.ts # Server configuration loader
├── constants.ts # Grant type URIs, token types, defaults
└── mcp-tools.ts # MCP echo server/tool registrationSecurity Notes
- Refresh tokens are encrypted at rest using AES-256-GCM
- The
data/tokens.jsonfile andENCRYPTION_KEYmust be kept secure - Refresh tokens expire (typically ~90 days with sliding window) — users will need to re-login via
/map-userwhen they expire - The local MCP server verifies the access token at startup; if the token expires mid-session the server must be restarted with a fresh token
- In production, replace file-based token storage with a proper secrets store (e.g., Azure Key Vault)
