001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp.v20260728; 018 019import java.security.Principal; 020import java.util.*; 021 022import org.apache.juneau.commons.concurrent.*; 023 024/** 025 * Pluggable SPI for sealing/unsealing the opaque {@code requestState} continuation token (MCP {@code 2026-07-28} 026 * SEP-2322 Multi-Round-Trip Requests). 027 * 028 * <p> 029 * Mirrors the "pluggable SPI + built-in default" shape sub-project E established for the client-side 030 * {@code McpResponseCache} (not on this module's classpath): this interface plus {@link AeadRequestStateCodec} 031 * (the built-in default) is the server-side, seal/unseal counterpart. 032 * 033 * <p> 034 * Implementations must treat any verification failure (tamper, AAD mismatch, or an implementation-specific 035 * integrity check) as an {@link Optional#empty()} return from {@link #unseal(String, String)} — never a 036 * thrown exception. Expiry is checked by the caller ({@code McpRevision}) against 037 * {@link McpRequestState#expiresAtMs()}, not by the codec itself, since expiry is a dispatcher-level policy (see 038 * {@link McpMrtrConfig}), not a codec integrity concern. 039 * 040 * <p> 041 * <b>Trust model — {@code requestState} is a bearer token.</b> The sealed token is a replayable credential: 042 * anyone who holds it can resume the paused operation until it expires, and it carries no caller identity. A 043 * codec (and every call site) must treat it accordingly — transport only over authenticated TLS, and 044 * <b>never log</b> a {@code requestState}. <b>Multi-use within TTL by default:</b> a captured token can be 045 * replayed any number of times within its TTL unless an operator configures a {@link ReplayCache} (see 046 * {@link McpMrtrConfig#setReplayCache(ReplayCache)}) to enforce single-use — the built-in 047 * {@link InMemoryReplayCache} is per-process only, so cross-node single-use requires a shared implementation. A 048 * handler must still write idempotent resume side effects unless it knows a single-use {@link ReplayCache} is in 049 * effect; the max-rounds cap bounds chain depth, not replay count (see {@link McpMrtrConfig}). 050 * 051 * <p> 052 * <b>Canonical AAD format.</b> The dispatcher binds each token to the request that produced it by passing a 053 * single canonical AAD string: {@code method + '\u0000' + protocolVersion + '\u0000' + target} — the 054 * JSON-RPC method name, the negotiated protocol version, and the operation target (the tool {@code name} for 055 * {@code tools/call}, the prompt {@code name} for {@code prompts/get}, or the resource {@code uri} for 056 * {@code resources/read}; empty for any other method), all NUL ({@code U+0000})-separated (see 057 * {@code McpRevision#aad}). NUL is chosen because it can never appear in a method name, a protocol-version 058 * literal, or the trailing target field, so the concatenation is unambiguous. The trailing target field binds 059 * the token to the specific operation it paused against, not merely the method, so a token minted while paused 060 * on one tool/prompt/resource cannot be resumed against a different one of the same kind. An implementation 061 * only needs to treat the AAD as opaque bytes; the format is documented here so seal and unseal call sites (and 062 * any custom codec) agree on exactly what is authenticated. 063 * 064 * <p> 065 * <b>Authenticated principal.</b> Both {@link #seal(McpRequestState, String, Principal)} and 066 * {@link #unseal(String, String, Principal)} receive the caller's authenticated {@link Principal} — the same 067 * identity the resource-server layer establishes (see {@code McpResourceServerSupport#principal}). This is the 068 * <i>seam</i> a hardened codec uses to fold the caller identity into its authenticated data, so a 069 * {@code requestState} minted for principal A cannot be resumed by principal B. The built-in 070 * {@link AeadRequestStateCodec} binds exactly that: it folds a canonical, deterministic {@code iss|sub} identity 071 * (see {@link AeadRequestStateCodec#principalIdentity(Principal)}) into its AEAD authenticated data, so a 072 * mismatched principal fails the GCM tag check and {@link #unseal} returns {@link Optional#empty()}. The 073 * dispatcher guarantees the principal is delivered to the codec at both seal and unseal; a custom codec 074 * remains free to bind a different identity attribute (subject claim, issuer+subject, full claim set) or none at 075 * all. The principal is <b>nullable</b>: when RS auth is disabled or the caller is anonymous it is <jk>null</jk>, 076 * and every codec must seal/unseal cleanly (no NPE) in that case. The two-argument 077 * {@link #seal(McpRequestState, String)} / {@link #unseal(String, String)} convenience overloads simply pass a 078 * <jk>null</jk> (no-principal) identity. 079 */ 080public interface RequestStateCodec { 081 082 /** 083 * Seals a payload into an opaque token, exposing the authenticated caller identity to the codec. 084 * 085 * @param state The payload to seal. Must not be <jk>null</jk>. 086 * @param aad Additional authenticated data (the dispatcher passes the canonical 087 * {@code method + '\u0000' + protocolVersion + '\u0000' + target} form — see the class Javadoc). Must 088 * not be <jk>null</jk>. 089 * @param principal The authenticated caller (see the class Javadoc). May be <jk>null</jk> for an anonymous 090 * caller or when resource-server auth is disabled. 091 * @return The opaque token. Never <jk>null</jk>. 092 */ 093 String seal(McpRequestState state, String aad, Principal principal); 094 095 /** 096 * Unseals a token, verifying integrity against the supplied AAD, with the authenticated caller identity 097 * available to the codec. 098 * 099 * <p> 100 * <b>The {@code token} is arbitrary, untrusted client input.</b> It arrives verbatim from the follow-up 101 * request's {@code requestState} field, so an implementation must assume nothing about its length or content: 102 * it must not allocate buffers sized from the raw token, and it must not throw — any malformed, oversized, 103 * tampered, or AAD-mismatched input must return {@link Optional#empty()} (see the class Javadoc contract). 104 * 105 * @param token The token previously returned by {@link #seal(McpRequestState, String, Principal)}. Must not be 106 * <jk>null</jk>. 107 * @param aad The AAD to verify against. Must match the value passed to 108 * {@link #seal(McpRequestState, String, Principal)} exactly, or verification fails. 109 * @param principal The authenticated caller (see the class Javadoc). May be <jk>null</jk> for an anonymous 110 * caller or when resource-server auth is disabled. A codec that binds the principal must fail 111 * verification when it differs from the sealing principal. 112 * @return The original payload, or {@link Optional#empty()} if verification fails for any reason (tamper, 113 * AAD mismatch, malformed token, principal mismatch). 114 */ 115 Optional<McpRequestState> unseal(String token, String aad, Principal principal); 116 117 /** 118 * No-principal convenience overload of {@link #seal(McpRequestState, String, Principal)} (anonymous caller). 119 * 120 * @param state The payload to seal. Must not be <jk>null</jk>. 121 * @param aad Additional authenticated data. Must not be <jk>null</jk>. 122 * @return The opaque token. Never <jk>null</jk>. 123 */ 124 default String seal(McpRequestState state, String aad) { 125 return seal(state, aad, null); 126 } 127 128 /** 129 * No-principal convenience overload of {@link #unseal(String, String, Principal)} (anonymous caller). 130 * 131 * @param token The token previously returned by {@link #seal(McpRequestState, String)}. Must not be <jk>null</jk>. 132 * @param aad The AAD to verify against. 133 * @return The original payload, or {@link Optional#empty()} if verification fails for any reason. 134 */ 135 default Optional<McpRequestState> unseal(String token, String aad) { 136 return unseal(token, aad, null); 137 } 138}