0
votes

I have build a mobile backend for my android application using google cloud endpoints (version 1, using android studio). I wish to authenticate my users via email/password, so I am using Firebase Authentication to do so. The Firebase Authentication sdk allows me to get each user's token on the client side (in android), and the firebase admin sdk allows me to check the validity of a token on the backend. I understand that in cloud endpoints I can provide my own custom authenticator (see: Google Cloud Endpoints and user's authentication), and I plan to call the firebase admin sdk within my custom authenticator to verify the user's presented token.

My problem is, since I am using google cloud endpoints to build my backend, I do not know where to insert the code to do the initialization of the firebase admin object that is required before I can verify any tokens. In a regular app engine environment, you would do this initialization in the init() method of the HTTPServlet (see https://github.com/GoogleCloudPlatform/firebase-appengine-backend/blob/master/src/main/java/com/google/cloud/solutions/flexenv/backend/MessageProcessorServlet.java ), but cloud endpoints hides this from you by automatically supplying a "SystemServiceServlet" as the HTTPServlet. I have tried subclassing the SystemServiceServlet and overriding the init() method, but then deployment of the endpoint to app engine fails since, apparently, creation of the android client libraries requires that there must be a SystemServiceServlet used (and it must be named "SystemServiceServlet").

I could do the initialization of the firebase admin app in each of the api methods that cloud endpoints provides (e.g., in the insert method of my api), but this seems like it would be extremely inefficient. How would I go about using the Firebase admin sdk in a backend built using google cloud endpoints?

Thanks very much for your time

2
If all you wanna do is generate custom token, use this library to generate the custom token --> github.com/jwtk/jjwt. It's easy to use and you don't face the issue with initialisation using this library.Rahul Sainani

2 Answers

5
votes

Since I couldn't find an appropriate place to initialize the Firebase admin code within Cloud Endpoints I wrote my own server-side Java code to verify a Firebase token as per https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library

Here is a helper class you can use to validate a user's Firebase token and get their Firebase user uid (this code uses the jose.4.j library from https://bitbucket.org/b_c/jose4j/wiki/Home to do JWT manipulation):

public class TokenManager {
    private final static String PROJECT_ID = "your_firebase_project_id";
    private final static String AUDIENCE = PROJECT_ID;
    private final static String ISSUER = "https://securetoken.google.com/" + PROJECT_ID;
    private final static String KEYS_URL = "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]";

    /**
     * Parses and verifies a FirebaseUser ID token (JWT) and returns the associated user's uid
     *
     * @param token the firebase user's token
     * @return the firebase user UID
     * @throws UnauthorizedException if the token is invalid.
     */
    public static String verfiyToken(String token) throws UnauthorizedException{
        JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder()
                .setSkipAllValidators()
                .setDisableRequireSignature()
                .setSkipSignatureVerification()
                .build();

        //The first JwtConsumer is basically just used to parse the JWT into a JwtContext object.
        JwtContext jwtContext;
        try {
            jwtContext = firstPassJwtConsumer.process(token);
        } catch (InvalidJwtException e) {
            throw new UnauthorizedException(e.getMessage());
        }

        // get the key id from the header of the JWT
        List<JsonWebStructure> list = jwtContext.getJoseObjects();
        String kid = list.get(0).getKeyIdHeaderValue();
        String keyAsString;
        try {
            keyAsString = getPublicKey(kid);
        } catch (IOException e) {
            throw new UnauthorizedException(e.getMessage());
        }

        // decode the key into proper format
        InputStream certIs = new ByteArrayInputStream(Charset.forName("UTF-8").encode(keyAsString).array());

        CertificateFactory certificateFactory;
        try {
            certificateFactory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new UnauthorizedException(e.getMessage());
        }

        X509Certificate cert;
        try {
            cert = (X509Certificate) certificateFactory.generateCertificate(certIs);
        } catch (CertificateException e) {
            throw new UnauthorizedException(e.getMessage());
        }
        PublicKey key = cert.getPublicKey();

        // now that we have the public key, we can verify the JWT
        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                .setRequireExpirationTime() // the JWT must have an expiration time
                .setMaxFutureValidityInMinutes(300) // but the  expiration time can't be too crazy
                .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
                .setRequireSubject() // the JWT must have a subject claim
                .setExpectedIssuer(ISSUER) // whom the JWT needs to have been issued by
                .setExpectedAudience(AUDIENCE) // to whom the JWT is intended for
                .setVerificationKey(key) // verify the signature with the public key
                .build(); // create the JwtConsumer instance

        JwtClaims jwtClaims;
        try {
            //  Validate the JWT and process it to the Claims
            jwtClaims = jwtConsumer.processToClaims(token);
        } catch (InvalidJwtException e)  {
            throw new UnauthorizedException(e.getMessage());
        }

        String userUid;

        try {
            userUid = jwtClaims.getSubject();
        } catch(MalformedClaimException e) {
            throw new UnauthorizedException(e.getMessage());
        }
        return userUid;
    }


    /**
     * Grab the certificate corresponding to the keyid specified in the JWT
     *
     * @param kid key id corresponding to one of the public keys listed at public keys listed at
     *            https://www.googleapis.com/robot/v1/metadata/x509/[email protected]
     * @return the certificate
     * @throws IOException if the process fails
     */
    private static String getPublicKey(String kid) throws IOException {
        URL url = new URL(KEYS_URL);
        HttpURLConnection request = (HttpURLConnection) url.openConnection();
        request.connect();

        JsonParser jp = new JsonParser(); //from gson
        JsonElement root = jp.parse(new InputStreamReader((InputStream) request.getContent()));
        JsonObject rootobj = root.getAsJsonObject();
        String publicKey = rootobj.get(kid).getAsString();

        return publicKey;
    }
}
0
votes

@Dan7620, has proposed another alternative way, but it doesn't solve the problem. Here is a simple solution, which uses Firebase Admin SDK, properly configured and initialized in Cloud Endpoints module. I will summarize the steps here:

  1. Place your serviceAccountKey.json in your apps' WEB-INF folder.
  2. Insert this in your appengineweb.xml:

    <resource-files>
        <include path="/**.json" />
    </resource-files>
    
  3. Define a class somewhere like the one below. Have a seperate init() method compulsorily:

    public class FirebaseService {
    
    public static void init() {
        try {
            FileInputStream serviceAccount = new FileInputStream(new File("WEB-INF/serviceAccountKey.json"));
            FirebaseOptions options = new FirebaseOptions.Builder()
                .setCredential(FirebaseCredentials.fromCertificate(serviceAccount))
                .setDatabaseUrl("https://[YOUR_APP_NAME].firebaseio.com/")
                .build();
            FirebaseApp.initializeApp(options);
            System.out.print("In Firebase Init module...!!");
        } catch(FileNotFoundException ignored) {}
    }
    
  4. Invoke this method in any of the static{} code in any of the Endpoints defined by you. For example:

    static {
        ObjectifyService.register(FCMTokenMap.class);
        FirebaseService.init();
    }
    
    1. You can invoke other Firebase methods which involve Database, FCM, etc from anywhere..!