Class AeadRequestStateCodec

java.lang.Object
org.apache.juneau.rest.server.mcp.v20260728.AeadRequestStateCodec
All Implemented Interfaces:
RequestStateCodec

public class AeadRequestStateCodec extends Object implements RequestStateCodec
Built-in default RequestStateCodec: AES-256-GCM keyed by a pluggable KeyProvider.

The no-arg constructor uses a fresh, per-process EphemeralKeyProvider — not restart-durable and not shareable across process instances by design. Each instance generates its own random key and keyId at construction time; a token sealed by one instance can never be unsealed by another (including the same process after a restart). Operators who need cross-restart or multi-instance resumption must supply a shared/rotating KeyProvider (e.g. StaticKeyProvider) via AeadRequestStateCodec(KeyProvider) instead of relying on this default. This is a documented, tested property (see AeadRequestStateCodec_Test), not an accidental limitation.

Sealed-token format (all opaque to callers, versioned so it can evolve): version '.' base64url(keyId) '.' base64url(nonce) '.' base64url(ciphertext+tag). The current version literal is "1"; an unrecognized version fails unseal(java.lang.String, java.lang.String, java.security.Principal) closed. The keyId segment is cleartext base64url of the UTF-8 keyId bytes — it has to be read before the key it names can be resolved for decryption — but it is folded into the AEAD's authenticated data, so a swapped keyId fails the GCM tag check exactly like any other tamper. The plaintext is the JSON serialization of the McpRequestState record.

The AAD passed to seal(org.apache.juneau.rest.server.mcp.v20260728.McpRequestState, java.lang.String, java.security.Principal)/unseal(java.lang.String, java.lang.String, java.security.Principal) is authenticated but never encrypted (standard AES-GCM AAD semantics). The dispatcher passes the canonical method + '' + protocolVersion() + '' + target (NUL-separated) form as the caller-supplied AAD (see McpRevision#aad) — the trailing target field (the tool/prompt name or resource uri) binds the token to the specific operation it paused against, not merely the method, so it can't be resumed against a different tool/prompt/resource. This codec composes that caller-supplied aad, the KeyProvider's keyId (per its implicit keyId-authentication contract), and principalIdentity(principal) (the principal binding described below) into the cipher's actual AAD using the same self-delimiting, length-prefixed framing described on principalIdentity(Principal) — so the three-field outer composition is unambiguous exactly as the two-field inner one is, even if a field happened to contain a NUL.

Principal-bound AAD. seal(org.apache.juneau.rest.server.mcp.v20260728.McpRequestState, java.lang.String, java.security.Principal)/unseal(java.lang.String, java.lang.String, java.security.Principal) fold the caller's authenticated Principal identity into the AEAD's authenticated data, so a requestState minted for one caller cannot be resumed by another: a mismatched principal fails the GCM tag check and unseal(java.lang.String, java.lang.String, java.security.Principal) returns Optional.empty(). The bound identity is a canonical, deterministic iss|sub (issuer + subject) string derived by principalIdentity(Principal) — issuer-scoped so a bare sub cannot collide across IdPs. A null (anonymous / RS-auth-disabled) principal binds fail-closed to a fixed "anonymous" sentinel rather than skipping the binding, so an anonymous-sealed token and an authenticated-sealed token are never mutually resumable (anonymous↔anonymous still round-trips). See principalIdentity(Principal) for the exact canonical form and its collision-safety guarantee.

Random per-seal nonces are safe up to roughly 2^32 seals under a single key (the AES-GCM birthday bound). The ephemeral default's key is per-process and never approaches that. A long-lived custom KeyProvider key (e.g. a StaticKeyProvider entry left in place for months) is the case where this bound actually matters — rotate to a fresh keyId well before it, not after.

unseal(java.lang.String, java.lang.String, java.security.Principal) is wired to untrusted, client-supplied requestState strings, so it applies defensive length guards (a plausibly-sized token, and a bounded keyId segment) before any base64 decode; an out-of-bounds input returns Optional.empty() rather than allocating from an attacker-controlled length.

  • Constructor Details

    • AeadRequestStateCodec

      Constructor. Uses a fresh, per-process EphemeralKeyProvider — the zero-config, non-shareable, non-durable default described in the class Javadoc.
    • AeadRequestStateCodec

      public AeadRequestStateCodec(KeyProvider keyProvider)
      Constructor.
      Parameters:
      keyProvider - The source of sealing/resolving keys. Must not be null.
  • Method Details

    • seal

      public String seal(McpRequestState state, String aad, Principal principal)
      Description copied from interface: RequestStateCodec
      Seals a payload into an opaque token, exposing the authenticated caller identity to the codec.
      Specified by:
      seal in interface RequestStateCodec
      Parameters:
      state - The payload to seal. Must not be null.
      aad - Additional authenticated data (the dispatcher passes the canonical method + '' + protocolVersion + '' + target form — see the class Javadoc). Must not be null.
      principal - The authenticated caller (see the class Javadoc). May be null for an anonymous caller or when resource-server auth is disabled.
      Returns:
      The opaque token. Never null.
    • unseal

      public Optional<McpRequestState> unseal(String token, String aad, Principal principal)
      Description copied from interface: RequestStateCodec
      Unseals a token, verifying integrity against the supplied AAD, with the authenticated caller identity available to the codec.

      The token is arbitrary, untrusted client input. It arrives verbatim from the follow-up request's requestState field, so an implementation must assume nothing about its length or content: it must not allocate buffers sized from the raw token, and it must not throw — any malformed, oversized, tampered, or AAD-mismatched input must return Optional.empty() (see the class Javadoc contract).

      Specified by:
      unseal in interface RequestStateCodec
      Parameters:
      token - The token previously returned by RequestStateCodec.seal(McpRequestState, String, Principal). Must not be null.
      aad - The AAD to verify against. Must match the value passed to RequestStateCodec.seal(McpRequestState, String, Principal) exactly, or verification fails.
      principal - The authenticated caller (see the class Javadoc). May be null for an anonymous caller or when resource-server auth is disabled. A codec that binds the principal must fail verification when it differs from the sealing principal.
      Returns:
      The original payload, or Optional.empty() if verification fails for any reason (tamper, AAD mismatch, malformed token, principal mismatch).