17
17
import com .google .api .client .auth .oauth2 .Credential .AccessMethod ;
18
18
import com .google .api .client .http .GenericUrl ;
19
19
import com .google .api .client .http .HttpExecuteInterceptor ;
20
+ import com .google .api .client .http .HttpRequest ;
20
21
import com .google .api .client .http .HttpRequestInitializer ;
21
22
import com .google .api .client .http .HttpTransport ;
23
+ import com .google .api .client .http .UrlEncodedContent ;
22
24
import com .google .api .client .json .JsonFactory ;
25
+ import com .google .api .client .util .Base64 ;
23
26
import com .google .api .client .util .Beta ;
27
+ import com .google .api .client .util .Data ;
24
28
import com .google .api .client .util .Clock ;
25
29
import com .google .api .client .util .Joiner ;
26
30
import com .google .api .client .util .Lists ;
29
33
import com .google .api .client .util .store .DataStoreFactory ;
30
34
31
35
import java .io .IOException ;
36
+ import java .security .MessageDigest ;
37
+ import java .security .NoSuchAlgorithmException ;
38
+ import java .security .SecureRandom ;
32
39
import java .util .Collection ;
33
40
import java .util .Collections ;
41
+ import java .util .Map ;
34
42
35
43
import static com .google .api .client .util .Strings .isNullOrEmpty ;
36
44
@@ -85,6 +93,9 @@ public class AuthorizationCodeFlow {
85
93
/** Authorization server encoded URL. */
86
94
private final String authorizationServerEncodedUrl ;
87
95
96
+ /** The Proof Key for Code Exchange (PKCE) or {@code null} if this flow should not use PKCE. */
97
+ private final PKCE pkce ;
98
+
88
99
/** Credential persistence store or {@code null} for none. */
89
100
@ Beta
90
101
@ Deprecated
@@ -159,6 +170,7 @@ protected AuthorizationCodeFlow(Builder builder) {
159
170
clock = Preconditions .checkNotNull (builder .clock );
160
171
credentialCreatedListener = builder .credentialCreatedListener ;
161
172
refreshListeners = Collections .unmodifiableCollection (builder .refreshListeners );
173
+ pkce = builder .pkce ;
162
174
}
163
175
164
176
/**
@@ -182,8 +194,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro
182
194
* </pre>
183
195
*/
184
196
public AuthorizationCodeRequestUrl newAuthorizationUrl () {
185
- return new AuthorizationCodeRequestUrl (authorizationServerEncodedUrl , clientId ).setScopes (
186
- scopes );
197
+ AuthorizationCodeRequestUrl url = new AuthorizationCodeRequestUrl (authorizationServerEncodedUrl , clientId );
198
+ url .setScopes (scopes );
199
+ if (pkce != null ) {
200
+ url .setCodeChallenge (pkce .getChallenge ());
201
+ url .setCodeChallengeMethod (pkce .getChallengeMethod ());
202
+ }
203
+ return url ;
187
204
}
188
205
189
206
/**
@@ -206,9 +223,20 @@ static TokenResponse requestAccessToken(AuthorizationCodeFlow flow, String code)
206
223
* @param authorizationCode authorization code.
207
224
*/
208
225
public AuthorizationCodeTokenRequest newTokenRequest (String authorizationCode ) {
226
+ HttpExecuteInterceptor pkceClientAuthenticationWrapper = new HttpExecuteInterceptor () {
227
+ @ Override
228
+ public void intercept (HttpRequest request ) throws IOException {
229
+ clientAuthentication .intercept (request );
230
+ if (pkce != null ) {
231
+ Map <String , Object > data = Data .mapOf (UrlEncodedContent .getContent (request ).getData ());
232
+ data .put ("code_verifier" , pkce .getVerifier ());
233
+ }
234
+ }
235
+ };
236
+
209
237
return new AuthorizationCodeTokenRequest (transport , jsonFactory ,
210
238
new GenericUrl (tokenServerEncodedUrl ), authorizationCode ).setClientAuthentication (
211
- clientAuthentication ).setRequestInitializer (requestInitializer ).setScopes (scopes );
239
+ pkceClientAuthenticationWrapper ).setRequestInitializer (requestInitializer ).setScopes (scopes );
212
240
}
213
241
214
242
/**
@@ -412,6 +440,61 @@ public interface CredentialCreatedListener {
412
440
void onCredentialCreated (Credential credential , TokenResponse tokenResponse ) throws IOException ;
413
441
}
414
442
443
+ /**
444
+ * An implementation of <a href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange</a>
445
+ * which, according to the <a href="https://tools.ietf.org/html/rfc8252#section-6">OAuth 2.0 for Native Apps RFC</a>,
446
+ * is mandatory for public native apps.
447
+ */
448
+ private static class PKCE {
449
+ private final String verifier ;
450
+ private String challenge ;
451
+ private String challengeMethod ;
452
+
453
+ public PKCE () {
454
+ verifier = generateVerifier ();
455
+ generateChallenge (verifier );
456
+ }
457
+
458
+ private static String generateVerifier () {
459
+ SecureRandom sr = new SecureRandom ();
460
+ byte [] code = new byte [32 ];
461
+ sr .nextBytes (code );
462
+ return Base64 .encodeBase64URLSafeString (code );
463
+ }
464
+
465
+ /**
466
+ * Create the PKCE code verifier. It uses the S256 method but
467
+ * falls back to using the 'plain' method in the unlikely case
468
+ * that the SHA-256 MessageDigest algorithm implementation can't be
469
+ * loaded.
470
+ */
471
+ private void generateChallenge (String verifier ) {
472
+ try {
473
+ byte [] bytes = verifier .getBytes ();
474
+ MessageDigest md = MessageDigest .getInstance ("SHA-256" );
475
+ md .update (bytes , 0 , bytes .length );
476
+ byte [] digest = md .digest ();
477
+ challenge = Base64 .encodeBase64URLSafeString (digest );
478
+ challengeMethod = "S256" ;
479
+ } catch (NoSuchAlgorithmException e ) {
480
+ challenge = verifier ;
481
+ challengeMethod = "plain" ;
482
+ }
483
+ }
484
+
485
+ public String getVerifier () {
486
+ return verifier ;
487
+ }
488
+
489
+ public String getChallenge () {
490
+ return challenge ;
491
+ }
492
+
493
+ public String getChallengeMethod () {
494
+ return challengeMethod ;
495
+ }
496
+ }
497
+
415
498
/**
416
499
* Authorization code flow builder.
417
500
*
@@ -448,6 +531,8 @@ public static class Builder {
448
531
/** Authorization server encoded URL. */
449
532
String authorizationServerEncodedUrl ;
450
533
534
+ PKCE pkce ;
535
+
451
536
/** Credential persistence store or {@code null} for none. */
452
537
@ Deprecated
453
538
@ Beta
@@ -784,6 +869,16 @@ public Builder setRequestInitializer(HttpRequestInitializer requestInitializer)
784
869
return this ;
785
870
}
786
871
872
+ /**
873
+ * Enables Proof Key for Code Exchange (PKCE) for this Athorization Code Flow.
874
+ * @since 1.31
875
+ */
876
+ @ Beta
877
+ public Builder enablePKCE () {
878
+ this .pkce = new PKCE ();
879
+ return this ;
880
+ }
881
+
787
882
/**
788
883
* Sets the collection of scopes.
789
884
*
0 commit comments