HaoK / Random

Random stuff
1 stars 2 forks source link

BCL JWT #11

Open HaoK opened 1 year ago

HaoK commented 1 year ago

BCL JWT API:

Generic Jwt class composing a JwtHeader class (strong typed IDictionary<string, string>), and string Payload.


class JwtHeader : IDictionary<string, string>

class Jwt
{
    /// <summary>
    /// The metadata, including algorithm, type
    /// </summary>
    public JwtHeader Header { get; set; }

    /// <summary>
    /// The payload of the token.
    /// </summary>
    public string? Payload { get; set; }

    // The JWT signature is computed from the header and payload

    public static Task<string> CreateAsync(Jwt jwt, string algorithm, JsonWebKey? key);
    public static Task<Jwt?> ReadAsync(string jwt, string algorithm, JsonWebKey? key);
}

   // Usage examples

   // Create a standard jwt using HS256 with the specified JWK
   Jwt.CreateAsync(jwt, JWSAlg.HS256, jsonWebKey);

   // Manually create a HS256 jwt with a json payload
   new Jwt(JWSAlg.HS256) { Payload = jsonPayload };

   // Manually create a custom jwt using the header
   new Jwt(new JwtHeader("custom")) { Payload = customFormat }

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

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 implementation of HS256, with crypto/signature gen skipped
internal sealed class JwtAlgHS256 : IJwtAlgorithm
{
    public Task<string> CreateJwtAsync(Jwt jwt, JsonWebKey? key)
    {
        jwt.Header = new JwtHeader(JWSAlg.HS256);
        jwt.Header.Type = "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}");
    }

    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(new JwtHeader(header));
        data.Payload = Base64UrlEncoder.Decode(sections[1]);
        return Task.FromResult<Jwt?>(data);
    }

    public Task<bool> ValidateKeyAsync(JsonWebKey? key)
        => Task.FromResult(key != null && key.Kid != null);
HaoK commented 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; }
}