• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020, Google LLC
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 LLC 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.HttpBackOffUnsuccessfulResponseHandler;
36 import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler.BackOffRequired;
37 import com.google.api.client.http.HttpRequest;
38 import com.google.api.client.http.HttpResponse;
39 import com.google.api.client.http.HttpTransport;
40 import com.google.api.client.json.GenericJson;
41 import com.google.api.client.json.webtoken.JsonWebSignature;
42 import com.google.api.client.util.Base64;
43 import com.google.api.client.util.Clock;
44 import com.google.api.client.util.ExponentialBackOff;
45 import com.google.api.client.util.Key;
46 import com.google.auth.http.HttpTransportFactory;
47 import com.google.common.base.Preconditions;
48 import com.google.common.cache.CacheBuilder;
49 import com.google.common.cache.CacheLoader;
50 import com.google.common.cache.LoadingCache;
51 import com.google.common.collect.ImmutableMap;
52 import com.google.common.collect.ImmutableSet;
53 import com.google.common.util.concurrent.UncheckedExecutionException;
54 import java.io.ByteArrayInputStream;
55 import java.io.IOException;
56 import java.io.UnsupportedEncodingException;
57 import java.math.BigInteger;
58 import java.security.AlgorithmParameters;
59 import java.security.GeneralSecurityException;
60 import java.security.KeyFactory;
61 import java.security.NoSuchAlgorithmException;
62 import java.security.PublicKey;
63 import java.security.cert.CertificateException;
64 import java.security.cert.CertificateFactory;
65 import java.security.spec.ECGenParameterSpec;
66 import java.security.spec.ECParameterSpec;
67 import java.security.spec.ECPoint;
68 import java.security.spec.ECPublicKeySpec;
69 import java.security.spec.InvalidKeySpecException;
70 import java.security.spec.InvalidParameterSpecException;
71 import java.security.spec.RSAPublicKeySpec;
72 import java.util.List;
73 import java.util.Map;
74 import java.util.Set;
75 import java.util.concurrent.ExecutionException;
76 import java.util.concurrent.TimeUnit;
77 
78 /**
79  * Handle verification of Google-signed JWT tokens.
80  *
81  * @author Jeff Ching
82  * @since 0.21.0
83  */
84 public class TokenVerifier {
85   private static final String IAP_CERT_URL = "https://www.gstatic.com/iap/verify/public_key-jwk";
86   private static final String FEDERATED_SIGNON_CERT_URL =
87       "https://www.googleapis.com/oauth2/v3/certs";
88   private static final Set<String> SUPPORTED_ALGORITHMS = ImmutableSet.of("RS256", "ES256");
89 
90   private final String audience;
91   private final String certificatesLocation;
92   private final String issuer;
93   private final PublicKey publicKey;
94   private final Clock clock;
95   private final LoadingCache<String, Map<String, PublicKey>> publicKeyCache;
96 
TokenVerifier(Builder builder)97   private TokenVerifier(Builder builder) {
98     this.audience = builder.audience;
99     this.certificatesLocation = builder.certificatesLocation;
100     this.issuer = builder.issuer;
101     this.publicKey = builder.publicKey;
102     this.clock = builder.clock;
103     this.publicKeyCache =
104         CacheBuilder.newBuilder()
105             .expireAfterWrite(1, TimeUnit.HOURS)
106             .build(new PublicKeyLoader(builder.httpTransportFactory));
107   }
108 
newBuilder()109   public static Builder newBuilder() {
110     return new Builder()
111         .setClock(Clock.SYSTEM)
112         .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY);
113   }
114 
115   /**
116    * Verify an encoded JWT token.
117    *
118    * @param token encoded JWT token
119    * @return the parsed JsonWebSignature instance for additional validation if necessary
120    * @throws VerificationException thrown if any verification fails
121    */
verify(String token)122   public JsonWebSignature verify(String token) throws VerificationException {
123     JsonWebSignature jsonWebSignature;
124     try {
125       jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token);
126     } catch (IOException e) {
127       throw new VerificationException("Error parsing JsonWebSignature token", e);
128     }
129 
130     // Verify the expected audience if an audience is provided in the verifyOptions
131     if (audience != null && !audience.equals(jsonWebSignature.getPayload().getAudience())) {
132       throw new VerificationException("Expected audience does not match");
133     }
134 
135     // Verify the expected issuer if an issuer is provided in the verifyOptions
136     if (issuer != null && !issuer.equals(jsonWebSignature.getPayload().getIssuer())) {
137       throw new VerificationException("Expected issuer does not match");
138     }
139 
140     Long expiresAt = jsonWebSignature.getPayload().getExpirationTimeSeconds();
141     if (expiresAt != null && expiresAt <= clock.currentTimeMillis() / 1000) {
142       throw new VerificationException("Token is expired");
143     }
144 
145     // Short-circuit signature types
146     if (!SUPPORTED_ALGORITHMS.contains(jsonWebSignature.getHeader().getAlgorithm())) {
147       throw new VerificationException(
148           "Unexpected signing algorithm: expected either RS256 or ES256");
149     }
150 
151     PublicKey publicKeyToUse = publicKey;
152     if (publicKeyToUse == null) {
153       try {
154         String certificateLocation = getCertificateLocation(jsonWebSignature);
155         publicKeyToUse =
156             publicKeyCache.get(certificateLocation).get(jsonWebSignature.getHeader().getKeyId());
157       } catch (ExecutionException | UncheckedExecutionException e) {
158         throw new VerificationException("Error fetching PublicKey from certificate location", e);
159       }
160     }
161 
162     if (publicKeyToUse == null) {
163       throw new VerificationException(
164           "Could not find PublicKey for provided keyId: "
165               + jsonWebSignature.getHeader().getKeyId());
166     }
167 
168     try {
169       if (jsonWebSignature.verifySignature(publicKeyToUse)) {
170         return jsonWebSignature;
171       }
172       throw new VerificationException("Invalid signature");
173     } catch (GeneralSecurityException e) {
174       throw new VerificationException("Error validating token", e);
175     }
176   }
177 
getCertificateLocation(JsonWebSignature jsonWebSignature)178   private String getCertificateLocation(JsonWebSignature jsonWebSignature)
179       throws VerificationException {
180     if (certificatesLocation != null) return certificatesLocation;
181 
182     switch (jsonWebSignature.getHeader().getAlgorithm()) {
183       case "RS256":
184         return FEDERATED_SIGNON_CERT_URL;
185       case "ES256":
186         return IAP_CERT_URL;
187     }
188 
189     throw new VerificationException("Unknown algorithm");
190   }
191 
192   public static class Builder {
193     private String audience;
194     private String certificatesLocation;
195     private String issuer;
196     private PublicKey publicKey;
197     private Clock clock;
198     private HttpTransportFactory httpTransportFactory;
199 
200     /**
201      * Set a target audience to verify.
202      *
203      * @param audience the audience claim to verify
204      * @return the builder
205      */
setAudience(String audience)206     public Builder setAudience(String audience) {
207       this.audience = audience;
208       return this;
209     }
210 
211     /**
212      * Override the location URL that contains published public keys. Defaults to well-known Google
213      * locations.
214      *
215      * @param certificatesLocation URL to published public keys
216      * @return the builder
217      */
setCertificatesLocation(String certificatesLocation)218     public Builder setCertificatesLocation(String certificatesLocation) {
219       this.certificatesLocation = certificatesLocation;
220       return this;
221     }
222 
223     /**
224      * Set the issuer to verify.
225      *
226      * @param issuer the issuer claim to verify
227      * @return the builder
228      */
setIssuer(String issuer)229     public Builder setIssuer(String issuer) {
230       this.issuer = issuer;
231       return this;
232     }
233 
234     /**
235      * Set the PublicKey for verifying the signature. This will ignore the key id from the JWT token
236      * header.
237      *
238      * @param publicKey the public key to validate the signature
239      * @return the builder
240      */
setPublicKey(PublicKey publicKey)241     public Builder setPublicKey(PublicKey publicKey) {
242       this.publicKey = publicKey;
243       return this;
244     }
245 
246     /**
247      * Set the clock for checking token expiry. Used for testing.
248      *
249      * @param clock the clock to use. Defaults to the system clock
250      * @return the builder
251      */
setClock(Clock clock)252     public Builder setClock(Clock clock) {
253       this.clock = clock;
254       return this;
255     }
256 
257     /**
258      * Set the HttpTransportFactory used for requesting public keys from the certificate URL. Used
259      * mostly for testing.
260      *
261      * @param httpTransportFactory the HttpTransportFactory used to build certificate URL requests
262      * @return the builder
263      */
setHttpTransportFactory(HttpTransportFactory httpTransportFactory)264     public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) {
265       this.httpTransportFactory = httpTransportFactory;
266       return this;
267     }
268 
269     /**
270      * Build the custom TokenVerifier for verifying tokens.
271      *
272      * @return the customized TokenVerifier
273      */
build()274     public TokenVerifier build() {
275       return new TokenVerifier(this);
276     }
277   }
278 
279   /** Custom CacheLoader for mapping certificate urls to the contained public keys. */
280   static class PublicKeyLoader extends CacheLoader<String, Map<String, PublicKey>> {
281     private static final int DEFAULT_NUMBER_OF_RETRIES = 2;
282     private static final int INITIAL_RETRY_INTERVAL_MILLIS = 1000;
283     private static final double RETRY_RANDOMIZATION_FACTOR = 0.1;
284     private static final double RETRY_MULTIPLIER = 2;
285     private final HttpTransportFactory httpTransportFactory;
286 
287     /**
288      * Data class used for deserializing a JSON Web Key Set (JWKS) from an external HTTP request.
289      */
290     public static class JsonWebKeySet extends GenericJson {
291       @Key public List<JsonWebKey> keys;
292     }
293 
294     /** Data class used for deserializing a single JSON Web Key. */
295     public static class JsonWebKey {
296       @Key public String alg;
297 
298       @Key public String crv;
299 
300       @Key public String kid;
301 
302       @Key public String kty;
303 
304       @Key public String use;
305 
306       @Key public String x;
307 
308       @Key public String y;
309 
310       @Key public String e;
311 
312       @Key public String n;
313     }
314 
PublicKeyLoader(HttpTransportFactory httpTransportFactory)315     PublicKeyLoader(HttpTransportFactory httpTransportFactory) {
316       super();
317       this.httpTransportFactory = httpTransportFactory;
318     }
319 
320     @Override
load(String certificateUrl)321     public Map<String, PublicKey> load(String certificateUrl) throws Exception {
322       HttpTransport httpTransport = httpTransportFactory.create();
323       JsonWebKeySet jwks;
324       HttpRequest request =
325           httpTransport
326               .createRequestFactory()
327               .buildGetRequest(new GenericUrl(certificateUrl))
328               .setParser(OAuth2Utils.JSON_FACTORY.createJsonObjectParser());
329       request.setNumberOfRetries(DEFAULT_NUMBER_OF_RETRIES);
330 
331       ExponentialBackOff backoff =
332           new ExponentialBackOff.Builder()
333               .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL_MILLIS)
334               .setRandomizationFactor(RETRY_RANDOMIZATION_FACTOR)
335               .setMultiplier(RETRY_MULTIPLIER)
336               .build();
337 
338       request.setUnsuccessfulResponseHandler(
339           new HttpBackOffUnsuccessfulResponseHandler(backoff)
340               .setBackOffRequired(BackOffRequired.ALWAYS));
341 
342       HttpResponse response = request.execute();
343       jwks = response.parseAs(JsonWebKeySet.class);
344 
345       ImmutableMap.Builder<String, PublicKey> keyCacheBuilder = new ImmutableMap.Builder<>();
346       if (jwks.keys == null) {
347         // Fall back to x509 formatted specification
348         for (String keyId : jwks.keySet()) {
349           String publicKeyPem = (String) jwks.get(keyId);
350           keyCacheBuilder.put(keyId, buildPublicKey(publicKeyPem));
351         }
352       } else {
353         for (JsonWebKey key : jwks.keys) {
354           try {
355             keyCacheBuilder.put(key.kid, buildPublicKey(key));
356           } catch (NoSuchAlgorithmException
357               | InvalidKeySpecException
358               | InvalidParameterSpecException ignored) {
359             ignored.printStackTrace();
360           }
361         }
362       }
363 
364       ImmutableMap<String, PublicKey> keyCache = keyCacheBuilder.build();
365 
366       if (keyCache.isEmpty()) {
367         throw new VerificationException(
368             "No valid public key returned by the keystore: " + certificateUrl);
369       }
370 
371       return keyCache;
372     }
373 
buildPublicKey(JsonWebKey key)374     private PublicKey buildPublicKey(JsonWebKey key)
375         throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
376       if ("ES256".equals(key.alg)) {
377         return buildEs256PublicKey(key);
378       } else if ("RS256".equals((key.alg))) {
379         return buildRs256PublicKey(key);
380       } else {
381         return null;
382       }
383     }
384 
buildPublicKey(String publicPem)385     private PublicKey buildPublicKey(String publicPem)
386         throws CertificateException, UnsupportedEncodingException {
387       return CertificateFactory.getInstance("X.509")
388           .generateCertificate(new ByteArrayInputStream(publicPem.getBytes("UTF-8")))
389           .getPublicKey();
390     }
391 
buildRs256PublicKey(JsonWebKey key)392     private PublicKey buildRs256PublicKey(JsonWebKey key)
393         throws NoSuchAlgorithmException, InvalidKeySpecException {
394       Preconditions.checkArgument("RSA".equals(key.kty));
395       Preconditions.checkNotNull(key.e);
396       Preconditions.checkNotNull(key.n);
397 
398       BigInteger modulus = new BigInteger(1, Base64.decodeBase64(key.n));
399       BigInteger exponent = new BigInteger(1, Base64.decodeBase64(key.e));
400 
401       RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
402       KeyFactory factory = KeyFactory.getInstance("RSA");
403       return factory.generatePublic(spec);
404     }
405 
buildEs256PublicKey(JsonWebKey key)406     private PublicKey buildEs256PublicKey(JsonWebKey key)
407         throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
408       Preconditions.checkArgument("EC".equals(key.kty));
409       Preconditions.checkArgument("P-256".equals(key.crv));
410 
411       BigInteger x = new BigInteger(1, Base64.decodeBase64(key.x));
412       BigInteger y = new BigInteger(1, Base64.decodeBase64(key.y));
413       ECPoint pubPoint = new ECPoint(x, y);
414       AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
415       parameters.init(new ECGenParameterSpec("secp256r1"));
416       ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class);
417       ECPublicKeySpec pubSpec = new ECPublicKeySpec(pubPoint, ecParameters);
418       KeyFactory kf = KeyFactory.getInstance("EC");
419       return kf.generatePublic(pubSpec);
420     }
421   }
422 
423   /** Custom exception for wrapping all verification errors. */
424   public static class VerificationException extends Exception {
VerificationException(String message)425     public VerificationException(String message) {
426       super(message);
427     }
428 
VerificationException(String message, Throwable cause)429     public VerificationException(String message, Throwable cause) {
430       super(message, cause);
431     }
432   }
433 }
434