I am building a React app backed by Azure functions written in C#. I've implemented JWT authentication via Userfront which is working fine on the front end but I'm struggling to verify the token using the public key in the functions.
I've tried numerous approaches, JWT-DotNet being the most recent but to no avail.
Can anyone please provide a working code example?
Here is what I have currently (which errors when creating the new RS256Algorithm with "Cannot find the requested object."):
var headers = req.Headers;
if (!headers.TryGetValue("Authorization", out var tokenHeader))
return new StatusCodeResult(StatusCodes.Status403Forbidden);
var token = tokenHeader[0].Replace("Bearer ", "");
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("Userfront_PublicKey"));
var urlEncoder = new JwtBase64UrlEncoder();
var publicKey = urlEncoder.Encode(plainTextBytes);
try
{
IJsonSerializer serializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IJwtAlgorithm algorithm = new RS256Algorithm(new X509Certificate2(plainTextBytes));
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder,algorithm);
var json = decoder.Decode(token[0], publicKey, verify: true);
}
catch (TokenExpiredException)
...
catch (SignatureVerificationException)
...
Assuming that the environment variable "Userfront_PublicKey" contains a PEM-encoded RSA public key, i.e.:
-----BEGIN RSA PUBLIC KEY-----
(your base64-encoded RSA public key)
-----END RSA PUBLIC KEY-----
then I would try the following (not tested, sorry):
var headers = req.Headers;
if (!headers.TryGetValue("Authorization", out var tokenHeader))
return new StatusCodeResult(StatusCodes.Status403Forbidden);
var token = tokenHeader[0].Replace("Bearer ", "");
var publicKeyPem = Environment.GetEnvironmentVariable("Userfront_PublicKey");
var publicKey = RSA.Create();
publicKey.ImportFromPem(publicKeyPem);
try
{
var json = JwtBuilder.Create()
.WithAlgorithm(new RS256Algorithm(publicKey))
.MustVerifySignature()
.Decode(token);
}
catch (TokenExpiredException)
...
catch (SignatureVerificationException)
...
I'm having trouble getting my .NET Core client to generate OAuth access tokens for a salesforce endpoint that requires OAuth of type 'JWT Bearer Flow'.
It seems there are limited .NET Framework examples that show a .NET client doing this, however none that show a .NET Core client doing it
e.g.
https://salesforce.stackexchange.com/questions/53662/oauth-jwt-token-bearer-flow-returns-invalid-client-credentials
So in my .NET Core 3.1 app i've generated a self signed certificate, added the private key to the above example's code when loading in the certificate, however a System.InvalidCastExceptionexception exception occurs on this line:
var rsa = certificate.GetRSAPrivateKey() as RSACryptoServiceProvider;
Exception:
System.InvalidCastException: 'Unable to cast object of type 'System.Security.Cryptography.RSACng' to type 'System.Security.Cryptography.RSACryptoServiceProvider'.'
It appears that this private key is used in the JWT Bearer Flow as part of the signature, and perhaps RSACryptoServiceProvider is not used in .NET core as it was in .NET Framework.
My question is this - is there actually a way in .NET Core to generate access tokens for the OAuth JWT Bearer Flow?
Full code that I'm using:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var token = GetAccessToken();
}
static dynamic GetAccessToken()
{
// get the certificate
var certificate = new X509Certificate2(#"C:\temp\cert.pfx");
// create a header
var header = new { alg = "RS256" };
// create a claimset
var expiryDate = GetExpiryDate();
var claimset = new
{
iss = "xxxxxx",
prn = "xxxxxx",
aud = "https://test.salesforce.com",
exp = expiryDate
};
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = ToBase64UrlString(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = ToBase64UrlString(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
// signature
var rsa = (RSACryptoServiceProvider) certificate.GetRSAPrivateKey();
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;
var client = new WebClient();
client.Encoding = Encoding.UTF8;
var uri = "https://login.salesforce.com/services/oauth2/token";
var content = new NameValueCollection();
content["assertion"] = jwt;
content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));
var result = JsonConvert.DeserializeObject<dynamic>(response);
return result;
}
static int GetExpiryDate()
{
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var currentUtcTime = DateTime.UtcNow;
var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;
return exp;
}
static string ToBase64UrlString(byte[] input)
{
return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
Well - it turns out posting to stackoverflow gets the brain cogs turning.
The answer ended up being doing a deep dive to find a similar issue here and using the solution from x509certificate2 sign for jwt in .net core 2.1
I ended up replacing the following code:
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);
With this code which makes use of the System.IdentityModel.Tokens.Jwt nuget package:
var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);
Full code after solution:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var token = GetAccessToken();
}
static dynamic GetAccessToken()
{
// get the certificate
var certificate = new X509Certificate2(#"C:\temp\cert.pfx");
// create a header
var header = new { alg = "RS256" };
// create a claimset
var expiryDate = GetExpiryDate();
var claimset = new
{
iss = "xxxxx",
prn = "xxxxx",
aud = "https://test.salesforce.com",
exp = expiryDate
};
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = ToBase64UrlString(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = ToBase64UrlString(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signature;
var client = new WebClient();
client.Encoding = Encoding.UTF8;
var uri = "https://test.salesforce.com/services/oauth2/token";
var content = new NameValueCollection();
content["assertion"] = jwt;
content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));
var result = JsonConvert.DeserializeObject<dynamic>(response);
return result;
}
static int GetExpiryDate()
{
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var currentUtcTime = DateTime.UtcNow;
var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;
return exp;
}
static string ToBase64UrlString(byte[] input)
{
return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
I am replying to this question just because such a similar answer would have helped me a lot when I landed on this page the first time.
First of all you don't have to generate the JWT from the C# client.
To generate a JWT token you can use this website: https://jwt.io/
There is a very well done video showing how to generate a JWT token:
https://www.youtube.com/watch?v=cViU2-xVscA&t=1680s
Once generated, use it from your C# client to call the get access_token endpoint
https://developer.salesforce.com/docs/atlas.en-us.api_iot.meta/api_iot/qs_auth_access_token.htm
(Watch the video on YT)
If all is correct you will get the access_token
To run the API calls, all you need is the access_token and not the JWT.
Once you have it add it to the HTTP calls like this
public static void AddBearerToken(this HttpRequestMessage request, string accessToken)
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
}
From time to time the access_token will expire. To check its validity you can call the token introspect api
https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oidc_token_introspection_endpoint.htm&type=5
You need to pass two additional parameters: client_id and client_secret
The client_id is the Consumer Key. You get it from the Connected App in Salesforce
The client_server is the Consumer Secret. You get it from Connected App in Salesforce
If the introspect token API returns a response with
{ active: false, ... }
it means that the access_token is expired and you need to issue a new one.
To issue a new access_token simply call the "/services/oauth2/token" again using the same JWT.
I am trying to write an interface to our Phabricator install to allow out internal improvement system to create tasks. However, I cannot figure out why I keep getting a certificate error.
"{\"result\":null,\"error_code\":\"ERR-INVALID-CERTIFICATE\",\"error_info\":\"Your authentication certificate for this server is invalid.\"}"
The following is my code;
private void CreateSession()
{
int token = (int)((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds);
var result = this.Do(
"conduit.connect",
new
{
client = this.ClientName,
clientVersion = this.ClientVersion,
clientDescription = "HIS to Fabricator Connector",
user = this.User,
authToken = token,
authSignature = SHA1HashStringForUTF8String(token + this.Certificate)
});
this.m_SessionKey = result.sessionKey;
this.m_ConnectionID = result.connectionID;
}
public static string SHA1HashStringForUTF8String(string s)
{
byte[] bytes = Encoding.UTF8.GetBytes(s);
var sha1 = SHA1.Create();
byte[] hashBytes = sha1.ComputeHash(bytes);
return HexStringFromBytes(hashBytes);
}
public static string HexStringFromBytes(byte[] bytes)
{
var sb = new StringBuilder();
foreach (byte b in bytes)
{
var hex = b.ToString("x2");
sb.Append(hex);
}
return sb.ToString();
}
This returns the following JSON;
"{\"client\":\"HIS\",\"clientVersion\":\"1\",\"clientDescription\":\"HIS to Fabricator Connector\",\"user\":\"KYLIE\",\"authToken\":1449486922,\"authSignature\":\"ec020edbd5082d3971c2c11ef4f4917244fc4a78\"}"
I think the issue is the certificate I am passing. I am using;
api-3ydcae2gtmf6u6uer2zow465j6px
which I obtained from the Conduit API Tokens page.
Any pointers?
You have to get the token via .../conduit/token
Use that token to query .../api/conduit.getcertificate
As result you get the certificate -> profit! :)
PS: it's neither api- nor cli- token to query the certificate! ;)
I'm trying to communicate with my app's enabled BigQuery API via the server to server method.
I've ticked all the boxes on this Google guide for constructing my JWT as best I can in C#.
And I've Base64Url encoded everything that was necessary.
However, the only response I get from google is a 400 Bad Request
"error" : "invalid_request"
I've made sure of all of the following from these other SO questions:
The signature is properly encrypted using RSA and SHA256
I am using POST and using application/x-www-form-urlencoded content type
Escaped all the backslashes in the claim set
Tried various grant_type and assertion values in the POST data
I get the same result when I use Fiddler. The error message is frustratingly lacking in detail! What else can I try?! Here's my code:
class Program
{
static void Main(string[] args)
{
// certificate
var certificate = new X509Certificate2(#"<Path to my certificate>.p12", "notasecret");
// header
var header = new { typ = "JWT", alg = "RS256" };
// claimset
var times = GetExpiryAndIssueDate();
var claimset = new
{
iss = "<email address of the client id of my app>",
scope = "https://www.googleapis.com/auth/bigquery",
aud = "https://accounts.google.com/o/oauth2/token",
iat = times[0],
exp = times[1],
};
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = Base64UrlEncode(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = Base64UrlEncode(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
// signiture
var rsa = certificate.PrivateKey as RSACryptoServiceProvider;
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = Base64UrlEncode(signatureBytes);
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;
Console.WriteLine(jwt);
var client = new HttpClient();
var uri = "https://accounts.google.com/o/oauth2/token";
var post = new Dictionary<string, string>
{
{"assertion", jwt},
{"grant_type", "urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer"}
};
var content = new FormUrlEncodedContent(post);
var result = client.PostAsync(uri, content).Result;
Console.WriteLine(result);
Console.WriteLine(result.Content.ReadAsStringAsync().Result);
Console.ReadLine();
}
private static int[] GetExpiryAndIssueDate()
{
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var issueTime = DateTime.Now;
var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds;
return new[]{iat, exp};
}
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
}
Looks like my guess in the comment above was correct. I got your code working by changing:
"urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer"
to:
"urn:ietf:params:oauth:grant-type:jwt-bearer"
Looks like you were accidentally double-encoding it.
I now get a response which looks something like:
{
"access_token" : "1/_5pUwJZs9a545HSeXXXXXuNGITp1XtHhZXXxxyyaacqkbc",
"token_type" : "Bearer",
"expires_in" : 3600
}
Edited Note: please make sure to have the correct date/time/timezone/dst configuration on your server. Having the clock off by even a few seconds will result in an invalid_grant error. http://www.time.gov will give the official time from the US govt, including in UTC.
Be sure to use DateTime.UtcNow instead of DateTime.Now in the GetExpiryAndIssueDate method.
I feel like I'm taking crazy pills here. Usually there's always a million library and samples floating around the web for any given task. I'm trying to implement authentication with a Google "Service Account" by use of JSON Web Tokens (JWT) as described here.
However there is only client libraries in PHP, Python, and Java. Even searching for JWT examples outside of Google's authentication, there is only crickets and drafts on the JWT concept. Is this really so new and possibly a Google proprietary system?
The java sample which is the closest I could manage to interpret looks pretty intensive and intimidating. There's got to be something out there in C# that I could at least start with. Any help with this would be great!
I found a base implementation of a Json Web Token and expanded on it with the Google flavor. I still haven't gotten it completely worked out but it's 97% there. This project lost it's steam, so hopefully this will help someone else get a good head-start:
Note:
Changes I made to the base implementation (Can't remember where I found it,) are:
Changed HS256 -> RS256
Swapped the JWT and alg order in the header. Not sure who got it wrong, Google or the spec, but google takes it the way It is below according to their docs.
public enum JwtHashAlgorithm
{
RS256,
HS384,
HS512
}
public class JsonWebToken
{
private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;
static JsonWebToken()
{
HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
{
{ JwtHashAlgorithm.RS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
{ JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
{ JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
};
}
public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
{
return Encode(payload, Encoding.UTF8.GetBytes(key), algorithm);
}
public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
{
var segments = new List<string>();
var header = new { alg = algorithm.ToString(), typ = "JWT" };
byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
//byte[] payloadBytes = Encoding.UTF8.GetBytes(#"{"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5#developer.gserviceaccount.com","scope":"https://www.googleapis.com/auth/prediction","aud":"https://accounts.google.com/o/oauth2/token","exp":1328554385,"iat":1328550785}");
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
var stringToSign = string.Join(".", segments.ToArray());
var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
public static string Decode(string token, string key)
{
return Decode(token, key, true);
}
public static string Decode(string token, string key, bool verify)
{
var parts = token.Split('.');
var header = parts[0];
var payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
var headerData = JObject.Parse(headerJson);
var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
var payloadData = JObject.Parse(payloadJson);
if (verify)
{
var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
var keyBytes = Encoding.UTF8.GetBytes(key);
var algorithm = (string)headerData["alg"];
var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
var decodedCrypto = Convert.ToBase64String(crypto);
var decodedSignature = Convert.ToBase64String(signature);
if (decodedCrypto != decodedSignature)
{
throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
}
}
return payloadData.ToString();
}
private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
{
switch (algorithm)
{
case "RS256": return JwtHashAlgorithm.RS256;
case "HS384": return JwtHashAlgorithm.HS384;
case "HS512": return JwtHashAlgorithm.HS512;
default: throw new InvalidOperationException("Algorithm not supported.");
}
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
// from JWT spec
private static byte[] Base64UrlDecode(string input)
{
var output = input;
output = output.Replace('-', '+'); // 62nd char of encoding
output = output.Replace('_', '/'); // 63rd char of encoding
switch (output.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new System.Exception("Illegal base64url string!");
}
var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;
}
}
And then my google specific JWT class:
public class GoogleJsonWebToken
{
public static string Encode(string email, string certificateFilePath)
{
var utc0 = new DateTime(1970,1,1,0,0,0,0, DateTimeKind.Utc);
var issueTime = DateTime.Now;
var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds; // Expiration time is up to 1 hour, but lets play on safe side
var payload = new
{
iss = email,
scope = "https://www.googleapis.com/auth/gan.readonly",
aud = "https://accounts.google.com/o/oauth2/token",
exp = exp,
iat = iat
};
var certificate = new X509Certificate2(certificateFilePath, "notasecret");
var privateKey = certificate.Export(X509ContentType.Cert);
return JsonWebToken.Encode(payload, privateKey, JwtHashAlgorithm.RS256);
}
}
I've never used it but there is a JWT implementation on NuGet.
Package: https://nuget.org/packages/JWT
Source: https://github.com/johnsheehan/jwt
.NET 4.0 compatible: https://www.nuget.org/packages/jose-jwt/
You can also go here: https://jwt.io/ and click "libraries".
This is my implementation of (Google) JWT Validation in .NET.
It is based on other implementations on Stack Overflow and GitHub gists.
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace QuapiNet.Service
{
public class JwtTokenValidation
{
public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");
var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
}
}
private string CLIENT_ID = "xxx.apps.googleusercontent.com";
public async Task<ClaimsPrincipal> ValidateToken(string idToken)
{
var certificates = await this.FetchGoogleCertificates();
TokenValidationParameters tvp = new TokenValidationParameters()
{
ValidateActor = false, // check the profile ID
ValidateAudience = true, // check the client ID
ValidAudience = CLIENT_ID,
ValidateIssuer = true, // check token came from Google
ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
{
return certificates
.Where(x => x.Key.ToUpper() == kid.ToUpper())
.Select(x => new X509SecurityKey(x.Value));
},
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromHours(13)
};
JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);
return cp;
}
}
}
Note that, in order to use it, you need to add a reference to the NuGet package System.Net.Http.Formatting.Extension. Without this, the compiler will not recognize the ReadAsAsync<> method.
It would be better to use standard and famous libraries instead of writing the code from scratch.
JWT for encoding and decoding JWT tokens
Bouncy Castle supports encryption and decryption, especially RS256 get it here
Using these libraries you can generate a JWT token and sign it using RS256 as below.
public string GenerateJWTToken(string rsaPrivateKey)
{
var rsaParams = GetRsaParameters(rsaPrivateKey);
var encoder = GetRS256JWTEncoder(rsaParams);
// create the payload according to the Google's doc
var payload = new Dictionary<string, object>
{
{ "iss", ""},
{ "sub", "" },
// and other key-values according to the doc
};
// add headers. 'alg' and 'typ' key-values are added automatically.
var header = new Dictionary<string, object>
{
{ "kid", "{your_private_key_id}" },
};
var token = encoder.Encode(header,payload, new byte[0]);
return token;
}
private static IJwtEncoder GetRS256JWTEncoder(RSAParameters rsaParams)
{
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(rsaParams);
var algorithm = new RS256Algorithm(csp, csp);
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
return encoder;
}
private static RSAParameters GetRsaParameters(string rsaPrivateKey)
{
var byteArray = Encoding.ASCII.GetBytes(rsaPrivateKey);
using (var ms = new MemoryStream(byteArray))
{
using (var sr = new StreamReader(ms))
{
// use Bouncy Castle to convert the private key to RSA parameters
var pemReader = new PemReader(sr);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
}
}
}
ps: the RSA private key should have the following format:
-----BEGIN RSA PRIVATE KEY-----
{base64 formatted value}
-----END RSA PRIVATE KEY-----
Here is another REST-only working example for Google Service Accounts accessing G Suite Users and Groups, authenticating through JWT. This was only possible through reflection of Google libraries, since Google documentation of these APIs are beyond terrible. Anyone used to code in MS technologies will have a hard time figuring out how everything goes together in Google services.
$iss = "<name>#<serviceaccount>.iam.gserviceaccount.com"; # The email address of the service account.
$sub = "impersonate.user#mydomain.com"; # The user to impersonate (required).
$scope = "https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly";
$certPath = "D:\temp\mycertificate.p12";
$grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
# Auxiliary functions
function UrlSafeEncode([String] $Data) {
return $Data.Replace("=", [String]::Empty).Replace("+", "-").Replace("/", "_");
}
function UrlSafeBase64Encode ([String] $Data) {
return (UrlSafeEncode -Data ([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Data))));
}
function KeyFromCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$privateKeyBlob = $Certificate.PrivateKey.ExportCspBlob($true);
$key = New-Object System.Security.Cryptography.RSACryptoServiceProvider;
$key.ImportCspBlob($privateKeyBlob);
return $key;
}
function CreateSignature ([Byte[]] $Data, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$sha256 = [System.Security.Cryptography.SHA256]::Create();
$key = (KeyFromCertificate $Certificate);
$assertionHash = $sha256.ComputeHash($Data);
$sig = [Convert]::ToBase64String($key.SignHash($assertionHash, "2.16.840.1.101.3.4.2.1"));
$sha256.Dispose();
return $sig;
}
function CreateAssertionFromPayload ([String] $Payload, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$header = #"
{"alg":"RS256","typ":"JWT"}
"#;
$assertion = New-Object System.Text.StringBuilder;
$assertion.Append((UrlSafeBase64Encode $header)).Append(".").Append((UrlSafeBase64Encode $Payload)) | Out-Null;
$signature = (CreateSignature -Data ([System.Text.Encoding]::ASCII.GetBytes($assertion.ToString())) -Certificate $Certificate);
$assertion.Append(".").Append((UrlSafeEncode $signature)) | Out-Null;
return $assertion.ToString();
}
$baseDateTime = New-Object DateTime(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc);
$timeInSeconds = [Math]::Truncate([DateTime]::UtcNow.Subtract($baseDateTime).TotalSeconds);
$jwtClaimSet = #"
{"scope":"$scope","email_verified":false,"iss":"$iss","sub":"$sub","aud":"https://oauth2.googleapis.com/token","exp":$($timeInSeconds + 3600),"iat":$timeInSeconds}
"#;
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "notasecret", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable);
$jwt = CreateAssertionFromPayload -Payload $jwtClaimSet -Certificate $cert;
# Retrieve the authorization token.
$authRes = Invoke-WebRequest -Uri "https://oauth2.googleapis.com/token" -Method Post -ContentType "application/x-www-form-urlencoded" -UseBasicParsing -Body #"
assertion=$jwt&grant_type=$([Uri]::EscapeDataString($grantType))
"#;
$authInfo = ConvertFrom-Json -InputObject $authRes.Content;
$resUsers = Invoke-WebRequest -Uri "https://www.googleapis.com/admin/directory/v1/users?domain=<required_domain_name_dont_trust_google_documentation_on_this>" -Method Get -Headers #{
"Authorization" = "$($authInfo.token_type) $($authInfo.access_token)"
}
$users = ConvertFrom-Json -InputObject $resUsers.Content;
$users.users | ft primaryEmail, isAdmin, suspended;
Here is the list of classes and functions:
open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.IdentityModel.Tokens
open System.IdentityModel.Tokens
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.JsonWebTokens
open System.Text
open Newtonsoft.Json
open System.Security.Claims
let theKey = "VerySecretKeyVerySecretKeyVerySecretKey"
let securityKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(theKey))
let credentials = SigningCredentials(securityKey, SecurityAlgorithms.RsaSsaPssSha256)
let expires = DateTime.UtcNow.AddMinutes(123.0) |> Nullable
let token = JwtSecurityToken(
"lahoda-pro-issuer",
"lahoda-pro-audience",
claims = null,
expires = expires,
signingCredentials = credentials
)
let tokenString = JwtSecurityTokenHandler().WriteToken(token)
Using System.Security.Cryptography.RSA I've modified John Sheehan's JWT library code that was expanded by #Levitikon to use RS256, RSASSA-PKCS1-v1_5 with the SHA-256 hash algorithm
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// adapted from<br/>
/// https://github.com/jwt-dotnet/jwt<br/>
/// https://stackoverflow.com/questions/10055158/is-there-any-json-web-token-jwt-example-in-c<br/>
/// https://stackoverflow.com/a/10106800/6620171<br/>
/// <br/>
/// JSON Web Token (JWT) is a compact, URL-safe means of representing<br/>
/// claims to be transferred between two parties. The claims in a JWT<br/>
/// are encoded as a JSON object that is used as the payload of a JSON<br/>
/// Web Signature (JWS) structure or as the plaintext of a JSON Web<br/>
/// Encryption(JWE) structure, enabling the claims to be digitally<br/>
/// signed or integrity protected with a Message Authentication Code<br/>
/// (MAC) and/or encrypted.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7519
/// </summary>
internal class JsonWebToken
{
/// <summary>
/// JWS uses cryptographic algorithms to digitally sign or create a MAC<br/>
/// of the contents of the JWS Protected Header and the JWS Payload.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3
/// </summary>
public enum JwsAlgorythm
{
/// <summary>
/// RSASSA-PKCS1-v1_5 using SHA-256<br/>
/// This section defines the use of the RSASSA-PKCS1-v1_5 digital<br/>
/// signature algorithm as defined in Section 8.2 of RFC 3447 [RFC3447]<br/>
/// (commonly known as PKCS #1), using SHA-2 [SHS] hash functions.<br/>
/// The RSASSA-PKCS1-v1_5 SHA-256 digital signature is generated as<br/>
/// follows: generate a digital signature of the JWS Signing Input using<br/>
/// RSASSA-PKCS1-v1_5-SIGN and the SHA-256 hash function with the desired<br/>
/// private key. This is the JWS Signature value.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3.3<br/>
/// https://www.rfc-editor.org/rfc/rfc3447#section-8.2<br/>
/// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
/// </summary>
RS256,
/// <summary>
/// No digital signature or MAC performed<br/>
/// JWSs MAY also be created that do not provide integrity protection.<br/>
/// Such a JWS is called an Unsecured JWS. An Unsecured JWS uses the<br/>
/// "alg" value "none" and is formatted identically to other JWSs, but<br/>
/// MUST use the empty octet sequence as its JWS Signature value.<br/>
/// Recipients MUST verify that the JWS Signature value is the empty<br/>
/// octet sequence.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3.6<br/>
/// https://www.rfc-editor.org/rfc/rfc7519#section-6
/// </summary>
none
}
public static string Encode(object payload, JwsAlgorythm algo, RSA rsa)
{
if (payload == null) { throw new ArgumentNullException("payload"); }
if (algo != JwsAlgorythm.RS256 && algo != JwsAlgorythm.none) { throw new ArgumentException("Invalid JwsAlgorythm specified"); }
if (rsa == null && algo == JwsAlgorythm.RS256) { throw new ArgumentNullException("Encoding of secured JWT requires an RSA object"); }
List<string> segments = new List<string>();
var header = new { typ = "JWT", alg = algo.ToString() };
string strHeader = JsonConvert.SerializeObject(header, Formatting.None);
string strPayload = JsonConvert.SerializeObject(payload, Formatting.None);
byte[] headerBytes = Encoding.UTF8.GetBytes(strHeader);
byte[] payloadBytes = Encoding.UTF8.GetBytes(strPayload);
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
if (algo == JwsAlgorythm.none)
{
segments.Add(string.Empty);
return string.Join(".", segments.ToArray());
}
string stringToSign = string.Join(".", segments.ToArray());
byte[] bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] signature = rsa.SignData(bytesToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
public static Tuple<string, string> Decode(string token, JwsAlgorythm algo, bool verify, RSA rsa = null)
{
if (algo != JwsAlgorythm.RS256 && algo != JwsAlgorythm.none) { throw new ArgumentException("Invalid JwsAlgorythm specified"); }
if (verify && rsa == null && algo == JwsAlgorythm.RS256) { throw new ArgumentNullException("Verification of secured JWT requires an RSA object"); }
string[] parts = token.Split('.');
string header = parts[0];
string payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
JObject headerData = JObject.Parse(headerJson);
string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
JObject payloadData = JObject.Parse(payloadJson);
if (verify)
{
if (algo == JwsAlgorythm.none)
{
if (crypto.Length != 0)
{
throw new ApplicationException(string.Format("Invalid signature"));
}
}
else if (algo == JwsAlgorythm.RS256)
{
byte[] bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
bool valid = rsa.VerifyData(bytesToSign, crypto, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
if (!valid)
{
throw new ApplicationException(string.Format("Invalid signature"));
}
}
}
return new Tuple<string, string>(headerData.ToString(), payloadData.ToString());
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
string output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
// from JWT spec
private static byte[] Base64UrlDecode(string input)
{
string output = input;
output = output.Replace('-', '+'); // 62nd char of encoding
output = output.Replace('_', '/'); // 63rd char of encoding
switch (output.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new Exception("Invalid base64url string");
}
byte[] converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;
}
}
Usage:
X509Certificate2 cert = new X509Certificate2("C:\\test\\keypair.pfx", "notasecret");
long secsSinceEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var jwt = new
{
exp = secsSinceEpoch + 600,
iss = "my client id",
aud = "h++ps://webapi.com",
sub = "my subscriber id",
iat = secsSinceEpoch,
nbf = secsSinceEpoch,
jti = RandomString(21),
};
string jwtEncoded = JsonWebToken.Encode(jwt, JsonWebToken.JwsAlgorythm.RS256, cert.GetRSAPrivateKey());
Tuple<string, string> jwtDecoded = JsonWebToken.Decode(jwtEncoded, JsonWebToken.JwsAlgorythm.RS256, true, cert.GetRSAPublicKey());
Console.WriteLine(jwtDecoded);
Output:
({
"typ": "JWT",
"alg": "RS256"
}, {
"exp": 1668732075,
"iss": "my client id",
"aud": "h++ps://webapi.com",
"sub": "my subscriber id",
"iat": 1668731475,
"nbf": 1668731475,
"jti": "tCUpk2i5bNBVBcj7LzV5U"
})