A complete Java implementation of the Model Context Protocol (MCP) authorization specification with OAuth 2.1 and dynamic client registration support.
✅ OAuth 2.1 Authorization Code Flow with PKCE (RFC 7636) ✅ Dynamic Client Registration (RFC 7591) ✅ Authorization Server Discovery (RFC 8414) ✅ Protected Resource Metadata (RFC 9728) ✅ Resource Indicators (RFC 8707) ✅ Automatic Token Refresh ✅ Bearer Token Authentication
This implementation follows the MCP authorization specification and includes:
MCPClient- Main client for making authenticated MCP requestsMetadataDiscovery- Discovers OAuth 2.0 server and resource metadataDynamicClientRegistration- Registers clients without manual configurationPKCEGenerator- Generates PKCE parameters for secure authorizationAuthorizationFlow- Handles OAuth 2.1 authorization code flow- Model classes - Type-safe representations of OAuth responses
- Java 17 or higher
- Bazel 8+ (uses Bzlmod for dependency management)
# Build the library
bazel build //:mcp-authorization-client
# Run the example
bazel run //:example
# Run tests
bazel test //:testsimport io.mcp.client.MCPClient;
import com.google.gson.JsonObject;
// Create client for your MCP server
MCPClient client = new MCPClient("https://mcp.example.com");// Start authorization flow with dynamic client registration
MCPClient.AuthorizationContext authContext = client.initializeAuthorization(
"My Application", // Client name
"http://localhost:8080/callback" // Redirect URI
);
// Get the authorization URL for the user
String authUrl = authContext.getAuthorizationUrl();
System.out.println("Visit: " + authUrl);This automatically:
- Discovers the authorization server from the MCP server
- Registers your client dynamically (no manual setup!)
- Generates PKCE parameters
- Builds the authorization URL with resource indicators
After the user authorizes, exchange the code for tokens:
// Extract code from callback URL (e.g., ?code=abc123)
String authorizationCode = "abc123";
// Complete the authorization flow
client.completeAuthorization(authorizationCode, authContext);// Create a JSON-RPC request
JsonObject request = new JsonObject();
request.addProperty("jsonrpc", "2.0");
request.addProperty("method", "resources/list");
request.addProperty("id", 1);
// Make authenticated MCP request
String response = client.makeRequest("/mcp/v1", request);
System.out.println(response);The client automatically:
- Includes the
Authorization: Bearer <token>header - Refreshes tokens when expired
- Handles 401/403 errors
See src/main/java/io/mcp/example/ExampleClient.java for a complete working example with:
- Local callback server for authorization
- Multiple authenticated requests
- Error handling
┌──────────┐ ┌──────────────┐
│ User │ │ MCP Server │
└────┬─────┘ └──────┬───────┘
│ │
│ 1. Make unauthenticated request │
│ ──────────────────────────────────────────────────────>
│ │
│ 2. 401 + WWW-Authenticate header │
│ <──────────────────────────────────────────────────────
│ │
┌────┴─────┐ │
│ Client │ 3. Discover protected resource metadata │
│ discovers├──────────────────────────────────────────────────>
│ metadata │ /.well-known/oauth-protected-resource │
└────┬─────┘ │
│ │
│ 4. Get authorization server location │
│ <──────────────────────────────────────────────────────
│ │
│ ┌────────────────┐ │
│ 5. Discover │ Authorization │ │
│ metadata │ Server │ │
│ ──────────────────> │ │
│ /.well-known/ │ │ │
│ oauth-authorization-server │ │
│ └────────┬───────┘ │
│ │ │
│ 6. Dynamic client │ │
│ registration (RFC 7591) │ │
│ ───────────────────────────> │
│ │ │
│ 7. Client credentials │ │
│ (client_id, client_secret)│ │
│ <─────────────────────────── │
│ │ │
│ 8. Authorization request │ │
│ + PKCE code_challenge │ │
│ + resource parameter │ │
│ ───────────────────────────> │
│ │ │
│ 9. User authorizes │ │
│ ───────────────────────────> │
│ │ │
│ 10. Authorization code │ │
│ <─────────────────────────── │
│ │ │
│ 11. Token request │ │
│ + code_verifier (PKCE) │ │
│ + resource parameter │ │
│ ───────────────────────────> │
│ │ │
│ 12. Access token │ │
│ + refresh token │ │
│ <─────────────────────────── │
│ │ │
│ 13. Authenticated MCP request │
│ Authorization: Bearer <token> │
│ ──────────────────────────────────────────────────────>
│ │
│ 14. MCP response │
│ <──────────────────────────────────────────────────────
│ │
This section provides a detailed walkthrough of how authorization codes, PKCE parameters, and tokens are handled by each entity in the OAuth 2.1 flow.
- Java MCP Client - Your application (includes temporary callback server on port 8080)
- Authorization Server - OAuth provider that issues tokens
- User's Browser - Intermediary that carries the authorization code
The Java client generates PKCE parameters before starting the authorization flow:
PKCEGenerator.PKCEParams pkceParams = PKCEGenerator.generate();Generated values:
code_verifier: Random 128-character string (e.g.,"Kx8f2JdP...mN3zQ")code_challenge: SHA-256 hash of verifier (e.g.,"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")code_challenge_method:"S256"
What the client keeps:
- ✅
code_verifier- Stored inAuthorizationContext, never sent to browser
What the client sends:
- ✅
code_challenge- Included in authorization URL (safe to send - it's a hash)
The client builds an authorization URL and directs the user to visit it:
http://localhost:5001/oauth/authorize
?response_type=code
&client_id=client_abc123
&redirect_uri=http://localhost:8080/callback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM ← Challenge (hash)
&code_challenge_method=S256
&resource=http://localhost:5001
&state=uuid-123
Authorization server receives and stores:
authorization_codes[auth_code] = {
"client_id": "client_abc123",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", # Stored for later
"code_challenge_method": "S256",
"redirect_uri": "http://localhost:8080/callback",
"expires_at": now + 5_minutes,
"used": False
}After user approval, the authorization server redirects the browser:
http://localhost:8080/callback?code=code_xyz789abc&state=uuid-123
The Java client's temporary callback server receives this request and extracts the authorization code.
Key security property: The code_verifier never appears in this URL - only the authorization code does.
The client exchanges the authorization code for tokens by sending:
POST http://localhost:5001/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=code_xyz789abc
&redirect_uri=http://localhost:8080/callback
&client_id=client_abc123
&code_verifier=Kx8f2JdP...mN3zQ ← Verifier sent here (not challenge!)
&resource=http://localhost:5001Authorization server verification:
- Retrieves stored
code_challengefrom Phase 2 - Computes
SHA256(code_verifier)from the request - Compares computed hash with stored challenge:
computed_challenge = SHA256(code_verifier) # "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
stored_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
if computed_challenge == stored_challenge:
# ✓ Client proved it initiated the original request
# Issue tokensWhy this works:
- Only the client that generated the
code_verifiercan provide it - An attacker who intercepts the authorization code cannot use it without the verifier
- The verifier never went through the browser, so it couldn't be stolen
The authorization server issues tokens:
{
"access_token": "access_ABC123XYZ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh_DEF456...",
"scope": "mcp:read mcp:write"
}Client stores:
access_token- Used for authenticated MCP requestsrefresh_token- Used to obtain new access tokenstokenExpiresAt- Calculated fromexpires_in
The client makes authenticated requests with the access token:
POST http://localhost:5001/mcp/v1
Authorization: Bearer access_ABC123XYZ...
Content-Type: application/json
{"jsonrpc":"2.0","method":"resources/list","id":1}The MCP server validates the token and processes the request.
When the access token expires, the client uses the refresh token:
POST http://localhost:5001/oauth/token
Authorization: Basic base64(client_id:client_secret)
grant_type=refresh_token
&refresh_token=refresh_DEF456...
&client_id=client_abc123
&resource=http://localhost:5001The authorization server issues a new access token (refresh token stays valid).
| Entity | Generates | Stores | Sends | Validates |
|---|---|---|---|---|
| Java Client | code_verifiercode_challenge |
code_verifieraccess_tokenrefresh_tokentokenExpiresAt |
code_challenge (authorization)code_verifier (token exchange)access_token (MCP requests) |
- |
| Authorization Server | authorization_codeaccess_tokenrefresh_token |
code_challengeauthorization_codes{}access_tokens{}refresh_tokens{} |
authorization_code (via redirect) |
✓ code_challenge vs code_verifier✓ authorization_code validity✓ access_token validity |
| Browser | - | - | authorization_code (via redirect) |
- |
code_verifier- NEVER leaves the Java client (not in browser, not in URL)code_challenge- Safe to send through browser (one-way hash)authorization_code- Goes through browser BUT useless without verifier (expires in 5 minutes, single-use)access_token- Never goes through browser (direct server-to-server exchange)refresh_token- Never goes through browser, stored securely by client
This separation ensures that even if an attacker intercepts the authorization code from the browser, they cannot exchange it for tokens without the code_verifier, which never left the client application.
Prevents authorization code interception attacks by requiring clients to prove they made the original authorization request.
// PKCE is automatically handled
PKCEGenerator.PKCEParams pkce = PKCEGenerator.generate();
// code_challenge sent in authorization request
// code_verifier sent in token requestBinds access tokens to specific MCP servers, preventing token misuse:
// Automatically included in authorization and token requests
// resource=https://mcp.example.com- Access tokens are never sent in URL query parameters (header only)
- Tokens are validated for the correct audience
- Short-lived access tokens with refresh token rotation
- Automatic token refresh before expiration
- All authorization server endpoints must use HTTPS
- Redirect URIs must be
localhostor HTTPS
MCPClient(String mcpServerUri)
MCPClient(String mcpServerUri, OkHttpClient httpClient)initializeAuthorization(String clientName, String redirectUri)
Returns: AuthorizationContext
Discovers metadata, registers the client, and generates the authorization URL.
completeAuthorization(String authorizationCode, AuthorizationContext context)
Exchanges the authorization code for access and refresh tokens.
makeRequest(String endpoint, JsonObject jsonRpcRequest)
Returns: String (response body)
Makes an authenticated MCP request with automatic token refresh.
generate()
Returns: PKCEParams
Generates PKCE code_verifier, code_challenge, and code_challenge_method (S256).
discoverProtectedResource(String mcpServerUri)
Returns: ProtectedResourceMetadata
Discovers OAuth 2.0 protected resource metadata (RFC 9728).
discoverAuthorizationServer(String authServerUri)
Returns: AuthorizationServerMetadata
Discovers OAuth 2.0 authorization server metadata (RFC 8414).
register(String registrationEndpoint, String clientName, String... redirectUris)
Returns: ClientCredentials
Registers a new OAuth 2.0 client dynamically (RFC 7591).
The client throws IOException for various error conditions:
- 401 Unauthorized: Invalid or expired access token
- 403 Forbidden: Insufficient permissions/scopes
- 400 Bad Request: Malformed authorization request
- Discovery failures: Missing metadata endpoints
- Token refresh failures: Invalid refresh token
try {
String response = client.makeRequest("/mcp/v1", request);
} catch (IOException e) {
if (e.getMessage().contains("Unauthorized")) {
// Re-authorize the user
} else if (e.getMessage().contains("Forbidden")) {
// Request additional scopes
}
}The project includes unit tests for all components:
bazel test //:testsThis implementation conforms to:
- MCP Authorization Specification
- OAuth 2.1 (IETF Draft)
- RFC 7591 - Dynamic Client Registration
- RFC 7636 - PKCE
- RFC 8414 - Authorization Server Metadata
- RFC 8707 - Resource Indicators
- RFC 9728 - Protected Resource Metadata
MIT License - see LICENSE file for details.
Contributions are welcome! Please open an issue or submit a pull request.
For issues and questions:
- Check the MCP specification
- Open an issue on GitHub
- Review the example client code