Created
March 21, 2025 16:34
-
-
Save AvrumFeldman/ad2c4df00d4f666f65d4ec8b323c5cad to your computer and use it in GitHub Desktop.
Google Client Credential OAuth flow
This file contains hidden or 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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.