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.auth.oauth.flow;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.UriUtils.*;
021
022import java.net.*;
023import java.time.*;
024import java.util.*;
025import java.util.function.*;
026
027import com.nimbusds.oauth2.sdk.*;
028import com.nimbusds.oauth2.sdk.auth.*;
029import com.nimbusds.oauth2.sdk.http.*;
030import com.nimbusds.oauth2.sdk.id.*;
031
032/**
033 * Client-credentials grant (RFC 6749 §4.4) flow helper.
034 *
035 * <p>
036 * The neutral core performs a single token acquisition and does NOT cache; token caching is layered on top by
037 * the caller (e.g. the server-side {@code CachingClientCredentialsFlow} decorator or the MCP client's token
038 * provider).  Supports RFC 8707 resource-indicator binding via {@link Builder#resource(URI)}.
039 *
040 * <h5 class='topic'>Usage</h5>
041 *
042 * <p class='bjava'>
043 *    <jk>var</jk> token = OAuthClientCredentialsFlow.<jsm>create</jsm>()
044 *       .tokenEndpoint(URI.<jsm>create</jsm>(<js>"https://idp.example.com/oauth2/token"</js>))
045 *       .clientId(<js>"worker-service"</js>)
046 *       .clientSecret(<js>"..."</js>)
047 *       .scope(<js>"read:orders"</js>, <js>"write:orders"</js>)
048 *       .build()
049 *       .acquire();
050 * </p>
051 *
052 * <h5 class='section'>See Also:</h5>
053 * <ul>
054 *    <li class='link'><a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4">RFC 6749 &sect;4.4</a>
055 * </ul>
056 *
057 * @since 10.0.0
058 */
059@SuppressWarnings({
060   "java:S1192" // Duplicate string literals are OAuth protocol parameter names (e.g. "grant_type", "client_id"); intentional
061})
062public class OAuthClientCredentialsFlow {
063
064   /**
065    * Static creator.
066    *
067    * @return A new builder.
068    */
069   public static Builder create() {
070      return new Builder();
071   }
072
073   /**
074    * Builder.
075    */
076   public static class Builder {
077      private URI tokenEndpoint;
078      private String clientId;
079      private Supplier<String> clientSecretSupplier;
080      private final Set<String> scopes = st();
081      private URI resource;
082      private Duration httpTimeout = Flows.DEFAULT_HTTP_TIMEOUT;
083      private Consumer<HTTPRequest> httpRequestConfigurator;
084
085      /** Constructor. */
086      protected Builder() {}
087
088      /**
089       * Sets the token endpoint URL.  Required.
090       *
091       * @param value The endpoint URL.  Must not be <jk>null</jk>.
092       * @return This object.
093       */
094      public Builder tokenEndpoint(URI value) {
095         tokenEndpoint = assertSecureOrLoopback(reqnn("value", value));
096         return this;
097      }
098
099      /**
100       * Sets the OAuth client ID.  Required.
101       *
102       * @param value The client ID.  Must not be <jk>null</jk> or blank.
103       * @return This object.
104       */
105      public Builder clientId(String value) {
106         clientId = reqnb("value", value);
107         return this;
108      }
109
110      /**
111       * Sets the OAuth client secret as a literal string.
112       *
113       * @param value The client secret.  Must not be <jk>null</jk> or blank.
114       * @return This object.
115       */
116      public Builder clientSecret(String value) {
117         reqnb("value", value);
118         clientSecretSupplier = () -> value;
119         return this;
120      }
121
122      /**
123       * Sets the OAuth client secret via a supplier.
124       *
125       * @param value The supplier.  Must not be <jk>null</jk>.
126       * @return This object.
127       */
128      public Builder clientSecretSupplier(Supplier<String> value) {
129         clientSecretSupplier = reqnn("value", value);
130         return this;
131      }
132
133      /**
134       * Adds requested OAuth scopes.
135       *
136       * @param values The scopes.
137       * @return This object.
138       */
139      public Builder scope(String... values) {
140         reqnn("values", values);
141         for (var v : values) {
142            reqnb("scope", v);
143            scopes.add(v);
144         }
145         return this;
146      }
147
148      /**
149       * Sets the RFC 8707 {@code resource} indicator sent on the token request.
150       *
151       * <p>
152       * The canonical URI of the protected resource the acquired token is intended for.  Setting this binds the
153       * token's audience so a malicious server cannot replay it against a different resource.
154       *
155       * @param value The canonical resource URI.  Must not be <jk>null</jk>.
156       * @return This object.
157       */
158      public Builder resource(URI value) {
159         resource = reqnn("value", value);
160         return this;
161      }
162
163      /**
164       * Sets the connect/read timeout applied to the token request.  Default 10 seconds.
165       *
166       * @param value The timeout.  Must not be <jk>null</jk> and must be positive.
167       * @return This object.
168       */
169      public Builder httpTimeout(Duration value) {
170         reqnn("value", value);
171         req(!value.isZero() && !value.isNegative(), "httpTimeout must be positive (was %s)", value);
172         httpTimeout = value;
173         return this;
174      }
175
176      /**
177       * Sets a callback that customizes the Nimbus {@link HTTPRequest}.
178       *
179       * @param value The callback.  Must not be <jk>null</jk>.
180       * @return This object.
181       */
182      public Builder httpRequestConfigurator(Consumer<HTTPRequest> value) {
183         httpRequestConfigurator = reqnn("value", value);
184         return this;
185      }
186
187      /**
188       * Builds the flow.
189       *
190       * @return A new {@link OAuthClientCredentialsFlow}.
191       */
192      public OAuthClientCredentialsFlow build() {
193         if (tokenEndpoint == null)
194            throw new IllegalStateException("OAuthClientCredentialsFlow requires tokenEndpoint(...)");
195         if (clientId == null)
196            throw new IllegalStateException("OAuthClientCredentialsFlow requires clientId(...)");
197         if (clientSecretSupplier == null)
198            throw new IllegalStateException("OAuthClientCredentialsFlow requires clientSecret(...) or clientSecretSupplier(...)");
199         return new OAuthClientCredentialsFlow(this);
200      }
201   }
202
203   private final URI tokenEndpoint;
204   private final String clientId;
205   private final Supplier<String> clientSecretSupplier;
206   private final Set<String> scopes;
207   private final URI resource;
208   private final Duration httpTimeout;
209   private final Consumer<HTTPRequest> httpRequestConfigurator;
210
211   /**
212    * Constructor.
213    *
214    * @param b The builder.
215    */
216   protected OAuthClientCredentialsFlow(Builder b) {
217      this.tokenEndpoint = b.tokenEndpoint;
218      this.clientId = b.clientId;
219      this.clientSecretSupplier = b.clientSecretSupplier;
220      this.scopes = u(cp(b.scopes));
221      this.resource = b.resource;
222      this.httpTimeout = b.httpTimeout;
223      this.httpRequestConfigurator = b.httpRequestConfigurator;
224   }
225
226   /**
227    * Acquires a token from the IdP.
228    *
229    * @return The acquired token.
230    */
231   public OAuthToken acquire() {
232      var clientAuth = new ClientSecretBasic(new ClientID(clientId), new Secret(clientSecretSupplier.get()));
233      Scope nimbusScope = scopes.isEmpty() ? null : new Scope(scopes.toArray(new String[0]));
234      var reqBuilder = new TokenRequest.Builder(tokenEndpoint, clientAuth, new ClientCredentialsGrant())
235         .scope(nimbusScope);
236      if (resource != null)
237         reqBuilder.resource(resource);
238      return Flows.send(reqBuilder.build(), httpTimeout, httpRequestConfigurator);
239   }
240}