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.auth.oauth; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.net.*; 022import java.time.*; 023import java.util.*; 024import java.util.function.*; 025 026import org.apache.juneau.rest.auth.oauth.flow.*; 027 028import com.nimbusds.oauth2.sdk.http.*; 029 030/** 031 * Server-side caching decorator layering an optional {@link TokenCache} over the role-neutral 032 * {@link OAuthClientCredentialsFlow}. 033 * 034 * <p> 035 * The neutral flow performs the RFC 6749 §4.4 client-credentials token acquisition; this decorator adds the 036 * server-side {@code (clientId, scope)}-keyed cache so repeat callers within the cache window reuse the token rather 037 * than hitting the IdP on each call. When no {@link Builder#tokenCache(TokenCache) cache} is configured the decorator 038 * simply delegates to the neutral flow. 039 * 040 * <h5 class='topic'>Usage</h5> 041 * 042 * <p class='bjava'> 043 * <jk>var</jk> token = CachingClientCredentialsFlow.<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>) 048 * .tokenCache(BoundedLruTokenCache.<jsm>create</jsm>()) 049 * .build() 050 * .acquire(); 051 * </p> 052 * 053 * @since 10.0.0 054 */ 055@SuppressWarnings({ 056 "java:S1192" // Repeated "value" is the argument-name literal passed to null checks; constants would obscure each call site. 057}) 058public class CachingClientCredentialsFlow { 059 060 /** 061 * Static creator. 062 * 063 * @return A new builder. 064 */ 065 public static Builder create() { 066 return new Builder(); 067 } 068 069 /** 070 * Builder. 071 */ 072 public static class Builder { 073 private URI tokenEndpoint; 074 private String clientId; 075 private Supplier<String> clientSecretSupplier; 076 private final Set<String> scopes = st(); 077 private TokenCache tokenCache; 078 private Duration cacheSkew = Duration.ofSeconds(30); 079 private Consumer<HTTPRequest> httpRequestConfigurator; 080 081 /** Constructor. */ 082 protected Builder() {} 083 084 /** 085 * Sets the token endpoint URL. Required. 086 * 087 * @param value The endpoint URL. Must not be <jk>null</jk>. 088 * @return This object. 089 */ 090 public Builder tokenEndpoint(URI value) { 091 tokenEndpoint = reqnn("value", value); 092 return this; 093 } 094 095 /** 096 * Sets the OAuth client ID. Required. 097 * 098 * @param value The client ID. Must not be <jk>null</jk> or blank. 099 * @return This object. 100 */ 101 public Builder clientId(String value) { 102 clientId = reqnb("value", value); 103 return this; 104 } 105 106 /** 107 * Sets the OAuth client secret as a literal string. 108 * 109 * @param value The client secret. Must not be <jk>null</jk> or blank. 110 * @return This object. 111 */ 112 public Builder clientSecret(String value) { 113 reqnb("value", value); 114 clientSecretSupplier = () -> value; 115 return this; 116 } 117 118 /** 119 * Sets the OAuth client secret via a supplier. 120 * 121 * @param value The supplier. Must not be <jk>null</jk>. 122 * @return This object. 123 */ 124 public Builder clientSecretSupplier(Supplier<String> value) { 125 clientSecretSupplier = reqnn("value", value); 126 return this; 127 } 128 129 /** 130 * Adds requested OAuth scopes. 131 * 132 * @param values The scopes. 133 * @return This object. 134 */ 135 public Builder scope(String... values) { 136 reqnn("values", values); 137 for (var v : values) { 138 reqnb("scope", v); 139 scopes.add(v); 140 } 141 return this; 142 } 143 144 /** 145 * Configures the cache for the acquired token. 146 * 147 * @param value The cache. Must not be <jk>null</jk>. 148 * @return This object. 149 */ 150 public Builder tokenCache(TokenCache value) { 151 tokenCache = reqnn("value", value); 152 return this; 153 } 154 155 /** 156 * Sets the skew tolerance subtracted from the cached token's expiry. Default 30s. 157 * 158 * @param value The skew. Must be non-negative. 159 * @return This object. 160 */ 161 public Builder cacheSkew(Duration value) { 162 reqnn("value", value); 163 if (value.isNegative()) 164 throw new IllegalArgumentException("cacheSkew must be non-negative"); 165 cacheSkew = value; 166 return this; 167 } 168 169 /** 170 * Sets a callback that customizes the Nimbus {@link HTTPRequest}. 171 * 172 * @param value The callback. Must not be <jk>null</jk>. 173 * @return This object. 174 */ 175 public Builder httpRequestConfigurator(Consumer<HTTPRequest> value) { 176 httpRequestConfigurator = reqnn("value", value); 177 return this; 178 } 179 180 /** 181 * Builds the flow. 182 * 183 * @return A new {@link CachingClientCredentialsFlow}. 184 */ 185 public CachingClientCredentialsFlow build() { 186 if (tokenEndpoint == null) 187 throw new IllegalStateException("CachingClientCredentialsFlow requires tokenEndpoint(...)"); 188 if (clientId == null) 189 throw new IllegalStateException("CachingClientCredentialsFlow requires clientId(...)"); 190 if (clientSecretSupplier == null) 191 throw new IllegalStateException("CachingClientCredentialsFlow requires clientSecret(...) or clientSecretSupplier(...)"); 192 return new CachingClientCredentialsFlow(this); 193 } 194 } 195 196 private final OAuthClientCredentialsFlow delegate; 197 private final TokenCache tokenCache; 198 private final Duration cacheSkew; 199 private final String cacheKey; 200 201 /** 202 * Constructor. 203 * 204 * @param b The builder. 205 */ 206 protected CachingClientCredentialsFlow(Builder b) { 207 var scopes = u(cp(b.scopes)); 208 var db = OAuthClientCredentialsFlow.create() 209 .tokenEndpoint(b.tokenEndpoint) 210 .clientId(b.clientId) 211 .clientSecretSupplier(b.clientSecretSupplier); 212 if (!scopes.isEmpty()) 213 db.scope(scopes.toArray(new String[0])); 214 if (b.httpRequestConfigurator != null) 215 db.httpRequestConfigurator(b.httpRequestConfigurator); 216 this.delegate = db.build(); 217 this.tokenCache = b.tokenCache; 218 this.cacheSkew = b.cacheSkew; 219 this.cacheKey = "cc|" + b.clientId + "|" + String.join(" ", scopes); 220 } 221 222 /** 223 * Acquires a token, returning the cached token when a fresh one is present. 224 * 225 * @return The acquired token. 226 */ 227 public OAuthToken acquire() { 228 if (tokenCache != null) { 229 var hit = tokenCache.getToken(cacheKey, Instant.now(), cacheSkew); 230 if (hit.isPresent()) 231 return hit.get(); 232 } 233 var token = delegate.acquire(); 234 if (tokenCache != null) 235 tokenCache.putToken(cacheKey, token); 236 return token; 237 } 238}