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 package com.google.auth.oauth2; 32 33 import static org.junit.Assert.assertEquals; 34 import static org.junit.Assert.assertNotNull; 35 import static org.junit.Assert.assertTrue; 36 import static org.junit.Assert.fail; 37 38 import com.google.api.client.http.HttpTransport; 39 import com.google.api.client.http.LowLevelHttpRequest; 40 import com.google.api.client.http.LowLevelHttpResponse; 41 import com.google.api.client.testing.http.MockHttpTransport; 42 import com.google.api.client.testing.http.MockLowLevelHttpRequest; 43 import com.google.api.client.testing.http.MockLowLevelHttpResponse; 44 import com.google.api.client.util.Clock; 45 import com.google.auth.http.HttpTransportFactory; 46 import com.google.auth.oauth2.TokenVerifier.VerificationException; 47 import com.google.common.io.CharStreams; 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.io.InputStreamReader; 51 import java.io.Reader; 52 import java.util.Arrays; 53 import java.util.List; 54 import org.junit.Ignore; 55 import org.junit.Test; 56 import org.junit.runner.RunWith; 57 import org.junit.runners.JUnit4; 58 59 @RunWith(JUnit4.class) 60 public class TokenVerifierTest { 61 private static final String ES256_TOKEN = 62 "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; 63 64 private static final String FEDERATED_SIGNON_RS256_TOKEN = 65 "eyJhbGciOiJSUzI1NiIsImtpZCI6ImY5ZDk3YjRjYWU5MGJjZDc2YWViMjAwMjZmNmI3NzBjYWMyMjE3ODMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL3BhdGgiLCJhenAiOiJpbnRlZ3JhdGlvbi10ZXN0c0BjaGluZ29yLXRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6ImludGVncmF0aW9uLXRlc3RzQGNoaW5nb3ItdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1ODc2Mjk4ODgsImlhdCI6MTU4NzYyNjI4OCwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0MDI5MjkyODUzMDk5OTc4MjkzIn0.Pj4KsJh7riU7ZIbPMcHcHWhasWEcbVjGP4yx_5E0iOpeDalTdri97E-o0dSSkuVX2FeBIgGUg_TNNgJ3YY97T737jT5DUYwdv6M51dDlLmmNqlu_P6toGCSRC8-Beu5gGmqS2Y82TmpHH9Vhoh5PsK7_rVHk8U6VrrVVKKTWm_IzTFhqX1oYKPdvfyaNLsXPbCt_NFE0C3DNmFkgVhRJu7LtzQQN-ghaqd3Ga3i6KH222OEI_PU4BUTvEiNOqRGoMlT_YOsyFN3XwqQ6jQGWhhkArL1z3CG2BVQjHTKpgVsRyy_H6WTZiju2Q-XWobgH-UPSZbyymV8-cFT9XKEtZQ"; 66 private static final String LEGACY_FEDERATED_SIGNON_CERT_URL = 67 "https://www.googleapis.com/oauth2/v1/certs"; 68 69 private static final String SERVICE_ACCOUNT_RS256_TOKEN = 70 "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MjdiNmI0OTQwMmI5Y2Y5NWJlNGU4ZmQzOGFhN2U3YzExNjQ0YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Nsb3VkdGFza3MuZ29vZ2xlYXBpcy5jb20vdjIvcHJvamVjdHMvZ2Nsb3VkLWRldmVsL2xvY2F0aW9ucyIsImF6cCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjYwODgwNjczLCJpYXQiOjE2NjA4NzcwNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjExMjgxMDY3Mjk2MzcyODM2NjQwNiJ9.Q2tG-hN6UHecbzaCIlg58K9msp58nLZWs03CBGO_D6F3cI4LKQEUzsbcztZqmNGWd0ld4zkrKzIP9cQosa_xold4hEzSX_ORRHYQLimLYaQmP3rKqWPMsbIupPdpnGqBDzAYjc7Pw9pQBzuZJj8e3FEG6a5tblDfMcgeklXZIkwzN7ypWCbFDoDP2STSYJYZ-LQIB0-Zlex7dm2KhyB8QSkMQK60YvpXz4L1OtwG7spk3yUCWxul6hYF76klST0iS6DH03YdaDpt4gRXkTUKyTRfB10h-WhCAKKRzmT6d_IT9ApIyqPhimkgkBHhLNyjK8lgAJdk9CLriSEOgVpsow"; 71 private static final String SERVICE_ACCOUNT_CERT_URL = 72 "https://www.googleapis.com/oauth2/v3/certs"; 73 74 private static final List<String> ALL_TOKENS = 75 Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN); 76 77 // Fixed to 2020-02-26 08:00:00 to allow expiration tests to pass 78 private static final Clock FIXED_CLOCK = 79 new Clock() { 80 @Override 81 public long currentTimeMillis() { 82 return 1582704000000L; 83 } 84 }; 85 86 @Test verifyExpiredToken()87 public void verifyExpiredToken() { 88 for (String token : ALL_TOKENS) { 89 TokenVerifier tokenVerifier = TokenVerifier.newBuilder().build(); 90 try { 91 tokenVerifier.verify(token); 92 fail("Should have thrown a VerificationException"); 93 } catch (TokenVerifier.VerificationException e) { 94 assertTrue(e.getMessage().contains("expired")); 95 } 96 } 97 } 98 99 @Test verifyExpectedAudience()100 public void verifyExpectedAudience() { 101 TokenVerifier tokenVerifier = 102 TokenVerifier.newBuilder().setAudience("expected audience").build(); 103 for (String token : ALL_TOKENS) { 104 try { 105 tokenVerifier.verify(token); 106 fail("Should have thrown a VerificationException"); 107 } catch (TokenVerifier.VerificationException e) { 108 assertTrue(e.getMessage().contains("audience does not match")); 109 } 110 } 111 } 112 113 @Test verifyExpectedIssuer()114 public void verifyExpectedIssuer() { 115 TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setIssuer("expected issuer").build(); 116 for (String token : ALL_TOKENS) { 117 try { 118 tokenVerifier.verify(token); 119 fail("Should have thrown a VerificationException"); 120 } catch (TokenVerifier.VerificationException e) { 121 assertTrue(e.getMessage().contains("issuer does not match")); 122 } 123 } 124 } 125 126 @Test verifyEs256Token404CertificateUrl()127 public void verifyEs256Token404CertificateUrl() { 128 // Mock HTTP requests 129 HttpTransportFactory httpTransportFactory = 130 new HttpTransportFactory() { 131 @Override 132 public HttpTransport create() { 133 return new MockHttpTransport() { 134 @Override 135 public LowLevelHttpRequest buildRequest(String method, String url) 136 throws IOException { 137 return new MockLowLevelHttpRequest() { 138 @Override 139 public LowLevelHttpResponse execute() throws IOException { 140 MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 141 response.setStatusCode(404); 142 response.setContentType("application/json"); 143 response.setContent(""); 144 return response; 145 } 146 }; 147 } 148 }; 149 } 150 }; 151 TokenVerifier tokenVerifier = 152 TokenVerifier.newBuilder() 153 .setClock(FIXED_CLOCK) 154 .setHttpTransportFactory(httpTransportFactory) 155 .build(); 156 157 try { 158 tokenVerifier.verify(ES256_TOKEN); 159 fail("Should not be able to continue without exception."); 160 } catch (TokenVerifier.VerificationException exception) { 161 assertTrue( 162 exception.getMessage().contains("Error fetching PublicKey from certificate location")); 163 } 164 } 165 166 @Test verifyEs256TokenPublicKeyMismatch()167 public void verifyEs256TokenPublicKeyMismatch() { 168 // Mock HTTP requests 169 HttpTransportFactory httpTransportFactory = 170 new HttpTransportFactory() { 171 @Override 172 public HttpTransport create() { 173 return new MockHttpTransport() { 174 @Override 175 public LowLevelHttpRequest buildRequest(String method, String url) { 176 return new MockLowLevelHttpRequest() { 177 @Override 178 public LowLevelHttpResponse execute() throws IOException { 179 MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 180 response.setStatusCode(200); 181 response.setContentType("application/json"); 182 response.setContent(""); 183 return response; 184 } 185 }; 186 } 187 }; 188 } 189 }; 190 TokenVerifier tokenVerifier = 191 TokenVerifier.newBuilder() 192 .setClock(FIXED_CLOCK) 193 .setHttpTransportFactory(httpTransportFactory) 194 .build(); 195 try { 196 tokenVerifier.verify(ES256_TOKEN); 197 fail("Should have failed verification"); 198 } catch (TokenVerifier.VerificationException e) { 199 assertTrue(e.getMessage().contains("Error fetching PublicKey")); 200 } 201 } 202 203 @Test verifyPublicKeyStoreIntermittentError()204 public void verifyPublicKeyStoreIntermittentError() throws VerificationException, IOException { 205 // mock responses 206 MockLowLevelHttpResponse response404 = 207 new MockLowLevelHttpResponse() 208 .setStatusCode(404) 209 .setContentType("application/json") 210 .setContent(""); 211 212 MockLowLevelHttpResponse responseEmpty = 213 new MockLowLevelHttpResponse() 214 .setStatusCode(200) 215 .setContentType("application/json") 216 .setContent("{\"keys\":[]}"); 217 218 MockLowLevelHttpResponse responseGood = 219 new MockLowLevelHttpResponse() 220 .setStatusCode(200) 221 .setContentType("application/json") 222 .setContent(readResourceAsString("iap_keys.json")); 223 224 // Mock HTTP requests 225 MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); 226 227 transportFactory.transport.addResponseSequence( 228 response404, response404, response404, responseEmpty, responseGood); 229 230 TokenVerifier tokenVerifier = 231 TokenVerifier.newBuilder() 232 .setClock(FIXED_CLOCK) 233 .setHttpTransportFactory(transportFactory) 234 .build(); 235 236 try { 237 tokenVerifier.verify(ES256_TOKEN); 238 fail("Should not be able to continue without exception."); 239 } catch (TokenVerifier.VerificationException exception) { 240 assertTrue(exception.getMessage().contains("Error fetching PublicKey")); 241 } 242 243 try { 244 tokenVerifier.verify(ES256_TOKEN); 245 fail("Should not be able to continue without exception."); 246 } catch (TokenVerifier.VerificationException exception) { 247 assertTrue(exception.getCause().getMessage().contains("No valid public key")); 248 } 249 250 assertNotNull(tokenVerifier.verify(ES256_TOKEN)); 251 } 252 253 @Test verifyEs256Token()254 public void verifyEs256Token() throws VerificationException, IOException { 255 HttpTransportFactory httpTransportFactory = 256 mockTransport( 257 "https://www.gstatic.com/iap/verify/public_key-jwk", 258 readResourceAsString("iap_keys.json")); 259 TokenVerifier tokenVerifier = 260 TokenVerifier.newBuilder() 261 .setClock(FIXED_CLOCK) 262 .setHttpTransportFactory(httpTransportFactory) 263 .build(); 264 assertNotNull(tokenVerifier.verify(ES256_TOKEN)); 265 } 266 267 @Test verifyRs256Token()268 public void verifyRs256Token() throws VerificationException, IOException { 269 HttpTransportFactory httpTransportFactory = 270 mockTransport( 271 "https://www.googleapis.com/oauth2/v3/certs", 272 readResourceAsString("federated_keys.json")); 273 TokenVerifier tokenVerifier = 274 TokenVerifier.newBuilder() 275 .setClock(FIXED_CLOCK) 276 .setHttpTransportFactory(httpTransportFactory) 277 .build(); 278 assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); 279 } 280 281 @Test verifyRs256TokenWithLegacyCertificateUrlFormat()282 public void verifyRs256TokenWithLegacyCertificateUrlFormat() 283 throws TokenVerifier.VerificationException, IOException { 284 HttpTransportFactory httpTransportFactory = 285 mockTransport( 286 LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); 287 TokenVerifier tokenVerifier = 288 TokenVerifier.newBuilder() 289 .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) 290 .setClock(FIXED_CLOCK) 291 .setHttpTransportFactory(httpTransportFactory) 292 .build(); 293 assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); 294 } 295 296 @Test 297 @Ignore verifyServiceAccountRs256Token()298 public void verifyServiceAccountRs256Token() throws VerificationException { 299 final Clock clock = 300 new Clock() { 301 @Override 302 public long currentTimeMillis() { 303 return 1660880573000L; 304 } 305 }; 306 TokenVerifier tokenVerifier = 307 TokenVerifier.newBuilder() 308 .setClock(clock) 309 .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) 310 .build(); 311 assertNotNull(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); 312 } 313 readResourceAsString(String resourceName)314 static String readResourceAsString(String resourceName) throws IOException { 315 InputStream inputStream = 316 TokenVerifierTest.class.getClassLoader().getResourceAsStream(resourceName); 317 try (final Reader reader = new InputStreamReader(inputStream)) { 318 return CharStreams.toString(reader); 319 } 320 } 321 mockTransport(String url, String certificates)322 static HttpTransportFactory mockTransport(String url, String certificates) { 323 final String certificatesContent = certificates; 324 final String certificatesUrl = url; 325 return new HttpTransportFactory() { 326 @Override 327 public HttpTransport create() { 328 return new MockHttpTransport() { 329 @Override 330 public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 331 assertEquals(certificatesUrl, url); 332 return new MockLowLevelHttpRequest() { 333 @Override 334 public LowLevelHttpResponse execute() throws IOException { 335 MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 336 response.setStatusCode(200); 337 response.setContentType("application/json"); 338 response.setContent(certificatesContent); 339 return response; 340 } 341 }; 342 } 343 }; 344 } 345 }; 346 } 347 } 348