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.*; 031import com.nimbusds.oauth2.sdk.token.*; 032 033/** 034 * Refresh-token grant (RFC 6749 §6) flow helper. 035 * 036 * <p> 037 * Supports RFC 8707 resource-indicator binding via {@link Builder#resource(URI)}. 038 * 039 * <p> 040 * Wraps Nimbus's {@link RefreshTokenGrant}. The IdP may return a rotated refresh token; callers should 041 * persist the {@link OAuthToken#refreshToken()} value if present. 042 * 043 * @since 10.0.0 044 */ 045@SuppressWarnings({ 046 "java:S1192" // Duplicate string literals are OAuth protocol parameter names (e.g. "grant_type", "refresh_token"); intentional 047}) 048public class OAuthRefreshTokenFlow { 049 050 /** 051 * Static creator. 052 * 053 * @return A new builder. 054 */ 055 public static Builder create() { 056 return new Builder(); 057 } 058 059 /** 060 * Builder. 061 */ 062 public static class Builder { 063 private URI tokenEndpoint; 064 private String clientId; 065 private Supplier<String> clientSecretSupplier; 066 private String refreshToken; 067 private final Set<String> scopes = st(); 068 private URI resource; 069 private Duration httpTimeout = Flows.DEFAULT_HTTP_TIMEOUT; 070 private Consumer<HTTPRequest> httpRequestConfigurator; 071 072 /** Constructor. */ 073 protected Builder() {} 074 075 /** 076 * Sets the token endpoint URL. Required. 077 * 078 * @param value The endpoint. 079 * @return This object. 080 */ 081 public Builder tokenEndpoint(URI value) { 082 tokenEndpoint = assertSecureOrLoopback(reqnn("value", value)); 083 return this; 084 } 085 086 /** 087 * Sets the OAuth client ID. Required. 088 * 089 * @param value The client ID. 090 * @return This object. 091 */ 092 public Builder clientId(String value) { 093 clientId = reqnb("value", value); 094 return this; 095 } 096 097 /** 098 * Sets the OAuth client secret. May be omitted for public clients. 099 * 100 * @param value The secret. 101 * @return This object. 102 */ 103 public Builder clientSecret(String value) { 104 reqnb("value", value); 105 clientSecretSupplier = () -> value; 106 return this; 107 } 108 109 /** 110 * Sets the OAuth client secret via a supplier. 111 * 112 * @param value The supplier. 113 * @return This object. 114 */ 115 public Builder clientSecretSupplier(Supplier<String> value) { 116 clientSecretSupplier = reqnn("value", value); 117 return this; 118 } 119 120 /** 121 * Sets the refresh-token value. Required. 122 * 123 * @param value The refresh token. 124 * @return This object. 125 */ 126 public Builder refreshToken(String value) { 127 refreshToken = reqnb("value", value); 128 return this; 129 } 130 131 /** 132 * Adds optional scope narrowing. 133 * 134 * @param values The scopes. 135 * @return This object. 136 */ 137 public Builder scope(String... values) { 138 reqnn("values", values); 139 for (var v : values) { 140 reqnb("scope", v); 141 scopes.add(v); 142 } 143 return this; 144 } 145 146 /** 147 * Sets the RFC 8707 {@code resource} indicator sent on the token request. 148 * 149 * @param value The canonical resource URI. Must not be <jk>null</jk>. 150 * @return This object. 151 */ 152 public Builder resource(URI value) { 153 resource = reqnn("value", value); 154 return this; 155 } 156 157 /** 158 * Sets the connect/read timeout applied to the token request. Default 10 seconds. 159 * 160 * @param value The timeout. Must not be <jk>null</jk> and must be positive. 161 * @return This object. 162 */ 163 public Builder httpTimeout(Duration value) { 164 reqnn("value", value); 165 req(!value.isZero() && !value.isNegative(), "httpTimeout must be positive (was %s)", value); 166 httpTimeout = value; 167 return this; 168 } 169 170 /** 171 * Sets the HTTP request configurator. 172 * 173 * @param value The callback. 174 * @return This object. 175 */ 176 public Builder httpRequestConfigurator(Consumer<HTTPRequest> value) { 177 httpRequestConfigurator = reqnn("value", value); 178 return this; 179 } 180 181 /** 182 * Builds the flow. 183 * 184 * @return A new {@link OAuthRefreshTokenFlow}. 185 */ 186 public OAuthRefreshTokenFlow build() { 187 if (tokenEndpoint == null) 188 throw new IllegalStateException("OAuthRefreshTokenFlow requires tokenEndpoint(...)"); 189 if (clientId == null) 190 throw new IllegalStateException("OAuthRefreshTokenFlow requires clientId(...)"); 191 if (refreshToken == null) 192 throw new IllegalStateException("OAuthRefreshTokenFlow requires refreshToken(...)"); 193 return new OAuthRefreshTokenFlow(this); 194 } 195 } 196 197 private final URI tokenEndpoint; 198 private final String clientId; 199 private final Supplier<String> clientSecretSupplier; 200 private final String refreshToken; 201 private final Set<String> scopes; 202 private final URI resource; 203 private final Duration httpTimeout; 204 private final Consumer<HTTPRequest> httpRequestConfigurator; 205 206 /** 207 * Constructor. 208 * 209 * @param b The builder. 210 */ 211 protected OAuthRefreshTokenFlow(Builder b) { 212 this.tokenEndpoint = b.tokenEndpoint; 213 this.clientId = b.clientId; 214 this.clientSecretSupplier = b.clientSecretSupplier; 215 this.refreshToken = b.refreshToken; 216 this.scopes = u(cp(b.scopes)); 217 this.resource = b.resource; 218 this.httpTimeout = b.httpTimeout; 219 this.httpRequestConfigurator = b.httpRequestConfigurator; 220 } 221 222 /** 223 * Acquires a fresh access token. 224 * 225 * @return The acquired token. 226 */ 227 public OAuthToken acquire() { 228 var grant = new RefreshTokenGrant(new RefreshToken(refreshToken)); 229 Scope nimbusScope = scopes.isEmpty() ? null : new Scope(scopes.toArray(new String[0])); 230 TokenRequest.Builder reqBuilder; 231 if (clientSecretSupplier != null) { 232 var clientAuth = new ClientSecretBasic(new ClientID(clientId), new Secret(clientSecretSupplier.get())); 233 reqBuilder = new TokenRequest.Builder(tokenEndpoint, clientAuth, grant).scope(nimbusScope); 234 } else { 235 reqBuilder = new TokenRequest.Builder(tokenEndpoint, new ClientID(clientId), grant).scope(nimbusScope); 236 } 237 if (resource != null) 238 reqBuilder.resource(resource); 239 return Flows.send(reqBuilder.build(), httpTimeout, httpRequestConfigurator); 240 } 241}