• caglararli@hotmail.com
  • 05386281520

Google Login with idToken and JWT (Java)

Çağlar Arlı      -    18 Views

Google Login with idToken and JWT (Java)

We are integrating Google login in our app and we intend to use the idToken for authentication. We are already sending the idToken from the app to the back-end server, and intend to verify the token as explained at:

https://developers.google.com/identity/sign-in/android/backend-auth#verify-the-integrity-of-the-id-token

It says that I could either use a JWT library or use a Google library to do the verification and extract the data. We opted to use the JWT library we are already using (auth0) instead of adding yet another library (each library added is one more potential issue when maintaining the application, because of possible clash of dependencies, as well as because it increases the attack surface area, so we choose to use JWT).

The docs only shows examples about the case of using the libraries they provide (one example in whichever language about using pure JWT verification would be enough to explain how to do that in any language, but unfortunately there's no example).

We implemented as follows:

JWKUtil class:

package my.package.util;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

import com.auth0.jwk.JwkProvider;
import com.auth0.jwt.interfaces.RSAKeyProvider;

public class JWKUtil {
    public static RSAKeyProvider getKeyProvider(JwkProvider provider) {
        return new RSAKeyProvider() {
            @Override
            public RSAPublicKey getPublicKeyById(String kid) {
                try {
                    return (RSAPublicKey) provider.get(kid).getPublicKey();
                } catch(RuntimeException e) {
                    throw e;
                } catch(Exception e) {
                    throw new RuntimeException(e);
                }
            }

            @Override
            public RSAPrivateKey getPrivateKey() {
                return null;
            }

            @Override
            public String getPrivateKeyId() {
                return null;
            }
        };
    }
}

GoogleLoginUtil class (for all purposes, the TokenInvalidException class can be considered as just a wrapper of Exception):

package my.package.util.external;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.interfaces.RSAKeyProvider;

import my.package.exceptions.TokenInvalidException;
import my.package.util.JWKUtil;

public class GoogleLoginUtil {

    public static DecodedJWT verifyToken(String idToken, String audience) throws TokenInvalidException {
        try {
            JwkProvider provider = new JwkProviderBuilder(new URL("https://www.googleapis.com/oauth2/v3/certs"))
                // cache up to 100 JWKs for up to 30 minutes
                .cached(100, 30, TimeUnit.MINUTES)
                // Connect timeout of 3 seconds, read timeout of 5 seconds (values are in milliseconds)
                .timeouts(3000, 5000)
                .build();

            RSAKeyProvider keyProvider = JWKUtil.getKeyProvider(provider);

            Algorithm algorithm = Algorithm.RSA256(keyProvider);

            JWTVerifier verifier = JWT.require(algorithm).build();

            DecodedJWT decodedJWT = verifier.verify(idToken);

            if (!decodedJWT.getAlgorithm().equals("RS256")) {
                throw new TokenInvalidException("Algorithm not supported: " + decodedJWT.getAlgorithm());
            }

            if (!decodedJWT.getType().equals("JWT")) {
                throw new TokenInvalidException("Type not supported: " + decodedJWT.getType());
            }

            Set<String> allowedIssuers = new HashSet<String>();
            allowedIssuers.add("accounts.google.com");
            allowedIssuers.add("https://accounts.google.com");

            if (!allowedIssuers.contains(decodedJWT.getIssuer())) {
                throw new TokenInvalidException("Issuer not supported: " + decodedJWT.getIssuer());
            }

            if (!decodedJWT.getAudience().contains(audience)) {
                throw new TokenInvalidException("Audience not supported: " + decodedJWT.getAudience());
            }

            if (decodedJWT.getExpiresAt().before(new Date())) {
                throw new TokenInvalidException("Token expired: " + decodedJWT.getExpiresAt());
            }

            boolean emailVerified = decodedJWT.getClaim("email_verified").asBoolean();

            if (!emailVerified) {
                throw new TokenInvalidException("Email not verified: " + decodedJWT.getClaim("email_verified").asString());
            }

            return decodedJWT;
            
            // It will be used like the following:
            // String subject = decodedJWT.getSubject();
            // String email = decodedJWT.getClaim("email").asString();
            // String name = decodedJWT.getClaim("name").asString();
            // String picture = decodedJWT.getClaim("picture").asString();
            // String givenName = decodedJWT.getClaim("given_name").asString();
            // String familyName = decodedJWT.getClaim("family_name").asString();
            // String locale = decodedJWT.getClaim("locale").asString();
            // ...
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

}

From what I know, the provider class gets the public key from the url to validate the JWT signature, so as to know that the token was (supposedly) generated with a private key owned by who owns the url (https://www.googleapis.com/oauth2/v3/certs), which is Google. Maybe not exactly as I said, because I don't know a lot about JWT, but I think it's something along those lines.

Then:

  1. The issuer is verified so as to make sure that it's accounts.google.com (because Google may issue tokens for other cases aside from authentication).
  2. I verify the audience to know if it was requested by my app (app specific validation, using the same google client id I used when authenticating in the app).
  3. Finally, I verify if the token is not expired.

From what I tested, it's working as intended.

I want to know if the verifications above are enough (if it's the correct way to do the verification), or if something is wrong or an additional validation is advised (that is, I want to know if the code above is secure).