• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015, Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *    * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *
15  *    * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 package com.google.auth.oauth2;
33 
34 import com.google.api.client.http.GenericUrl;
35 import com.google.api.client.http.HttpRequest;
36 import com.google.api.client.http.HttpRequestFactory;
37 import com.google.api.client.http.HttpResponse;
38 import com.google.api.client.http.UrlEncodedContent;
39 import com.google.api.client.json.GenericJson;
40 import com.google.api.client.json.JsonObjectParser;
41 import com.google.api.client.util.GenericData;
42 import com.google.api.client.util.Joiner;
43 import com.google.api.client.util.Preconditions;
44 import com.google.auth.http.HttpTransportFactory;
45 import com.google.common.collect.ImmutableList;
46 import com.google.errorprone.annotations.CanIgnoreReturnValue;
47 import java.io.IOException;
48 import java.net.URI;
49 import java.net.URL;
50 import java.util.ArrayList;
51 import java.util.Collection;
52 import java.util.Date;
53 import java.util.List;
54 import java.util.Map;
55 
56 /** Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization. */
57 public class UserAuthorizer {
58 
59   static final URI DEFAULT_CALLBACK_URI = URI.create("/oauth2callback");
60 
61   private final String TOKEN_STORE_ERROR = "Error parsing stored token data.";
62   private final String FETCH_TOKEN_ERROR = "Error reading result of Token API:";
63 
64   private final ClientId clientId;
65   private final Collection<String> scopes;
66   private final TokenStore tokenStore;
67   private final URI callbackUri;
68 
69   private final HttpTransportFactory transportFactory;
70   private final URI tokenServerUri;
71   private final URI userAuthUri;
72   private final PKCEProvider pkce;
73 
74   /**
75    * Constructor with all parameters.
76    *
77    * @param clientId Client ID to identify the OAuth2 consent prompt
78    * @param scopes OAuth2 scopes defining the user consent
79    * @param tokenStore Implementation of a component for long term storage of tokens
80    * @param callbackUri URI for implementation of the OAuth2 web callback
81    * @param transportFactory HTTP transport factory, creates the transport used to get access
82    *     tokens.
83    * @param tokenServerUri URI of the end point that provides tokens
84    * @param userAuthUri URI of the Web UI for user consent
85    * @param pkce PKCE implementation
86    */
UserAuthorizer( ClientId clientId, Collection<String> scopes, TokenStore tokenStore, URI callbackUri, HttpTransportFactory transportFactory, URI tokenServerUri, URI userAuthUri, PKCEProvider pkce)87   private UserAuthorizer(
88       ClientId clientId,
89       Collection<String> scopes,
90       TokenStore tokenStore,
91       URI callbackUri,
92       HttpTransportFactory transportFactory,
93       URI tokenServerUri,
94       URI userAuthUri,
95       PKCEProvider pkce) {
96     this.clientId = Preconditions.checkNotNull(clientId);
97     this.scopes = ImmutableList.copyOf(Preconditions.checkNotNull(scopes));
98     this.callbackUri = (callbackUri == null) ? DEFAULT_CALLBACK_URI : callbackUri;
99     this.transportFactory =
100         (transportFactory == null) ? OAuth2Utils.HTTP_TRANSPORT_FACTORY : transportFactory;
101     this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri;
102     this.userAuthUri = (userAuthUri == null) ? OAuth2Utils.USER_AUTH_URI : userAuthUri;
103     this.tokenStore = (tokenStore == null) ? new MemoryTokensStorage() : tokenStore;
104     this.pkce = pkce;
105   }
106 
107   /**
108    * Returns the Client ID user to identify the OAuth2 consent prompt.
109    *
110    * @return The Client ID.
111    */
getClientId()112   public ClientId getClientId() {
113     return clientId;
114   }
115 
116   /**
117    * Returns the scopes defining the user consent.
118    *
119    * @return The collection of scopes defining the user consent.
120    */
getScopes()121   public Collection<String> getScopes() {
122     return scopes;
123   }
124 
125   /**
126    * Returns the URI for implementation of the OAuth2 web callback.
127    *
128    * @return The URI for the OAuth2 web callback.
129    */
getCallbackUri()130   public URI getCallbackUri() {
131     return callbackUri;
132   }
133 
134   /**
135    * Returns the URI for implementation of the OAuth2 web callback, optionally relative to the
136    * specified URI.
137    *
138    * <p>The callback URI is often relative to enable an application to be tested from more than one
139    * place so this can be used to resolve it relative to another URI.
140    *
141    * @param baseUri The URI to resolve the callback URI relative to.
142    * @return The resolved URI.
143    */
getCallbackUri(URI baseUri)144   public URI getCallbackUri(URI baseUri) {
145     if (callbackUri.isAbsolute()) {
146       return callbackUri;
147     }
148     if (baseUri == null || !baseUri.isAbsolute()) {
149       throw new IllegalStateException(
150           "If the callback URI is relative, the baseUri passed must" + " be an absolute URI");
151     }
152     return baseUri.resolve(callbackUri);
153   }
154 
155   /**
156    * Returns the implementation of a component for long term storage of tokens.
157    *
158    * @return The token storage implementation for long term storage of tokens.
159    */
getTokenStore()160   public TokenStore getTokenStore() {
161     return tokenStore;
162   }
163 
164   /**
165    * Return an URL that performs the authorization consent prompt web UI.
166    *
167    * @param userId Application's identifier for the end user.
168    * @param state State that is passed on to the OAuth2 callback URI after the consent.
169    * @param baseUri The URI to resolve the OAuth2 callback URI relative to.
170    * @return The URL that can be navigated or redirected to.
171    */
getAuthorizationUrl(String userId, String state, URI baseUri)172   public URL getAuthorizationUrl(String userId, String state, URI baseUri) {
173     return this.getAuthorizationUrl(userId, state, baseUri, null);
174   }
175 
176   /**
177    * Return an URL that performs the authorization consent prompt web UI.
178    *
179    * @param userId Application's identifier for the end user.
180    * @param state State that is passed on to the OAuth2 callback URI after the consent.
181    * @param baseUri The URI to resolve the OAuth2 callback URI relative to.
182    * @param additionalParameters Additional query parameters to be added to the authorization URL.
183    * @return The URL that can be navigated or redirected to.
184    */
getAuthorizationUrl( String userId, String state, URI baseUri, Map<String, String> additionalParameters)185   public URL getAuthorizationUrl(
186       String userId, String state, URI baseUri, Map<String, String> additionalParameters) {
187     URI resolvedCallbackUri = getCallbackUri(baseUri);
188     String scopesString = Joiner.on(' ').join(scopes);
189 
190     GenericUrl url = new GenericUrl(userAuthUri);
191     url.put("response_type", "code");
192     url.put("client_id", clientId.getClientId());
193     url.put("redirect_uri", resolvedCallbackUri);
194     url.put("scope", scopesString);
195     if (state != null) {
196       url.put("state", state);
197     }
198     url.put("access_type", "offline");
199     url.put("approval_prompt", "force");
200     if (userId != null) {
201       url.put("login_hint", userId);
202     }
203     url.put("include_granted_scopes", true);
204 
205     if (additionalParameters != null) {
206       for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {
207         url.put(entry.getKey(), entry.getValue());
208       }
209     }
210 
211     if (pkce != null) {
212       url.put("code_challenge", pkce.getCodeChallenge());
213       url.put("code_challenge_method", pkce.getCodeChallengeMethod());
214     }
215     return url.toURL();
216   }
217 
218   /**
219    * Attempts to retrieve credentials for the approved end user consent.
220    *
221    * @param userId Application's identifier for the end user.
222    * @return The loaded credentials or null if there are no valid approved credentials.
223    * @throws IOException If there is error retrieving or loading the credentials.
224    */
getCredentials(String userId)225   public UserCredentials getCredentials(String userId) throws IOException {
226     Preconditions.checkNotNull(userId);
227     if (tokenStore == null) {
228       throw new IllegalStateException("Method cannot be called if token store is not specified.");
229     }
230     String tokenData = tokenStore.load(userId);
231     if (tokenData == null) {
232       return null;
233     }
234     GenericJson tokenJson = OAuth2Utils.parseJson(tokenData);
235     String accessTokenValue =
236         OAuth2Utils.validateString(tokenJson, "access_token", TOKEN_STORE_ERROR);
237     Long expirationMillis =
238         OAuth2Utils.validateLong(tokenJson, "expiration_time_millis", TOKEN_STORE_ERROR);
239     Date expirationTime = new Date(expirationMillis);
240     List<String> scopes =
241         OAuth2Utils.validateOptionalListString(
242             tokenJson, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
243     AccessToken accessToken =
244         AccessToken.newBuilder()
245             .setExpirationTime(expirationTime)
246             .setTokenValue(accessTokenValue)
247             .setScopes(scopes)
248             .build();
249     String refreshToken =
250         OAuth2Utils.validateOptionalString(tokenJson, "refresh_token", TOKEN_STORE_ERROR);
251     UserCredentials credentials =
252         UserCredentials.newBuilder()
253             .setClientId(clientId.getClientId())
254             .setClientSecret(clientId.getClientSecret())
255             .setRefreshToken(refreshToken)
256             .setAccessToken(accessToken)
257             .setHttpTransportFactory(transportFactory)
258             .setTokenServerUri(tokenServerUri)
259             .build();
260     monitorCredentials(userId, credentials);
261     return credentials;
262   }
263 
264   /**
265    * Returns a UserCredentials instance by exchanging an OAuth2 authorization code for tokens.
266    *
267    * @param code Code returned from OAuth2 consent prompt.
268    * @param baseUri The URI to resolve the OAuth2 callback URI relative to.
269    * @return the UserCredentials instance created from the authorization code.
270    * @throws IOException An error from the server API call to get the tokens.
271    */
getCredentialsFromCode(String code, URI baseUri)272   public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws IOException {
273     return getCredentialsFromCode(code, baseUri, null);
274   }
275 
276   /**
277    * Returns a UserCredentials instance by exchanging an OAuth2 authorization code for tokens.
278    *
279    * @param code Code returned from OAuth2 consent prompt.
280    * @param baseUri The URI to resolve the OAuth2 callback URI relative to.
281    * @param additionalParameters Additional parameters to be added to the post body of token
282    *     endpoint request.
283    * @return the UserCredentials instance created from the authorization code.
284    * @throws IOException An error from the server API call to get the tokens.
285    */
getCredentialsFromCode( String code, URI baseUri, Map<String, String> additionalParameters)286   public UserCredentials getCredentialsFromCode(
287       String code, URI baseUri, Map<String, String> additionalParameters) throws IOException {
288     Preconditions.checkNotNull(code);
289     URI resolvedCallbackUri = getCallbackUri(baseUri);
290 
291     GenericData tokenData = new GenericData();
292     tokenData.put("code", code);
293     tokenData.put("client_id", clientId.getClientId());
294     tokenData.put("client_secret", clientId.getClientSecret());
295     tokenData.put("redirect_uri", resolvedCallbackUri);
296     tokenData.put("grant_type", "authorization_code");
297 
298     if (additionalParameters != null) {
299       for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {
300         tokenData.put(entry.getKey(), entry.getValue());
301       }
302     }
303 
304     if (pkce != null) {
305       tokenData.put("code_verifier", pkce.getCodeVerifier());
306     }
307 
308     UrlEncodedContent tokenContent = new UrlEncodedContent(tokenData);
309     HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
310     HttpRequest tokenRequest =
311         requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), tokenContent);
312     tokenRequest.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
313 
314     HttpResponse tokenResponse = tokenRequest.execute();
315 
316     GenericJson parsedTokens = tokenResponse.parseAs(GenericJson.class);
317     String accessTokenValue =
318         OAuth2Utils.validateString(parsedTokens, "access_token", FETCH_TOKEN_ERROR);
319     int expiresInSecs = OAuth2Utils.validateInt32(parsedTokens, "expires_in", FETCH_TOKEN_ERROR);
320     Date expirationTime = new Date(new Date().getTime() + expiresInSecs * 1000);
321     String scopes =
322         OAuth2Utils.validateOptionalString(
323             parsedTokens, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
324     AccessToken accessToken =
325         AccessToken.newBuilder()
326             .setExpirationTime(expirationTime)
327             .setTokenValue(accessTokenValue)
328             .setScopes(scopes)
329             .build();
330     String refreshToken =
331         OAuth2Utils.validateOptionalString(parsedTokens, "refresh_token", FETCH_TOKEN_ERROR);
332 
333     return UserCredentials.newBuilder()
334         .setClientId(clientId.getClientId())
335         .setClientSecret(clientId.getClientSecret())
336         .setRefreshToken(refreshToken)
337         .setAccessToken(accessToken)
338         .setHttpTransportFactory(transportFactory)
339         .setTokenServerUri(tokenServerUri)
340         .build();
341   }
342 
343   /**
344    * Exchanges an authorization code for tokens and stores them.
345    *
346    * @param userId Application's identifier for the end user.
347    * @param code Code returned from OAuth2 consent prompt.
348    * @param baseUri The URI to resolve the OAuth2 callback URI relative to.
349    * @return UserCredentials instance created from the authorization code.
350    * @throws IOException An error from the server API call to get the tokens or store the tokens.
351    */
getAndStoreCredentialsFromCode(String userId, String code, URI baseUri)352   public UserCredentials getAndStoreCredentialsFromCode(String userId, String code, URI baseUri)
353       throws IOException {
354     Preconditions.checkNotNull(userId);
355     Preconditions.checkNotNull(code);
356     UserCredentials credentials = getCredentialsFromCode(code, baseUri);
357     storeCredentials(userId, credentials);
358     monitorCredentials(userId, credentials);
359     return credentials;
360   }
361 
362   /**
363    * Revokes the authorization for tokens stored for the user.
364    *
365    * @param userId Application's identifier for the end user.
366    * @throws IOException An error calling the revoke API or deleting the state.
367    */
revokeAuthorization(String userId)368   public void revokeAuthorization(String userId) throws IOException {
369     Preconditions.checkNotNull(userId);
370     if (tokenStore == null) {
371       throw new IllegalStateException("Method cannot be called if token store is not specified.");
372     }
373     String tokenData = tokenStore.load(userId);
374     if (tokenData == null) {
375       return;
376     }
377     IOException deleteTokenException = null;
378     try {
379       // Delete the stored version first. If token reversion fails it is less harmful to have an
380       // non revoked token to hold on to a potentially revoked token.
381       tokenStore.delete(userId);
382     } catch (IOException e) {
383       deleteTokenException = e;
384     }
385 
386     GenericJson tokenJson = OAuth2Utils.parseJson(tokenData);
387     String accessTokenValue =
388         OAuth2Utils.validateOptionalString(tokenJson, "access_token", TOKEN_STORE_ERROR);
389     String refreshToken =
390         OAuth2Utils.validateOptionalString(tokenJson, "refresh_token", TOKEN_STORE_ERROR);
391     // If both tokens are present, either can be used
392     String revokeToken = (refreshToken != null) ? refreshToken : accessTokenValue;
393 
394     GenericUrl revokeUrl = new GenericUrl(OAuth2Utils.TOKEN_REVOKE_URI);
395     GenericData genericData = new GenericData();
396     genericData.put("token", revokeToken);
397     UrlEncodedContent content = new UrlEncodedContent(genericData);
398 
399     HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
400     HttpRequest tokenRequest = requestFactory.buildPostRequest(revokeUrl, content);
401     tokenRequest.execute();
402 
403     if (deleteTokenException != null) {
404       throw deleteTokenException;
405     }
406   }
407 
408   /**
409    * Puts the end user credentials in long term storage.
410    *
411    * @param userId Application's identifier for the end user.
412    * @param credentials UserCredentials instance for the authorized consent.
413    * @throws IOException An error storing the credentials.
414    */
storeCredentials(String userId, UserCredentials credentials)415   public void storeCredentials(String userId, UserCredentials credentials) throws IOException {
416     if (tokenStore == null) {
417       throw new IllegalStateException("Cannot store tokens if tokenStore is not specified.");
418     }
419     AccessToken accessToken = credentials.getAccessToken();
420     String acessTokenValue = null;
421     String scopes = null;
422     Date expiresBy = null;
423     List<String> grantedScopes = new ArrayList<>();
424 
425     if (accessToken != null) {
426       acessTokenValue = accessToken.getTokenValue();
427       expiresBy = accessToken.getExpirationTime();
428       grantedScopes = accessToken.getScopes();
429     }
430     String refreshToken = credentials.getRefreshToken();
431     GenericJson tokenStateJson = new GenericJson();
432     tokenStateJson.setFactory(OAuth2Utils.JSON_FACTORY);
433     tokenStateJson.put("access_token", acessTokenValue);
434     tokenStateJson.put(OAuth2Utils.TOKEN_RESPONSE_SCOPE, grantedScopes);
435     tokenStateJson.put("expiration_time_millis", expiresBy.getTime());
436     if (refreshToken != null) {
437       tokenStateJson.put("refresh_token", refreshToken);
438     }
439     String tokenState = tokenStateJson.toString();
440     tokenStore.store(userId, tokenState);
441   }
442 
443   /**
444    * Adds a listen to rewrite the credentials when the tokens are refreshed.
445    *
446    * @param userId Application's identifier for the end user.
447    * @param credentials UserCredentials instance to listen to.
448    */
monitorCredentials(String userId, UserCredentials credentials)449   protected void monitorCredentials(String userId, UserCredentials credentials) {
450     credentials.addChangeListener(new UserCredentialsListener(userId));
451   }
452 
453   /**
454    * Implementation of listener used by monitorCredentials to rewrite the credentials when the
455    * tokens are refreshed.
456    */
457   private class UserCredentialsListener implements OAuth2Credentials.CredentialsChangedListener {
458     private final String userId;
459 
460     /** Construct new listener. */
UserCredentialsListener(String userId)461     public UserCredentialsListener(String userId) {
462       this.userId = userId;
463     }
464 
465     /** Handle change event by rewriting to token store. */
466     @Override
onChanged(OAuth2Credentials credentials)467     public void onChanged(OAuth2Credentials credentials) throws IOException {
468       UserCredentials userCredentials = (UserCredentials) credentials;
469       storeCredentials(userId, userCredentials);
470     }
471   }
472 
newBuilder()473   public static Builder newBuilder() {
474     return new Builder();
475   }
476 
toBuilder()477   public Builder toBuilder() {
478     return new Builder(this);
479   }
480 
481   public static class Builder {
482 
483     private ClientId clientId;
484     private TokenStore tokenStore;
485     private URI callbackUri;
486     private URI tokenServerUri;
487     private URI userAuthUri;
488     private Collection<String> scopes;
489     private HttpTransportFactory transportFactory;
490     private PKCEProvider pkce;
491 
Builder()492     protected Builder() {}
493 
Builder(UserAuthorizer authorizer)494     protected Builder(UserAuthorizer authorizer) {
495       this.clientId = authorizer.clientId;
496       this.scopes = authorizer.scopes;
497       this.transportFactory = authorizer.transportFactory;
498       this.tokenServerUri = authorizer.tokenServerUri;
499       this.tokenStore = authorizer.tokenStore;
500       this.callbackUri = authorizer.callbackUri;
501       this.userAuthUri = authorizer.userAuthUri;
502       this.pkce = new DefaultPKCEProvider();
503     }
504 
505     @CanIgnoreReturnValue
setClientId(ClientId clientId)506     public Builder setClientId(ClientId clientId) {
507       this.clientId = clientId;
508       return this;
509     }
510 
511     @CanIgnoreReturnValue
setTokenStore(TokenStore tokenStore)512     public Builder setTokenStore(TokenStore tokenStore) {
513       this.tokenStore = tokenStore;
514       return this;
515     }
516 
517     @CanIgnoreReturnValue
setScopes(Collection<String> scopes)518     public Builder setScopes(Collection<String> scopes) {
519       this.scopes = scopes;
520       return this;
521     }
522 
523     @CanIgnoreReturnValue
setTokenServerUri(URI tokenServerUri)524     public Builder setTokenServerUri(URI tokenServerUri) {
525       this.tokenServerUri = tokenServerUri;
526       return this;
527     }
528 
529     @CanIgnoreReturnValue
setCallbackUri(URI callbackUri)530     public Builder setCallbackUri(URI callbackUri) {
531       this.callbackUri = callbackUri;
532       return this;
533     }
534 
535     @CanIgnoreReturnValue
setUserAuthUri(URI userAuthUri)536     public Builder setUserAuthUri(URI userAuthUri) {
537       this.userAuthUri = userAuthUri;
538       return this;
539     }
540 
541     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)542     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
543       this.transportFactory = transportFactory;
544       return this;
545     }
546 
547     @CanIgnoreReturnValue
setPKCEProvider(PKCEProvider pkce)548     public Builder setPKCEProvider(PKCEProvider pkce) {
549       if (pkce != null) {
550         if (pkce.getCodeChallenge() == null
551             || pkce.getCodeVerifier() == null
552             || pkce.getCodeChallengeMethod() == null) {
553 
554           throw new IllegalArgumentException(
555               "PKCE provider contained null implementations. PKCE object must implement all PKCEProvider methods.");
556         }
557       }
558       this.pkce = pkce;
559       return this;
560     }
561 
getClientId()562     public ClientId getClientId() {
563       return clientId;
564     }
565 
getTokenStore()566     public TokenStore getTokenStore() {
567       return tokenStore;
568     }
569 
getScopes()570     public Collection<String> getScopes() {
571       return scopes;
572     }
573 
getTokenServerUri()574     public URI getTokenServerUri() {
575       return tokenServerUri;
576     }
577 
getCallbackUri()578     public URI getCallbackUri() {
579       return callbackUri;
580     }
581 
getUserAuthUri()582     public URI getUserAuthUri() {
583       return userAuthUri;
584     }
585 
getHttpTransportFactory()586     public HttpTransportFactory getHttpTransportFactory() {
587       return transportFactory;
588     }
589 
getPKCEProvider()590     public PKCEProvider getPKCEProvider() {
591       return pkce;
592     }
593 
build()594     public UserAuthorizer build() {
595       return new UserAuthorizer(
596           clientId,
597           scopes,
598           tokenStore,
599           callbackUri,
600           transportFactory,
601           tokenServerUri,
602           userAuthUri,
603           pkce);
604     }
605   }
606 }
607