Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Last active February 2, 2022 19:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DinoChiesa/d07545ad65c5efedcd1b2bd892e7313f to your computer and use it in GitHub Desktop.
Save DinoChiesa/d07545ad65c5efedcd1b2bd892e7313f to your computer and use it in GitHub Desktop.
// GenerateGcpAccessTokenForServiceAccount.java
// ------------------------------------------------------------------
//
// Requires Java 11 or later
//
// compile: javac -cp
// lib/nimbus-jose-jwt-8.22.jar:lib/json-smart-1.3.2.jar:lib/bcprov-jdk15on-1.64.jar:lib/bcpkix-jdk15on-1.64.jar %f
//
// run: java -classpath
// lib/nimbus-jose-jwt-8.22.jar:lib/json-smart-1.3.2.jar:lib/bcprov-jdk15on-1.64.jar:lib/bcpkix-jdk15on-1.64.jar %n
//
// Author: Dino
// Last saved: <2022-February-02 11:55:13>
// ------------------------------------------------------------------
//
// Copyright (c) 2022 Google LLC
// All rights reserved.
//
// ------------------------------------------------------------------
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.Security;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.stream.Collectors;
import net.minidev.json.JSONObject;
import net.minidev.json.JSONValue;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
public class GenerateGcpAccessTokenForServiceAccount {
private final String optString = "k:"; // getopt style
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String CLOUD_PLATFORM = "https://www.googleapis.com/auth/cloud-platform";
public GenerateGcpAccessTokenForServiceAccount(String[] args) throws java.lang.Exception {
getOpts(args, optString);
}
private Hashtable<String, Object> options = new Hashtable<String, Object>();
private void getOpts(String[] args, String optString) throws java.lang.Exception {
// Parse command line args for args in the following format:
// -a value -b value2 ... ...
final String argPrefix = "-";
String patternString = "^" + argPrefix + "([" + optString.replaceAll(":", "") + "])";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(patternString);
int L = args.length;
for (int i = 0; i < L; i++) {
String arg = args[i];
java.util.regex.Matcher m = p.matcher(arg);
if (!m.matches()) {
throw new java.lang.Exception(
"The command line arguments are improperly formed. Use a form like '-a value' or just '-b' .");
}
char ch = arg.charAt(1);
int pos = optString.indexOf(ch);
if ((pos != optString.length() - 1) && (optString.charAt(pos + 1) == ':')) {
if (i + 1 < L) {
i++;
Object current = this.options.get(m.group(1));
ArrayList<String> newList;
if (current == null) {
// not a previously-seen option
this.options.put(m.group(1), args[i]);
} else {
throw new java.lang.Exception("duplicated argument.");
}
} else {
throw new java.lang.Exception("Incorrect arguments.");
}
} else {
// a "no-value" argument, like -v for verbose
options.put(m.group(1), (Boolean) true);
}
}
}
public static class KeyParseException extends Exception {
private static final long serialVersionUID = 0L;
KeyParseException(String message) {
super(message);
}
KeyParseException(String message, Throwable th) {
super(message, th);
}
}
public static PrivateKey decodePrivateKey(String privateKeyString) throws KeyParseException {
try {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PEMParser pemParser = new PEMParser(new StringReader(privateKeyString));
Object object = pemParser.readObject();
if (object == null) {
throw new KeyParseException("unable to read anything when decoding private key");
}
if (object instanceof PrivateKeyInfo) {
// produced by openssl genpkey without encryption
return (PrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
}
throw new KeyParseException("unknown object type when decoding private key");
} catch (KeyParseException exc0) {
throw exc0;
} catch (Exception exc1) {
throw new KeyParseException("cannot instantiate private key", exc1);
}
}
public static Date getExpiryDate() {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.SECOND, 120);
return cal.getTime();
}
private static String readFileAsUtf8String(String path) throws IOException {
List<String> linelist = Files.readAllLines(Paths.get(path), StandardCharsets.UTF_8);
String fileContent = String.join("\n", linelist).trim();
return fileContent;
}
private String getAccessToken(String assertion) throws Exception {
String[][] parameters =
new String[][] {
new String[] {"assertion", assertion},
new String[] {"grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"}
};
String form =
Arrays.asList(parameters).stream()
.map(param -> param[0] + "=" + URLEncoder.encode(param[1], StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
HttpRequest request =
HttpRequest.newBuilder()
.uri(new URI("https://oauth2.googleapis.com/token"))
.headers("content-type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
public void run() throws Exception {
String keyfilePath = (String) this.options.get("k");
if (keyfilePath == null) {
throw new Exception("missing required argument (-k <keyfile>)");
}
if (!keyfilePath.endsWith(".json")) {
throw new Exception("keyfile should end in .json");
}
JSONObject jsonKey =
(JSONObject) JSONValue.parseWithException(readFileAsUtf8String(keyfilePath));
String privateKeyString = (String) jsonKey.get("private_key");
if (privateKeyString == null) {
throw new Exception("that does not look like a service account keyfile");
}
JWTClaimsSet.Builder claimsBuilder =
new JWTClaimsSet.Builder()
.issueTime(new Date())
.expirationTime(getExpiryDate())
.claim("scope", CLOUD_PLATFORM)
.issuer((String) jsonKey.get("client_email"))
.audience((String) jsonKey.get("token_uri"));
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsBuilder.build());
JWSSigner signer = new RSASSASigner(decodePrivateKey(privateKeyString));
signedJWT.sign(signer);
String jwt = signedJWT.serialize();
// System.out.println(jwt);
String tokenResponse = getAccessToken(jwt);
// System.out.println(tokenResponse);
JSONObject payload = (JSONObject) JSONValue.parseWithException(tokenResponse);
System.out.printf("\n");
System.out.printf(
"access_token: %s\n", ((String) (payload.get("access_token"))).replaceAll("\\.+$", ""));
System.out.printf("expires_in: %s\n", payload.get("expires_in"));
}
public static void usage() {
System.out.println(
"GenerateGcpAccessTokenForServiceAccount: generate an access token for GCP, given a service account key file.\n");
System.out.println("Usage:\n java GenerateGcpAccessTokenForServiceAccount -k <keyfile>");
}
public static void main(String[] args) {
try {
new GenerateGcpAccessTokenForServiceAccount(args).run();
} catch (java.lang.Exception exc1) {
System.out.println("Exception:" + exc1.toString());
usage();
exc1.printStackTrace();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment