Monday, January 16, 2017

Google Oauth2 Redux

I'm just going to leave this here. It's nothing particularly interesting but there was some googling and what not in arriving at the proper configuration and usage.

I had been using google oauth in one of my projects and that worked until I added a dependency that conflicted with google's oauth dependencies and gave me the following error:

java.lang.NoSuchMethodError: com.google.common.primitives.UnsignedInteger.asUnsigned(I)Lcom/google/common/primitives/UnsignedInteger;at com.google.api.client.util.Data.<clinit>(Data.java:81)at com.google.api.client.json.JsonParser.parseValue(JsonParser.java:502)at com.google.api.client.json.JsonParser.parse(JsonParser.java:289)

I upgraded to the latest version of the google oauth libraries (1.22.0), however some aspects of the api had changed and the proper usage wasn't exactly clear. After some digging around I finally I got it working

First the maven dependencies:

<dependency>
   <groupId>com.google.api-client</groupId>
   <artifactId>google-api-client</artifactId>
   <version>1.22.0</version>
</dependency>
<dependency>
   <groupId>com.google.apis</groupId>
   <artifactId>google-api-services-oauth2</artifactId>
   <version>v2-rev124-1.22.0</version>
</dependency>
<dependency>
   <groupId>com.google.oauth-client</groupId>
   <artifactId>google-oauth-client</artifactId>
   <version>1.22.0</version>
</dependency>
<dependency>
   <groupId>com.google.oauth-client</groupId>
   <artifactId>google-oauth-client-servlet</artifactId>
   <version>1.22.0</version>
   <exclusions>
      <exclusion>
         <groupId>javax.servlet</groupId>
         <artifactId>servlet-api</artifactId>
      </exclusion>
   </exclusions>
</dependency>
<dependency>
   <groupId>com.google.oauth-client</groupId>
   <artifactId>google-oauth-client-jetty</artifactId>
   <version>1.22.0</version>
   <exclusions>
      <exclusion>  <!-- I'm explicitly including jetty so don't need this -->         <groupId>org.mortbay.jetty</groupId>
         <artifactId>jetty</artifactId>
      </exclusion>
   </exclusions>
</dependency>

And my Jetty deps. Any other servlet container should work too

<dependency>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-server</artifactId>
   <version>8.1.8.v20121106</version>
</dependency>
<dependency>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-servlet</artifactId>
   <version>8.1.8.v20121106</version>
</dependency>
<dependency>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-util</artifactId>
   <version>8.1.8.v20121106</version>
</dependency>

The first time I started the app I got the following error:

java.lang.SecurityException: class "javax.servlet.FilterRegistration"'s signer information does not match signer information of other classes in the same package

The occurs because the servlet-api conflicted with the Jetty8 servlet api. To fix I added an exclusion to the  com.google.oauth-client dependency.

There are two servlets needed. One initiates the oauth process by redirecting to google, while the other receives the redirect back from google with auth tokens. Since they both share code I created a separate class to avoid duplicating code

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.api.client.extensions.servlet.auth.oauth2.AbstractAuthorizationCodeServlet;
import org.apache.log4j.Logger;

import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;


// some doc https://developers.google.com/api-client-library/java/google-api-java-client/oauth2public class Oauth2Servlet extends AbstractAuthorizationCodeServlet {

   private final Logger log = Logger.getLogger(this.getClass());
   
   public final static HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

   public final static JsonFactory JSON_FACTORY = new JacksonFactory();

   public final static List<String> SCOPES = Arrays.asList("https://www.googleapis.com/auth/userinfo.profile",
         "https://www.googleapis.com/auth/userinfo.email");

   @Override   protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws IOException {
      log.debug("Auth successful");
      response.sendRedirect(OauthCommon.SERVLET_CONTEXT + "/");
   }

   @Override   protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
      return OAUTH_COMMON.getRedirectUri(req);
   }

   @Override   protected AuthorizationCodeFlow initializeFlow() throws IOException {
      return OAUTH_COMMON.initializeFlow();
   }

   @Override   protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
      return OAUTH_COMMON.getUserId(req);
   }
}

Here's the callback servlet. When onSuccess is called we have a oauth token and can get make calls to oauth endpoints to get user info. In this case I've requested the userinfo and profile scopes so I can get basic user info simply by calling oauth2.userinfo().get().execute()

import java.io.IOException;
import java.util.Date;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.api.services.oauth2.model.Userinfoplus;
import org.apache.log4j.Logger;

import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.servlet.auth.oauth2.AbstractAuthorizationCodeCallbackServlet;


import com.google.api.services.oauth2.Oauth2;

public class Oauth2ServletCallback extends AbstractAuthorizationCodeCallbackServlet {

   private final Logger log = Logger.getLogger(this.getClass());

   @Override   protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential) throws ServletException, IOException {
      log.debug("Callback success:  token: " + credential.getAccessToken() + ", refresh token " + credential.getRefreshToken() + ", expires " + new Date(credential.getExpirationTimeMilliseconds()));

      Oauth2 oauth2 = new Oauth2.Builder(Oauth2Servlet.HTTP_TRANSPORT, Oauth2Servlet.JSON_FACTORY, credential).setApplicationName("").build();

      Userinfoplus userinfo = null;
      
      try {
         userinfo = oauth2.userinfo().get().execute();

         log.debug("User email is " + userinfo.getEmail() + ", name is " + userinfo.getName());
         
          req.getSession().setAttribute("AUTH", credential);
          req.getSession().setAttribute("USER", userinfo.getName());
          req.getSession().setAttribute("EMAIL", userinfo.getEmail());

          // redirect back to our app          resp.sendRedirect(OauthCommon.SERVLET_CONTEXT + "/");
          return;
      } catch (Exception e) {
         log.warn("Unable to get userinfo", e);
      }
   }

   @Override   protected void onError(HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse) throws ServletException, IOException {
      throw new RuntimeException("grrrrrrrrrr: " + errorResponse);
   }

   @Override   protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
      return OAUTH_COMMON.getRedirectUri(req);
   }

   @Override   protected AuthorizationCodeFlow initializeFlow() throws IOException {
      return OAUTH_COMMON.initializeFlow();
   }

   @Override   protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
      return OAUTH_COMMON.getUserId(req);
   }
}

And lastly here's the shared code

import java.io.IOException;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.MemoryDataStoreFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Userinfoplus;
import org.apache.log4j.Logger;

public class OauthCommon {

   private final Logger log = Logger.getLogger(this.getClass());
public final static OauthCommon OAUTH_COMMON = new OauthCommon(); public final static String SERVLET_CONTEXT = "/app"; public final static String OAUTH_CALLBACK_PATH = "/oauthcallback"; public final static String OAUTH_PATH = "/oauth"; public final static String CALL_BACK_URI = SERVLET_CONTEXT + OAUTH_CALLBACK_PATH; public final static String OAUTH_URI = SERVLET_CONTEXT + OAUTH_PATH; protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException { GenericUrl url = new GenericUrl(req.getRequestURL().toString()); url.setRawPath(CALL_BACK_URI); return url.build(); } public AuthorizationCodeFlow initializeFlow() throws IOException { return new GoogleAuthorizationCodeFlow.Builder( new NetHttpTransport(), JacksonFactory.getDefaultInstance(), "YOUR CLIENT ID GOES HERE", "YOUR SECRET GOES HERE", Oauth2Servlet.SCOPES ).setDataStoreFactory(new MemoryDataStoreFactory()).setAccessType("offline").build(); } public String getUserId(HttpServletRequest req) throws ServletException, IOException { String id = req.getSession().getId(); log.debug("Session id is " + id); return id; } }

If you acquired a auth token by another means and wanted to use the google oauth library to retrieve data, you could do like so

      GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
      Oauth2 oauth2 = new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential).setApplicationName(
            "Oauth2").build();
      Userinfoplus userinfo = oauth2.userinfo().get().execute();

I'm using embedded Jetty with a self-signed cert and configuring the servlets as follows:

SslContextFactory sslContextFactory = new SslContextFactory("../keystore");
sslContextFactory.setKeyStorePassword("supersecret");
SslSelectChannelConnector selectChannelConnector = new SslSelectChannelConnector(sslContextFactory);
selectChannelConnector.setPort(8443);
server.setConnectors(new Connector[] { selectChannelConnector });

ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath(OauthCommon.SERVLET_CONTEXT);
server.setHandler(context);

context.addServlet(new ServletHolder(new MyAppServlet()),"/");
context.addServlet(new ServletHolder(new Oauth2Servlet()), OauthCommon.OAUTH_PATH);
context.addServlet(new ServletHolder(new Oauth2ServletCallback()), OauthCommon.OAUTH_CALLBACK_PATH);
context.addServlet(new ServletHolder(new LogoutServlet()), "/logout");

log.debug("Started Jetty");

server.start();
server.join();







No comments:

Post a Comment