Open HaoK opened 1 year ago
/// <summary>
/// Constants for 'alg' https://www.rfc-editor.org/rfc/rfc7518#section-3.1
/// </summary>
public static class JWSAlg
{
public static readonly string HS256 = "HS256";
public static readonly string HS384 = "HS384";
public static readonly string HS512 = "HS512";
public static readonly string RS256 = "RS256";
public static readonly string RS384 = "RS384";
public static readonly string RS512 = "RS512";
public static readonly string ES256 = "ES256";
public static readonly string ES384 = "ES384";
public static readonly string ES512 = "ES512";
public static readonly string PS256 = "PS256";
public static readonly string PS384 = "PS384";
public static readonly string PS512 = "PS512";
public static readonly string None = "none";
}
// Base unit of data that we pass the header and payload to BCL
public class JwtData
{
/// <summary>
/// The metadata, including algorithm, type
/// </summary>
public IDictionary<string, string> Header { get; } = new Dictionary<string, string>();
/// <summary>
/// The payload of the token.
/// </summary>
public string Payload { get; set; };
// The signature is computed from the header and payload, BCL handles this part for us
}
// Base create API:
public static string Create(string alg, JwtData data)
{
// BCL looks up the alg, makes sure the appropriate key in header
// computes the signature using the key, and creates the token string
// Note: we can pass additional custom data in headers, and the payload is a string and opaque to BCL
return "header.payload.signature";
}
public static JwtData ReadJwt(string jwt)
{
// Unpack the jwt into back into the data structure, we will need to unpack the payload from the string (into claims)
}
// Today's identity model code:
public string CreateJwtIdentityModel()
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.CreateJwtSecurityToken(
Issuer,
Audience,
Identity,
NotBefore.UtcDateTime,
Expires.UtcDateTime,
//REVIEW: Do we want this configurable?
issuedAt: DateTime.UtcNow,
SigningCredentials);
return handler.WriteToken(jwtToken);
}
// Becomes
public string CreateJwtBcl()
{
// Stores the key in the right header for RS256 when Build() is called
var jwtData = new JwtBuilder(alg: JWSAlg.RS256, SigningCredentials)
// Or alternatively: new JwtBuilder(alg: JWSAlg.None) for unencrypted jwt, no key required)
.SetIssuer(Issuer)
.SetAudience(Audience)
.SetNotBefore(NotBefore)
.SetExpires(Expires)
.ImportPayload(Identity) // Turns the claims identity into the payload string
return BclApi.Create(JWSAlg.RS256, jwtData.Build());
}
// Reading side will return back a JwtData, which has headers and a string payload that we need to deserialize from?
JwtData BclApi.Read(string jwt);
// Jose example
// RSA signatures require a public and private RSA key pair,
// the public key must be made known to the JWS recipient to
// allow the signatures to be verified
RSAKey rsaJWK = new RSAKeyGenerator(2048)
.keyID("123")
.generate();
RSAKey rsaPublicJWK = rsaJWK.toPublicJWK();
// Create RSA-signer with the private key
JWSSigner signer = new RSASSASigner(rsaJWK);
// Prepare JWS object with simple string as payload
JWSObject jwsObject = new JWSObject(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(),
new Payload("In RSA we trust!"));
// Compute the RSA signature
jwsObject.sign(signer);
// To serialize to compact form, produces something like
// eyJhbGciOiJSUzI1NiJ9.SW4gUlNBIHdlIHRydXN0IQ.IRMQENi4nJyp4er2L
// mZq3ivwoAjqa1uUkSBKFIX7ATndFF5ivnt-m8uApHO4kfIFOrW7w2Ezmlg3Qd
// maXlS9DhN0nUk_hGI3amEjkKd0BWYCB8vfUbUv0XGjQip78AI4z1PrFRNidm7
// -jPDm5Iq0SZnjKjCNS5Q15fokXZc8u0A
String s = jwsObject.serialize();
// To parse the JWS and verify it, e.g. on client-side
jwsObject = JWSObject.parse(s);
JWSVerifier verifier = new RSASSAVerifier(rsaPublicJWK);
assertTrue(jwsObject.verify(verifier));
assertEquals("In RSA we trust!", jwsObject.getPayload().toString());
// Algorithms will be registered via their 'alg' header value, i.e. 'HS256, ES256, None' and specified by name
internal static class BclJwt
{
// TODO: make this like IOptions, and have a IJwtAlgorithmManager which loads IEnumerable<IJwtAlgorithm>, and make Name part of IJwtAlgorithm and enforce uniqueness
public static IDictionary<string, IJwtAlgorithm> Algorithms { get; } = new Dictionary<string, IJwtAlgorithm>();
static BclJwt()
{
Algorithms[JWSAlg.None] = new JwtAlgNone();
Algorithms[JWSAlg.HS256] = new JwtAlgHS256();
}
// The main public API that ASP.Net will call to create Jwt tokens specifying an algorithm and a key to use
public static Task<string> CreateJwtAsync(Jwt jwt, string algorithm, JsonWebKey? key)
{
if (!Algorithms.ContainsKey(algorithm))
{
throw new InvalidOperationException($"Unknown algorithm: {algorithm}.");
}
return Algorithms[algorithm].CreateJwtAsync(jwt, key);
}
// The main public API that ASP.Net will call to read Jwt tokens specifying an algorithm and a key to use
public static Task<Jwt?> ReadJwtAsync(string jwt, string algorithm, JsonWebKey? key)
{
if (!Algorithms.ContainsKey(algorithm))
{
throw new InvalidOperationException($"Unknown algorithm: {algorithm}.");
}
return Algorithms[algorithm].ReadJwtAsync(jwt, key);
}
}
// BCL will implement these for the various Jwt algorithms
internal interface IJwtAlgorithm
{
/// <summary>
/// Ensures the necessary data for this Jwt Algorithm is contained in the key (if provided).
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public abstract Task<bool> ValidateKeyAsync(JsonWebKey? key);
/// <summary>
/// Create a Jwt using the specified key for this algorithm.
/// </summary>
/// <param name="jwt"></param>
/// <param name="key"></param>
/// <returns></returns>
public abstract Task<string> CreateJwtAsync(Jwt jwt, JsonWebKey? key);
/// <summary>
/// Attempts to decode the jwtToken using the specified key for this algorithm.
/// </summary>
/// <param name="jwtToken">The jwtToken string.</param>
/// <param name="key">The JWK used for signing.</param>
/// <returns>The JWT data.</returns>
public abstract Task<Jwt?> ReadJwtAsync(string jwtToken, JsonWebKey? key);
}
// Example alg none that just uses the payload string as the jwt
internal class JwtAlgNone : IJwtAlgorithm
{
public Task<string> CreateJwtAsync(Jwt jwt, JsonWebKey? key)
// Just send the payload as the jwt
=> Task.FromResult(jwt.Payload ?? string.Empty);
public Task<Jwt?> ReadJwtAsync(string jwtToken, JsonWebKey? key)
{
var data = new Jwt();
data.Header["alg"] = JWSAlg.None;
data.Header["typ"] = "JWT";
data.Payload = jwtToken;
return Task.FromResult<Jwt?>(data);
}
public Task<bool> ValidateKeyAsync(JsonWebKey? key)
=> Task.FromResult(true);
}
// Example alg which should use the JWK to do HS256 signature, but just sends the keyid as the signature instead
internal class JwtAlgHS256 : IJwtAlgorithm
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public Task<string> CreateJwtAsync(Jwt jwt, JsonWebKey? key)
{
jwt.Header["alg"] = JWSAlg.HS256;
jwt.Header["typ"] = "JWT";
// TODO: This should actually do HS256 using the key to sign, instead of just sending the key as the signature
return Task.FromResult($"{Base64UrlEncoder.Encode(JsonSerializer.Serialize(jwt.Header))}.{Base64UrlEncoder.Encode(jwt.Payload)}.{key!.Kid}");
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public Task<Jwt?> ReadJwtAsync(string jwtToken, JsonWebKey? key)
{
if (key == null)
{
return Task.FromResult<Jwt?>(null);
}
var sections = jwtToken.Split('.');
if (sections.Length != 3)
{
// Expected 3 sections
return Task.FromResult<Jwt?>(null);
}
var header = JsonSerializer.Deserialize<IDictionary<string, string>>(Base64UrlEncoder.Decode(sections[0]));
// TODO: Actually do HS256 signing
if (header?["alg"] != "HS256" || header?["typ"] != "JWT" || sections[2] != key.Kid)
{
// Expected HS256 alg and key to be the last section
return Task.FromResult<Jwt?>(null);
}
var data = new Jwt();
data.Header = header;
data.Payload = Base64UrlEncoder.Decode(sections[1]);
return Task.FromResult<Jwt?>(data);
}
public Task<bool> ValidateKeyAsync(JsonWebKey? key)
=> Task.FromResult(key != null);
}
/// <summary>
/// https://www.rfc-editor.org/rfc/rfc7517
/// </summary>
public sealed class JsonWebKey
{
/// <summary>
///
/// </summary>
/// <param name="kty"></param>
public JsonWebKey(string kty) => Kty = kty;
/// <summary>
///
/// </summary>
public IDictionary<string, string> AdditionalData { get; } = new Dictionary<string, string>();
/// <summary>
///
/// </summary>
public string? Alg { get; set; }
/// <summary>
///
/// </summary>
public string? Kid { get; set; }
/// <summary>
///
/// </summary>
public IList<string>? KeyOps { get; set; }
/// <summary>
///
/// </summary>
public string Kty { get; set; }
/// <summary>
///
/// </summary>
public string? Use { get; set; }
/// <summary>
///
/// </summary>
public string? X5c { get; set; }
/// <summary>
///
/// </summary>
public string? X5t { get; set; }
/// <summary>
///
/// </summary>
public string? X5tS256 { get; set; }
/// <summary>
///
/// </summary>
public string? X5u { get; set; }
//public string Crv { get; set; }
//public string D { get; set; }
//public string E { get; set; }
//public string Dp { get; set; }
//public string Dq { get; set; }
//public string K { get; set; }
//public string N { get; set; }
//public string P { get; set; }
//public string Q { get; set; }
//public string Qi { get; set; }
//public string Y { get; set; }
}
internal sealed class Jwt
{
/// <summary>
/// The metadata, including algorithm, type
/// </summary>
public IDictionary<string, string> Header { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The payload of the token.
/// </summary>
public string? Payload { get; set; }
}
BCL JWT API:
Generic Jwt class composing a JwtHeader class (strong typed
IDictionary<string, string>
), and string Payload.JsonWebKey
(string dictionary bag) as the placeholder signing key abstraction currently (TBD to design something akin to SigningKey abstraction in identity model)IJwtAlgorithm
to plug in jwt algorithms