Friday, October 25, 2013

Simple Google Oauth2 with Jetty/Servlets

Oauth isn't very complicated but you might get a different impression by looking at Google's Java Oauth client and related libraries. I spent some time working with those libraries, thinking they must be doing something magical, but they're not and they're needlessly complex. I got steered towards Google's client by searching for java google oauth example, which at the time only returned results for the Google Java client. Here I present a simple, and hopefully easy to follow (for Java/Servlets developers) example of authenticating users with Google on a web application. All the necessary information to implement this code here can be found in Google's Oauth2 documentation, here and here.

First we need to obtain credentials for our application. Go to the api console and create a new application. Under "Registered apps", click "Register App", select "web application" and register. Now expand "OAuth 2.0 Client ID" and enter a web origin and redirect page:
WEB ORIGIN
http://localhost:8089
REDIRECT URI
http://localhost:8089/callback

The origin and redirect url must match the address of the web application. I'll use localhost to keep it simple.

Click generate and the client id and secret will update:
CLIENT ID
904170821502.apps.googleusercontent.com
CLIENT SECRET
fFDwuKdd0Eqc5AE6dsTRLyen


You may also want to go to the Console Screen and update the Product Name. This is the name of your app that users see when they are redirected to Google.

The basic Oauth workflow is:
  • Redirect the user to Google, specifying permissions you are requesting and other parameters in the querystring. The user will be asked to sign-in to Google and authorize access to your application.
  • If the user accepts, Google redirects back to your application and provides a code parameter in the querystring which can be exchanged for a access_token via a web service call to Google. The access_token can be used to access Google APIs, on behalf of the user.

    
{
        "access_token": "ya29.AHES6ZQS-BsKiPxdU_iKChTsaGCYZGcuqhm_A5bef8ksNoU",
        "token_type": "Bearer",
        "expires_in": 3600,
        "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5ZmE5NmFjZWNkOGQyZWRjZmFiMjk0NDRhOTgyN2UwZmFiODlhYTYifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiZW1haWwiOiJhbmRyZXcucmFwcEBnbWFpbC5jb20iLCJhdWQiOiI1MDgxNzA4MjE1MDIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdF9oYXNoIjoieUpVTFp3UjVDX2ZmWmozWkNublJvZyIsInN1YiI6IjExODM4NTYyMDEzNDczMjQzMTYzOSIsImF6cCI6IjUwODE3MDgyMTUwMi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTM4Mjc0MjAzNSwiZXhwIjoxMzgyNzQ1OTM1fQ.Va3kePMh1FlhT1QBdLGgjuaiI3pM9xv9zWGMA9cbbzdr6Tkdy9E-8kHqrFg7cRiQkKt4OKp3M9H60Acw_H15sV6MiOah4vhJcxt0l4-08-A84inI4rsnFn5hp8b-dJKVyxw1Dj1tocgwnYI03czUV3cVqt9wptG34vTEcV3dsU8",
        "refresh_token": "1/Hc1oTSLuw7NMc3qSQMTNqN6MlmgVafc78IZaGhwYS-o"
}
  • Now you can make web service calls to any authorized API. The access_token is only valid for an hour. After that you the user would need to re-authorize your application; however with offline access, you can request a new token, via a refresh_token. In this example I request offline access so a refresh token is provided.
In eclipse I start a Jetty server on 8089 with two endpoints: /signin and /callback. The /signin endpoint will start the Oauth process by redirecting to Google. If authorization is granted, Google will redirect back to /callback with a code parameter.

The code starts a Jetty server on port 8089. Replace clientId and clientSecret with your app's values. Start the application and go to localhost:8089/signin

If everything goes well, you'll see the the user's email, id and whether they are verified, in JSON format.

Aside from Jetty, this code uses just a few libraries to simplify things a bit: httpclient (get/post requests), json-simple (json parsing) and guava (concise maps).

package com.googleoauth;

package com.googleoauth;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import com.google.common.collect.ImmutableMap;


public class GoogleOauthServer {

 private Server server = new Server(8089);

 private final String clientId = "428385348633.apps.googleusercontent.com";
 private final String clientSecret = "zJpDtrqk7is9OwjDNWi5CzOK";
 
 public static void main(String[] args) throws Exception {
  new GoogleOauthServer().startJetty();
 }
 
 public void startJetty() throws Exception {

        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        server.setHandler(context);
 
        // map servlets to endpoints
        context.addServlet(new ServletHolder(new SigninServlet()),"/signin");        
        context.addServlet(new ServletHolder(new CallbackServlet()),"/callback");        
        
        server.start();
        server.join();
 }

 class SigninServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
   
   // redirect to google for authorization
   StringBuilder oauthUrl = new StringBuilder().append("https://accounts.google.com/o/oauth2/auth")
   .append("?client_id=").append(clientId) // the client id from the api console registration
   .append("&response_type=code")
   .append("&scope=openid%20email") // scope is the api permissions we are requesting
   .append("&redirect_uri=http://localhost:8089/callback") // the servlet that google redirects to after authorization
   .append("&state=this_can_be_anything_to_help_correlate_the_response%3Dlike_session_id")
   .append("&access_type=offline") // here we are asking to access to user's data while they are not signed in
   .append("&approval_prompt=force"); // this requires them to verify which account to use, if they are already signed in
   
   resp.sendRedirect(oauthUrl.toString());
  } 
 }
 
 class CallbackServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
   // google redirects with
   //http://localhost:8089/callback?state=this_can_be_anything_to_help_correlate_the_response%3Dlike_session_id&code=4/ygE-kCdJ_pgwb1mKZq3uaTEWLUBd.slJWq1jM9mcUEnp6UAPFm0F2NQjrgwI&authuser=0&prompt=consent&session_state=a3d1eb134189705e9acf2f573325e6f30dd30ee4..d62c
   
   // if the user denied access, we get back an error, ex
   // error=access_denied&state=session%3Dpotatoes
   
   if (req.getParameter("error") != null) {
    resp.getWriter().println(req.getParameter("error"));
    return;
   }
   
   // google returns a code that can be exchanged for a access token
   String code = req.getParameter("code");
   
   // get the access token by post to Google
   String body = post("https://accounts.google.com/o/oauth2/token", ImmutableMap.<String,String>builder()
     .put("code", code)
     .put("client_id", clientId)
     .put("client_secret", clientSecret)
     .put("redirect_uri", "http://localhost:8089/callback")
     .put("grant_type", "authorization_code").build());

   // ex. returns
//   {
//       "access_token": "ya29.AHES6ZQS-BsKiPxdU_iKChTsaGCYZGcuqhm_A5bef8ksNoU",
//       "token_type": "Bearer",
//       "expires_in": 3600,
//       "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5ZmE5NmFjZWNkOGQyZWRjZmFiMjk0NDRhOTgyN2UwZmFiODlhYTYifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiZW1haWwiOiJhbmRyZXcucmFwcEBnbWFpbC5jb20iLCJhdWQiOiI1MDgxNzA4MjE1MDIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdF9oYXNoIjoieUpVTFp3UjVDX2ZmWmozWkNublJvZyIsInN1YiI6IjExODM4NTYyMDEzNDczMjQzMTYzOSIsImF6cCI6IjUwODE3MDgyMTUwMi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTM4Mjc0MjAzNSwiZXhwIjoxMzgyNzQ1OTM1fQ.Va3kePMh1FlhT1QBdLGgjuaiI3pM9xv9zWGMA9cbbzdr6Tkdy9E-8kHqrFg7cRiQkKt4OKp3M9H60Acw_H15sV6MiOah4vhJcxt0l4-08-A84inI4rsnFn5hp8b-dJKVyxw1Dj1tocgwnYI03czUV3cVqt9wptG34vTEcV3dsU8",
//       "refresh_token": "1/Hc1oTSLuw7NMc3qSQMTNqN6MlmgVafc78IZaGhwYS-o"
//   }
   
   JSONObject jsonObject = null;
   
   // get the access token from json and request info from Google
   try {
    jsonObject = (JSONObject) new JSONParser().parse(body);
   } catch (ParseException e) {
    throw new RuntimeException("Unable to parse json " + body);
   }
   
   // google tokens expire after an hour, but since we requested offline access we can get a new token without user involvement via the refresh token
   String accessToken = (String) jsonObject.get("access_token");
     
   // you may want to store the access token in session
   req.getSession().setAttribute("access_token", accessToken);
   
   // get some info about the user with the access token
   String json = get(new StringBuilder("https://www.googleapis.com/oauth2/v1/userinfo?access_token=").append(accessToken).toString());
   
   // now we could store the email address in session
   
   // return the json of the user's basic info
   resp.getWriter().println(json);
  } 
 }
 
 // makes a GET request to url and returns body as a string
 public String get(String url) throws ClientProtocolException, IOException {
  return execute(new HttpGet(url));
 }
 
 // makes a POST request to url with form parameters and returns body as a string
 public String post(String url, Map<String,String> formParameters) throws ClientProtocolException, IOException { 
  HttpPost request = new HttpPost(url);
   
  List <NameValuePair> nvps = new ArrayList <NameValuePair>();
  
  for (String key : formParameters.keySet()) {
   nvps.add(new BasicNameValuePair(key, formParameters.get(key))); 
  }

  request.setEntity(new UrlEncodedFormEntity(nvps));
  
  return execute(request);
 }
 
 // makes request and checks response code for 200
 private String execute(HttpRequestBase request) throws ClientProtocolException, IOException {
  HttpClient httpClient = new DefaultHttpClient();
  HttpResponse response = httpClient.execute(request);
     
  HttpEntity entity = response.getEntity();
     String body = EntityUtils.toString(entity);

  if (response.getStatusLine().getStatusCode() != 200) {
   throw new RuntimeException("Expected 200 but got " + response.getStatusLine().getStatusCode() + ", with body " + body);
  }

     return body;
 }
}
Here the are maven dependencies
  <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>
  <dependency>
   <groupId>org.apache.httpcomponents</groupId>
   <artifactId>httpclient</artifactId>
   <version>4.2.1</version>
  </dependency>
  <dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>14.0.1</version>
  </dependency>
  <dependency>
   <groupId>com.googlecode.json-simple</groupId>
   <artifactId>json-simple</artifactId>
   <version>1.1.1</version>
  </dependency>      
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.16</version>
  </dependency>

All the code here can be found at github

A useful tool for learning/developing with Oauth is the the Google Oauth Playground

6 comments:

  1. This is very helpful. Thanks.

    One question, if I need to access users' google calendar using google calendar api with the oauth token I just get from your code, how do I initiate the calendar client? The current google examples uses the following logic.

    GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
    httpTransport, JSON_FACTORY, clientSecrets,
    Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(dataStoreFactory)
    .build();



    Credential credential=(new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user"));

    client = new com.google.api.services.calendar.Calendar.Builder(
    httpTransport, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build();

    see http://code.google.com/p/google-api-java-client/source/browse/calendar-cmdline-sample/src/main/java/com/google/api/services/samples/calendar/cmdline/CalendarSample.java?repo=samples


    How do I construct the Credential object from the access token I just get from

    String accessToken = (String) jsonObject.get("access_token");

    Your help is very much appreciated.

    ReplyDelete
    Replies
    1. As you may have inferred from my post, I'm not a fan of the Google oauth client libraries. Similar to how I requested user info with the access token:

      String json = get(new StringBuilder("https://www.googleapis.com/oauth2/v1/userinfo?access_token=").append(accessToken).toString());

      you would make a request to the Calendar api with the token, ex:

      https://www.googleapis.com/calendar/v3/users/me/calendarList/calendarId?access_token=your token

      See the calendar documentation for more info https://developers.google.com/google-apps/calendar/v3/reference/calendarList/get

      Delete
  2. Can you please write how to get a new access token with a refresh token?

    ReplyDelete
  3. Thanks a ton for this awesome post! Wild how too much abstraction can make something simple crazy hard.... when it's actually super simple... Kudos!

    ReplyDelete
  4. My conclusion was the same, the Google oath library is a jungle while the underlying protocol is in reality very simple. The helper libraries Google have done for the individual services are similar, a strange combination of abstracting the WS API but at the same time not doing it (api caller object with a "build()" call, then model objects which you *can't* use to lazy load other referred objects). Perhaps there is some design principle behind them all but if there is, I haven't found any description of it. And while I'm complaining, the different libraries vary a lot in quality (Blogger works ok but Freebase build() return null with no exception making it difficult to debug, and when you have to resort to reading the source of a library you might as well roll your own). Interesting that many one-man libraries out here are better than what Google produces.

    ReplyDelete