me.lazerka.gae.jersey.oauth2.google.TokenVerifierGoogleRemote.java Source code

Java tutorial

Introduction

Here is the source code for me.lazerka.gae.jersey.oauth2.google.TokenVerifierGoogleRemote.java

Source

/*
 * Copyright (c) 2016 Dzmitry Lazerka
 *
 * 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 me.lazerka.gae.jersey.oauth2.google;

import com.google.api.client.auth.oauth2.TokenErrorResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.json.JsonFactory;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.base.Stopwatch;
import me.lazerka.gae.jersey.oauth2.facebook.BasicTokenVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.security.InvalidKeyException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.GET;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Filter that verifies token by making HTTPS call to Google endpoint, so Google servers verify it.
 *
 * This MUST be done through HTTPS.
 *
 * Documentation: https://developers.google.com/identity/sign-in/android/backend-auth
 *
 * @author Dzmitry Lazerka
 */
@Singleton
public class TokenVerifierGoogleRemote extends BasicTokenVerifier {
    private static final Logger logger = LoggerFactory.getLogger(TokenVerifierGoogleRemote.class);

    public static final String AUTH_SCHEME = "GoogleSignIn/Remote";

    private static final URI TOKEN_INFO = URI.create("https://www.googleapis.com/oauth2/v3/tokeninfo");

    final URLFetchService urlFetchService;
    final JsonFactory jsonFactory;
    final String oauthClientId;

    public TokenVerifierGoogleRemote(URLFetchService urlFetchService, JsonFactory jsonFactory,
            String oauthClientId) {
        this.urlFetchService = urlFetchService;
        this.jsonFactory = jsonFactory;
        this.oauthClientId = oauthClientId;
    }

    @Override
    public GoogleUserPrincipal verify(String authToken) throws IOException, InvalidKeyException {
        logger.trace("Requesting endpoint to validate token");

        URL url = UriBuilder.fromUri(TOKEN_INFO).queryParam("id_token", authToken).build().toURL();

        HTTPRequest httpRequest = new HTTPRequest(url, GET, validateCertificate());

        Stopwatch stopwatch = Stopwatch.createStarted();
        HTTPResponse response = urlFetchService.fetch(httpRequest);
        logger.debug("Remote call took {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));

        int responseCode = response.getResponseCode();
        String content = new String(response.getContent(), UTF_8);

        if (responseCode != 200) {
            logger.warn("{}: {}", responseCode, content);

            String msg = "Endpoint response code " + responseCode;

            // Something is wrong with our request.
            // If signature is invalid, then response code is 403.
            if (responseCode >= 400 && responseCode < 500) {
                try {
                    TokenErrorResponse tokenErrorResponse = jsonFactory.fromString(content,
                            TokenErrorResponse.class);
                    msg += ": " + tokenErrorResponse.getErrorDescription();
                } catch (IOException e) {
                    logger.warn("Cannot parse response as " + TokenErrorResponse.class.getSimpleName());
                }
            }

            throw new InvalidKeyException(msg);
        }

        // Signature verification is done remotely (the whole point of this class).
        // Expiration verification is done

        Payload payload = jsonFactory.fromString(content, Payload.class);

        // Issuers verification have been done remotely.

        Set<String> trustedClientIds = Collections.singleton(oauthClientId);
        // Note containsAll.
        if (!trustedClientIds.containsAll(payload.getAudienceAsList())) {
            throw new InvalidKeyException("Audience invalid");
        }

        if (!payload.getEmailVerified()) {
            throw new InvalidKeyException("Email not verified");
        }

        return new GoogleUserPrincipal(payload.getSubject(), payload.getEmail());
    }

    @Override
    public String getAuthenticationScheme() {
        return AUTH_SCHEME;
    }
}