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}