0
votes

I have created an app in the Google Developer's Console, then created OAuth2 credentials. I have a client_id and client_secret. Now, I want to use these to obtain an access token for two-legged calls into the Google Drive API. I am using Google's oauth2 client in java:

import com.google.api.client.auth.oauth2.ClientCredentialsTokenRequest;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.TokenResponse;
...
public void oauth2Test() {
   String clientId = "...";
   String clientSecret = "...";
   ClientCredentialsTokenRequest request = new ClientCredentialsTokenRequest(
         new NetHttpTransport(),
         new JacksonFactory(),
         new GenericUrl("https://accounts.google.com/o/oauth2/auth"));
   request.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret));
   TokenResponse response;
   try {
      response = request.execute();      
   } catch (Exception ex) {
      ex.printStackTrace();
   }
}

However, I get a "400 Bad Request" with message

"Required parameter is missing: response_type".

What is the correct way to obtain an access token in the two-legged request model? Note: I only have the client_id and client_secret, I do not have the full API token.

EDIT: My original question was imprecise. While I prefer to start only with client_id and client_secret, that is not necessary. It is OK to use google-specific APIs to obtain access tokens and it is OK to use GoogleCredential. What is necessary is that I am able to use whatever access token(s) are obtained from the authorization process in a generic REST call. In other words, given google app credentials, which can be {client_id,client_secret}, or a google service account key in either JSON or P12 format, how do I obtain access token(s) and how are they used in the REST API call -- do I set the Authorization header or something else?

The first answer points out that client_credential isn't supported, which I've verified. But I still need a path to get the bearer token, so that I can use it in REST calls without specific Google client API libraries. So I started with code that works, but uses the Google libraries. It requires a JSON credential file.

InputStream is = getClass().getResourceAsStream("JSONCredFile");
GoogleCredential credential = GoogleCredential.fromStream(is).createScoped(scopes);
Drive service = new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
        .setApplicationName("My app")
        .build();
FileList result = service.files().list().setPageSize(10)
        .setFields("nextPageToken, files(id, name)")
        .execute();

By hooking up an SSLSocket proxy to the credential (details omitted), I was able to trace the outbound communication:

POST /token HTTP/1.1
Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.23.0 (gzip)
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: oauth2.googleapis.com
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 771

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=<lots of encoded stuff>

The reply is a gzip-encoded bearer token, which is used in the API call:

GET /drive/v3/files?fields=nextPageToken,%20files(id,%20name)&pageSize=10 HTTP/1.1
Accept-Encoding: gzip
Authorization: Bearer ya29.c.Eln_BSgrx0afa85mdMstW5jzEvM5dotWpctSXl-DE1jeO2mmu1h0FErr_EZO05YnC-B1yz30IBwOyFXoWr_wwKxlZk08R6eZldNU-EAfrQ1yNftymn_Qqc_pfg

Clearly this is the JWT profile of oauth2. But now what? Somehow I need to get the bearer token without actually making the API call through the specific library. The Google OAuth2 libraries don't seem to support this request type, at least I don't see a "JWT" flavor of TokenRequest. I can cook up the OAuth2 call directly, or create a subclass of TokenRequest that supports JWT?

Any better ideas?

2

2 Answers

0
votes

Google doesn't support grant_type=client_credentials which is how you'd do 2LO with an OAuth client ID and secret.

You can use service accounts to do 2LO: https://developers.google.com/identity/protocols/OAuth2ServiceAccount

0
votes

OK, I finally figured out how to make the JWT, send the OAuth2 request, and extract the access token. For easier integration with the google OAuth2 client, I subclassed TokenRequest:

import com.google.api.client.auth.oauth2.TokenRequest;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Collection;
import java.util.stream.Collectors;
import org.joda.time.DateTime;

/**
 * @author jlilley
 */
public class JWTTokenRequest extends TokenRequest {
    private String serviceKeyJson;
    private boolean doRsa = true;
    public JWTTokenRequest(HttpTransport transport, JsonFactory jsonFactory, GenericUrl tokenServerUrl) {
        super(transport, jsonFactory, tokenServerUrl, "urn:ietf:params:oauth:grant-type:jwt-bearer");
    }
    @Override
    public JWTTokenRequest setTokenServerUrl(GenericUrl tokenServerUrl) {
        return (JWTTokenRequest)super.setTokenServerUrl(tokenServerUrl);
    }
    public JWTTokenRequest setServiceKey(String json) throws Exception {
        this.serviceKeyJson = json;
        return this;
    }
    public JWTTokenRequest setServiceKey(InputStream is) throws Exception {
        try (BufferedReader buffer = new BufferedReader(new InputStreamReader(is))) {
            return setServiceKey(buffer.lines().collect(Collectors.joining("\n")));
        }
    }
    @Override
    public JWTTokenRequest setScopes(Collection<String> scopes) {
        return (JWTTokenRequest) super.setScopes(scopes);
    }
    @Override
    public JWTTokenRequest set(String fieldName, Object value) {
        return (JWTTokenRequest) super.set(fieldName, value);
    }    
    /**
     * Create a JWT for the given project id, signed with the given RSA key.
     */
    private String signJwtRsa(JwtBuilder jwtBuilder, PKCS8EncodedKeySpec spec) {
        try {
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return jwtBuilder.signWith(SignatureAlgorithm.RS256, kf.generatePrivate(spec)).compact();
        } catch (Exception ex) {
            throw new RuntimeException("Error signing JWT", ex);
        }
    }

    /**
     * Create a JWT for the given project id, signed with the given ES key.
     */
    private String signJwtEs(JwtBuilder jwtBuilder, PKCS8EncodedKeySpec spec) {
        try {
            KeyFactory kf = KeyFactory.getInstance("EC");
            return jwtBuilder.signWith(SignatureAlgorithm.ES256, kf.generatePrivate(spec)).compact();
        } catch (Exception ex) {
            throw new RuntimeException("Error signing JWT", ex);
        }
    }

    /**
     * Finalize the JWT and set it in the assertion property of the web service call
     * @throws java.io.IOException
     */
    void makeAssertion() {
        JsonParser parser = new JsonParser();
        JsonObject jsonDoc = (JsonObject) parser.parse(serviceKeyJson);
        String pkStr = jsonDoc.get("private_key").getAsString()
                .replace("\n", "")
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "");
        byte[] pkBytes = Base64.getDecoder().decode(pkStr);
        DateTime now = new DateTime();
        JwtBuilder jwtBuilder = Jwts.builder()
                .setIssuedAt(now.toDate())
                .setExpiration(now.plusMinutes(60).toDate())
                .setAudience(getTokenServerUrl().toString())
                .setIssuer(jsonDoc.get("client_email").getAsString());
        if (getScopes() != null) {
            jwtBuilder = jwtBuilder.claim("scope", getScopes());
        }
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(pkBytes);
        String pkId = jsonDoc.get("private_key_id").getAsString();
        jwtBuilder.setHeaderParam("kid", pkId);
        jwtBuilder.setHeaderParam("typ", "JWT");
        set("assertion", doRsa ? signJwtRsa(jwtBuilder, spec) : signJwtEs(jwtBuilder, spec));
    }

    /**
     * Finalize the JWT, set it in the assertion property of the web service call, and make the token request.
     */
    @Override
    public TokenResponse execute() throws IOException {
        makeAssertion();
        return super.execute();
    }
}

Give this, I can set up the token request from the service account JSON key file, execute() it, and fetch the resulting access token. Note that token renewal responsibility is up to the caller.