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.pkce.*;
032import com.nimbusds.openid.connect.sdk.*;
033
034/**
035 * Authorization-code grant (RFC 6749 §4.1) flow helper with mandatory PKCE per RFC 7636.
036 *
037 * <p>
038 * Wraps Nimbus's {@link AuthorizationRequest} (for URL construction) + {@link AuthorizationCodeGrant} +
039 * {@link TokenRequest} (for the exchange) + {@link CodeChallenge#compute(CodeChallengeMethod, CodeVerifier)
040 * PKCE S256 challenge generation}.  {@link #buildAuthorizationUrl(String, CodeChallenge)} emits a plain OAuth
041 * 2.0 authorization request; {@link #buildAuthenticationUrl(String, CodeChallenge, String, Consumer)} emits an
042 * OpenID Connect authentication request carrying a {@code nonce}.  Supports RFC 8707 resource-indicator binding
043 * via {@link Builder#resource(URI)}.
044 *
045 * <h5 class='topic'>Usage</h5>
046 *
047 * <p class='bjava'>
048 *    <jk>var</jk> flow = OAuthAuthorizationCodeFlow.<jsm>create</jsm>()
049 *       .authorizationEndpoint(URI.<jsm>create</jsm>(<js>"https://idp.example.com/oauth2/authorize"</js>))
050 *       .tokenEndpoint(URI.<jsm>create</jsm>(<js>"https://idp.example.com/oauth2/token"</js>))
051 *       .clientId(<js>"web-app"</js>)
052 *       .redirectUri(URI.<jsm>create</jsm>(<js>"https://app.example.com/callback"</js>))
053 *       .scope(<js>"openid"</js>, <js>"profile"</js>)
054 *       .build();
055 *
056 *    <jk>var</jk> verifier = <jk>new</jk> CodeVerifier();
057 *    <jk>var</jk> challenge = CodeChallenge.<jsm>compute</jsm>(CodeChallengeMethod.<jsf>S256</jsf>, verifier);
058 *    <jk>var</jk> authUrl = flow.buildAuthorizationUrl(<js>"some-state"</js>, challenge);
059 *
060 *    <jc>// After the IdP redirects back with ?code=...</jc>
061 *    <jk>var</jk> token = flow.exchange(<jv>code</jv>, verifier);
062 * </p>
063 *
064 * <p>
065 * The flow does NOT manage state / PKCE-verifier storage; that's the caller's responsibility.
066 *
067 * @since 10.0.0
068 */
069@SuppressWarnings({
070   "deprecation", // Nimbus CodeChallenge overload is deprecated; no simpler alternative yet.
071   "java:S1192", // Duplicate string literals are OAuth protocol parameter names (e.g. "code", "grant_type"); intentional
072   "java:S9358" // Ternary chooses a different Scope constructor arity; moving it inside one call is not equivalent.
073})
074public class OAuthAuthorizationCodeFlow {
075
076   /**
077    * Static creator.
078    *
079    * @return A new builder.
080    */
081   public static Builder create() {
082      return new Builder();
083   }
084
085   /**
086    * Builder.
087    */
088   public static class Builder {
089      private URI authorizationEndpoint;
090      private URI tokenEndpoint;
091      private String clientId;
092      private Supplier<String> clientSecretSupplier;
093      private URI redirectUri;
094      private final Set<String> scopes = st();
095      private URI resource;
096      private Duration httpTimeout = Flows.DEFAULT_HTTP_TIMEOUT;
097      private Consumer<HTTPRequest> httpRequestConfigurator;
098
099      /** Constructor. */
100      protected Builder() {}
101
102      /**
103       * Sets the authorization endpoint URL.  Required.
104       *
105       * @param value The endpoint URL.
106       * @return This object.
107       */
108      public Builder authorizationEndpoint(URI value) {
109         authorizationEndpoint = assertSecureOrLoopback(reqnn("value", value));
110         return this;
111      }
112
113      /**
114       * Sets the token endpoint URL.  Required.
115       *
116       * @param value The endpoint URL.
117       * @return This object.
118       */
119      public Builder tokenEndpoint(URI value) {
120         tokenEndpoint = assertSecureOrLoopback(reqnn("value", value));
121         return this;
122      }
123
124      /**
125       * Sets the OAuth client ID.  Required.
126       *
127       * @param value The client ID.
128       * @return This object.
129       */
130      public Builder clientId(String value) {
131         clientId = reqnb("value", value);
132         return this;
133      }
134
135      /**
136       * Sets the client secret.  May be omitted for public clients (PKCE-only).
137       *
138       * @param value The client secret.
139       * @return This object.
140       */
141      public Builder clientSecret(String value) {
142         reqnb("value", value);
143         clientSecretSupplier = () -> value;
144         return this;
145      }
146
147      /**
148       * Sets the client secret via a supplier.
149       *
150       * @param value The supplier.
151       * @return This object.
152       */
153      public Builder clientSecretSupplier(Supplier<String> value) {
154         clientSecretSupplier = reqnn("value", value);
155         return this;
156      }
157
158      /**
159       * Sets the redirect URI.  Required.
160       *
161       * @param value The redirect URI.
162       * @return This object.
163       */
164      public Builder redirectUri(URI value) {
165         redirectUri = reqnn("value", value);
166         return this;
167      }
168
169      /**
170       * Adds requested scopes.
171       *
172       * @param values The scopes.
173       * @return This object.
174       */
175      public Builder scope(String... values) {
176         reqnn("values", values);
177         for (var v : values) {
178            reqnb("scope", v);
179            scopes.add(v);
180         }
181         return this;
182      }
183
184      /**
185       * Sets the RFC 8707 {@code resource} indicator sent on both the authorization request and the token exchange.
186       *
187       * @param value The canonical resource URI.  Must not be <jk>null</jk>.
188       * @return This object.
189       */
190      public Builder resource(URI value) {
191         resource = reqnn("value", value);
192         return this;
193      }
194
195      /**
196       * Sets the connect/read timeout applied to the token-exchange request.  Default 10 seconds.
197       *
198       * @param value The timeout.  Must not be <jk>null</jk> and must be positive.
199       * @return This object.
200       */
201      public Builder httpTimeout(Duration value) {
202         reqnn("value", value);
203         req(!value.isZero() && !value.isNegative(), "httpTimeout must be positive (was %s)", value);
204         httpTimeout = value;
205         return this;
206      }
207
208      /**
209       * Sets the HTTP request configurator (applied to the token-exchange request).
210       *
211       * @param value The callback.
212       * @return This object.
213       */
214      public Builder httpRequestConfigurator(Consumer<HTTPRequest> value) {
215         httpRequestConfigurator = reqnn("value", value);
216         return this;
217      }
218
219      /**
220       * Builds the flow.
221       *
222       * @return A new {@link OAuthAuthorizationCodeFlow}.
223       */
224      public OAuthAuthorizationCodeFlow build() {
225         if (authorizationEndpoint == null)
226            throw new IllegalStateException("OAuthAuthorizationCodeFlow requires authorizationEndpoint(...)");
227         if (tokenEndpoint == null)
228            throw new IllegalStateException("OAuthAuthorizationCodeFlow requires tokenEndpoint(...)");
229         if (clientId == null)
230            throw new IllegalStateException("OAuthAuthorizationCodeFlow requires clientId(...)");
231         if (redirectUri == null)
232            throw new IllegalStateException("OAuthAuthorizationCodeFlow requires redirectUri(...)");
233         return new OAuthAuthorizationCodeFlow(this);
234      }
235   }
236
237   private final URI authorizationEndpoint;
238   private final URI tokenEndpoint;
239   private final String clientId;
240   private final Supplier<String> clientSecretSupplier;
241   private final URI redirectUri;
242   private final Set<String> scopes;
243   private final URI resource;
244   private final Duration httpTimeout;
245   private final Consumer<HTTPRequest> httpRequestConfigurator;
246
247   /**
248    * Constructor.
249    *
250    * @param b The builder.
251    */
252   protected OAuthAuthorizationCodeFlow(Builder b) {
253      this.authorizationEndpoint = b.authorizationEndpoint;
254      this.tokenEndpoint = b.tokenEndpoint;
255      this.clientId = b.clientId;
256      this.clientSecretSupplier = b.clientSecretSupplier;
257      this.redirectUri = b.redirectUri;
258      this.scopes = u(cp(b.scopes));
259      this.resource = b.resource;
260      this.httpTimeout = b.httpTimeout;
261      this.httpRequestConfigurator = b.httpRequestConfigurator;
262   }
263
264   /**
265    * Builds the {@code authorize?...} URL the user-agent should be redirected to.
266    *
267    * @param state The opaque {@code state} parameter (CSRF protection).  Must not be <jk>null</jk> or
268    *    blank.
269    * @param codeChallenge The PKCE code challenge.  Must not be <jk>null</jk>.  Always uses S256.
270    * @return The authorization URL.
271    */
272   public URI buildAuthorizationUrl(String state, CodeChallenge codeChallenge) {
273      reqnb("state", state);
274      reqnn("codeChallenge", codeChallenge);
275      Scope nimbusScope = scopes.isEmpty() ? null : new Scope(scopes.toArray(new String[0]));
276      var requestBuilder = new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), new ClientID(clientId))
277         .endpointURI(authorizationEndpoint)
278         .redirectionURI(redirectUri)
279         .scope(nimbusScope)
280         .state(new State(state))
281         .codeChallenge(codeChallenge, CodeChallengeMethod.S256);
282      if (resource != null)
283         requestBuilder.resource(resource);
284      return requestBuilder.build().toURI();
285   }
286
287   /**
288    * Builds an OpenID Connect {@code authorize?...} URL carrying an OIDC {@code nonce} parameter.
289    *
290    * <p>
291    * Unlike {@link #buildAuthorizationUrl(String, CodeChallenge)} (which emits a plain OAuth 2.0
292    * {@link AuthorizationRequest} with no {@code nonce}), this method emits an OIDC
293    * {@link AuthenticationRequest} so the IdP echoes the supplied {@code nonce} back inside the issued
294    * ID token.  An OpenID Connect relying party stores the {@code nonce} before the redirect and
295    * verifies the {@code nonce} claim on the returned ID token to defeat token replay.
296    *
297    * <p>
298    * The configured {@code scope(...)} set is used as-is; OIDC requires it to contain {@code openid}.
299    * The optional {@code customizer} receives the underlying {@link com.nimbusds.openid.connect.sdk.AuthenticationRequest.Builder} so
300    * callers can set OIDC-specific parameters (e.g. {@code prompt}, {@code max_age}, {@code acr_values})
301    * without this module needing to surface each one.
302    *
303    * @param state The opaque {@code state} parameter (CSRF protection).  Must not be <jk>null</jk> or
304    *    blank.
305    * @param codeChallenge The PKCE code challenge.  Must not be <jk>null</jk>.  Always uses S256.
306    * @param nonce The OIDC {@code nonce} value.  Must not be <jk>null</jk> or blank.
307    * @param customizer Optional hook on the {@link com.nimbusds.openid.connect.sdk.AuthenticationRequest.Builder} for OIDC-specific
308    *    parameters.  May be <jk>null</jk>.
309    * @return The authorization URL.
310    */
311   public URI buildAuthenticationUrl(String state, CodeChallenge codeChallenge, String nonce, Consumer<AuthenticationRequest.Builder> customizer) {
312      reqnb("state", state);
313      reqnn("codeChallenge", codeChallenge);
314      reqnb("nonce", nonce);
315      Scope nimbusScope = scopes.isEmpty() ? new Scope("openid") : new Scope(scopes.toArray(new String[0]));
316      var builder = new AuthenticationRequest.Builder(new ResponseType(ResponseType.Value.CODE), nimbusScope, new ClientID(clientId), redirectUri)
317         .endpointURI(authorizationEndpoint)
318         .state(new State(state))
319         .nonce(new Nonce(nonce))
320         .codeChallenge(codeChallenge, CodeChallengeMethod.S256);
321      if (customizer != null)
322         customizer.accept(builder);
323      return builder.build().toURI();
324   }
325
326   /**
327    * Exchanges the authorization code returned by the IdP for an access token.
328    *
329    * @param code The {@code code} query parameter from the IdP redirect.  Must not be <jk>null</jk> or
330    *    blank.
331    * @param codeVerifier The PKCE verifier matching the challenge passed to
332    *    {@link #buildAuthorizationUrl(String, CodeChallenge)}.  Must not be <jk>null</jk>.
333    * @return The acquired token.
334    */
335   public OAuthToken exchange(String code, CodeVerifier codeVerifier) {
336      reqnb("code", code);
337      reqnn("codeVerifier", codeVerifier);
338      var grant = new AuthorizationCodeGrant(new AuthorizationCode(code), redirectUri, codeVerifier);
339      TokenRequest.Builder reqBuilder;
340      if (clientSecretSupplier != null) {
341         var clientAuth = new ClientSecretBasic(new ClientID(clientId), new Secret(clientSecretSupplier.get()));
342         reqBuilder = new TokenRequest.Builder(tokenEndpoint, clientAuth, grant);
343      } else {
344         reqBuilder = new TokenRequest.Builder(tokenEndpoint, new ClientID(clientId), grant);
345      }
346      if (resource != null)
347         reqBuilder.resource(resource);
348      return Flows.send(reqBuilder.build(), httpTimeout, httpRequestConfigurator);
349   }
350
351   /**
352    * Returns the authorization endpoint URL.
353    *
354    * @return The endpoint.
355    */
356   public URI getAuthorizationEndpoint() {
357      return authorizationEndpoint;
358   }
359
360   /**
361    * Returns the token endpoint URL.
362    *
363    * @return The endpoint.
364    */
365   public URI getTokenEndpoint() {
366      return tokenEndpoint;
367   }
368
369   /**
370    * Returns the client ID.
371    *
372    * @return The client ID.
373    */
374   public String getClientId() {
375      return clientId;
376   }
377
378   /**
379    * Returns the redirect URI.
380    *
381    * @return The redirect URI.
382    */
383   public URI getRedirectUri() {
384      return redirectUri;
385   }
386
387   /**
388    * Returns the requested scopes.
389    *
390    * @return An unmodifiable view.
391    */
392   public Set<String> getScopes() {
393      return scopes;
394   }
395}