How to encode JWT for APN tokenization (.NET, C#) - c#

I'm trying to send Push Notifications to APN with tokenization. I tried to use a few libraries like jose-jwt and Microsot Jwt class for creating JWT token, but I can't wrap my head around it.
I get stuck on creating JWT and signing it with private key.
For communicating with certificates, I used PushSharp and it worked just fine. Can anyone help me with a working similar example but with tokens?
edit: following Apple's documentation here: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1
Sample code: the closest I came to something would look like this, but I don't know how to create CngKey properly
var payload = new Dictionary<string, object>()
{
{ "iss", issuer },
{ "iat", DateTime.UtcNow }
};
var headers = new Dictionary<string, object>()
{
{ "kid", keyIdentifier}
};
CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256); //how to create this CngKey
string token = Jose.JWT.Encode(payload, key, JwsAlgorithm.ES256, headers);

Thanks for your answers, had to contact many supports to get this done. Here is what the final result looked like.
/// <summary>
/// Method returns ECDSA signed JWT token format, from json header, json payload and privateKey (pure string extracted from *.p8 file - PKCS#8 format)
/// </summary>
/// <param name="privateKey">ECDSA256 key</param>
/// <param name="header">JSON header, i.e. "{\"alg\":\"ES256\" ,\"kid\":\"1234567899"\"}"</param>
/// <param name="payload">JSON payload, i.e. {\"iss\":\"MMMMMMMMMM"\",\"iat\":"122222222229"}"</param>
/// <returns>base64url encoded JWT token</returns>
public static string SignES256(string privateKey, string header, string payload)
{
CngKey key = CngKey.Import(
Convert.FromBase64String(privateKey),
CngKeyBlobFormat.Pkcs8PrivateBlob);
using (ECDsaCng dsa = new ECDsaCng(key))
{
dsa.HashAlgorithm = CngAlgorithm.Sha256;
var unsignedJwtData =
Url.Base64urlEncode(Encoding.UTF8.GetBytes(header)) + "." + Url.Base64urlEncode(Encoding.UTF8.GetBytes(payload));
var signature =
dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
return unsignedJwtData + "." + Url.Base64urlEncode(signature);
}
}

Here is what I use:
var privateKeyContent = System.IO.File.ReadAllText(AUTHKEYFILE);
var privateKey = privateKeyContent.Split('\n')[1];
var secretKeyFile = Convert.FromBase64String(privateKey);
var secretKey = CngKey.Import(secretKeyFile,CngKeyBlobFormat.Pkcs8PrivateBlob);

if you want to run your app in MacOS or linux, you can't use CngKey
in this case you can use
ECDsa algorithm for encoding the JWT
private ECDsa encoder(string privateKey)
{
var result = ECDsa.Create();
result.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
return result;
}

Here is what i use;
public static string getAppleJWT(string privateKeyPath, string teamId, string keyId)
{
var private_key = getPrivateKey(privateKeyPath);
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var issueTime = DateTime.Now;
var payload = new Dictionary<string, object>()
{
{ "iss", teamId },
{ "iat", (int)issueTime.Subtract(utc0).TotalSeconds }
};
var headers = new Dictionary<string, object>()
{
{ "alg", "ES256"},
{ "kid", keyId}
};
return Jose.JWT.Encode(payload, private_key, JwsAlgorithm.ES256, headers);
}
public static CngKey getPrivateKey(string privateKeyPath)
{
var privateKeyLines = System.IO.File.ReadAllLines(privateKeyPath).ToList();
privateKeyLines.RemoveAt(privateKeyLines.Count - 1);
privateKeyLines.RemoveAt(0);
var privateKey = string.Join("", privateKeyLines);
var secretKeyFile = Convert.FromBase64String(privateKey);
var secretKey = CngKey.Import(secretKeyFile, CngKeyBlobFormat.Pkcs8PrivateBlob);
return secretKey;
}

Pulling all the pieces together and extending some of the prior answers
This utilizes Microsoft.IdentityModel.Tokens which removes Windows dependencies.
Tested this under .NET 6
How to create the token:
public static string CreateSecurityToken(string tokenId, string teamId, string tokenKey, DateTime utcGenerateDateTime)
{
var key = ECDsa.Create();
key?.ImportPkcs8PrivateKey(Convert.FromBase64String(tokenKey), out _);
var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId };
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);
var descriptor = new SecurityTokenDescriptor
{
IssuedAt = utcGenerateDateTime,
Issuer = teamId,
SigningCredentials = credentials
};
var handler = new JwtSecurityTokenHandler();
var encodedToken = handler.CreateEncodedJwt(descriptor);
return encodedToken;
}
How to validate the token:
public static SecurityToken ValidateSecurityToken(string token, string tokenId, string tokenKey)
{
if (token == null)
return null;
var key = ECDsa.Create();
key?.ImportPkcs8PrivateKey(Convert.FromBase64String(tokenKey), out _);
var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId };
try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out var validatedToken);
return validatedToken;
}
catch
{
return null;
}
}

Related

Signing data with RSA512 v2 [duplicate]

I am using the jose-jwt library and want to create a signed JWT in C# using the RS256 algorithm for encryption. I have no experience with cryptography, so please excuse my ignorance. I see the following example in the docs:
var payload = new Dictionary<string, object>()
{
{ "sub", "mr.x#contoso.com" },
{ "exp", 1300819380 }
};
var privateKey=new X509Certificate2("my-key.p12", "password", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet).PrivateKey as RSACryptoServiceProvider;
string token=Jose.JWT.Encode(payload, privateKey, JwsAlgorithm.RS256);
which shows the use of a p12 file, but how do I use an RSA key file of the form below? I am looking at the docs for X509Certificate2, but I see no option for RSA private keys. It appears to only accept PKCS7, which I understand to be public keys.
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
-----END RSA PRIVATE KEY-----
Finally, what is the difference between the two options listed in the docs, and how do I choose between the two?
-------------------------- OPTION 1 --------------------------
RS-* and PS-* family
CLR:
RS256, RS384, RS512 and PS256, PS384, PS512 signatures require
RSACryptoServiceProvider (usually private) key of corresponding
length. CSP need to be forced to use Microsoft Enhanced RSA and AES
Cryptographic Provider. Which usually can be done be re-importing
RSAParameters. See http://clrsecurity.codeplex.com/discussions/243156
for details.
-------------------------- OPTION 2 --------------------------
CORECLR: RS256, RS384, RS512 signatures require RSA (usually private) key of corresponding length.
I know this post is old, but it took me forever to figure this out, so I thought I would share.
To test I created RSA keys using OpenSSL:
openssl genrsa -out privateKey.pem 512
openssl rsa -in privateKey.pem -pubout -out publicKey.pem
You will need the following 2 nuget packages:
https://github.com/dvsekhvalnov/jose-jwt
http://www.bouncycastle.org/csharp/
Test Code
public static void Test()
{
string publicKey = File.ReadAllText(#"W:\Dev\Temp\rsa_keys\publicKey.pem");
string privateKey = File.ReadAllText(#"W:\Dev\Temp\rsa_keys\privateKey.pem");
var claims = new List<Claim>();
claims.Add(new Claim("claim1", "value1"));
claims.Add(new Claim("claim2", "value2"));
claims.Add(new Claim("claim3", "value3"));
var token = CreateToken(claims, privateKey);
var payload = DecodeToken(token, publicKey);
}
Create Token
public static string CreateToken(List<Claim> claims, string privateRsaKey)
{
RSAParameters rsaParams;
using (var tr = new StringReader(privateRsaKey))
{
var pemReader = new PemReader(tr);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
if (keyPair == null)
{
throw new Exception("Could not read RSA private key");
}
var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters;
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
}
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParams);
Dictionary<string, object> payload = claims.ToDictionary(k => k.Type, v => (object)v.Value);
return Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
}
}
Decode Token
public static string DecodeToken(string token, string publicRsaKey)
{
RSAParameters rsaParams;
using (var tr = new StringReader(publicRsaKey))
{
var pemReader = new PemReader(tr);
var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters;
if (publicKeyParams == null)
{
throw new Exception("Could not read RSA public key");
}
rsaParams = DotNetUtilities.ToRSAParameters(publicKeyParams);
}
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParams);
// This will throw if the signature is invalid
return Jose.JWT.Decode(token, rsa, Jose.JwsAlgorithm.RS256);
}
}
I found https://jwt.io/ a great resource to test your tokens
The key to this question is using JWT and Bouncy castle libraries for encoding the token and signing it respectively.
JWT for encoding and decoding JWT tokens
Bouncy Castle supports encryption and decryption, especially RS256 get it here
First, you need to transform the private key to the form of RSA parameters. Then you need to pass the RSA parameters to the RSA algorithm as the private key. Lastly, you use the JWT library to encode and sign the token.
public string GenerateJWTToken(string rsaPrivateKey)
{
var rsaParams = GetRsaParameters(rsaPrivateKey);
var encoder = GetRS256JWTEncoder(rsaParams);
// create the payload according to your need
var payload = new Dictionary<string, object>
{
{ "iss", ""},
{ "sub", "" },
// and other key-values
};
// add headers. 'alg' and 'typ' key-values are added automatically.
var header = new Dictionary<string, object>
{
{ "{header_key}", "{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-----
I used http://travistidwell.com/jsencrypt/demo/
to create 2048 bit keys
Using .NET 6:
string CreateRsaToken(string someClaimValue)
{
var rsaPrivateKey = #"-----BEGIN RSA PRIVATE KEY-----
MIIEogIBA...";
using var rsa = RSA.Create();
rsa.ImportFromPem(rsaPrivateKey);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
{
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};
var now = DateTime.Now;
var jwt = new JwtSecurityToken(
audience: "all",
issuer: "TheBitCoinKing",
claims: new[] {
new Claim("SomeClaim", someClaimValue),
},
notBefore: now,
expires: now.AddMinutes(1),
signingCredentials: signingCredentials
);
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
packages:
Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt
If you want to use a certificate, you can retrieve it by it's thumbprint using this method
private X509Certificate2 GetByThumbprint(string Thumbprint)
{
var localStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
localStore.Open(OpenFlags.ReadOnly);
return localStore.Certificates//.Find(X509FindType.FindByKeyUsage, X509KeyUsageFlags.DigitalSignature, false)
.Find(X509FindType.FindByThumbprint, Thumbprint, false)
.OfType<X509Certificate2>().First();
}
and then:
private JwtSecurityToken GenerateJWT()
{
var securityKey = new Microsoft.IdentityModel.Tokens.X509SecurityKey(GetByThumbprint("YOUR-CERT-THUMBPRINT-HERE"));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, "RS256");
var JWTHeader = new JwtHeader(credentials);
var payload = new JwtPayload
{
{ "iss", "Issuer-here"},
{ "exp", (Int32)(DateTime.UtcNow.AddHours(1).Subtract(new DateTime(1970, 1, 1))).TotalSeconds},
{ "iat", (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds}
};
var token = new JwtSecurityToken(JWTHeader, payload);
return token;
}
Using BouncyCastle and Jose nuget package, the following code works for me.
public static string CreateToken(Dictionary<string, object> payload)
{
string jwt = string.Empty;
RsaPrivateCrtKeyParameters keyPair;
var cert = ConfigurationManager.AppSettings["cert"];
/// cert begins -----BEGIN PRIVATE KEY----- and ends with -END PRIVATE KEY-----";
using (var sr = new StringReader(cert))
{
PemReader pr = new PemReader(sr);
keyPair = (RsaPrivateCrtKeyParameters)pr.ReadObject();
}
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters(keyPair);
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParams);
jwt = Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
}
return jwt;
}
The GetRSAPrivateKey is only available in .NET 4.6. See the URL below.
https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.rsacertificateextensions.getrsaprivatekey(v=vs.110).aspx
Posting the code to create RS256 JWT token for GCP OAuth Token API call, using the Service Account JSON Key-:
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace GCP
{
class JWTTokenGenerationForGCPOAuthTokenAPI
{
public static string GenerateJWTToken()
{
var rsaParams = ReadAsymmetricKeyParameter();
var encoder = GetRS256JWTEncoder(rsaParams);
var iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var exp = DateTimeOffset.UtcNow.AddMinutes(60).ToUnixTimeSeconds();
// create the payload according to your need
// iss is the Service Account Email ID
var payload = new Dictionary<string, object>
{
{ "iss", "<service-account>#<project-id>.iam.gserviceaccount.com"},
{ "scope", "https://www.googleapis.com/auth/cloud-platform" },
{ "aud", "https://oauth2.googleapis.com/token" },
{ "exp", exp},
{ "iat", iat}
};
//Final token
var token = encoder.Encode(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;
}
public static RSAParameters ReadAsymmetricKeyParameter()
{
\\ This key is fetched from the GCP Service Account JSON File.
\\"private_key": "-----BEGIN PRIVATE KEY-----\n<long-code>-----END PRIVATE KEY-----\n",
\\ pick <long-code> from above. Replace all \n with actual new line like shown below.
string pkey = #"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDSoGKK/Dzb8MBy
################################################################
################################################################
################################################################
################################################################
twySMqKKWnIC/zZljrvp4w==";
RsaPrivateCrtKeyParameters rsaPrivateCrtKeyParameters1;
var keyBytes = Convert.FromBase64String(pkey);
var asymmetricKeyParameter = PrivateKeyFactory.CreateKey(keyBytes);
rsaPrivateCrtKeyParameters1 = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
RSAParameters r = DotNetUtilities.ToRSAParameters(rsaPrivateCrtKeyParameters1);
return r;
}
}
}
Code Done in : .NET Framework 4.6.1
Nuget Packages :
Bounty Castle - Install-Package BouncyCastle -Version 1.8.6.1
I thought I would add my findings as I had to do this last night. The examples in this page really helped but they didn't work straightaway.
In my specific case, I was trying to generate a JWT token for DocuSign, for some other reason I can't use their SDK and generating the JWT token manually was the right approach for my use case.
var privateKeybyteArray = Encoding.ASCII.GetBytes(#"-----BEGIN RSA PRIVATE KEY-----
xxxxxxxxxxxxxxxxxx
-----END RSA PRIVATE KEY-----");
var payload = new Dictionary<string, object>
{
{ "iss", "3a31fd58-xxxx-xxxx-xxxx-17639ade3c1b" },
{ "sub", "40a3a606-xxxx-xxxx-xxxx-762c6e7dadb6" },
{ "aud", "account-d.docusign.com" },
{ "iat", DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds },
{ "exp", DateTime.UtcNow.AddHours(1).Subtract(new DateTime(1970, 1, 1)).TotalSeconds },
{ "scope", "signature" }
};
var rsaPrivateKey = new RSAParameters();
using (var ms = new MemoryStream(privateKeybyteArray))
{
using (var sr = new StreamReader(ms))
{
var pemReader = new PemReader(sr);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
rsaPrivateKey = DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
}
}
var csprivate = new RSACryptoServiceProvider();
csprivate.ImportParameters(rsaPrivateKey);
var algorithm = new RS256Algorithm(csprivate, csprivate);
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(payload, privateKeybyteArray);
I used the JWT-dotnet package. I found the website jsonwebtoken.io really good as it generates the .NET code needed for generating the token, it didn't quite work but it helped for figuring out what I was doing wrong
NET 5.0 or later has a method called "ImportFromPem", so you can read private-key in PEM format as it is.
void GetToken(System.IO.FileInfo privateKey, string service_account, string client_id)
{
using (var rsa = System.Security.Cryptography.RSA.Create())
{
rsa.ImportFromPem(System.IO.File.ReadAllText(privateKey.FullName));
var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
{
Issuer = client_id,
Claims = new System.Collections.Generic.Dictionary<string, object>() { ["sub"] = service_account },
SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
IssuedAt = System.DateTime.UtcNow,
Expires = System.DateTime.UtcNow.AddMinutes(60),
};
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
string token = handler.WriteToken(handler.CreateJwtSecurityToken(descriptor));
}
}
Nuget: 「System.IdentityModel.Tokens.Jwt」
And, maybe error: cannot access a disposed object. object name 'rsa' -> Ref
var signingCredentials = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)
{
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};
If you use a public certificate and .NET 4.6,for decoding you can use:
string token = "eyJhbGciOiJSUzI1NiIsInR....";
string certificate = "MIICnzCCAYcCBgFd2yEPx....";
var publicKey = new X509Certificate2(Convert.FromBase64String(certificate )).GetRSAPublicKey();
string decoded = JWT.Decode(token, publicKey, JwsAlgorithm.RS256);
RS256 is a Signature Algorithm not an Encryption Algorithm
Encryption is done with the public key
Here is the code to create an encrypted JWT:
var cert = new X509Certificate2(".\\key.cer");
var rsa = (RSACryptoServiceProvider) cert.PublicKey.Key;
var payload = new Dictionary<string, object>()
{
{"sub", "mr.x#contoso.com"},
{"exp", 1300819380}
};
var encryptedToken =
JWT.Encode(
payload,
rsa,
JweAlgorithm.RSA_OAEP,
JweEncryption.A256CBC_HS512,
null);

Sign JWT token using Azure Key Vault

I'm using a private key to sign a JWT token, which works as expected. However, I'd like to leverage Azure Key Vault to do the signing for me, so that the private key doesn't leave KeyVault. I'm struggling to get this to work, but not sure why.
Here's the code that doesn't use KeyVault and does work...
var handler = new JwtSecurityTokenHandler();
var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds();
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Iss, clientId),
new Claim(JwtRegisteredClaimNames.Sub, integrationUser),
new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
new Claim(JwtRegisteredClaimNames.Exp, expiryTime.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Add JTI for additional security against replay attacks
};
var privateKey = File.ReadAllText(#"selfsigned.key")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "");
var privateKeyRaw = Convert.FromBase64String(privateKey);
var provider = new RSACryptoServiceProvider();
provider.ImportPkcs8PrivateKey(new ReadOnlySpan<byte>(privateKeyRaw), out _);
var rsaSecurityKey = new RsaSecurityKey(provider);
var token = new JwtSecurityToken
(
new JwtHeader(new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)),
new JwtPayload(claims)
);
var token = handler.WriteToken(token);
This works, and if I copy the JWT into jwt.io, and also paste the public key - it says that the signature is verified...
The token also works against the API I'm calling too.
However, if signing with KeyVault...
var handler = new JwtSecurityTokenHandler();
var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds();
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Iss, clientId),
new Claim(JwtRegisteredClaimNames.Sub, integrationUser),
new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
new Claim(JwtRegisteredClaimNames.Exp, expiryTime.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Add JTI for additional security against replay attacks
};
var header = #"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonConvert.SerializeObject(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";
// Sign token
var credential = new InteractiveBrowserCredential();
var client = new KeyClient(vaultUri: new Uri(kvUri), credential);
var key = (KeyVaultKey)client.GetKey("dan-test");
var cryptoClient = new CryptographyClient(keyId: key.Id, credential);
var digest = new SHA256CryptoServiceProvider().ComputeHash(Encoding.Unicode.GetBytes(headerAndPayload));
var signature = await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest);
var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature.Signature)}";
(uses Azure.Security.KeyVault.Keys and Azure.Identity nuget packages)
This doesn't work. The first two parts of the token - ie. header and payload are identical to the JWT that does work. The only thing that's different is the signature at the end.
I'm out of ideas! Note that this is closely related to this Stackoverflow question, where the answers seem to suggest what I'm doing should be correct.
Your code is mostly correct, though you should use either Encoding.UTF8 or Encoding.ASCII (since the base64url characters are all valid ASCII and you eliminate any BOM concerns) to get the bytes for headerAndPayload.
I was able to get this to work and found that https://jwt.io is rather vague when it says you can paste either a public key or certificate. It has to be PEM-encoded, and if posting an RSA public key you have to use the less-common "BEGIN RSA PUBLIC KEY" label instead of the more-common "BEGIN PUBLIC KEY".
I tried a few things that all should've worked, and when I found that using a certificate from Key Vault did with "BEGIN CERTIFICATE", I went back to trying "BEGIN PUBLIC KEY". It wasn't until, on a whim, when I changed it to "BEGIN RSA PUBLIC KEY" the JWT was successfully verified.
Below is the code I tried using certificate URI:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.IdentityModel.Tokens;
var arg = args.Length > 0 ? args[0] : throw new Exception("Key Vault key URI required");
var uri = new Uri(arg, UriKind.Absolute);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Iss, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var header = #"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonSerializer.Serialize(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";
var id = new KeyVaultKeyIdentifier(uri);
var credential = new DefaultAzureCredential();
var certClient = new CertificateClient(id.VaultUri, credential);
KeyVaultCertificate cert = await certClient.GetCertificateAsync(id.Name);
using X509Certificate2 pfx = await certClient.DownloadCertificateAsync(id.Name, id.Version);
var pem = PemEncoding.Write("CERTIFICATE".AsSpan(), pfx.RawData);
Console.WriteLine($"Certificate (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();
using var rsaKey = pfx.GetRSAPublicKey();
var pubkey = rsaKey.ExportRSAPublicKey();
pem = PemEncoding.Write("RSA PUBLIC KEY".AsSpan(), pubkey.AsSpan());
Console.WriteLine($"Public key (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();
var cryptoClient = new CryptographyClient(cert.KeyId, credential);
using var sha256 = SHA256.Create();
var digest = sha256.ComputeHash(Encoding.ASCII.GetBytes(headerAndPayload));
var signature = (await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest)).Signature;
var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature)}";
Console.WriteLine($"JWT:\n\n{token}");
For using only a key, the following should work:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.IdentityModel.Tokens;
var arg = args.Length > 0 ? args[0] : throw new Exception("Key Vault key URI required");
var uri = new Uri(arg, UriKind.Absolute);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Iss, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var header = #"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonSerializer.Serialize(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";
var id = new KeyVaultKeyIdentifier(uri);
var credential = new DefaultAzureCredential();
var keyClient = new KeyClient(id.VaultUri, credential);
KeyVaultKey key = await keyClient.GetKeyAsync(id.Name, id.Version);
using var rsaKey = key.Key.ToRSA();
var pubkey = rsaKey.ExportRSAPublicKey();
var pem = PemEncoding.Write("RSA PUBLIC KEY".AsSpan(), pubkey.AsSpan());
Console.WriteLine($"Public key (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();
var cryptoClient = new CryptographyClient(key.Id, credential);
using var sha256 = SHA256.Create();
var digest = sha256.ComputeHash(Encoding.ASCII.GetBytes(headerAndPayload));
var signature = (await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest)).Signature;
var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature)}";
Console.WriteLine($"JWT:\n\n{token}");
To generate token you can created your own implementation of CryptoProviderFactory in SigningCredentials.
var credentials = new SigningCredentials(new RsaSecurityKey(RSA.Create()), algorithm: SecurityAlgorithms.RsaSha256);
credentials.CryptoProviderFactory = _cryptoProviderFactory;
var descriptor = new SecurityTokenDescriptor
{
Subject = _owinContext.Request.User.Identity as ClaimsIdentity,
Expires = DateTime.UtcNow.AddHours(4),
SigningCredentials = credentials,
Audience = _configuration.AccessTokenAudience,
Issuer = _configuration.AccessTokenIssuer,
IssuedAt = DateTime.UtcNow,
};
var token = tokenHandler.CreateToken(descriptor);
SignatureProviderFactory implementation:
public class CustomCryptoProviderFactory : CryptoProviderFactory
{
private readonly CryptographyClient _cryptoClient;
public CustomCryptoProviderFactory()
{
var client = new KeyClient(new Uri("{url}"), new DefaultAzureCredential());
var key = client.GetKey("{key-name}");
_cryptoClient = new CryptographyClient(new Uri(key.Value.Key.Id), new DefaultAzureCredential());
}
public override SignatureProvider CreateForSigning(SecurityKey key, string algorithm)
{
return new CustomSignatureProvider(_cryptoClient, key, algorithm);
}
public override SignatureProvider CreateForSigning(SecurityKey key, string algorithm, bool cacheProvider)
{
return new CustomSignatureProvider(_cryptoClient, key, algorithm);
}
public override SignatureProvider CreateForVerifying(SecurityKey key, string algorithm)
{
return new CustomSignatureProvider(_cryptoClient, key, algorithm;
}
public override SignatureProvider CreateForVerifying(SecurityKey key, string algorithm, bool cacheProvider)
{
return new CustomSignatureProvider(_cryptoClient, key, algorithm);
}
}
CustomSignatureProvider implementation
public class CustomSignatureProvider : SignatureProvider
{
private readonly CryptographyClient _cryptoClient;
public CustomSignatureProvider(CryptographyClient cryptoClient,
SecurityKey key,
string algorithm)
: base(key, algorithm)
{
_cryptoClient = cryptoClient;
}
public override byte[] Sign(byte[] input)
{
var result = _cryptoClient.Sign(SignatureAlgorithm.RS256, GetSHA256(input));
return result.Signature;
}
public override bool Verify(byte[] input, byte[] signature)
{
var verificationResult = _cryptoClient.Verify(SignatureAlgorithm.RS256,
GetSHA256(input),
signature);
return verificationResult.IsValid;
}
protected override void Dispose(bool disposing)
{
}
private byte[] GetSHA256(byte[] input)
{
var sha = SHA256.Create();
return sha.ComputeHash(input);
}
}

Verifying a JWT signature using azure keyvault always returns false

I'm trying to verify a signature using Azure keyvault client, however it's always returning false.
I've managed to sign it using the KeyVaultClient.SignAsync method however when attempting to use KeyVaultClient.VerifyAsync the result always comes back false.
// Added for completeness, this method seems to be working correctly
private static async Task<string> SignJwt(KeyVaultClient client)
{
var claimsToSign = new[]
{
new Claim("sub", "UserId123"),
new Claim("custom", "MyValue")
};
var token = new JwtSecurityToken(
issuer: "AuthApp",
audience: "Consumer",
claims: claimsToSign
);
var header = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(new Dictionary<string, string>()
{
{JwtHeaderParameterNames.Alg, JsonWebKeySignatureAlgorithm.ES256},
{JwtHeaderParameterNames.Typ, "JWT"}
}));
var byteData = Encoding.UTF8.GetBytes(header + "." + token.EncodedPayload);
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var signature = await client.SignAsync(KeyVaultBaseUrl, KeyName, KeyVersion, JsonWebKeySignatureAlgorithm.ES256, digest);
var fullJwt = $"{header}.{token.EncodedPayload}.{Base64UrlEncoder.Encode(signature.Result)}";
return fullJwt;
}
// This always returns a false result
private static async Task<KeyVerifyResult> ValidateJwt()
{
// Example of a JWT produced by the SignJwt method
var jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVc2VySWQxMjMiLCJjdXN0b20iOiJNeVZhbHVlIiwiaXNzIjoiQXV0aEFwcCIsImF1ZCI6IkNvbnN1bWVyIn0.6tYkBcoFojJVJBhdNST49v4A3VWC1Rqizx_FzmSRICQubDEfXVopfP7Rs9tOBi9YzTCbod9o3hmHzIxANoIh7A";
var client = new KeyVaultClient(GetAccessTokenAsync, new HttpClient());
var jwtParts = jwt.Split('.');
var header = jwtParts[0];
var body = jwtParts[1];
var signature = Encoding.UTF8.GetBytes(Base64UrlEncoder.Decode(jwtParts[2]));
var byteData = Encoding.UTF8.GetBytes($"{header}.{body}");
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var verified = await client.VerifyAsync(KeyVaultBaseUrl, KeyName, KeyVersion, JsonWebKeySignatureAlgorithm.ES256, digest, signature);
return verified;
}
private static async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
{
var appCredentials = new ClientCredential(ClientId, ClientSecret);
var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
var result = await context.AcquireTokenAsync(resource, appCredentials);
return result.AccessToken;
}
Any ideas why ValidateJwt always returns false instead of true?
Solved it.
For anyone interested I wasn't properly base 64 encoding the signature in the SignJwt method:
Instead of using Base64UrlEncoder.Encode/Decode, use Convert.ToBase64String/FromBase64String
For the code snippet in the question to work, I needed to update the SignJwt and ValidateJwt methods to:
private static async Task<string> SignJwt(KeyVaultClient client)
{
var claimsToSign = new[]
{
new Claim("sub", "UserId123"),
new Claim("custom", "MyValue")
};
var token = new JwtSecurityToken(
issuer: "AuthApp",
audience: "Consumer",
claims: claimsToSign
);
var header = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new Dictionary<string, string>()
{
{JwtHeaderParameterNames.Alg, JsonWebKeySignatureAlgorithm.ES256},
{JwtHeaderParameterNames.Typ, "JWT"}
})));
var byteData = Encoding.UTF8.GetBytes(header + "." + token.EncodedPayload);
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var signature = await client.SignAsync(KeyVaultBaseUrl, KeyName, KeyVersion, JsonWebKeySignatureAlgorithm.ES256, digest);
var fullJwt = $"{header}.{token.EncodedPayload}.{Base64UrlEncoder.Encode(signature.Result)}";
return fullJwt;
}
private static async Task<KeyVerifyResult> ValidateJwt()
{
// Example of a JWT produced by the SignJwt method
var jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVc2VySWQxMjMiLCJjdXN0b20iOiJNeVZhbHVlIiwiaXNzIjoiQXV0aEFwcCIsImF1ZCI6IkNvbnN1bWVyIn0.6tYkBcoFojJVJBhdNST49v4A3VWC1Rqizx_FzmSRICQubDEfXVopfP7Rs9tOBi9YzTCbod9o3hmHzIxANoIh7A";
var client = new KeyVaultClient(GetAccessTokenAsync, new HttpClient());
var jwtParts = jwt.Split('.');
var header = jwtParts[0];
var body = jwtParts[1];
var signature = Encoding.UTF8.GetBytes(Convert.FromBase64String(jwtParts[2]));
var byteData = Encoding.UTF8.GetBytes($"{header}.{body}");
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var verified = await client.VerifyAsync(KeyVaultBaseUrl, KeyName, KeyVersion, JsonWebKeySignatureAlgorithm.ES256, digest, signature);
return verified;
}

Verifying JWT signed with the RS256 algorithm using public key in C#

Ok, I understand that the question I am asking may be pretty obvious, but unfortunately I lack the knowledge on this subject and this task seems to be quite tricky for me.
I have an id token (JWT) returned by OpenID Connect Provider. Here it is:
eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ
Its header and payload are decoded as this:
{
"kid":"1e9gdk7",
"alg":"RS256"
}.
{
"iss": "http://server.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"c_hash": "LDktKdoQak3Pk0cnXxCltA"
}
From the OIDC provider's discovery, I've got the public key (JWK):
{
"kty":"RSA",
"kid":"1e9gdk7",
"n":"w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ",
"e":"AQAB"
}
So, the question is how exactly in C# can I verify this JWT using the public key for the RS256 algorithm I've got? It would be awesome if there is a good tutorial describing this procedure explicitly. However, an example of how to do this using System.IdentityModel.Tokens.Jwt will also work fine.
UPDATE:
I understand, that I need to do something like the code below, but I have no idea where to get 'key' for calculating SHA256 hash.
string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
string[] tokenParts = tokenStr.Split('.');
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters() {
Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
Exponent = FromBase64Url("AQAB")
});
HMACSHA256 sha = new HMACSHA256(key);
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));
byte[] signature = rsa.Encrypt(hash, false);
string strSignature = Base64UrlEncode(signature);
if (String.Compare(strSignature, tokenParts[2], false) == 0)
return true;
Thanks to jwilleke, I have got a solution. To verify the RS256 signature of a JWT, it is needed to use the RSAPKCS1SignatureDeformatter class and its VerifySignature method.
Here is the exact code for my sample data:
string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
string[] tokenParts = tokenStr.Split('.');
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters() {
Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
Exponent = FromBase64Url("AQAB")
});
SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
if (rsaDeformatter.VerifySignature(hash, FromBase64Url(tokenParts[2])))
MessageBox.Show("Signature is verified");
//...
static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
Here is an example using IdentityModel.Tokens.Jwt for validation:
string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters()
{
Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
Exponent = FromBase64Url("AQAB")
});
var validationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = false,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
SecurityToken validatedSecurityToken = null;
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(tokenStr, validationParameters, out validatedSecurityToken);
JwtSecurityToken validatedJwt = validatedSecurityToken as JwtSecurityToken;
For anyone that is looking for a quick method to validate RS256 with a public key that has "-----BEGIN PUBLIC KEY-----"/"-----END PUBLIC KEY------"
Here are two methods with the help of BouncyCastle.
public bool ValidateJasonWebToken(string fullKey, string jwtToken)
{
try
{
var rs256Token = fullKey.Replace("-----BEGIN PUBLIC KEY-----", "");
rs256Token = rs256Token.Replace("-----END PUBLIC KEY-----", "");
rs256Token = rs256Token.Replace("\n", "");
Validate(jwtToken, rs256Token);
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
private void Validate(string token, string key)
{
var keyBytes = Convert.FromBase64String(key); // your key here
AsymmetricKeyParameter asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
RsaKeyParameters rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
RSAParameters rsaParameters = new RSAParameters
{
Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned(),
Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned()
};
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParameters);
var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = false,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
var handler = new JwtSecurityTokenHandler();
var result = handler.ValidateToken(token, validationParameters, out var validatedToken);
}
}
This is a combination of http://codingstill.com/2016/01/verify-jwt-token-signed-with-rs256-using-the-public-key/ and #olaf answer that uses system.IdentityModel.Tokens.Jwt
NET Core
To use this in a .NET core web api (.NET Framework see below) in a AddJwtBearer() auth flow I enhanced NvMat's great answer:
Very important is to not use the RSACryptoServiceProvider in an using statement.
private TokenValidationParameters GetTokenValidationParameters(string key)
{
var rs256Token = key.Value.Replace("-----BEGIN PUBLIC KEY-----", "");
rs256Token = rs256Token.Replace("-----END PUBLIC KEY-----", "");
rs256Token = rs256Token.Replace("\n", "");
var keyBytes = Convert.FromBase64String(rs256Token);
var asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
var rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
var rsaParameters = new RSAParameters
{
Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned(),
Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned()
};
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParameters);
var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = false,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new RsaSecurityKey(rsa),
};
return validationParameters;
}
Then you are able to use authentication in the startup like this:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.IncludeErrorDetails = true;
options.TokenValidationParameters = GetTokenValidationParameters(configuration["Key"]);
options.Audience = configuration["ClientId"];
});
NET Framework
It is also possible to use this approach in a .NET Framework web api project. All you have to do is add this line to your startup Configure() method:
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions()
{
TokenValidationParameters = GetTokenValidationParameters(ConfigurationManager.AppSettings["Key"])
});
One important thing: Make sure you use a verion >=5.0.0 of the JwtSecurityTokenHandler
I had problems with the 4.X.X versions.
You can do this very easily with Jwt.Net. This function will decode and verify the signature of a JWT and return the payload as a dictionary of claims:
private IDictionary<string, object> Decode(string token, string modulus, string exponent)
{
var urlEncoder = new JwtBase64UrlEncoder();
var rsaKey = RSA.Create();
rsaKey.ImportParameters(new RSAParameters() {
Modulus = urlEncoder.Decode(modulus),
Exponent = urlEncoder.Decode(exponent)
});
var claims = new JwtBuilder()
.WithAlgorithm(new RS256Algorithm(rsaKey))
.MustVerifySignature()
.Decode<IDictionary<string, object>>(token);
return claims;
}
Sample use:
string jwt = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
string modulus = "w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ";
string exponent = "AQAB";
try
{
IDictionary<string, object> claims = Decode(jwt, modulus, exponent);
}
catch (SignatureVerificationException)
{
// signature invalid, handle it here
}
.NET JWT Signature Verification using System.Security.Cryptography - No 3rd Party DLLs
var errorMessage = string.Empty;
// Google RSA well known Public Key data is available at https://accounts.google.com/.well-known/openid-configuration by navigating to the path described in the "jwks_uri" parameter.
// {
// e: "AQAB", // RSA Exponent
// n: "ya_7gV....", // RSA Modulus aka Well Known Public Key
// alg: "RS256" // RSA Algorithm
// }
var verified = VerifyJWT_RS256_Signature(
jwt: "oicjwt....",
publicKey: "ya_7gV....",
exponent: "AQAB",
errorMessage: out errorMessage);
if (!verified)
{
// TODO: log error:
// TODO: Do something
}
NOTE: The following method verifies OpenID Connect JWT Signatures signed with Asymetric RS256 keys. OpenID Connect providers may opt to use other versions of Asymetric keys or even Symetric keys like HS256. This method does not directly support other key types.
public static bool VerifyJWT_RS256_Signature(string jwt, string publicKey, string exponent, out string errorMessage)
{
if (string.IsNullOrEmpty(jwt))
{
errorMessage = "Error verifying JWT token signature: Javascript Web Token was null or empty.";
return false;
}
var jwtArray = jwt.Split('.');
if (jwtArray.Length != 3 && jwtArray.Length != 5)
{
errorMessage = "Error verifying JWT token signature: Javascript Web Token did not match expected format. Parts count was " + jwtArray.Length + " when it should have been 3 or 5.";
return false;
}
if (string.IsNullOrEmpty(publicKey))
{
errorMessage = "Error verifying JWT token signature: Well known RSA Public Key modulus was null or empty.";
return false;
}
if (string.IsNullOrEmpty(exponent))
{
errorMessage = "Error verifying JWT token signature: Well known RSA Public Key exponent was null or empty.";
return false;
}
try
{
string publicKeyFixed = (publicKey.Length % 4 == 0 ? publicKey : publicKey + "====".Substring(publicKey.Length % 4)).Replace("_", "/").Replace("-", "+");
var publicKeyBytes = Convert.FromBase64String(publicKeyFixed);
var jwtSignatureFixed = (jwtArray[2].Length % 4 == 0 ? jwtArray[2] : jwtArray[2] + "====".Substring(jwtArray[2].Length % 4)).Replace("_", "/").Replace("-", "+");
var jwtSignatureBytes = Convert.FromBase64String(jwtSignatureFixed);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters()
{
Modulus = publicKeyBytes,
Exponent = Convert.FromBase64String(exponent)
}
);
SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(jwtArray[0] + '.' + jwtArray[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
if (!rsaDeformatter.VerifySignature(hash, jwtSignatureBytes))
{
errorMessage = "Error verifying JWT token signature: hash did not match expected value.";
return false;
}
}
catch (Exception ex)
{
errorMessage = "Error verifying JWT token signature: " + ex.Message;
return false;
//throw ex;
}
errorMessage = string.Empty;
return true;
}
NOTE: Verifying the signature of an OpenID Connect JWT (Javascript Web Token) is only one necessary step of the JWT verification process. Make sure to set a NONCE value which your system can use to prevent Replay attacks. Make sure to validate each parameter of the JWT package for completeness and accuracy.
Try to use JwtUtils nuget package
It has pretty simple API:
string publicKey = "#MIIJKgIBAAKCAgEA9GF97STxVGbXpBFmudS/RRT58mfiR/+t2zb4f/uF3qmYb
yuJy2v8xOMbHvMkoKLPLc590zGV88HNvzJHkF5N5HWTB9ZZEWcehf6RcTA==";
if (JWT.RS256.ValidateSignature("{YOUR_JWT_TOKEN}", publicKey))
{
// Token signature valid
}

OAuth Bearer token Authentication is not passing signature validation

I get the following error on the token consumer. Any help resolving this will be most appreciated. Thanks.
"IDX10503: Signature validation failed.
Keys tried:
'System.IdentityModel.Tokens.SymmetricSecurityKey '. Exceptions
caught: 'System.InvalidOperationException: IDX10636:
SignatureProviderFactory.CreateForVerifying returned null for key:
'System.IdentityModel.Tokens.SymmetricSecurityKey',
signatureAlgorithm:
'http://www.w3.org/2001/04/xmldsig-more#hmac-sha256'. at
Microsoft.IdentityModel.Logging.LogHelper.Throw(String message, Type
exceptionType, EventLevel logLevel, Exception innerException) at
System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateSignature(Byte[]
encodedBytes, Byte[] signature, SecurityKey key, String algorithm) at
System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateSignature(String
token, TokenValidationParameters validationParameters) '. token:
'token info was here'"
Token Generation Code on OAuth server
using (var ctlr = new EntityController())
{
var authRepo = ctlr.GetAuthModelRepository();
string clientId;
ticket.Properties.Dictionary.TryGetValue(WebConstants.OwinContextProps.OAuthClientIdPropertyKey, out clientId);
if (string.IsNullOrWhiteSpace(clientId))
{
throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");
}
//audience record
var client = authRepo.FindAuthClientByOAuthClientID(clientId);
var issued = ticket.Properties.IssuedUtc;
var expires = ticket.Properties.ExpiresUtc;
var hmac = new HMACSHA256(Convert.FromBase64String(client.Secret));
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(hmac.Key),
Algorithms.HmacSha256Signature, Algorithms.Sha256Digest);
TokenValidationParameters validationParams =
new TokenValidationParameters()
{
ValidAudience = clientId,
ValidIssuer = _issuer,
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
RequireSignedTokens = true,
RequireExpirationTime = true,
ValidateIssuerSigningKey = true,
IssuerSigningToken = new BinarySecretSecurityToken(hmac.Key)
};
var jwtHandler = new JwtSecurityTokenHandler();
var jwt = new JwtSecurityToken(_issuer, clientId, ticket.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);
jwtOnTheWire = jwtHandler.WriteToken(jwt);
SecurityToken validatedToken = null;
jwtHandler.ValidateToken(jwtOnTheWire, validationParams,out validatedToken);
if (validatedToken == null)
return "token_validation_failed";
}
return jwtOnTheWire;
Token Consumption\validation ASP.Net 5 vNext site within Owin Startup.cs
public void ConfigureServices(IServiceCollection services)
services.ConfigureOAuthBearerAuthentication(config =>
{
//oauth validation
var clientSecret = "not the real secret";
var hmac = new HMACSHA256(Convert.FromBase64String(clientSecret));
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(hmac.Key), Algorithms.HmacSha256Signature, Algorithms.Sha256Digest);
config.TokenValidationParameters.ValidAudience = "myappname";
config.TokenValidationParameters.ValidIssuer = "mydomain.com";
config.TokenValidationParameters.RequireSignedTokens = true;
config.TokenValidationParameters.RequireExpirationTime = true;
config.TokenValidationParameters.ValidateLifetime = true;
config.TokenValidationParameters.ValidateIssuerSigningKey = true;
config.TokenValidationParameters.ValidateSignature = true;
config.TokenValidationParameters.ValidateAudience = true;
config.TokenValidationParameters.IssuerSigningKey = signingCredentials.SigningKey;
});
public void Configure(IApplicationBuilder app)
app.UseOAuthBearerAuthentication(config =>
{
config.AuthenticationScheme = "Bearer";
config.AutomaticAuthentication = true;
});
I was able to add my own signature validation to the TokenValidationParameters Then I compared the incoming Raw signature of the JWT to the compiled signature in this code and if it matches the signature is valid.
Why this didn't happen using the builtin signature validation is beyond me, maybe it's a possible bug in beta 6 of the vNext Identity token framework.
public void ConfigureServices(IServiceCollection services)
config.TokenValidationParameters.SignatureValidator =
delegate (string token, TokenValidationParameters parameters)
{
var clientSecret = "not the real secret";
var jwt = new JwtSecurityToken(token);
var hmac = new HMACSHA256(Convert.FromBase64String(clientSecret));
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(hmac.Key), SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest);
var signKey = signingCredentials.SigningKey as SymmetricSecurityKey;
var encodedData = jwt.EncodedHeader + "." + jwt.EncodedPayload;
var compiledSignature = Encode(encodedData, signKey.Key);
//Validate the incoming jwt signature against the header and payload of the token
if (compiledSignature != jwt.RawSignature)
{
throw new Exception("Token signature validation failed.");
}
return jwt;
};
Encode helper method
public string Encode(string input, byte[] key)
{
HMACSHA256 myhmacsha = new HMACSHA256(key);
byte[] byteArray = Encoding.UTF8.GetBytes(input);
MemoryStream stream = new MemoryStream(byteArray);
byte[] hashValue = myhmacsha.ComputeHash(stream);
return Base64UrlEncoder.Encode(hashValue);
}

Categories