/* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.firebase.auth.internal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import com.google.api.client.auth.openidconnect.IdToken; import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebSignature.Header; import com.google.api.client.json.webtoken.JsonWebToken; import com.google.api.client.json.webtoken.JsonWebToken.Payload; import com.google.api.client.testing.http.FixedClock; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.io.BaseEncoding; import com.google.firebase.auth.FirebaseToken; import com.google.firebase.auth.TestOnlyImplFirebaseAuthTrampolines; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import org.hamcrest.collection.IsIterableContainingInOrder; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; /** * Unit tests for {@link FirebaseTokenVerifier}. */ public class FirebaseTokenVerifierTest { private static final JsonFactory FACTORY = new GsonFactory(); private static final FixedClock CLOCK = new FixedClock(2002000L * 1000); private static final String PROJECT_ID = "proj-test-101"; private static final String ISSUER = "https://securetoken.google.com/" + PROJECT_ID; private static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; private static final String UID = "someUid"; private static final String ALGORITHM = "RS256"; private static final String LEGACY_CUSTOM_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkIjp7" + "InVpZCI6IjEiLCJhYmMiOiIwMTIzNDU2Nzg5fiFAIyQlXiYqKClfKy09YWJjZGVmZ2hpamtsbW5vcHF" + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw" + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj" + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI"; @Rule public ExpectedException thrown = ExpectedException.none(); private PrivateKey privateKey; private FirebaseTokenVerifier verifier; private void initCrypto(String privateKey, String certificate) throws NoSuchAlgorithmException, InvalidKeySpecException { byte[] privateBytes = BaseEncoding.base64().decode(privateKey); KeySpec spec = new PKCS8EncodedKeySpec(privateBytes); String serviceAccountCertificates = String.format("{\"%s\" : \"%s\"}", PRIVATE_KEY_ID, certificate); MockHttpTransport mockTransport = new MockHttpTransport.Builder() .setLowLevelHttpResponse( new MockLowLevelHttpResponse().setContent(serviceAccountCertificates)) .build(); this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec); this.verifier = new FirebaseTokenVerifier.Builder() .setClock(CLOCK) .setPublicKeysManager( new GooglePublicKeysManager.Builder(mockTransport, FACTORY) .setClock(CLOCK) .setPublicCertsEncodedUrl(FirebaseTokenVerifier.CLIENT_CERT_URL) .build()) .setProjectId(PROJECT_ID) .build(); } @Before public void setUp() throws Exception { initCrypto(ServiceAccount.EDITOR.getPrivateKey(), ServiceAccount.EDITOR.getCert()); } private JsonWebSignature.Header createHeader() throws Exception { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm(ALGORITHM); header.setType("JWT"); header.setKeyId(PRIVATE_KEY_ID); return header; } private JsonWebToken.Payload createPayload() { JsonWebToken.Payload payload = new JsonWebToken.Payload(); payload.setIssuer(ISSUER); payload.setAudience(PROJECT_ID); payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); payload.setSubject(UID); return payload; } private String createToken(JsonWebSignature.Header header, JsonWebToken.Payload payload) throws GeneralSecurityException, IOException { return JsonWebSignature.signUsingRsaSha256(privateKey, FACTORY, header, payload); } @Test public void verifyToken() throws Exception { FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), createPayload())); IdToken.Payload payload = (IdToken.Payload) token.getClaims(); assertThat(payload.getAudienceAsList(), IsIterableContainingInOrder.contains(PROJECT_ID)); assertEquals(ISSUER, payload.getIssuer()); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_MissingKeyId() throws Exception { Header header = createHeader(); header.setKeyId(null); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(header, createPayload())); thrown.expectMessage("Firebase ID token has no \"kid\" claim."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_MissingKeyId_CustomToken() throws Exception { Header header = createHeader(); header.setKeyId(null); Payload payload = createPayload(); payload.setAudience( "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" + ".v1.IdentityToolkit"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload)); thrown.expectMessage("verifyIdToken() expects an ID token, but was given a custom token."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_IncorrectAlgorithm() throws Exception { Header header = createHeader(); header.setAlgorithm("HS256"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(header, createPayload())); thrown.expectMessage("Firebase ID token has incorrect algorithm."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_IncorrectAudience() throws Exception { Payload payload = createPayload(); payload.setAudience( "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1." + "IdentityToolkit"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has incorrect \"aud\" (audience) claim."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_IncorrectIssuer() throws Exception { Payload payload = createPayload(); payload.setIssuer("https://foobar.google.com/" + PROJECT_ID); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_MissingSubject() throws Exception { Payload payload = createPayload(); payload.setSubject(null); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has no \"sub\" (subject) claim."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_EmptySubject() throws Exception { Payload payload = createPayload(); payload.setSubject(""); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has an empty string \"sub\" (subject) claim."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_LongSubject() throws Exception { Payload payload = createPayload(); payload.setSubject( "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv" + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage( "Firebase ID token has \"sub\" (subject) claim longer than 128 characters."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_NotYetIssued() throws Exception { Payload payload = createPayload(); payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000); payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has expired or is not yet valid."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_Expired() throws Exception { Payload payload = createPayload(); payload.setIssuedAtTimeSeconds(0L); payload.setExpirationTimeSeconds(3600L); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has expired or is not yet valid."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_WrongCert() throws Exception { initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert()); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), createPayload())); thrown.expectMessage("Firebase ID token isn't signed by a valid public key."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void legacyCustomToken() throws Exception { initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert()); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, LEGACY_CUSTOM_TOKEN); thrown.expectMessage( "verifyIdToken() expects an ID token, but was given a legacy custom token."); verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } }