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
019
020import java.net.*;
021import java.util.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023
024/**
025 * Fluent, insertion-ordered builder for a {@code WWW-Authenticate: Bearer ...} challenge value (RFC 6750 §3, RFC
026 * 9728 §5.1) emitted by the {@code 2026-07-28} MCP resource server on a {@code 401}.
027 *
028 * <p>
029 * Produces {@code Bearer <k>="<v>", ...} auth-params in the order added, so the emitted header parses cleanly on the
030 * client side via {@code org.apache.juneau.rest.client.mcp.auth.WwwAuthenticateChallenge} (which lower-cases keys and
031 * accepts quoted values).  Auth-param values are sanitized for the RFC 7235 quoted-string grammar: control characters
032 * ({@code < 0x20} and {@code 0x7F}, including CR/LF that would otherwise enable header injection) are stripped, and
033 * {@code "}/{@code \} are backslash-escaped ({@code \"} / {@code \\}) rather than mangled.
034 *
035 * <h5 class='section'>Extensibility:</h5>
036 * <p>
037 * The baseline emits {@link #realm(String) realm}, {@link #error(String) error},
038 * {@link #errorDescription(String) error_description}, {@link #scope(Collection) scope}, and
039 * {@link #resourceMetadata(URI) resource_metadata}.  The generic {@link #param(String, String)} seam lets a
040 * later layer (SEP-2350 scoped step-up) add further auth-params without touching this class &mdash; this class
041 * deliberately does not implement scope-accumulation/step-up.
042 *
043 * @since 10.0.0
044 */
045public final class McpBearerChallenge {
046
047   private final Map<String,String> params = new LinkedHashMap<>();
048
049   /**
050    * Static creator.
051    *
052    * @return A new, empty challenge builder.
053    */
054   public static McpBearerChallenge create() {
055      return new McpBearerChallenge();
056   }
057
058   private McpBearerChallenge() {}
059
060   /**
061    * Adds the {@code realm} auth-param.
062    *
063    * @param value The realm.  A <jk>null</jk> value is ignored.
064    * @return This object (for method chaining).
065    */
066   public McpBearerChallenge realm(String value) {
067      return param("realm", value);
068   }
069
070   /**
071    * Adds the {@code error} auth-param (e.g. {@code invalid_token}, {@code insufficient_scope}).
072    *
073    * @param value The error code.  A <jk>null</jk> value is ignored.
074    * @return This object (for method chaining).
075    */
076   public McpBearerChallenge error(String value) {
077      return param("error", value);
078   }
079
080   /**
081    * Adds the {@code error_description} auth-param.
082    *
083    * @param value The human-readable error description.  A <jk>null</jk> value is ignored.
084    * @return This object (for method chaining).
085    */
086   public McpBearerChallenge errorDescription(String value) {
087      return param("error_description", value);
088   }
089
090   /**
091    * Adds the space-delimited {@code scope} auth-param.
092    *
093    * @param scopes The scopes.  A <jk>null</jk> or empty collection is ignored.
094    * @return This object (for method chaining).
095    */
096   public McpBearerChallenge scope(Collection<String> scopes) {
097      if (ie(scopes))
098         return this;
099      return param("scope", String.join(" ", scopes));
100   }
101
102   /**
103    * Adds the {@code resource_metadata} auth-param (RFC 9728 &sect;5.1) pointing at the PRM document.
104    *
105    * @param value The PRM document URI.  A <jk>null</jk> value is ignored.
106    * @return This object (for method chaining).
107    */
108   public McpBearerChallenge resourceMetadata(URI value) {
109      return param("resource_metadata", value == null ? null : value.toString());
110   }
111
112   /**
113    * Adds an arbitrary auth-param &mdash; the extensibility seam for future challenge parameters.
114    *
115    * @param name The auth-param name.  Must not be <jk>null</jk> or blank.
116    * @param value The auth-param value.  A <jk>null</jk> value is ignored (the param is not added).
117    * @return This object (for method chaining).
118    */
119   public McpBearerChallenge param(String name, String value) {
120      reqnb("name", name);
121      if (value != null)
122         params.put(name, sanitize(value));
123      return this;
124   }
125
126   /**
127    * Builds the {@code WWW-Authenticate} challenge value.
128    *
129    * @return The header value (e.g. {@code Bearer realm="mcp", resource_metadata="https://host/.well-known/..."}), or
130    *    the bare {@code Bearer} scheme when no auth-params were added.
131    */
132   public String build() {
133      if (params.isEmpty())
134         return "Bearer";
135      var sb = new StringBuilder("Bearer ");
136      var first = true;
137      for (var e : params.entrySet()) {
138         if (!first)
139            sb.append(", ");
140         sb.append(e.getKey()).append("=\"").append(e.getValue()).append('"');
141         first = false;
142      }
143      return sb.toString();
144   }
145
146   @Override /* Overridden from Object */
147   public String toString() {
148      return build();
149   }
150
151   private static String sanitize(String v) {
152      var sb = new StringBuilder(v.length());
153      for (var i = 0; i < v.length(); i++) {
154         var c = v.charAt(i);
155         if (c < 0x20 || c == 0x7F)  // Strip all CTLs (incl. CR/LF) that would corrupt the response header.
156            continue;
157         if (c == '"' || c == '\\')  // RFC 7235 quoted-string: escape rather than mangle.
158            sb.append('\\');
159         sb.append(c);
160      }
161      return sb.toString();
162   }
163}