Skip to content

Instantly share code, notes, and snippets.

@AvrumFeldman
Created March 21, 2025 16:34
Show Gist options
  • Save AvrumFeldman/ad2c4df00d4f666f65d4ec8b323c5cad to your computer and use it in GitHub Desktop.
Save AvrumFeldman/ad2c4df00d4f666f65d4ec8b323c5cad to your computer and use it in GitHub Desktop.
Google Client Credential OAuth flow
param(
[parameter(mandatory)]
$path, # Path to google service account key
[parameter(mandatory)]
$TargetUser
)
$serviceAccountJson = Get-Content -Path $path | ConvertFrom-Json
$ServiceAccount = $serviceAccountJson.client_email
$clientID = $serviceAccountJson.client_id
$keyID = $serviceAccountJson.private_key_id
$Issued = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$Expiration = $Issued+3600
$PrivateKeyPem = $serviceAccountJson.private_key
$JWTHeader = [ordered]@{
alg = "RS256"
typ = "JWT"
} | ConvertTo-Json -Compress
$JWTHeaderBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($JWTHeader)) -replace '\+', '-' -replace '/', '_' -replace '='
$JWT = [ordered]@{
iss = $ServiceAccount
scope = "https://www.googleapis.com/auth/gmail.send"
aud = "https://oauth2.googleapis.com/token"
exp = $Expiration
iat = $Issued
sub = $TargetUser
} | ConvertTo-Json -Compress
$JWTBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($JWT)) -replace '\+', '-' -replace '/', '_' -replace '='
$BytesToSign = [Text.Encoding]::UTF8.GetBytes("$JWTHeaderBase64.$JWTBase64")
$code = @"
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
public static class Pkcs8ToRsaProvider
{
// Imports a PEM encoded PKCS#8 private key to RSACryptoServiceProvider
public static RSACryptoServiceProvider ImportPkcs8PrivateKey(string pemKey)
{
// Extract the base64 data from PEM
var base64Data = ExtractBase64FromPEM(pemKey);
var keyData = Convert.FromBase64String(base64Data);
// Parse the PKCS#8 structure to extract RSA parameters
var rsaParams = ParsePkcs8ToRSAParameters(keyData);
// Create and initialize RSACryptoServiceProvider
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParams);
return rsa;
}
// Extract base64 content from PEM format
private static string ExtractBase64FromPEM(string pem)
{
string pattern = @"-----BEGIN PRIVATE KEY-----\s*([A-Za-z0-9+/=\s]+)\s*-----END PRIVATE KEY-----";
Match match = Regex.Match(pem, pattern);
if (match.Success)
{
return match.Groups[1].Value.Replace("\n", "").Replace("\r", "").Replace(" ", "");
}
throw new FormatException("The input does not appear to be a valid PEM-encoded PKCS#8 private key");
}
// Parse PKCS#8 ASN.1 structure to extract RSA parameters
private static RSAParameters ParsePkcs8ToRSAParameters(byte[] pkcs8Data)
{
// Skip PKCS#8 header to RSA key identifier
// PKCS#8 structure: PrivateKeyInfo
int offset = 0;
// Read outer SEQUENCE
if (pkcs8Data[offset++] != 0x30)
throw new CryptographicException("Invalid PKCS#8 data: Expected SEQUENCE");
// Skip length bytes
ReadLength(pkcs8Data, ref offset);
// Read VERSION (should be 0)
if (pkcs8Data[offset++] != 0x02)
throw new CryptographicException("Invalid PKCS#8 version tag");
int versionLength = ReadLength(pkcs8Data, ref offset);
if (versionLength != 1 || pkcs8Data[offset++] != 0x00)
throw new CryptographicException("Invalid PKCS#8 version value");
// Read AlgorithmIdentifier SEQUENCE
if (pkcs8Data[offset++] != 0x30)
throw new CryptographicException("Invalid PKCS#8 data: Expected AlgorithmIdentifier SEQUENCE");
// Get algorithm sequence length
int algorithmSeqLength = ReadLength(pkcs8Data, ref offset);
int algorithmEndOffset = offset + algorithmSeqLength;
// Read algorithm OID
if (pkcs8Data[offset++] != 0x06)
throw new CryptographicException("Invalid PKCS#8 data: Expected Algorithm OID");
int oidLength = ReadLength(pkcs8Data, ref offset);
offset += oidLength; // Skip over the OID bytes
// Skip any remaining algorithm parameters until the end of the sequence
offset = algorithmEndOffset;
// Read OCTET STRING that contains the key data
if (pkcs8Data[offset++] != 0x04)
throw new CryptographicException("Invalid PKCS#8 data: Expected OCTET STRING");
int keyDataLength = ReadLength(pkcs8Data, ref offset);
// The RSA private key is within this octet string, in PKCS#1 format
byte[] rsaKeyData = new byte[keyDataLength];
Array.Copy(pkcs8Data, offset, rsaKeyData, 0, keyDataLength);
// Now parse the RSA key structure (PKCS#1 format)
return ParseRsaPrivateKey(rsaKeyData);
}
// Parse RSA private key (PKCS#1 format)
private static RSAParameters ParseRsaPrivateKey(byte[] keyData)
{
RSAParameters parameters = new RSAParameters();
int offset = 0;
// Read SEQUENCE
if (keyData[offset++] != 0x30)
throw new CryptographicException("Invalid RSA private key: Expected SEQUENCE");
// Skip sequence length
ReadLength(keyData, ref offset);
// Read VERSION (should be 0)
if (keyData[offset++] != 0x02)
throw new CryptographicException("Invalid RSA private key version tag");
int versionLength = ReadLength(keyData, ref offset);
if (versionLength != 1 || keyData[offset++] != 0x00)
throw new CryptographicException("Invalid RSA private key version value");
// Read MODULUS (n)
parameters.Modulus = ReadIntegerUnsigned(keyData, ref offset);
// Read PUBLIC EXPONENT (e)
parameters.Exponent = ReadIntegerUnsigned(keyData, ref offset);
// Read PRIVATE EXPONENT (d)
parameters.D = ReadIntegerUnsigned(keyData, ref offset);
// Read PRIME1 (p)
parameters.P = ReadIntegerUnsigned(keyData, ref offset);
// Read PRIME2 (q)
parameters.Q = ReadIntegerUnsigned(keyData, ref offset);
// Read EXPONENT1 (dp)
parameters.DP = ReadIntegerUnsigned(keyData, ref offset);
// Read EXPONENT2 (dq)
parameters.DQ = ReadIntegerUnsigned(keyData, ref offset);
// Read COEFFICIENT (InverseQ)
parameters.InverseQ = ReadIntegerUnsigned(keyData, ref offset);
return parameters;
}
// Helper: Read ASN.1 length, handling multi-byte encodings
private static int ReadLength(byte[] data, ref int offset)
{
int length = data[offset++];
if ((length & 0x80) != 0)
{
int byteCount = length & 0x7F;
length = 0;
for (int i = 0; i < byteCount; i++)
{
length = (length << 8) | data[offset++];
}
}
return length;
}
// Helper: Read ASN.1 INTEGER and convert to unsigned
private static byte[] ReadIntegerUnsigned(byte[] data, ref int offset)
{
if (data[offset++] != 0x02)
throw new CryptographicException("Expected INTEGER");
int length = ReadLength(data, ref offset);
byte[] value = new byte[length];
Array.Copy(data, offset, value, 0, length);
offset += length;
// If the first byte is zero (for positive sign in DER), remove it
if (value.Length > 0 && value[0] == 0)
{
byte[] temp = new byte[value.Length - 1];
Array.Copy(value, 1, temp, 0, temp.Length);
value = temp;
}
return value;
}
// Helper function to print byte array content for debugging
public static string BytesToHex(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in bytes)
{
sb.AppendFormat("{0:X2} ", b);
}
return sb.ToString();
}
}
"@
add-type -TypeDefinition $code
$key = [Pkcs8ToRsaProvider]::ImportPkcs8PrivateKey($PrivateKeyPem)
$signature = $key.SignData($BytesToSign, "SHA256")
$SignatureBase64 = [Convert]::ToBase64String($signature).Replace('+','-').Replace('/','_').Replace('=','')
$jwt = "$JWTHeaderBase64.$JWTBase64.$SignatureBase64"
$TokenRequestBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$JWT"
$Token = Invoke-RestMethod -Method Post -Uri "https://oauth2.googleapis.com/token" -ContentType "application/x-www-form-urlencoded" -Body $TokenRequestBody
$Token
# Documentation - https://developers.google.com/identity/protocols/oauth2/service-account#httprest
@AvrumFeldman
Copy link
Author

This gist allows to generate a signed JWT with a CER encoded PKCS#8 private key all in native PowerShell 5.1 without any external dependencies, to be used for Google APIs with the OAuth client credentials flow.

Disclaimer: The C# code was written by Claude 3.7 Sonnet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment