Skip to content

Commit

Permalink
Scoped account signing keys (#18)
Browse files Browse the repository at this point in the history
Enhancement to add scoped account keys to the account.   

e.g.

```
            claimsAccount.Account.SigningKeys.Add(new NatsAccountScopedSigningKey
            {
                 Key = kpScopedAccountSigningKey.GetPublicKey(),
                 Role = "chat_user",
                 Template = new NatsUser()
                 {

                 }
            });

```

The list of signing keys still also supports strings via
NatsAccountSigningKey which has implicit string operators.

Tested using $SYS.REQ.CLAIMS.UPDATE to push the new account, and then
using the new SetScoped method from NatsUserClaims signing with the
scoped account signing key.
  • Loading branch information
darkwatchuk authored Sep 13, 2024
1 parent 5a29473 commit b7be217
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 4 deletions.
42 changes: 41 additions & 1 deletion NATS.Jwt.Tests/Models/NatsAccountClaimsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace NATS.Jwt.Tests.Models;

public class NatsAccountClaimsTests
{


[Fact]
public void SerializeDeserialize_FullNatsAccountClaims_ShouldSucceed()
{
Expand Down Expand Up @@ -51,7 +53,7 @@ public void SerializeDeserialize_FullNatsAccountClaims_ShouldSucceed()
MaxBytesRequired = true,
},
SigningKeys =
new List<string>
new List<NatsAccountSigningKey>
{
"SKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"SKBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
Expand Down Expand Up @@ -265,4 +267,42 @@ public void Serialize_ShouldOmitDefaultValues()
Assert.DoesNotContain("\"nbf\"", json);
Assert.DoesNotContain("\"jti\"", json);
}

[Fact]
// Test just the SigningKeys part of json serialization/deserialization
public void SerializeDeserialize_NatsAccountSigningClaims_ShouldSucceed()
{
var claims = new NatsAccountClaims
{
Account = new NatsAccount
{
SigningKeys =
new List<NatsAccountSigningKey>
{
"SKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"SKBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
new NatsAccountScopedSigningKey
{
Key = "AC***SIGNINGKEY",
Role = "chat_user",
Template = new NatsUser
{
}
}
}
}
};

string json = JsonSerializer.Serialize(claims);
var deserialized = JsonSerializer.Deserialize<NatsAccountClaims>(json);

Assert.Equal(claims.Account.SigningKeys[0], deserialized.Account.SigningKeys[0]);
Assert.Equal(claims.Account.SigningKeys[1], deserialized.Account.SigningKeys[1]);

var claimsScopedKey = (NatsAccountScopedSigningKey)claims.Account.SigningKeys[2];
var deserializedScopedKey = (NatsAccountScopedSigningKey)deserialized.Account.SigningKeys[2];

Assert.Equal(claimsScopedKey.Key, deserializedScopedKey.Key);
Assert.Equal(claimsScopedKey.Role, deserializedScopedKey.Role);
}
}
104 changes: 104 additions & 0 deletions NATS.Jwt/Internal/NatsAccountSigningKeyConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using NATS.Jwt.Models;

namespace NATS.Jwt.Internal
{
/// <summary>
/// .
/// </summary>
internal class NatsAccountSigningKeyConverter : JsonConverter<List<NatsAccountSigningKey>>
{
/// <summary>
/// Converts a JSON representation of SigningKeys to their correct type.
/// </summary>
/// <param name="reader">The Utf8JsonReader used to read the JSON data.</param>
/// <param name="typeToConvert">The type of the object to be converted.</param>
/// <param name="options">The JsonSerializerOptions to be used during serialization.</param>
/// <returns>A list of NatsAccountSigningKey.</returns>
/// <exception cref="JsonException">yeah, this isn't done yet.</exception>
public override List<NatsAccountSigningKey>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}
else if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("Expected Null or Array");
}

List<NatsAccountSigningKey> results = [];

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return results;
}

if (reader.TokenType == JsonTokenType.String)
{
string? simpleSigningKey = reader.GetString();
if (simpleSigningKey != null)
{
results.Add(simpleSigningKey);
}
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
NatsAccountScopedSigningKey? scopedSigningKey = JsonSerializer.Deserialize(ref reader, JsonContext.Default.NatsAccountScopedSigningKey);
if (scopedSigningKey != null)
{
results.Add(scopedSigningKey);
}
}
else
{
throw new JsonException();
}
}

throw new JsonException();
}

/// <summary>
/// Writes the List of SigningKeys to its JSON representation.
/// </summary>
/// <param name="writer">The Utf8JsonWriter used to write the JSON data.</param>
/// <param name="value">The List of NatsAccountSigningKeys to be written.</param>
/// <param name="options">The JsonSerializerOptions to be used during serialization.</param>
public override void Write(Utf8JsonWriter writer, List<NatsAccountSigningKey> value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
writer.WriteStartArray();

foreach (NatsAccountSigningKey sk in value)
{
if (sk.GetType() == typeof(NatsAccountSigningKey))
{
writer.WriteStringValue((string)sk);
}
else if (sk.GetType() == typeof(NatsAccountScopedSigningKey))
{
JsonSerializer.Serialize(writer, (NatsAccountScopedSigningKey)sk, JsonContext.Default.NatsAccountScopedSigningKey);
}
}

writer.WriteEndArray();
}
}
}
}
2 changes: 2 additions & 0 deletions NATS.Jwt/JsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ namespace NATS.Jwt;
[JsonSerializable(typeof(NatsAuthorizationResponseClaims))]
[JsonSerializable(typeof(NatsAuthorizationResponse))]
[JsonSerializable(typeof(NatsGenericFieldsClaims))]
[JsonSerializable(typeof(NatsAccountSigningKey))]
[JsonSerializable(typeof(NatsAccountScopedSigningKey))]
internal sealed partial class JsonContext : JsonSerializerContext
{
}
4 changes: 3 additions & 1 deletion NATS.Jwt/Models/NatsAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Text.Json.Serialization;
using NATS.Jwt.Internal;

namespace NATS.Jwt.Models;

Expand Down Expand Up @@ -49,7 +50,8 @@ public record NatsAccount : NatsGenericFields
/// </value>
[JsonPropertyName("signing_keys")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public List<string> SigningKeys { get; set; }
[JsonConverter(typeof(NatsAccountSigningKeyConverter))]
public List<NatsAccountSigningKey> SigningKeys { get; set; }

/// <summary>
/// Gets or sets the dictionary of revocations for the account.
Expand Down
44 changes: 44 additions & 0 deletions NATS.Jwt/Models/NatsAccountScopedSigningKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;

namespace NATS.Jwt.Models
{
/// <summary>
/// Represents an Account Scoped Signing Key.
/// </summary>
public record NatsAccountScopedSigningKey : NatsAccountSigningKey
{
/// <summary>
/// Gets or sets the kind of scoped key.
/// </summary>
[JsonPropertyName("kind")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Kind { get; set; } = "user_scope";

/// <summary>
/// Gets or sets the Key, usually the public key.
/// </summary>
[JsonPropertyName("key")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Key { get; set; }

/// <summary>
/// Gets or sets Role.
/// </summary>
[JsonPropertyName("role")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Role { get; set; }

/// <summary>
/// Gets or sets the User Template to use.
/// </summary>
[JsonPropertyName("template")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public NatsUser Template { get; set; } = new();
}
}
41 changes: 41 additions & 0 deletions NATS.Jwt/Models/NatsAccountSigningKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) The NATS Authors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Text;

namespace NATS.Jwt.Models
{
/// <summary>
/// Represents an simple signing Key.
/// </summary>
public record NatsAccountSigningKey
{
private string _signingKey;

/// <summary>
/// An implicit operator to convert to a string.
/// </summary>
/// <param name="sk">A signing key.</param>
public static implicit operator string(NatsAccountSigningKey sk) => sk._signingKey;

/// <summary>
/// An implicit operator to convert from a string.
/// </summary>
/// <param name="value">A signing key.</param>
public static implicit operator NatsAccountSigningKey(string value)
{
return new NatsAccountSigningKey() { _signingKey = value };
}

/// <summary>
/// Returns the signing key as a string.
/// </summary>
/// <returns>The basic signing key.</returns>
public override string ToString()
{
return _signingKey;
}
}
}
17 changes: 15 additions & 2 deletions NATS.Jwt/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,23 @@ NATS.Jwt.Models.NatsAccount.Mappings.get -> System.Collections.Generic.Dictionar
NATS.Jwt.Models.NatsAccount.Mappings.set -> void
NATS.Jwt.Models.NatsAccount.Revocations.get -> System.Collections.Generic.Dictionary<string!, long>!
NATS.Jwt.Models.NatsAccount.Revocations.set -> void
NATS.Jwt.Models.NatsAccount.SigningKeys.get -> System.Collections.Generic.List<string!>!
NATS.Jwt.Models.NatsAccount.SigningKeys.get -> System.Collections.Generic.List<NATS.Jwt.Models.NatsAccountSigningKey!>!
NATS.Jwt.Models.NatsAccount.SigningKeys.set -> void
NATS.Jwt.Models.NatsAccount.Trace.get -> NATS.Jwt.Models.NatsMsgTrace!
NATS.Jwt.Models.NatsAccount.Trace.set -> void
NATS.Jwt.Models.NatsAccountClaims
NATS.Jwt.Models.NatsAccountClaims.Account.get -> NATS.Jwt.Models.NatsAccount!
NATS.Jwt.Models.NatsAccountClaims.Account.set -> void
NATS.Jwt.Models.NatsAccountScopedSigningKey
NATS.Jwt.Models.NatsAccountScopedSigningKey.Key.get -> string!
NATS.Jwt.Models.NatsAccountScopedSigningKey.Key.set -> void
NATS.Jwt.Models.NatsAccountScopedSigningKey.Kind.get -> string!
NATS.Jwt.Models.NatsAccountScopedSigningKey.Kind.set -> void
NATS.Jwt.Models.NatsAccountScopedSigningKey.Role.get -> string!
NATS.Jwt.Models.NatsAccountScopedSigningKey.Role.set -> void
NATS.Jwt.Models.NatsAccountScopedSigningKey.Template.get -> NATS.Jwt.Models.NatsUser!
NATS.Jwt.Models.NatsAccountScopedSigningKey.Template.set -> void
NATS.Jwt.Models.NatsAccountSigningKey
NATS.Jwt.Models.NatsActivation
NATS.Jwt.Models.NatsActivation.ImportSubject.get -> string!
NATS.Jwt.Models.NatsActivation.ImportSubject.set -> void
Expand Down Expand Up @@ -393,6 +403,9 @@ NATS.Jwt.NatsJwt.NewOperatorClaims(string! subject) -> NATS.Jwt.Models.NatsOpera
NATS.Jwt.NatsJwt.NewUserClaims(string! subject) -> NATS.Jwt.Models.NatsUserClaims!
NATS.Jwt.NatsJwtException
NATS.Jwt.NatsJwtException.NatsJwtException(string! message) -> void
static readonly NATS.Jwt.NatsJwt.NatsJwtHeader -> NATS.Jwt.Models.JwtHeader!
static NATS.Jwt.EncodingUtils.FromBase64UrlEncoded(string! encodedString) -> string!
static NATS.Jwt.EncodingUtils.ToBase64UrlEncoded(byte[]! bytes) -> string!
static readonly NATS.Jwt.NatsJwt.NatsJwtHeader -> NATS.Jwt.Models.JwtHeader!
override NATS.Jwt.Models.NatsAccountSigningKey.ToString() -> string!
static NATS.Jwt.Models.NatsAccountSigningKey.implicit operator NATS.Jwt.Models.NatsAccountSigningKey!(string! value) -> NATS.Jwt.Models.NatsAccountSigningKey!
static NATS.Jwt.Models.NatsAccountSigningKey.implicit operator string!(NATS.Jwt.Models.NatsAccountSigningKey! sk) -> string!

0 comments on commit b7be217

Please sign in to comment.