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 §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}