Class OfflineAuthorizationServer

java.lang.Object
org.apache.juneau.examples.mcp.secured.OfflineAuthorizationServer
All Implemented Interfaces:
AutoCloseable

public final class OfflineAuthorizationServer extends Object implements AutoCloseable
A minimal, self-contained, in-process stand-in for a real OAuth 2.1 authorization server (AS), so the secured MCP example (and its end-to-end test) run entirely offline — no external IdP, no network access, no pre-shared secrets checked into source control.

DEMO ONLY — NOT SUITABLE FOR PRODUCTION USE.

This is deliberately NOT a general-purpose or spec-complete authorization server. It exists purely to make SecuredExampleMcpServer's JwtTokenValidator-backed resource-server gate and SecuredExampleClient's McpTokenProvider-backed client-credentials flow both have something real to talk to. Concretely, on start() it:

  1. Generates a fresh RSA-2048 signing key (a new key every run — nothing is persisted, so restarting the example invalidates any previously-issued token, which is exactly the offline-demo property we want).
  2. Publishes only the public half of that key as a JWKSource, which SecuredExampleMcpServer feeds directly to JwtTokenValidator.jwkSource(...) — no JWKS HTTP endpoint is needed because the server and "AS" share this JVM.
  3. Starts a tiny HttpServer exposing two routes:
    • POST /token — the RFC 6749 §4.4 client-credentials grant against one fixed, randomly-generated demo client id/secret pair (see clientId() / clientSecret()). This is a REAL HTTP round trip: the client-side OAuthClientCredentialsFlow used by McpTokenProvider talks to it exactly as it would talk to a production IdP's token endpoint, just on localhost.
    • GET /.well-known/oauth-authorization-server — a minimal RFC 8414 Authorization Server Metadata document (issuer + token_endpoint only), so a real client can perform genuine discovery against issuerUri() instead of being handed the token endpoint out of band. See SecuredExampleClient for the client half of that handshake.

What this class intentionally does NOT implement (called out here and in the README so nobody mistakes this demo for a template for a real deployment):

  • No real scope-authorization decision — handleToken(com.sun.net.httpserver.HttpExchange) only checks the requested scope against a fixed allowlist (mcp.read/mcp.write); it never asks a resource owner or policy engine whether the client is ALLOWED that scope, which is what a real AS's consent/policy step does.
  • No refresh tokens, no consent screen, no user authentication of any kind — only the machine-to-machine client-credentials grant, which is all an MCP server-to-server OAuth demo needs.
  • No persistence, replay protection beyond the JWT exp/nbf window, or key rotation. The RFC 8414 document above is likewise minimal: no jwks_uri, no scopes_supported, no dynamic client registration — this AS's public key is handed directly to the validator in-process (see jwkSource()) rather than published for a real JWKS fetch.

TLS caveat: this class and SecuredExampleServer talk plain http only because both ends are loopback-only (127.0.0.1/localhost). OAuth 2.1 / RFC 9728 require https for any non-loopback resource identifier or token endpoint; a bearer token sent over plaintext to a non-loopback host is exposed to anyone on the network path.

A real deployment would point JwtTokenValidator at an actual IdP's JWKS URL and issuer, and would never generate or hand out client secrets like start() does here.

Since:
10.0.0
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    The scope minted onto a token when the client's request omits one.
    static final Set<String>
    The fixed allowlist of scopes this offline AS will grant (H2): a client requesting anything outside this set is rejected with RFC 6749 §5.2 invalid_scope, rather than being handed back whatever it asked for.
  • Method Summary

    Modifier and Type
    Method
    Description
    Returns the fixed demo client id a caller must present (via HTTP Basic) to acquire a token.
    Returns the randomly-generated demo client secret a caller must present (via HTTP Basic) to acquire a token.
    void
     
    Returns this authorization server's issuer identity (its own loopback base URL) — used as the JWT iss claim, the PRM document's authorization_servers entry, and the RFC 8414 discovery document's issuer field.
    com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext>
    Returns a JWKSource publishing only the public half of this server's signing key, ready to hand straight to JwtTokenValidator.jwkSource(...).
    Generates a fresh signing key and demo client credentials, and starts the /token and RFC 8414 discovery endpoints on an OS-assigned loopback-only port.
    Returns the loopback URL of the /token client-credentials endpoint.
    int
    Returns how many HTTP requests have hit the /token endpoint so far (M7): a test seam proving a caching McpTokenProvider genuinely reuses an acquired token across multiple dispatches instead of silently re-requesting one per call.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • DEFAULT_SCOPE

      public static final String DEFAULT_SCOPE
      The scope minted onto a token when the client's request omits one.
      See Also:
    • GRANTABLE_SCOPES

      public static final Set<String> GRANTABLE_SCOPES
      The fixed allowlist of scopes this offline AS will grant (H2): a client requesting anything outside this set is rejected with RFC 6749 §5.2 invalid_scope, rather than being handed back whatever it asked for.
  • Method Details

    • start

      public static OfflineAuthorizationServer start() throws Exception
      Generates a fresh signing key and demo client credentials, and starts the /token and RFC 8414 discovery endpoints on an OS-assigned loopback-only port.
      Returns:
      A running instance. Close it (or call close()) to stop the HTTP endpoint.
      Throws:
      Exception - If key generation or the HTTP endpoint fails to start.
    • jwkSource

      public com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource()
      Returns a JWKSource publishing only the public half of this server's signing key, ready to hand straight to JwtTokenValidator.jwkSource(...).
      Returns:
      The public-key JWK source. Never null.
    • issuerUri

      public URI issuerUri()
      Returns this authorization server's issuer identity (its own loopback base URL) — used as the JWT iss claim, the PRM document's authorization_servers entry, and the RFC 8414 discovery document's issuer field.
      Returns:
      The issuer URI. Never null.
    • tokenEndpoint

      public URI tokenEndpoint()
      Returns the loopback URL of the /token client-credentials endpoint.
      Returns:
      The token endpoint URL. Never null.
    • clientId

      public String clientId()
      Returns the fixed demo client id a caller must present (via HTTP Basic) to acquire a token.
      Returns:
      The client id. Never null.
    • clientSecret

      public String clientSecret()
      Returns the randomly-generated demo client secret a caller must present (via HTTP Basic) to acquire a token. Generated fresh on every start(); never persisted anywhere — SecuredExampleServer.main(java.lang.String[]) prints it to the console purely so a reader running the demo by hand has something to copy/paste.
      Returns:
      The client secret. Never null.
    • tokenRequestCount

      public int tokenRequestCount()
      Returns how many HTTP requests have hit the /token endpoint so far (M7): a test seam proving a caching McpTokenProvider genuinely reuses an acquired token across multiple dispatches instead of silently re-requesting one per call.
      Returns:
      The number of /token requests handled since start().
    • close

      public void close()
      Specified by:
      close in interface AutoCloseable