Search code examples
javaoauthjwtsignature

How can I compute OAuth's signature for service account?


I want to get a service account access token for Google APIs by using HTTP/REST method, but OAuth sends me an error on the signature. I'm using Java.

I received the following JSON string:

{
    "error": "invalid_grant",
    "error_description": "Invalid JWT Signature."
}

As I understand, the documentation, I need to compose a JWT (JSON Web Token) with a header, a claim set, and a signature. I managed to get a correct header and claim set but apparently, my signature computing is incorrect.

My imports:

import java.security.KeyFactory;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.PrivateKey;

Here is the header computing:

String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
byte[] bytes = header.getBytes();
header = Base64.getEncoder().encodeToString(bytes);

My claim set :

long time_milis = System.currentTimeMillis()/1000L;
String claim_set = "{\n"
     + "  \"iss\":\"something@something.iam.gserviceaccount.com\",\n"
     + "  \"scope\":\"https://www.googleapis.com/auth/devstorage.read_write\",\n"
     + "  \"aud\":\"https://www.googleapis.com/oauth2/v4/token\",\n"
     + "  \"exp\":" + (time_milis + 300) + ",\n"
     + "  \"iat\":" + time_milis + "\n"
     + "}";
bytes = claim_set.getBytes();
claim_set = Base64.getEncoder().encodeToString(bytes);

Be prepared for my signature attempt! (for evident security reasons I masked the private key):

String signature = header + "." + claim_set;
String privateKeyString = "-----BEGIN PRIVATE KEY-----\nturlututu123\n-----END PRIVATE KEY-----\n";
privateKeyString = private_key.replaceAll("-----END PRIVATE KEY-----", "").replaceAll("-----BEGIN PRIVATE KEY-----", "").replaceAll("\n", "");

Signature privateSignature = Signature.getInstance("SHA256withRSA");

KeyFactory keyFactory=KeyFactory.getInstance("RSA");

PrivateKey privateKey = keyFactory.generatePrivate(
    new PKCS8EncodedKeySpec(
        Base64.getDecoder().decode(privateKeyString)
    )
);

privateSignature.initSign(privateKey);

privateSignature.update(signature.getBytes("UTF-8"));
bytes = privateSignature.sign();
signature = Base64.getEncoder().encodeToString(bytes);

I send my request at https://www.googleapis.com/oauth2/v4/token in POST mode with the following header :

"Host: www.googleapis.com
Content-Type: application/x-www-form-urlencoded"

and in the body :

"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + header + "." + claim_set + "." + signature

I'm really stuck and have no idea how to go forward.


Solution

  • Finally, I found the answer! It was an encoding problem. In fact, when I encode my strings, I called the Base64.getEncoder() encoder instead of the Base64.getUrlEncoder() encoder. Now we can change all the scripts.

    Here is the header computing:

    String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
    byte[] bytes = header.getBytes();
    header = Base64.getUrlEncoder().encodeToString(bytes);
    

    My claim set :

    long time_milis = System.currentTimeMillis()/1000L;
    String claim_set = "{\n"
         + "  \"iss\":\"something@something.iam.gserviceaccount.com\",\n"
         + "  \"scope\":\"https://www.googleapis.com/auth/devstorage.read_write\",\n"
         + "  \"aud\":\"https://www.googleapis.com/oauth2/v4/token\",\n"
         + "  \"exp\":" + (time_milis + 300) + ",\n"
         + "  \"iat\":" + time_milis + "\n"
         + "}";
    bytes = claim_set.getBytes();
    claim_set = Base64.getUrlEncoder().encodeToString(bytes);
    

    And the signature (for evident security reasons I masked the private key):

    String signature = header + "." + claim_set;
    String privateKeyString = "-----BEGIN PRIVATE KEY-----\nturlututu123\n-----END PRIVATE KEY-----\n";
    privateKeyString = private_key.replaceAll("-----END PRIVATE KEY-----", "").replaceAll("-----BEGIN PRIVATE KEY-----", "").replaceAll("\n", "");
    
    Signature privateSignature = Signature.getInstance("SHA256withRSA");
    
    KeyFactory keyFactory=KeyFactory.getInstance("RSA");
    
    PrivateKey privateKey = keyFactory.generatePrivate(
        new PKCS8EncodedKeySpec(
            Base64.getDecoder().decode(privateKeyString)
        )
    );
    
    privateSignature.initSign(privateKey);
    
    privateSignature.update(signature.getBytes("UTF-8"));
    bytes = privateSignature.sign();
    signature = Base64.getUrlEncoder().encodeToString(bytes);
    

    Thanx to this post which helps me during the debug.