OAuth 2.0 server-to-server authorization flow between a Java application and a Google service

Published: May 01, 2018  •  java

When your application wants to access a Google service, it first needs to get authorization with OAuth 2.0. OAuth 2.0 is often used so that a user gives a permission to an application and the application then accesses a service on behalf of this user. But you can also use OAuth on the server side when a server application needs to access a service without any user interaction. In this scenario only two parties are involved, your server application and the Google service, therefore you often see the term "two-legged OAuth" for this kind of authorization flow. The related term "three-legged OAuth" refers to the scenario in which your application calls a Google service on behalf of a user and in which the user has to give a permission to the application.

In this blog post we will take a closer look at how the two-legged OAuth authorization flow works with a Java application that wants to access the Google Cloud Translation API. The following code is not something you have to write yourself when you develop a Java application because Google provides API Client Libraries for most of their services. The reason for this blog post is that I was curious about how Google implemented the server-to-server OAuth 2.0 authorization flow.

The authorization flow only requires one POST HTTP request to the OAuth endpoint. The body of the request carries a JSON Web Token (JWT). When the request is valid, the Google server sends back an access token that is valid for one hour. The application sends this access token with each request in the Authorization HTTP header to a Google service. After one hour, when the token expires, the application needs to request a new one. This request is built the same way as the initial token request, there is no special token refresh workflow.

flow

This workflow is comparable to a user login request with username and password that sends back a session cookie. The browser sends the cookie with each subsequence request to the server. Both, the session cookie and the access token, are sent in the HTTP header to the server.


Service Account

Before we start coding, we need to create a service account. This is an account that belongs to your application instead of to an individual user. The application calls the Google service on behalf of this service account. To get a service account you have to open the Google API Console and login with your Google account. If you don't already have an account, you need to create one first (https://accounts.google.com/sigNup).

First create a new project or select an existing project. Click on the menu "Library" and search for the Translation API, select the API and enable it. The Google Cloud Translation API is not a free service and you need to enable billing for this service. The console will ask to enable billing if it is not already enabled for this project.

step1
step2
step3
step4

As of the time of writing this blog post (May 2018) you have to pay 20 USD for 1,000,000 characters (including whitespace characters) you sent for translation. Prices are 'pro rata'. If you send 75,000 characters within a month, you are charged $1.50.
Checkout the pricing page for the current price and condition: https://cloud.google.com/translate/pricing

Next click on the "Credentials" menu and create a new Service account key.
When you click on "Create", the browser downloads the service account JSON file.
Copy this file to a safe location and don't commit it into a public repository, because everybody that has access to this file can call the service.

step5
step6
step7


Java

Now with everything set up we can start writing the Java application. I use Maven as build system and add these libraries as dependency to the project.

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.5</version>
    </dependency>

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.10.0</version>
    </dependency>

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>logging-interceptor</artifactId>
      <version>3.10.0</version>
    </dependency>

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.0</version>
    </dependency>

pom.xml

OkHttp is a HTTP client, jjwt simplifies the JSON Web Token creation, that we need for the OAuth 2.0 flow, and we use Jackson for JSON deserialization and serialization.

First we need to read the service account JSON file. For that I created a POJO (Credentials.java), then use a Jackson ObjectMapper instance to read the file and convert it into the POJO.

    Path credentialPath = Paths.get("./translationdemo-5674749bc948.json");
    ObjectMapper om = new ObjectMapper();

    Credentials credentials = om.readValue(Files.readAllBytes(credentialPath),
        Credentials.class);

TwoLegged.java

The service account file contains three fields that we need for our application:

First we extract the private key string and convert it into a java.security.interfaces.RSAPrivateKey instance. The private key string from the service account JSON file is stored in the PKCS #8 syntax. To use it in our application we have to remove the begin and end comment and the new line characters and then convert the base64 encoded string into a byte array. With a RSA KeyFactory we generate the private key instance.

    String privateKey = credentials.getPrivateKey();
    privateKey = privateKey.replace("\n", "");
    privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", "");
    privateKey = privateKey.replace("-----END PRIVATE KEY-----", "");

    byte[] decoded = Base64.getDecoder().decode(privateKey);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
    RSAPrivateKey privKey = (RSAPrivateKey) kf.generatePrivate(keySpec);

TwoLegged.java

Next we create the JWT. Google requires the following claims to be present in the JWT claim set. The order does not matter.

    String compactedJWT = Jwts.builder().setIssuer(credentials.getClientEmail())
        .setAudience(credentials.getTokenUri())
        .claim("scope", "https://www.googleapis.com/auth/cloud-translation")
        .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
        .setIssuedAt(Date.from(Instant.now())).signWith(SignatureAlgorithm.RS256, privKey)
        .compact();

TwoLegged.java

After setting all the claims, the JWT needs to be signed with the RSA SHA-256 algorithm and the private key we created in the previous step. In my tests the Google OAuth endpoint ignores the expiration time and always returns a token that is valid for one hour, even when setting the expiration time to just a couple of minutes from now.

The JWT is a string that looks like this:

eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0cmFuc2xhdGlvbmRlbW9AdHJhbnNsYXRpb25kZW1vLTE3NjgwNS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9jbG91ZC10cmFuc2xhdGlvbiIsImV4cCI6MTUyNTE5NjE5OCwiaWF0IjoxNTI1MTkyNTk4fQ.HuAKLLK17hmdN41l6g3yehEwoV7pVZsPgGicK20YDnmMHf_vCBZscoDRpIa38uZaPEIg8pcRSXtHDbTnbsE5sP8a2ug7LlOFAD_zwjlxpUPrFyAXL9TQLPFKQ26KAUfOYmGEQlfXDZfk2mg400BpJXGNk77HittfsPEnA-swQo-QTrEHB8pUwaA8voY-VKDQxlnWTXYjTYIyLWU0VV5pDHxP7uYZkkA5tguql2tZb_uFIl2zQx1UQcy39Z-djMPrTeQYa4lqStFKuy9YuqrMByb4FAWo3tOkWxwVf5h6w7h_FnjQjlQ_e2dK4aoQivbIsJ3fJ-lyqQ_4LKUp4aKQ3w

A JWT consist of three parts separated by a dot: Header, payload and signature.
A JWT is not encrypted, you can copy and paste it into a service like https://jwt.io/ and see the header and payload in clear text. But the JWT is signed with our private key. Google on the other side has our public key and is able to verify the signature of the JWT.

Header:

{
  "alg": "RS256"
}

Payload:

{
  "iss": "translationdemo@translationdemo-176805.iam.gserviceaccount.com",
  "aud": "https://accounts.google.com/o/oauth2/token",
  "scope": "https://www.googleapis.com/auth/cloud-translation",
  "exp": 1525196198,
  "iat": 1525192598
}

After generating the JWT, the application sends it to the URL specified in the token_uri field of the service account file to request the access token. This is a HTTP POST (over TLS) request and the body is URL encoded and contains the following two required parameters.

    RequestBody formBody = new FormBody.Builder()
        .add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
        .add("assertion", compactedJWT).build();
    Request request = new Request.Builder().url(credentials.getTokenUri()).post(formBody)
        .build();

TwoLegged.java

POST https://accounts.google.com/o/oauth2/token
Content-Type: application/x-www-form-urlencoded
Content-Length: 729

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0cmFuc2xhdGlvbmRlbW9AdHJhbnNsYXRpb25kZW1vLTE3NjgwNS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9jbG91ZC10cmFuc2xhdGlvbiIsImV4cCI6MTUyNTE5NjE5OCwiaWF0IjoxNTI1MTkyNTk4fQ.HuAKLLK17hmdN41l6g3yehEwoV7pVZsPgGicK20YDnmMHf_vCBZscoDRpIa38uZaPEIg8pcRSXtHDbTnbsE5sP8a2ug7LlOFAD_zwjlxpUPrFyAXL9TQLPFKQ26KAUfOYmGEQlfXDZfk2mg400BpJXGNk77HittfsPEnA-swQo-QTrEHB8pUwaA8voY-VKDQxlnWTXYjTYIyLWU0VV5pDHxP7uYZkkA5tguql2tZb_uFIl2zQx1UQcy39Z-djMPrTeQYa4lqStFKuy9YuqrMByb4FAWo3tOkWxwVf5h6w7h_FnjQjlQ_e2dK4aoQivbIsJ3fJ-lyqQ_4LKUp4aKQ3w

Google sends the following response back if the JWT is valid

{
  "access_token" : "ya29.c.ElqtBfvPoWakp5RvcGK71bqbl_KcPPyNXRa45xPmlMvqA7Y-x7g33I310mvXvKIkhuw-Uo5hIYzaqEr9rR5k0yF5NzcVrvuOd7yaBK49Flr1V_Ve4Zw17iWdHT0",
  "expires_in" : 3600,
  "token_type" : "Bearer"
}

I use a POJO (TokenResponse.java) and the Jackson ObjectMapper instance to convert it.

    byte[] response = sendRequest(client, request);
    TokenResponse tokenResponse = om.readValue(response, TokenResponse.class);
    System.out.println(tokenResponse);

TwoLegged.java

We are only interested in the access_token field, which contains the current valid access token. Our application has to send this token in the Authorization HTTP header in each request it sends to the Google Cloud Translation API. The token itself needs to be prepended with the word Bearer

    MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    String translationUrl = "https://translation.googleapis.com/language/translate/v2";
    Map<String, String> tr = new HashMap<>();
    tr.put("q", "Hello world");
    tr.put("target", "fr");
    tr.put("source", "en");

    RequestBody requestBody = RequestBody.create(JSON, om.writeValueAsBytes(tr));
    request = new Request.Builder().url(translationUrl)
        .header("Authorization", "Bearer " + tokenResponse.getAccessToken())
        .post(requestBody).build();

    response = sendRequest(client, request);
    JsonNode obj = om.readValue(response, JsonNode.class);

    String translatedText = null;
    JsonNode data = obj.get("data");
    if (data != null) {
      JsonNode translations = data.get("translations");
      if (translations != null && translations.isArray() && translations.size() > 0) {
        translatedText = translations.get(0).get("translatedText").asText();
      }
    }

    System.out.println(translatedText);

TwoLegged.java

The POST request to the Translation API looks like this. You see the access token in the Authorization header.

POST https://translation.googleapis.com/language/translate/v2
Content-Type: application/json; charset=utf-8
Content-Length: 47
Authorization: Bearer ya29.c.ElqtBfvPoWakp5RvcGK71bqbl_KcPPyNXRa45xPmlMvqA7Y-x7g33I310mvXvKIkhuw-Uo5hIYzaqEr9rR5k0yF5NzcVrvuOd7yaBK49Flr1V_Ve4Zw17iWdHT0

{"q":"Hello world","source":"en","target":"fr"}

As mentioned before the access token is only valid for 1 hour. After the token expires you have to access a new token with the same workflow as presented above. There is no special refresh token flow.

When you try to access a service with an expired token, you get an 401 Unauthorized response back from the service with a JSON response similar to this.

   {
   	"error": {
   		"code": 401,
   		"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
   		"errors": [{
   			"message": "Request had invalid authentication credentials.",
   			"reason": "authError"
   		}],
   		"status": "UNAUTHENTICATED"
   	}
   }

Client Library

As mentioned at the beginning of the blog post, you don't have to write all this code to access a Google service, because Google provides Java API Client Libraries that simplify the access to their services.

A client libraries has many advantages over a self built solution. The library ...

For accessing the Google Cloud Translation API we only have to add the google-cloud-translate library to our pom.xml

    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-translate</artifactId>
      <version>1.28.0</version>
    </dependency>

pom.xml

To get the same result as with our self built solution we only need the following code. The OAuth authorization flow is managed by the library under the hood, we only have to specify the path to the service account JSON file.

    Path credentialPath = Paths.get("./translationdemo-5674749bc948.json");
    ServiceAccountCredentials credentials = ServiceAccountCredentials
        .fromStream(Files.newInputStream(credentialPath));

    Translate translate = TranslateOptions.newBuilder().setCredentials(credentials)
        .build().getService();

    Translation translation = translate.translate("Hello World",
        TranslateOption.sourceLanguage("en"), TranslateOption.targetLanguage("fr"));

    System.out.println(translation.getTranslatedText());

GoogleLib.java


You find the code, presented in this blog post, on GitHub: https://github.com/ralscha/blog/tree/master/twolegged