/* * Copyright 2020, Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.google.auth.oauth2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.http.LowLevelHttpResponse; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.TokenVerifier.VerificationException; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.Arrays; import java.util.List; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class TokenVerifierTest { private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; private static final String FEDERATED_SIGNON_RS256_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImY5ZDk3YjRjYWU5MGJjZDc2YWViMjAwMjZmNmI3NzBjYWMyMjE3ODMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL3BhdGgiLCJhenAiOiJpbnRlZ3JhdGlvbi10ZXN0c0BjaGluZ29yLXRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6ImludGVncmF0aW9uLXRlc3RzQGNoaW5nb3ItdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1ODc2Mjk4ODgsImlhdCI6MTU4NzYyNjI4OCwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0MDI5MjkyODUzMDk5OTc4MjkzIn0.Pj4KsJh7riU7ZIbPMcHcHWhasWEcbVjGP4yx_5E0iOpeDalTdri97E-o0dSSkuVX2FeBIgGUg_TNNgJ3YY97T737jT5DUYwdv6M51dDlLmmNqlu_P6toGCSRC8-Beu5gGmqS2Y82TmpHH9Vhoh5PsK7_rVHk8U6VrrVVKKTWm_IzTFhqX1oYKPdvfyaNLsXPbCt_NFE0C3DNmFkgVhRJu7LtzQQN-ghaqd3Ga3i6KH222OEI_PU4BUTvEiNOqRGoMlT_YOsyFN3XwqQ6jQGWhhkArL1z3CG2BVQjHTKpgVsRyy_H6WTZiju2Q-XWobgH-UPSZbyymV8-cFT9XKEtZQ"; private static final String LEGACY_FEDERATED_SIGNON_CERT_URL = "https://www.googleapis.com/oauth2/v1/certs"; private static final String SERVICE_ACCOUNT_RS256_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MjdiNmI0OTQwMmI5Y2Y5NWJlNGU4ZmQzOGFhN2U3YzExNjQ0YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Nsb3VkdGFza3MuZ29vZ2xlYXBpcy5jb20vdjIvcHJvamVjdHMvZ2Nsb3VkLWRldmVsL2xvY2F0aW9ucyIsImF6cCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjYwODgwNjczLCJpYXQiOjE2NjA4NzcwNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjExMjgxMDY3Mjk2MzcyODM2NjQwNiJ9.Q2tG-hN6UHecbzaCIlg58K9msp58nLZWs03CBGO_D6F3cI4LKQEUzsbcztZqmNGWd0ld4zkrKzIP9cQosa_xold4hEzSX_ORRHYQLimLYaQmP3rKqWPMsbIupPdpnGqBDzAYjc7Pw9pQBzuZJj8e3FEG6a5tblDfMcgeklXZIkwzN7ypWCbFDoDP2STSYJYZ-LQIB0-Zlex7dm2KhyB8QSkMQK60YvpXz4L1OtwG7spk3yUCWxul6hYF76klST0iS6DH03YdaDpt4gRXkTUKyTRfB10h-WhCAKKRzmT6d_IT9ApIyqPhimkgkBHhLNyjK8lgAJdk9CLriSEOgVpsow"; private static final String SERVICE_ACCOUNT_CERT_URL = "https://www.googleapis.com/oauth2/v3/certs"; private static final List ALL_TOKENS = Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN); // Fixed to 2020-02-26 08:00:00 to allow expiration tests to pass private static final Clock FIXED_CLOCK = new Clock() { @Override public long currentTimeMillis() { return 1582704000000L; } }; @Test public void verifyExpiredToken() { for (String token : ALL_TOKENS) { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().build(); try { tokenVerifier.verify(token); fail("Should have thrown a VerificationException"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("expired")); } } } @Test public void verifyExpectedAudience() { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setAudience("expected audience").build(); for (String token : ALL_TOKENS) { try { tokenVerifier.verify(token); fail("Should have thrown a VerificationException"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("audience does not match")); } } } @Test public void verifyExpectedIssuer() { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setIssuer("expected issuer").build(); for (String token : ALL_TOKENS) { try { tokenVerifier.verify(token); fail("Should have thrown a VerificationException"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("issuer does not match")); } } } @Test public void verifyEs256Token404CertificateUrl() { // Mock HTTP requests HttpTransportFactory httpTransportFactory = new HttpTransportFactory() { @Override public HttpTransport create() { return new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { return new MockLowLevelHttpRequest() { @Override public LowLevelHttpResponse execute() throws IOException { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); response.setStatusCode(404); response.setContentType("application/json"); response.setContent(""); return response; } }; } }; } }; TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); try { tokenVerifier.verify(ES256_TOKEN); fail("Should not be able to continue without exception."); } catch (TokenVerifier.VerificationException exception) { assertTrue( exception.getMessage().contains("Error fetching PublicKey from certificate location")); } } @Test public void verifyEs256TokenPublicKeyMismatch() { // Mock HTTP requests HttpTransportFactory httpTransportFactory = new HttpTransportFactory() { @Override public HttpTransport create() { return new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) { return new MockLowLevelHttpRequest() { @Override public LowLevelHttpResponse execute() throws IOException { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); response.setStatusCode(200); response.setContentType("application/json"); response.setContent(""); return response; } }; } }; } }; TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); try { tokenVerifier.verify(ES256_TOKEN); fail("Should have failed verification"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("Error fetching PublicKey")); } } @Test public void verifyPublicKeyStoreIntermittentError() throws VerificationException, IOException { // mock responses MockLowLevelHttpResponse response404 = new MockLowLevelHttpResponse() .setStatusCode(404) .setContentType("application/json") .setContent(""); MockLowLevelHttpResponse responseEmpty = new MockLowLevelHttpResponse() .setStatusCode(200) .setContentType("application/json") .setContent("{\"keys\":[]}"); MockLowLevelHttpResponse responseGood = new MockLowLevelHttpResponse() .setStatusCode(200) .setContentType("application/json") .setContent(readResourceAsString("iap_keys.json")); // Mock HTTP requests MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); transportFactory.transport.addResponseSequence( response404, response404, response404, responseEmpty, responseGood); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) .setHttpTransportFactory(transportFactory) .build(); try { tokenVerifier.verify(ES256_TOKEN); fail("Should not be able to continue without exception."); } catch (TokenVerifier.VerificationException exception) { assertTrue(exception.getMessage().contains("Error fetching PublicKey")); } try { tokenVerifier.verify(ES256_TOKEN); fail("Should not be able to continue without exception."); } catch (TokenVerifier.VerificationException exception) { assertTrue(exception.getCause().getMessage().contains("No valid public key")); } assertNotNull(tokenVerifier.verify(ES256_TOKEN)); } @Test public void verifyEs256Token() throws VerificationException, IOException { HttpTransportFactory httpTransportFactory = mockTransport( "https://www.gstatic.com/iap/verify/public_key-jwk", readResourceAsString("iap_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); assertNotNull(tokenVerifier.verify(ES256_TOKEN)); } @Test public void verifyRs256Token() throws VerificationException, IOException { HttpTransportFactory httpTransportFactory = mockTransport( "https://www.googleapis.com/oauth2/v3/certs", readResourceAsString("federated_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test public void verifyRs256TokenWithLegacyCertificateUrlFormat() throws TokenVerifier.VerificationException, IOException { HttpTransportFactory httpTransportFactory = mockTransport( LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @Ignore public void verifyServiceAccountRs256Token() throws VerificationException { final Clock clock = new Clock() { @Override public long currentTimeMillis() { return 1660880573000L; } }; TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(clock) .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) .build(); assertNotNull(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); } static String readResourceAsString(String resourceName) throws IOException { InputStream inputStream = TokenVerifierTest.class.getClassLoader().getResourceAsStream(resourceName); try (final Reader reader = new InputStreamReader(inputStream)) { return CharStreams.toString(reader); } } static HttpTransportFactory mockTransport(String url, String certificates) { final String certificatesContent = certificates; final String certificatesUrl = url; return new HttpTransportFactory() { @Override public HttpTransport create() { return new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { assertEquals(certificatesUrl, url); return new MockLowLevelHttpRequest() { @Override public LowLevelHttpResponse execute() throws IOException { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); response.setStatusCode(200); response.setContentType("application/json"); response.setContent(certificatesContent); return response; } }; } }; } }; } }