Reputation: 574
I'm following Google's documentation, on how to create a JWT for a specific service account.
The document instructs on how to manually create a JWT, as well as how to compute the signing process, based on JWS guidelines.
I followed the exact procedure described by Google, but no matter what i do, the generated JWT cannot be validated by jwt.io, which fails with a Invalid Signature.
Follow the guidelines, i created the service account, and a specific key, which contains the Private Key details:
{
"type": "service_account",
"project_id": "myProject",
"private_key_id": "1212121212",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEdyjMCbews1orr6Ze+LIPkV2WF4d\nHSAqRqJrERR1Gb9gxKC/WQhMvCotp7zFTqLcUI3eUhR3tIgpLwZFpIxXZjOTwWoB\n6bWxOe39Suft1GYAR0prFcLmXtfw43B+9gVcMLOHBBTcxojBXkQ2bhjp7dGqvlUz\n3nO/1bqbzvd5I6bQif+tjLEceyIUbE/rJ6PgW9SVtfktrQIQQ9VGtAUya4IYSEL8\nJaXZxs6jAgMBAAECggEAFe1+3J0OYZcQPZb2AjSi+1oTm6GmWSJ6ssNpu9x8pq+f\nxPSfbaUjRGhTsCOnNIlzhnDACRQIOYHSJTrJFbMc2b2XdBPyqgPfdPNTf/QNtHOK\nqUbSwj2Ho6sJdYJ+QbaGOGgO8uM2QL+uFM3RHvwUiT2SlWHsukny3ATFUAVIYPUj\nxr0m6QKBgQDiH6vL1plGsIFVWR5M\nESsZdADubhDOtml6r81aKLXJPK9LeHwJOAgTFfZHJD4D4e7KSQfYlbf9tRE7c2PE\ntcj6BVrHdtYRqaXY+Q7BW2mXRb7IJKtVxZzljPY0HcDjpZ7UqXUB/sVbxT/zbt4B\n9lIegpLJyd6RpzYhjIDv8OIaTwKBgQDRfMLsTg0+nTzmmIurmD1IhdPa7KvhGMDn\nXSs0zRR4IRC2BCn5LHYYD4cgO+mmGWxcQZREQ220W3uXwRbSTJZT6ZtzP40AXx86\nTRop5NBZYDkdJ1t9qhi2aU//5mwn5ubC/42fBCwqmRXr0nOtLhKtEONRmGGyl7hk\nWXWII2z1bQKBgAMoNArVhTBSeIvLgbvIJZTmZLjvenaYX2KiH7jZhqg3mRoyUuvA2glpo9ARzB7ut\nR5LXq5GAwOBIzMhtZWTyE53O9jI5+8g/RB7WlUx\nsZt5bkf42zhsJwZnfV480Hx8GhnCnhGcTVjJbbN5AoGBAInRfNcLpgPtHWiQ5r9W\nANd+XDLpjIUQfh+0NaQeYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----\n",
"client_email": "test@email",
"client_id": "121212",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/myProject-dev.iam.gserviceaccount.com"
}
I'm using .net 5, and the .net implementation for manually creating and signing a JWT, would be:
private string GetToken()
{
var header = "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"ed36c257c59ebabb47b456828a858aa5fcda12xx\"}";
var claims = "{\"sub\":\"10217931234509168826\",\"email\":\"[email protected]\",\"iss\":\"https:\\//accounts.google.com\",\"aud\":\"MyAudience",\"exp\":1665530643,\"iat\":1665527043}";
var b64header = Convert.ToBase64String(Encoding.UTF8.GetBytes(header))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
var b64claims = Convert.ToBase64String(Encoding.UTF8.GetBytes(claims))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
var payload = b64header + "." + b64claims;
var message = Encoding.UTF8.GetBytes(payload);
var sig = Convert.ToBase64String(SignData(message))
.Replace('+', '-').Replace('/', '_').Replace("=", "");
return payload + "." + sig;
}
private static byte[] SignData(byte[] message)
{
var privateKeyContent = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEdyjMCbews1orr6Ze+LIPkV2WF4d\nHSAqRqJrERR1Gb9gxKC/WQhMvCotp7zFTqLcUI3eUhR3tIgpLwZFpIxXZjOTwWoB\n6bWxOe39Suft1GYAR0prFcLmXtfw43B+9gVcMLOHBBTLm6QKBgQDiH6vL1plGsIFVWR5M\nESsZdADubhDOtml6r81aKLXJPK9LeHwJOAgTFfZHJD4D4e7KSQfYlbf9tRE7c2PE\ntcj6BVrHdtYRqaXY+Q7BW2mXRb7IJKtVxYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----";
var rsa = RSA.Create();
var privateKey = privateKeyPem.Replace("-----BEGIN PRIVATE KEY-----", string.Empty).Replace("-----END PRIVATE KEY-----", string.Empty);
privateKey = privateKey.Replace("\n", string.Empty);
privateKey = privateKey.Replace("\r\n", string.Empty);
var privateKeyBytes = Convert.FromBase64String(privateKey);
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out int _);
return rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
I don't like this manual approach, so i used the .net Cryptography classes to create and sign the JWT Token:
private string GetToken2()
{
// keeping only the payload of the key
var privateKeyPem = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Cg7FW0NGwLeD\nrpc0r1Ayta23GxVw0KCA+d/TjSyuZ3lmKiObz9EGJpHSHbX4yrODA6FvYixUrAKm\nUSSMvFLUpYM2xoEgAKnwd6XVgdnjwk7wnIIsEzlJR5t9tWKLd1VL1133w6jigLv5kDzWQTLAoGBAL0B\n7fS672RBBgOgOtRVhWV7qYvq4aE0bkfRXfxD1GYWnzc6RoyUuvA2glpo9ARzB7ut\nR5LXq5GAwOBIzMhtZWzMZv7ypctiB5DYo/SMiBc7pAxTyE53O9jI5+8g/RB7WlUx\nsZt5bkf42zhsJwZnfV480Hx8GhnCnhGcTVjJbbN5AoGBAInRfNcLpgPtHWiQ5r9W\nANd+XDLpjIUQfh+0NaQeYPG7DM59oPRqUDs/BSp21nTmSnNC537H0OHlCScpmc7G\ncpj+/jtLIhTN0IwKosaH3mJpQ3AcUI7IooFKgYrC/bwCUQ5xX7CwqaOzTKf3MtX1\nngd7mPWTFkRDxCkCnvfUfcem\n-----END PRIVATE KEY-----\n";
var privateKey = privateKeyPem.Replace("-----BEGIN PRIVATE KEY-----", string.Empty).Replace("-----END PRIVATE KEY-----", string.Empty);
privateKey = privateKey.Replace("\n", string.Empty);
privateKey = privateKey.Replace(Environment.NewLine, string.Empty);
var privateKeyRaw = Convert.FromBase64String(privateKey);
// creating the RSA key
using var provider = new RSACryptoServiceProvider();
provider.ImportPkcs8PrivateKey(new ReadOnlySpan<byte>(privateKeyRaw), out _);
var rsaSecurityKey = new RsaSecurityKey(provider);
// Generating the token
var now = DateTime.UtcNow;
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, "10217931234509168826"),
new Claim("email", "myProject-dev.iam.gserviceaccount.com"),
};
var handler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken
(
"https://accounts.google.com",
"MyAudience",
claims,
now.AddMilliseconds(-30),
now.AddMinutes(60),
new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)
);
return handler.WriteToken(token);
}
The Google documentation indicates how to sign the JWT:
Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google API Console.
Both the implementation strictly follow the google documentation guidelines, but the generated JWT also fails to validate. I tried several representations of the private key, i.e., replacing the '\n', leaving the '\n', etc, etc, but it always fail.
Both implementations seems correct, but something is missing!
Any ideas on what is missing ?? Thank you in advance.
--------------------- EDIT 1 ---------------------------
To guarantee that the Primary Key data is being correctly handled, and avoid string replacement and encoding, i used the BouncyCastle library which is a lightweight cryptography API, to handle the Primary Key. So i load the JSON KEY file directly, and use BouncyCastle to load the RSA Parameters:
private RSAParameters GetPrivateKeyRSAParameters()
{
var path = "c:\\myproject-key-3433434.json";
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize<JsonCredentialParameters>(stream);
RSAParameters rsaParams;
using (var tr = new StringReader(credentialParameters.PrivateKey))
{
var pemReader = new PemReader(tr);
if (pemReader.ReadObject() is not AsymmetricKeyParameter key)
{
throw new Exception("Could not read private key");
}
var privateRsaParams = key as RsaPrivateCrtKeyParameters;
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
}
return rsaParams;
}
This way, instead of relying in handling the Primary Key as text, everything is handled by BouncyCastle.
For code block 1)
var rsa = RSA.Create();
rsa.ImportParameters(GetPrivateKeyRSAParameters());
For code block 2)
var rsaSecurityKey = new RsaSecurityKey(GetPrivateKeyRSAParameters());
So now i have the guarantee that the Primary Key data is being correctly handled, but the end result is the same, the resulting token has always an "Invalid Signature".
--------------------- EDIT 2 ---------------------------
Google has an example on how to do this in JAVA, in this document.
public static String generateJwt(final String saKeyfile, final String saEmail,
final String audience, final int expiryLength)
throws FileNotFoundException, IOException {
Date now = new Date();
Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
JWTCreator.Builder token = JWT.create()
.withIssuedAt(now)
.withExpiresAt(expTime)
.withIssuer(saEmail)
.withAudience(audience)
.withSubject(saEmail)
.withClaim("email", saEmail);
FileInputStream stream = new FileInputStream(saKeyfile);
ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
Algorithm algorithm = Algorithm.RSA256(null, key);
return token.sign(algorithm);
}
Replicating the code to .net, i assume it would be as:
private string GetToken5(string path)
{
var now = DateTime.UtcNow;
var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, "10217931236909168826") };
var handler = new JwtSecurityTokenHandler();
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var serviceAccountCredential = ServiceAccountCredential.FromServiceAccountData(stream);
var token = new JwtSecurityToken
(
"https://accounts.google.com",
"Audience",
claims,
now.AddMilliseconds(-30),
now.AddMinutes(60),
new SigningCredentials(new RsaSecurityKey(serviceAccountCredential.Key), SecurityAlgorithms.RsaSha256)
);
token.Header.Add("kid", "955104a37fa903e232339e83edb29b0c45");
return handler.WriteToken(token);
}
But, this also doesn't work.
There's still something missing ...
Upvotes: 2
Views: 1282
Reputation: 574
Coding wise, the last snippet is a correct way for generating a custom JWT for Google Auth.
The problem is that when a Service Account is created, it has a specific PUBLIC KEY which is used to verify the Token.
When creating the token with the Issuer as "https://accounts.google.com", when verifying the token, another public key was used, thus the validation failed.
The solution was to implement our own .well-known/openid-configuration Endpoint, and matching the Token claims with the information provided by this endpoint.
Upvotes: 2