Skip to content

A complete Java implementation of the Model Context Protocol (MCP) authorization specification with OAuth 2.1 and dynamic client registration support

License

Notifications You must be signed in to change notification settings

timduly4/mcp-authorization

Repository files navigation

MCP Authorization Client for Java

A complete Java implementation of the Model Context Protocol (MCP) authorization specification with OAuth 2.1 and dynamic client registration support.

Features

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 RefreshBearer Token Authentication

Architecture

This implementation follows the MCP authorization specification and includes:

  • MCPClient - Main client for making authenticated MCP requests
  • MetadataDiscovery - Discovers OAuth 2.0 server and resource metadata
  • DynamicClientRegistration - Registers clients without manual configuration
  • PKCEGenerator - Generates PKCE parameters for secure authorization
  • AuthorizationFlow - Handles OAuth 2.1 authorization code flow
  • Model classes - Type-safe representations of OAuth responses

Requirements

  • Java 17 or higher
  • Bazel 8+ (uses Bzlmod for dependency management)

Building

# Build the library
bazel build //:mcp-authorization-client

# Run the example
bazel run //:example

# Run tests
bazel test //:tests

Quick Start

1. Create an MCP Client

import io.mcp.client.MCPClient;
import com.google.gson.JsonObject;

// Create client for your MCP server
MCPClient client = new MCPClient("https://mcp.example.com");

2. Initialize Authorization

// 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

3. Complete Authorization

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);

4. Make Authenticated Requests

// 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

Complete Example

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

OAuth 2.1 Authorization Flow

┌──────────┐                                           ┌──────────────┐
│   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                                     │
     │ <──────────────────────────────────────────────────────
     │                                                        │

Authorization Code, PKCE, and Token Flow

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.

The Three Entities

  1. Java MCP Client - Your application (includes temporary callback server on port 8080)
  2. Authorization Server - OAuth provider that issues tokens
  3. User's Browser - Intermediary that carries the authorization code

Phase 1: PKCE Generation (Client)

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 in AuthorizationContext, never sent to browser

What the client sends:

  • code_challenge - Included in authorization URL (safe to send - it's a hash)

Phase 2: Authorization Request (Client → Browser → Auth Server)

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
}

Phase 3: Authorization Code Issuance (Auth Server → Browser → Client)

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.

Phase 4: Token Exchange with PKCE Proof (Client → Auth Server)

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:5001

Authorization server verification:

  1. Retrieves stored code_challenge from Phase 2
  2. Computes SHA256(code_verifier) from the request
  3. 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 tokens

Why this works:

  • Only the client that generated the code_verifier can 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

Phase 5: Token Issuance (Auth Server → Client)

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 requests
  • refresh_token - Used to obtain new access tokens
  • tokenExpiresAt - Calculated from expires_in

Phase 6: Authenticated MCP Requests (Client → MCP Server)

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.

Phase 7: Token Refresh (When Needed)

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:5001

The authorization server issues a new access token (refresh token stays valid).

Summary Table: What Each Entity Handles

Entity Generates Stores Sends Validates
Java Client code_verifier
code_challenge
code_verifier
access_token
refresh_token
tokenExpiresAt
code_challenge (authorization)
code_verifier (token exchange)
access_token (MCP requests)
-
Authorization Server authorization_code
access_token
refresh_token
code_challenge
authorization_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) -

Key Security Properties

  1. code_verifier - NEVER leaves the Java client (not in browser, not in URL)
  2. code_challenge - Safe to send through browser (one-way hash)
  3. authorization_code - Goes through browser BUT useless without verifier (expires in 5 minutes, single-use)
  4. access_token - Never goes through browser (direct server-to-server exchange)
  5. 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.

Security Features

PKCE (Proof Key for Code Exchange)

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 request

Resource Indicators (RFC 8707)

Binds access tokens to specific MCP servers, preventing token misuse:

// Automatically included in authorization and token requests
// resource=https://mcp.example.com

Token Security

  • 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

HTTPS Enforcement

  • All authorization server endpoints must use HTTPS
  • Redirect URIs must be localhost or HTTPS

API Reference

MCPClient

Constructor

MCPClient(String mcpServerUri)
MCPClient(String mcpServerUri, OkHttpClient httpClient)

Methods

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.

PKCEGenerator

generate() Returns: PKCEParams Generates PKCE code_verifier, code_challenge, and code_challenge_method (S256).

MetadataDiscovery

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).

DynamicClientRegistration

register(String registrationEndpoint, String clientName, String... redirectUris) Returns: ClientCredentials Registers a new OAuth 2.0 client dynamically (RFC 7591).

Error Handling

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
    }
}

Testing

The project includes unit tests for all components:

bazel test //:tests

Standards Compliance

This implementation conforms to:

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Support

For issues and questions:

About

A complete Java implementation of the Model Context Protocol (MCP) authorization specification with OAuth 2.1 and dynamic client registration support

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published