Home | Send Feedback

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

Published: 1. May 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 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 and hear 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 permit the application.

In this blog post, we're going to take a closer look at how the two-legged OAuth authorization flow works with a Java application that accesses 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 then must send this access token with each request in the Authorization HTTP header to a Google service. After one hour, when the token expires, the application has 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 a 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 to the server in the HTTP header.

Service Account

Before we start coding, we need to create a service account. This is an account that belongs to your application instead of 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 log in 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 is going to 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.
Check out 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.16.1</version>
    </dependency>

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp-jvm</artifactId>
      <version>5.0.0-alpha.12</version>
    </dependency>

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>logging-interceptor</artifactId>
      <version>5.0.0-alpha.12</version>
    </dependency>

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</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("./demos-6ff86309d67f.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 an RSA KeyFactory, we generate a 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 cleartext. 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 an 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 a 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/sign-in#before_you_begin.",
      "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 access to their services.

A client library 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>2.32.0</version>
    </dependency>

pom.xml

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

    Path credentialPath = Paths.get("./demos-6ff86309d67f.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