Last active
February 2, 2022 19:55
-
-
Save DinoChiesa/d07545ad65c5efedcd1b2bd892e7313f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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