I'm using dotnet core 6.0 to generate a signed token. I have a .Net Framework 4.8 client that needs to be able to validate the token. I can easily validate the token using dotnet core 6.0, but the corresponding libraries in .Net Framework 4.8 do not appear to follow the same approach.
Here is my dotnet core 6.0 code that generates my RSA key pair:
using System.Security.Cryptography;
public class KeyPairGenerator
{
public static void GenerateKeyPair(string directoryPath)
{
using RSA rsa = RSA.Create();
File.WriteAllText(Path.Join(directoryPath, "/private.key"), Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()));
File.WriteAllText(Path.Join(directoryPath, "/public.key"), Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()));
}
}
Here is my dotnet core 6.0 code that generates a signed token using the private key generated above:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
public class JwtService
{
private readonly SigningCredentials credentials;
public JwtService(string privateKey)
{
RSA rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out int bytesRead);
RsaSecurityKey securityKey = new RsaSecurityKey(rsa);
credentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
}
public string GenerateJwt(List<Claim> claims, DateTime validStartTime, int validDuration)
{
JwtSecurityToken token = new JwtSecurityToken
(
issuer: "test-service",
audience: "test-service-client",
claims: claims,
signingCredentials: credentials,
notBefore: validStartTime,
expires: validStartTime.AddMilliseconds(validDuration)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
When I save the token and public key on my file system, I can easily validate the token in dotnet core 6.0 as follows:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
public class Program
{
public static void Main(string[] args)
{
string token = File.ReadAllText(args[0]);
string publicKey = File.ReadAllText(args[1]);
RSA rsa = RSA.Create();
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKey), out int bytesRead);
RsaSecurityKey securityKey = new RsaSecurityKey(rsa);
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidIssuer = "test-service",
ValidAudience = "test-service-client",
IssuerSigningKey = securityKey
};
ClaimsPrincipal principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
Console.WriteLine("Token is valid.");
Console.Write("Claim1: {0}\nClaim2: {1}\nClaim3: {2}\n",
principal.Claims.First(c => c.Type == "Claim1"),
principal.Claims.First(c => c.Type == "Claim2"),
principal.Claims.First(c => c.Type == "Claim3"));
}
}
I have not been able to figure out how to validate the token using .Net Framework 4.8. I've tried to use the Framework version of the System.Security.Cryptography.RSA class, but it does not appear to have the same methods. This is causing me to question my entire approach.
EDIT: To be specific, the .Net Framework 4.8 version of the RSA class does not support ImportSubjectPublicKeyInfo()
. I have not been able to find an alternate method to import the public key.
.NET Framework does not support the import of X.509/SPKI keys. The most convenient way to import is with C#/BouncyCastle, see e.g. BouncyCastle.Cryptography.
Your code remains essentially unchanged on adaptation, only the key import is modified. Note that the PEM encoded key is imported, i.e. the header and footer of an X.509/SPKI key must be added (see the sample code below):
using System;
using System.IO;
using System.Security.Claims;
using System.Security.Cryptography;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Linq;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Parameters;
...
// Encrypted token
string token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJDbGFpbTEiOiJDbGFpbTEgdmFsIiwiQ2xhaW0yIjoiQ2xhaW0yIHZhbCIsIkNsYWltMyI6IkNsYWltMyB2YWwiLCJuYmYiOjE2OTAyOTE4OTgsImV4cCI6MTY5MDM3ODI5OCwiaXNzIjoidGVzdC1zZXJ2aWNlIiwiYXVkIjoidGVzdC1zZXJ2aWNlLWNsaWVudCJ9.Nn0aHjbkNuRGPH5yjoX0wWNBrbTrNC2-Ud0JrAzKqoWO03CLkOil6VBdDP3bcTFH5mooOdt28bnM_jqRHWD3_xRZvtmpHBhgFAJW4793JzVAN2hkNWPVjsKjVior47AJHSCKTCia6yf8h_HsIKwZWQw1nplfepU_OfHTahtulywpZtBseTVQjsugHccaQ3pWRUcOhgJF8baTQcjXYCOckEg_CuEvaNQjAmnMY_jyYSCs4vLrpGmQsWVRXw_c23Yz2cqw_wdJRKrG9wBUEyCfQ9A4JnvsJnXjsSzKClj6ldjU6MtxQraSNTafMFmup_P9242vj7-DJ7SQ_cJC7ALLOQ";
// Key import (replaces the import via ImportSubjectPublicKeyInfo())
// requires BouncyCastle, e.g.: https://www.nuget.org/packages/BouncyCastle.Cryptography/2.2.1
// - Convert Base64 encoded DER encoded X.509/SPKI key into a PEM encoded key (i.e. add header and footer)
// - Read PEM encoded key with PEM Reader
// - Extract the RSA parameters and import them with ImportParameters()
string pkcsSpki = @"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+nJlBXpxUhL5+lpkCmI8gngOvMol+ODy6fKUBdhDRnqPBrPE+uh2idHEFrQow6lk6PfKKAG/jMtICLDggN6Fvl47vbN9cJI2yO03oq3rpJFbEt5pqf/IksU1kZ4cB90IzQfFdL/JMMaWxwrLACHGUZutqAHQthTw9uCV/sUG7SjFHASxZZ8kLVFxfol55p+c7bGaaOY/J5w6LhhHkPro8KHVNtM3UA1JEy5vIhUIkP6OT0KaVga6jeeXhGq1DJQGEeBdRSWG3uqBf7eh7Wg4sMhMW2ruIU4MgXCTtgg4P3vEK5Oa6ymgo/pBfBY1Gm7uXF6HzqNfNrT43D8cpNlDQIDAQAB
-----END PUBLIC KEY-----";
RSA rsa = RSA.Create();
PemReader pemReader = new PemReader(new StringReader(pkcsSpki));
AsymmetricKeyParameter asymmetricKeyParameter = (AsymmetricKeyParameter)pemReader.ReadObject();
RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters(asymmetricKeyParameter as RsaKeyParameters);
rsa.ImportParameters(rsaParameters);
// Validate token
RsaSecurityKey securityKey = new RsaSecurityKey(rsa);
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidIssuer = "test-service",
ValidAudience = "test-service-client",
IssuerSigningKey = securityKey
};
try
{
ClaimsPrincipal principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
Console.WriteLine("Token is valid. Validated token: " + validatedToken);
Console.Write("Claim1: {0}\nClaim2: {1}\nClaim3: {2}\n",
principal.Claims.First(c => c.Type == "Claim1"),
principal.Claims.First(c => c.Type == "Claim2"),
principal.Claims.First(c => c.Type == "Claim3")
);
}
catch(Exception e)
{
Console.WriteLine("Validation failed: " + e.Message);
}
The code successfully validates the token and outputs the claims. I ran the test with .NET Framework 4.8.1.