Emulate microsoft
Emulated Microsoft Entra ID (Azure AD) OAuth 2.0 / OpenID Connect for local development and testing. Use when the user needs to test Microsoft sign-in locally, emulate Entra ID OIDC discovery, handle Microsoft token exchange, configure Azure AD OAuth clients, work with Microsoft Graph /me, or test PKCE/client credentials flows without hitting real Microsoft APIs. Triggers include "Microsoft OAuth", "Entra ID", "Azure AD", "emulate Microsoft", "mock Microsoft login", "test Microsoft sign-in", "Microsoft OIDC", "local Microsoft auth", or any task requiring a local Microsoft OAuth/OIDC provider.
git clone https://github.com/vercel-labs/emulate
T=$(mktemp -d) && git clone --depth=1 https://github.com/vercel-labs/emulate "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/microsoft" ~/.claude/skills/vercel-labs-emulate-microsoft && rm -rf "$T"
skills/microsoft/SKILL.mdMicrosoft Entra ID Emulator
Microsoft Entra ID (Azure AD) v2.0 OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE, client credentials, RS256 ID tokens, OIDC discovery, and a Microsoft Graph
/v1.0/me endpoint.
Start
# Microsoft only npx emulate --service microsoft # Default port (when run alone) # http://localhost:4000
Or programmatically:
import { createEmulator } from 'emulate' const microsoft = await createEmulator({ service: 'microsoft', port: 4005 }) // microsoft.url === 'http://localhost:4005'
Pointing Your App at the Emulator
Environment Variable
MICROSOFT_EMULATOR_URL=http://localhost:4005
OAuth URL Mapping
| Real Microsoft URL | Emulator URL |
|---|---|
| |
| |
| |
| |
| |
| |
| |
Auth.js / NextAuth.js
import MicrosoftEntraId from '@auth/core/providers/microsoft-entra-id' MicrosoftEntraId({ clientId: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, authorization: { url: `${process.env.MICROSOFT_EMULATOR_URL}/oauth2/v2.0/authorize`, params: { scope: 'openid email profile User.Read' }, }, token: { url: `${process.env.MICROSOFT_EMULATOR_URL}/oauth2/v2.0/token`, }, userinfo: { url: `${process.env.MICROSOFT_EMULATOR_URL}/oidc/userinfo`, }, issuer: process.env.MICROSOFT_EMULATOR_URL, })
Passport.js
import { OIDCStrategy } from 'passport-azure-ad' const MICROSOFT_URL = process.env.MICROSOFT_EMULATOR_URL ?? 'https://login.microsoftonline.com' new OIDCStrategy({ identityMetadata: `${MICROSOFT_URL}/.well-known/openid-configuration`, clientID: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, redirectUrl: 'http://localhost:3000/api/auth/callback/microsoft-entra-id', responseType: 'code', responseMode: 'query', scope: ['openid', 'email', 'profile'], }, verifyCallback)
MSAL.js
import { ConfidentialClientApplication } from '@azure/msal-node' const msalConfig = { auth: { clientId: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, authority: process.env.MICROSOFT_EMULATOR_URL, knownAuthorities: [process.env.MICROSOFT_EMULATOR_URL], }, } const cca = new ConfidentialClientApplication(msalConfig)
Seed Config
microsoft: users: - email: testuser@outlook.com name: Test User given_name: Test family_name: User tenant_id: 9188040d-6c67-4c5b-b112-36a304b66dad oauth_clients: - client_id: example-client-id client_secret: example-client-secret name: My Microsoft App redirect_uris: - http://localhost:3000/api/auth/callback/microsoft-entra-id tenant_id: 9188040d-6c67-4c5b-b112-36a304b66dad
When no OAuth clients are configured, the emulator accepts any
client_id. With clients configured, strict validation is enforced for client_id, client_secret, and redirect_uri.
API Endpoints
OIDC Discovery
# Default tenant curl http://localhost:4005/.well-known/openid-configuration # Tenant-scoped (common, organizations, consumers, or specific tenant ID) curl http://localhost:4005/common/v2.0/.well-known/openid-configuration
Returns the standard OIDC discovery document:
{ "issuer": "http://localhost:4005/{tenant}/v2.0", "authorization_endpoint": "http://localhost:4005/oauth2/v2.0/authorize", "token_endpoint": "http://localhost:4005/oauth2/v2.0/token", "userinfo_endpoint": "http://localhost:4005/oidc/userinfo", "end_session_endpoint": "http://localhost:4005/oauth2/v2.0/logout", "jwks_uri": "http://localhost:4005/discovery/v2.0/keys", "response_types_supported": ["code"], "subject_types_supported": ["pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "email", "profile", "User.Read", "offline_access"], "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"] }
JWKS
curl http://localhost:4005/discovery/v2.0/keys
Returns an RSA public key (
kid: emulate-microsoft-1) for verifying id_token signatures.
Authorization
# Browser flow: redirects to a user picker page curl -v "http://localhost:4005/oauth2/v2.0/authorize?\ client_id=example-client-id&\ redirect_uri=http://localhost:3000/api/auth/callback/microsoft-entra-id&\ scope=openid+email+profile&\ response_type=code&\ state=random-state&\ nonce=random-nonce"
Query parameters:
| Param | Description |
|---|---|
| OAuth client ID |
| Callback URL |
| Space-separated scopes () |
| Opaque state for CSRF protection |
| Nonce for ID token (optional) |
| (default) or |
| PKCE challenge (optional) |
| or (optional) |
Token Exchange
curl -X POST http://localhost:4005/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "code=<authorization_code>&\ client_id=example-client-id&\ client_secret=example-client-secret&\ redirect_uri=http://localhost:3000/api/auth/callback/microsoft-entra-id&\ grant_type=authorization_code"
Returns:
{ "access_token": "microsoft_...", "refresh_token": "r_microsoft_...", "id_token": "<jwt>", "token_type": "Bearer", "expires_in": 3600, "scope": "openid email profile" }
The
id_token is an RS256 JWT containing sub, oid, tid (tenant ID), email, name, preferred_username, ver ("2.0"), and optional nonce.
For PKCE, include
code_verifier in the token request.
Supports
Authorization: Basic header with base64-encoded client_id:client_secret as an alternative to body parameters.
Client Credentials
curl -X POST http://localhost:4005/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=example-client-id&\ client_secret=example-client-secret&\ grant_type=client_credentials&\ scope=https://graph.microsoft.com/.default"
Returns an
access_token only (no refresh_token or id_token).
Refresh Token
curl -X POST http://localhost:4005/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "refresh_token=r_microsoft_...&\ client_id=example-client-id&\ grant_type=refresh_token"
Returns a new
access_token, rotated refresh_token, and new id_token.
User Info
curl http://localhost:4005/oidc/userinfo \ -H "Authorization: Bearer microsoft_..."
Returns:
{ "sub": "<oid>", "email": "testuser@outlook.com", "name": "Test User", "given_name": "Test", "family_name": "User", "preferred_username": "testuser@outlook.com" }
Microsoft Graph /me
curl http://localhost:4005/v1.0/me \ -H "Authorization: Bearer microsoft_..."
Returns an OData-style response:
{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity", "displayName": "Test User", "mail": "testuser@outlook.com", "userPrincipalName": "testuser@outlook.com", "id": "<oid>" }
Logout
curl "http://localhost:4005/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000"
Redirects to the
post_logout_redirect_uri if provided and valid.
Token Revocation
curl -X POST http://localhost:4005/oauth2/v2.0/revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=microsoft_..."
Returns
200 OK. The token is removed from the emulator's token map.
Common Patterns
Full Authorization Code Flow
MICROSOFT_URL="http://localhost:4005" CLIENT_ID="example-client-id" CLIENT_SECRET="example-client-secret" REDIRECT_URI="http://localhost:3000/api/auth/callback/microsoft-entra-id" # 1. Open in browser (user picks a seeded account) # $MICROSOFT_URL/oauth2/v2.0/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc # 2. After user selection, emulator redirects to: # $REDIRECT_URI?code=<code>&state=abc # 3. Exchange code for tokens curl -X POST $MICROSOFT_URL/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code" # 4. Fetch user info with the access_token curl $MICROSOFT_URL/oidc/userinfo \ -H "Authorization: Bearer <access_token>"
PKCE Flow
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43) CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_') # 1. Authorize with challenge # $MICROSOFT_URL/oauth2/v2.0/authorize?...&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256 # 2. Token exchange with verifier curl -X POST $MICROSOFT_URL/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code&code_verifier=$CODE_VERIFIER"
OIDC Discovery-Based Setup
Libraries that support OIDC discovery can auto-configure from the discovery document:
import { Issuer } from 'openid-client' const microsoftIssuer = await Issuer.discover( process.env.MICROSOFT_EMULATOR_URL ?? 'https://login.microsoftonline.com/common/v2.0' ) const client = new microsoftIssuer.Client({ client_id: process.env.MICROSOFT_CLIENT_ID, client_secret: process.env.MICROSOFT_CLIENT_SECRET, redirect_uris: ['http://localhost:3000/api/auth/callback/microsoft-entra-id'], })