A complete implementation of the OAuth 2.1 authorization code flow with PKCE (Proof Key for Code Exchange) in Go, designed for educational purposes to understand OAuth message exchanges and security mechanisms.
- Project overview
- Architecture
- Quick start
- Step-by-step OAuth 2.1 flow walkthrough
- Understanding the components
- OAuth 2.1 vs OAuth 2.0
- Security features
- Message logging
- Troubleshooting
- Learning outcomes
- API reference
- Glossary
- Real-world OAuth implementation
- Next steps
This project demonstrates the OAuth 2.1 authorization code flow through three interconnected applications that communicate using standard OAuth protocols. It's designed to provide hands-on learning about OAuth security mechanisms, message exchanges, and implementation patterns.
- Educational: Learn OAuth 2.1 concepts through practical implementation
- Visual: See all OAuth messages with detailed logging and color coding
- Complete: Experience the full flow from authorization to resource access
- Secure: Implement modern OAuth 2.1 security features like mandatory PKCE
- Developers learning OAuth 2.1 implementation
- Engineers implementing OAuth in MCP servers or other applications
- Anyone wanting to understand OAuth message flows and security
The system consists of three independent applications:
graph TB
subgraph System["OAuth 2.1 Learning System"]
Client["Client Application<br/>Port 8080"]
AuthServer["Authorization Server<br/>Port 8081"]
ResourceServer["Resource Server<br/>Port 8082"]
end
User["User Browser"]
User -->|Start OAuth flow| Client
Client -->|Authorization request| AuthServer
AuthServer -->|Authorization code| Client
Client -->|Access protected resource| ResourceServer
style Client fill:#e1f5fe
style AuthServer fill:#f3e5f5
style ResourceServer fill:#fff3e0
style User fill:#e8f5e8
- Client Application: Web interface that initiates OAuth flows and consumes protected resources
- Authorization Server: Handles user authentication, authorization, and token issuance
- Resource Server: Protects and serves resources to properly authorized clients
- Go 1.21+ installed
- Three available ports: 8080, 8081, 8082
-
Clone and prepare:
git clone <repository-url> cd oauth-go go mod tidy
-
Start all three servers (in separate terminals):
# Terminal 1: Authorization Server go run cmd/auth-server/main.go # Terminal 2: Resource Server go run cmd/resource-server/main.go # Terminal 3: Client Application go run cmd/client/main.go
-
Access the demo:
- Open browser to http://localhost:8080
- Follow the OAuth flow using demo credentials
Three pre-configured accounts are available:
- alice / password123
- bob / secret456
- carol / mypass789
Follow this detailed walkthrough to understand each step of the OAuth 2.1 authorization code flow:
sequenceDiagram
participant User as User Browser
participant Client as Client App<br/>(Port 8080)
participant AuthServer as Authorization Server<br/>(Port 8081)
participant ResourceServer as Resource Server<br/>(Port 8082)
Note over User,ResourceServer: OAuth 2.1 Authorization Code Flow with PKCE
%% Step 1-2: Start OAuth Flow
User->>Client: 1. Visit http://localhost:8080
Client->>Client: Generate PKCE challenge
Client->>User: Display authorization URL with PKCE challenge
%% Step 3-4: Authorization Request
User->>AuthServer: 2. Click "Start OAuth Flow"<br/>GET /authorize?client_id=demo-client&<br/>redirect_uri=callback&scope=read&<br/>code_challenge=xyz&code_challenge_method=S256
AuthServer->>User: Display login form
%% Step 5-6: User Authentication
User->>AuthServer: 3. Submit credentials<br/>POST /login (alice/password123)
AuthServer->>AuthServer: Validate credentials with bcrypt
AuthServer->>AuthServer: Generate authorization code
AuthServer->>User: 4. Redirect to callback<br/>GET callback?code=abc123&state=demo-state-123
%% Step 7-8: Token Exchange
User->>Client: 5. Authorization callback received
Client->>Client: Store authorization code
Client->>User: Display "Exchange Code for Token" button
User->>Client: 6. Click "Exchange Code for Token"
Client->>AuthServer: 7. POST /token<br/>grant_type=authorization_code&<br/>code=abc123&code_verifier=original_verifier
AuthServer->>AuthServer: Verify PKCE (SHA256(verifier) == challenge)
AuthServer->>AuthServer: Generate access token
AuthServer->>Client: 8. Return access token<br/>{"access_token":"def456","token_type":"Bearer"}
%% Step 9-10: Resource Access
Client->>User: Display access token and resource buttons
User->>Client: 9. Click "Access Protected Resource"
Client->>ResourceServer: 10. GET /protected<br/>Authorization: Bearer def456
ResourceServer->>ResourceServer: Validate access token
ResourceServer->>Client: 11. Return protected resource content
Client->>User: Display protected resource
- Visit the client application at http://localhost:8080
- Observe the initial screen showing:
- Client details (ID, redirect URI, scope)
- Generated PKCE challenge details
- "Start OAuth Flow" button
What happens behind the scenes:
// Client generates PKCE challenge
pkce, _ := oauth.GeneratePKCEChallenge()
// Creates:
// - Code Verifier: 43-character random string
// - Code Challenge: SHA256 hash of verifier (Base64url encoded)
Console output:
[2024-08-15 10:30:15] CLIENT → USER-BROWSER
Authorization URL Generated:
authorization_url: http://localhost:8081/authorize?...
pkce_challenge: xyz123...
pkce_method: S256
- Click "Start OAuth Flow"
- Browser redirects to authorization server with parameters:
client_id=demo-client
redirect_uri=http://localhost:8080/callback
scope=read
state=demo-state-123
code_challenge=<SHA256_hash>
code_challenge_method=S256
Console output (Authorization Server):
[2024-08-15 10:30:16] CLIENT → AUTH-SERVER
Authorization Request:
client_id: demo-client
redirect_uri: http://localhost:8080/callback
scope: read
state: demo-state-123
code_challenge: xyz123...
code_challenge_method: S256
response_type: code
-
Login form appears showing:
- Application request details
- OAuth parameters
- Username/password fields
- Demo account credentials
-
Enter credentials (e.g., alice / password123)
-
Submit the form
What happens behind the scenes:
// Server validates credentials using bcrypt
user, err := userStore.Authenticate(username, password)
// If successful, generates authorization code
code, _ := oauth.GenerateRandomString(32)
Console output (Authorization Server):
[2024-08-15 10:30:45] USER → AUTH-SERVER
Login Attempt:
username: alice
password: [REDACTED]
- Successful authentication triggers redirect back to client
- Authorization code is appended to redirect URI
- Browser returns to http://localhost:8080/callback?code=xyz&state=demo-state-123
Console output (Authorization Server):
[2024-08-15 10:30:46] AUTH-SERVER → CLIENT
Authorization Response:
code: abc123...
state: demo-state-123
Console output (Client):
[2024-08-15 10:30:46] AUTH-SERVER → CLIENT
Authorization Callback:
code: abc123...
state: demo-state-123
error:
- Client processes callback and shows:
- "Authorization Code Received" message
- The actual authorization code
- "Exchange Code for Token" button
What happens behind the scenes:
// Client stores the authorization code for later use
c.authCode = code
- Click "Exchange Code for Token"
- Client sends POST request to token endpoint with:
grant_type=authorization_code
code=<authorization_code>
redirect_uri=http://localhost:8080/callback
client_id=demo-client
code_verifier=<original_43_char_string>
Console output (Client):
[2024-08-15 10:31:00] CLIENT → AUTH-SERVER
Token Exchange Request:
grant_type: authorization_code
redirect_uri: http://localhost:8080/callback
client_id: demo-client
code_verifier: cQpV8MttKOP6c0zDhnTN...
What happens behind the scenes (Authorization Server):
// 1. Retrieve stored authorization code
authCode, err := store.GetAuthorizationCode(code)
// 2. Verify PKCE challenge
if !oauth.VerifyPKCE(codeVerifier, authCode.CodeChallenge, "S256") {
return "invalid_grant"
}
// 3. Generate access token
accessToken, _ := oauth.GenerateRandomString(32)
Console output (Authorization Server):
[2024-08-15 10:31:01] AUTH-SERVER → CLIENT
Token Response:
access_token: def456...
token_type: Bearer
expires_in: 3600
scope: read
- Client displays success showing:
- "Access Token Received" message
- The access token value
- "Access Protected Resource" button
- "Get User Info" button
- Click "Access Protected Resource"
- Client sends GET request to resource server with:
Authorization: Bearer <access_token>
header
Console output (Client):
[2024-08-15 10:31:15] CLIENT → RESOURCE-SERVER
Protected Resource Request:
Headers:
Authorization: Bearer def456...
What happens behind the scenes (Resource Server):
// 1. Extract Bearer token from Authorization header
token := strings.TrimPrefix(authHeader, "Bearer ")
// 2. Validate token (in this demo, simplified validation)
accessToken, err := validateToken(token)
// 3. Serve protected resource
content, _ := os.ReadFile("data/protected-resource.txt")
Console output (Resource Server):
[2024-08-15 10:31:15] CLIENT → RESOURCE-SERVER
Resource Request:
path: /protected
method: GET
Headers:
Authorization: Bearer def456...
[2024-08-15 10:31:15] RESOURCE-SERVER → RESOURCE-SERVER
Token Validation Success:
user_id: validated-user
client_id: demo-client
scope: read
[2024-08-15 10:31:15] RESOURCE-SERVER → CLIENT
Protected Resource Response:
resource_size: 1234
content_type: text/plain
- Client displays the protected resource content
- Flow complete! User has successfully:
- Authenticated with the authorization server
- Authorized the client application
- Received an access token
- Accessed protected resources
Key responsibilities:
- User authentication and session management
- Authorization code generation and storage
- PKCE challenge verification
- Access token issuance
- Security validation (redirect URIs, client IDs, etc.)
Key endpoints:
GET /authorize
- OAuth authorization endpointPOST /login
- Processes user credentialsPOST /token
- Token exchange endpoint
Security features:
- Bcrypt password hashing
- Authorization code expiration (10 minutes)
- PKCE mandatory verification
- State parameter validation
Key responsibilities:
- Access token validation
- Protected resource serving
- Authorization enforcement
- User information provision
Key endpoints:
GET /protected
- Protected file resourceGET /userinfo
- User information endpointGET /health
- Health check
Security features:
- Bearer token validation
- Proper HTTP status codes
- WWW-Authenticate headers
Key responsibilities:
- OAuth flow initiation
- PKCE generation and verification
- Authorization code handling
- Token management
- Resource consumption
Key endpoints:
GET /
- Start OAuth flowGET /callback
- OAuth callback handlerGET /exchange
- Token exchange triggerGET /resource
- Access protected resourceGET /userinfo
- Get user information
Security features:
- PKCE implementation
- State parameter verification
- Secure token storage
- Proper error handling
This implementation showcases key OAuth 2.1 improvements:
- OAuth 2.0: PKCE optional for public clients
- OAuth 2.1: PKCE mandatory for all clients
- Security benefit: Prevents authorization code interception attacks
- OAuth 2.0: Supports implicit flow (tokens in URL fragments)
- OAuth 2.1: Implicit flow removed
- Security benefit: Eliminates token exposure in browser history/logs
- OAuth 2.0: Various security features optional
- OAuth 2.1: Security-first approach with mandatory protections
- Security benefit: Reduced attack surface by default
Purpose: Prevents authorization code interception attacks
Implementation:
- Code Verifier: 43-character cryptographically random string
- Code Challenge: Base64url-encoded SHA256 hash of verifier
- Challenge Method: S256 (SHA256)
Flow:
Client generates: verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Client sends: challenge = SHA256(verifier) = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Client proves: verifier matches challenge during token exchange
sequenceDiagram
participant Client as Client Application
participant AuthServer as Authorization Server
participant Attacker as 🔴 Potential Attacker
Note over Client,Attacker: PKCE Security Mechanism
%% PKCE Generation
Client->>Client: 1. Generate random verifier<br/>"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Client->>Client: 2. Create challenge<br/>SHA256(verifier) = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
%% Authorization Request with Challenge
Client->>AuthServer: 3. Authorization Request<br/>code_challenge="E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"<br/>code_challenge_method="S256"
AuthServer->>AuthServer: Store challenge with authorization code
AuthServer->>Client: 4. Authorization Code<br/>"abc123xyz" (via redirect)
%% Potential Interception
Note over Attacker: ❌ Attacker intercepts authorization code<br/>but cannot use it without verifier
%% Token Exchange with Verifier
Client->>AuthServer: 5. Token Request<br/>code="abc123xyz"<br/>code_verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
AuthServer->>AuthServer: 6. Verify PKCE<br/>SHA256(received_verifier) == stored_challenge
AuthServer->>Client: 7. ✅ Access Token<br/>(only if PKCE verification succeeds)
%% Attacker Attempt
Attacker->>AuthServer: 8. ❌ Token Request with stolen code<br/>code="abc123xyz"<br/>(missing code_verifier)
AuthServer->>Attacker: 9. ❌ Error: invalid_grant<br/>(PKCE verification fails)
Purpose: Prevents CSRF attacks
Implementation: Random state value maintained between authorization request and callback
Purpose: Limits window for code interception
Implementation: Codes expire after 10 minutes
Purpose: Protect user credentials
Implementation: Bcrypt hashing with default cost (10 rounds)
All OAuth messages are logged with detailed, color-coded output:
[timestamp] SOURCE DIRECTION DESTINATION
Message Type:
parameter: value
parameter: value
Headers:
header: value
--------------------------------------------------
- Blue: Client messages
- Green: Authorization server messages
- Yellow: Resource server messages
- Red: Error messages
- Cyan: Info messages
[2024-08-15 10:30:15] CLIENT → AUTH-SERVER
Authorization Request:
client_id: demo-client
scope: read
code_challenge: xyz123...
[2024-08-15 10:30:46] AUTH-SERVER → CLIENT
Authorization Response:
code: abc123...
state: demo-state-123
[2024-08-15 10:31:01] CLIENT → AUTH-SERVER
Token Exchange Request:
grant_type: authorization_code
code_verifier: cQpV8MttKOP6c0zDhnTN...
[2024-08-15 10:31:01] AUTH-SERVER → CLIENT
Token Response:
access_token: def456...
token_type: Bearer
expires_in: 3600
- Cause: Incorrect bcrypt hashes
- Solution: Use
go run cmd/hash-passwords/main.go
to generate new hashes
- Cause: Code not properly stored or expired
- Solution: Complete flow within 10 minutes, check client code storage
- Cause: Code verifier doesn't match challenge
- Solution: Ensure PKCE challenge is properly stored between requests
- Cause: Another process using the port
- Solution: Find with
lsof -i :8080
and kill, or change port in code
- Cause: Running from wrong directory
- Solution: Run from project root where
web/templates/
exists
- Check all three servers are running on correct ports
- Watch console logs for detailed OAuth message flows
- Verify demo accounts with password hashing utility
- Clear browser cache if experiencing unexpected redirects
- Use browser developer tools to inspect network requests
Successful flow pattern:
Authorization Request → Authorization Response → Token Request → Token Response → Resource Request → Resource Response
Failed authentication:
Authorization Request → Login Attempt → ERROR: Invalid credentials
PKCE failure:
Authorization Request → Authorization Response → Token Request → ERROR: PKCE verification failed
This implementation now includes Dynamic Client Registration, a key feature required for modern OAuth clients like Claude Code.
Dynamic Client Registration allows OAuth clients to register themselves programmatically at runtime, without requiring manual pre-configuration by the authorization server administrator.
- Claude Code compatibility: Required for Claude Code and similar tools
- Scalability: No manual client configuration needed
- Flexibility: Clients can register with custom redirect URIs
- Standards compliance: Follows RFC 7591 specification
Registration endpoint:
POST /register
Content-Type: application/json
{
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "My OAuth Client",
"application_type": "web"
}
Response:
{
"client_id": "abc123...",
"client_secret": "def456...",
"redirect_uris": ["https://client.example.com/callback"],
"response_types": ["code"],
"grant_types": ["authorization_code"],
"application_type": "web",
"client_name": "My OAuth Client",
"token_endpoint_auth_method": "client_secret_post"
}
1. Register a new client:
curl -X POST http://localhost:8081/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["http://localhost:3000/callback"],
"client_name": "Test Client",
"application_type": "web"
}'
2. Use the returned client_id in authorization flows: The dynamically registered client can now be used exactly like the demo client for the full OAuth flow.
The discovery endpoint now includes the registration endpoint:
curl http://localhost:8081/.well-known/oauth-authorization-server
Returns:
{
"issuer": "http://localhost:8081",
"authorization_endpoint": "http://localhost:8081/authorize",
"token_endpoint": "http://localhost:8081/token",
"registration_endpoint": "http://localhost:8081/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["read"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"]
}
After completing this walkthrough, you will understand:
- Authorization code flow with PKCE
- Client types and security considerations
- Token-based authorization
- Scope and permissions
- PKCE prevents code interception
- State parameter prevents CSRF
- Proper token validation
- Secure credential handling
- Multi-component OAuth architecture
- HTTP redirects and callbacks
- Token exchange protocols
- Error handling strategies
- Token expiration and refresh
- Scope validation
- Client authentication
- Production security requirements
- Dynamic Client Registration
- OAuth discovery endpoints
- Enhanced error handling
- Client validation and redirect URI security
Purpose: OAuth 2.1 authorization endpoint
Parameters:
client_id
(required): Client identifierredirect_uri
(required): Callback URLscope
(required): Requested permissionsstate
(required): CSRF protectioncode_challenge
(required): PKCE challengecode_challenge_method
(required): Must be "S256"response_type
(required): Must be "code"
Response: HTML login form or redirect to callback
Purpose: Process user authentication
Parameters (form data):
username
(required): User identifierpassword
(required): User password- OAuth parameters from authorization request
Response: Redirect to client callback with authorization code
Purpose: Exchange authorization code for access token
Parameters (form data):
grant_type
(required): Must be "authorization_code"code
(required): Authorization code from callbackredirect_uri
(required): Must match authorization requestclient_id
(required): Client identifiercode_verifier
(required): PKCE verifier
Response: JSON with access token
Purpose: Serve protected resource
Headers:
Authorization: Bearer <access_token>
(required)
Response: Protected file content
Purpose: Provide user information
Headers:
Authorization: Bearer <access_token>
(required)
Response: JSON with user details
Purpose: Health check endpoint
Response: JSON with server status
Purpose: Start OAuth flow
Response: HTML with authorization URL and PKCE details
Purpose: Handle OAuth callback
Parameters:
code
: Authorization code (success case)error
: Error code (error case)state
: CSRF protection value
Response: HTML showing code or error
Purpose: Trigger token exchange
Response: HTML showing access token or error
Purpose: Access protected resource
Response: HTML showing resource content
Purpose: Get user information
Response: HTML showing user details
Access Token: A credential used by clients to access protected resources. Contains authorization information and has a limited lifetime.
Authorization Code: A temporary credential representing the authorization granted by the resource owner. Exchanged for an access token.
Authorization Server: The server that authenticates the resource owner and issues access tokens after successful authorization.
Client: An application making protected resource requests on behalf of the resource owner with its authorization.
Client ID: A public identifier for the client application, issued by the authorization server.
Grant Type: The method used by the client to obtain an access token (e.g., authorization_code
).
PKCE (Proof Key for Code Exchange): A security extension that prevents authorization code interception attacks by requiring clients to prove possession of a cryptographic secret.
Redirect URI: The URI the authorization server redirects the user-agent to after authorization completion.
Resource Owner: The entity (typically a user) capable of granting access to a protected resource.
Resource Server: The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens.
Scope: A parameter defining the access privileges requested by the client or granted by the authorization server.
State Parameter: A value used by the client to maintain state between the authorization request and callback, also serves as CSRF protection.
Code Challenge: A derived value from the code verifier, sent during the authorization request (SHA256 hash).
Code Challenge Method: The method used to derive the code challenge from the verifier (always "S256" in OAuth 2.1).
Code Verifier: A cryptographically random string generated by the client, kept secret and used to prove ownership during token exchange.
200 OK: Successful request with response body.
302 Found: Redirect response used in OAuth flows.
400 Bad Request: Invalid request parameters or malformed request.
401 Unauthorized: Missing or invalid access token.
404 Not Found: Requested resource does not exist.
CSRF (Cross-Site Request Forgery): An attack that forces authenticated users to submit unauthorized requests.
Bearer Token: A type of access token where possession of the token is sufficient for access (no additional proof required).
Bcrypt: A password hashing function designed to be computationally expensive to resist brute-force attacks.
Base64url Encoding: A variant of Base64 encoding that is URL-safe (uses different characters for padding and special symbols).
After seeing the step-by-step educational flow, you might wonder how OAuth works in production applications. In reality, most technical steps happen automatically behind the scenes, creating a seamless user experience.
In real-world applications, users typically see:
- User visits your app (e.g., a photo-sharing service)
- Clicks "Login with Google" (or GitHub, Facebook, etc.)
- Gets redirected to Google's login page (if not already logged in)
- Sees authorization prompt: "PhotoApp wants to access your profile and email"
- Clicks "Allow"
- Immediately returns to PhotoApp, now logged in
That's it! Just 3-4 clicks from the user's perspective.
sequenceDiagram
participant User as User Browser
participant App as Your App
participant Provider as OAuth Provider<br/>(Google/GitHub/etc)
participant API as Provider API
User->>App: 1. Click "Login with Google"
App->>App: Generate PKCE challenge (hidden)
App->>User: 2. Redirect to Google
User->>Provider: 3. Login/authorize
Provider->>User: 4. Redirect back to app with code
User->>App: 5. Browser follows redirect
Note over App: All steps below happen automatically
App->>Provider: 6. Exchange code for token (AJAX/backend)
Provider->>App: 7. Return access token
App->>API: 8. Fetch user profile with token
API->>App: 9. Return user data
App->>User: 10. Show logged-in dashboard
In our learning project:
- Manual steps: User clicks "Exchange Code for Token"
- Visible tokens: Access tokens displayed on screen
- Separate steps: Each OAuth phase is a separate page/action
- Detailed logging: All messages shown for educational purposes
In production applications:
- Automatic token exchange: Happens via JavaScript or backend code
- Hidden tokens: Stored securely, never shown to users
- Seamless flow: User goes from "not logged in" to "logged in" in one flow
- Background API calls: User profile fetching happens automatically
Frontend JavaScript example:
// User clicks "Login with Google"
function loginWithGoogle() {
// Generate PKCE challenge (automatic)
const codeVerifier = generateRandomString();
const codeChallenge = sha256(codeVerifier);
// Store verifier securely
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Redirect to OAuth provider
window.location.href = `https://accounts.google.com/oauth/authorize?
client_id=your-client-id&
redirect_uri=https://yourapp.com/callback&
scope=openid email profile&
code_challenge=${codeChallenge}&
code_challenge_method=S256`;
}
// Automatic callback handler
function handleCallback() {
const code = new URLSearchParams(window.location.search).get('code');
const verifier = sessionStorage.getItem('pkce_verifier');
// Exchange code for token (AJAX, automatic)
fetch('/api/token-exchange', {
method: 'POST',
body: JSON.stringify({ code, code_verifier: verifier })
})
.then(response => response.json())
.then(data => {
// Store tokens securely
localStorage.setItem('access_token', data.access_token);
// Fetch user profile and show dashboard
showLoggedInDashboard(data.user);
});
}
Terminal applications can't redirect users to web browsers in the same way, so they use different OAuth flows:
This is perfect for CLI tools like Claude Code accessing your MCP server:
sequenceDiagram
participant CLI as Claude Code CLI
participant User as User
participant Browser as User Browser
participant AuthServer as Your MCP Auth Server
participant MCP as MCP Server
CLI->>AuthServer: 1. Request device code
AuthServer->>CLI: 2. Return device_code + user_code + verification_uri
CLI->>User: 3. Display: "Visit https://yourserver.com/device<br/>Enter code: ABCD-1234"
User->>Browser: 4. Opens verification URL
Browser->>AuthServer: 5. GET /device (shows code entry form)
User->>AuthServer: 6. Enter code ABCD-1234 + login
AuthServer->>AuthServer: 7. Associate device with user session
Note over CLI,AuthServer: Polling for authorization
CLI->>AuthServer: 8. Poll: "Is device authorized yet?"
AuthServer->>CLI: 9. "Still pending..."
CLI->>AuthServer: 10. Poll again
AuthServer->>CLI: 11. "Authorized! Here's access token"
CLI->>MCP: 12. Access MCP server with token
MCP->>CLI: 13. Return requested data
Here's exactly how this would work for Claude Code accessing your MCP server:
1. Initial setup (one-time):
$ claude-code configure mcp-server https://your-mcp-server.com
Starting OAuth setup for MCP server...
Visit: https://your-mcp-server.com/device
Enter code: WXYZ-5678
Waiting for authorization...
2. User authorizes in browser:
- User opens https://your-mcp-server.com/device
- Enters code WXYZ-5678
- Logs in with their credentials
- Grants permission to Claude Code
3. CLI receives authorization:
✅ Authorization successful!
Access token stored securely.
Claude Code can now access your MCP server.
4. Subsequent usage (automatic):
$ claude-code chat
# Claude Code automatically uses stored token to access MCP server
# No user interaction needed for subsequent requests
Some CLI tools use a temporary local web server:
$ claude-code auth
Starting local callback server on http://localhost:8080
Opening browser to: https://your-mcp-server.com/oauth/authorize?...
# Browser automatically opens
# User authorizes in browser
# Browser redirects to localhost:8080
# CLI receives callback and stores token
# Local server shuts down
✅ Authentication complete!
Real-world examples:
-
GitHub CLI (
gh auth login
):- Uses device flow
- Shows verification URL and code
- Polls GitHub until authorized
-
Google Cloud CLI (
gcloud auth login
):- Opens browser automatically
- Uses local callback server
- Stores tokens in user's config directory
-
AWS CLI (
aws configure sso
):- Device flow for headless environments
- Browser flow for desktop use
-
Docker CLI (
docker login
):- Simple username/password for Docker Hub
- OAuth for registry integrations
Token storage:
- Store in OS-specific secure locations
- Encrypt tokens at rest
- Use short-lived tokens with refresh capability
PKCE in CLI apps:
- Still required for security
- Generated and managed automatically
- User never sees technical details
Error handling:
- Graceful timeout if user doesn't authorize
- Clear instructions for manual token entry
- Fallback authentication methods
For your MCP server, you'd implement:
1. Device authorization endpoint:
// POST /oauth/device
func (s *Server) deviceAuthorization(w http.ResponseWriter, r *http.Request) {
deviceCode := generateRandomString(32)
userCode := generateUserFriendlyCode() // "ABCD-1234"
// Store device code with expiration
s.deviceCodes[deviceCode] = &DeviceCode{
UserCode: userCode,
ExpiresAt: time.Now().Add(10 * time.Minute),
Status: "pending",
}
json.NewEncoder(w).Encode(map[string]interface{}{
"device_code": deviceCode,
"user_code": userCode,
"verification_uri": "https://your-mcp-server.com/device",
"interval": 5, // polling interval in seconds
"expires_in": 600,
})
}
2. User verification page:
// GET /device - show code entry form
// POST /device - verify code and create user session
func (s *Server) deviceVerification(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
userCode := r.FormValue("user_code")
// Validate user login + associate device with user
// Update device code status to "authorized"
}
// Show HTML form for code entry
}
3. Token endpoint for CLI polling:
// POST /oauth/token (device flow)
func (s *Server) deviceToken(w http.ResponseWriter, r *http.Request) {
deviceCode := r.FormValue("device_code")
device := s.deviceCodes[deviceCode]
if device.Status == "pending" {
// Return "authorization_pending"
http.Error(w, "authorization_pending", 400)
return
}
if device.Status == "authorized" {
// Generate access token
accessToken := generateAccessToken(device.UserID)
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": accessToken,
"token_type": "Bearer",
"expires_in": 3600,
})
}
}
This approach gives you the security of OAuth 2.1 while providing a smooth experience for both web and CLI applications!
In our educational demo, we hardcoded all the OAuth endpoints (authorize, token, etc.). In production systems, you'll often see discovery endpoints that automatically provide this configuration:
These are standardized URLs that return JSON metadata about the OAuth/OpenID Connect server:
/.well-known/oauth-authorization-server
(RFC 8414) - OAuth 2.0 metadata/.well-known/openid-configuration
(OpenID Connect) - Includes OAuth + identity features
Manual configuration (our demo):
const authServer = {
authorizeEndpoint: "http://localhost:8081/authorize",
tokenEndpoint: "http://localhost:8081/token",
// Client has to know all endpoints
};
Discovery-based configuration:
// Client only needs the base URL
const config = await fetch("https://auth.example.com/.well-known/oauth-authorization-server");
const metadata = await config.json();
// Automatically discovers all endpoints
console.log(metadata.authorization_endpoint); // "https://auth.example.com/oauth/authorize"
console.log(metadata.token_endpoint); // "https://auth.example.com/oauth/token"
Keycloak discovery:
curl https://your-keycloak.com/auth/realms/myrealm/.well-known/openid-configuration
Google discovery:
curl https://accounts.google.com/.well-known/openid-configuration
GitHub (OAuth 2.0 only):
curl https://github.com/.well-known/oauth-authorization-server
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/oauth/authorize",
"token_endpoint": "https://auth.example.com/oauth/token",
"userinfo_endpoint": "https://auth.example.com/oauth/userinfo",
"device_authorization_endpoint": "https://auth.example.com/oauth/device",
"revocation_endpoint": "https://auth.example.com/oauth/revoke",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"]
}
- Dynamic configuration: No hardcoded URLs
- Environment flexibility: Same client code works with dev/staging/prod
- Feature detection: Client knows what the server supports
- Future-proofing: New endpoints automatically discovered
We didn't include discovery in our educational demo because:
- Learning focus: We wanted to show the explicit OAuth endpoints
- Simplicity: Direct endpoints are easier to understand initially
- Manual demonstration: Each step is visible and traceable
However, let's add a discovery endpoint to show how it works:
// GET /.well-known/oauth-authorization-server
func (as *AuthServer) discovery(w http.ResponseWriter, r *http.Request) {
metadata := map[string]interface{}{
"issuer": "http://localhost:8081",
"authorization_endpoint": "http://localhost:8081/authorize",
"token_endpoint": "http://localhost:8081/token",
"scopes_supported": []string{"read"},
"response_types_supported": []string{"code"},
"grant_types_supported": []string{"authorization_code"},
"code_challenge_methods_supported": []string{"S256"},
"token_endpoint_auth_methods_supported": []string{"none"}, // public client
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(metadata)
}
This makes client integration much more robust and follows OAuth 2.0 best practices!
-
Add refresh tokens with rotation:
type TokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int64 `json:"expires_in"` }
-
Implement token introspection (RFC 7662):
// POST /introspect func (as *AuthServer) introspect(w http.ResponseWriter, r *http.Request) { token := r.FormValue("token") // Validate and return token metadata }
-
Add scopes and permissions:
type Scope struct { Name string Description string Resources []string }
-
Implement client authentication:
type Client struct { ID string Secret string RedirectURIs []string Type string // "public" or "confidential" }
- Persistent storage: Replace in-memory stores with databases
- Rate limiting: Implement brute force protection
- Audit logging: Track all security events
- HTTPS enforcement: Never run OAuth over HTTP in production
- CORS handling: Properly configure cross-origin policies
- Token introspection: Enable distributed token validation
- Client registration: Dynamic client registration (RFC 7591)
- Device flow: Support for devices without browsers (RFC 8628)
- API Gateway integration: Token validation at gateway level
- Microservices: Distributed token validation
- Single Sign-On: Multiple client applications
- Mobile applications: Native app integration patterns
- Third-party providers: Integration with external OAuth providers
This project provides a solid foundation for understanding OAuth 2.1 and implementing secure authorization systems in production environments.