Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: enrich validator api #1958

Merged
merged 10 commits into from
Feb 26, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;

namespace Altinn.ApiClients.Dialogporten.Common;

internal static class ClaimsPrincipalExtensions
{
public static bool VerifyDialogId(this ClaimsPrincipal claimsPrincipal, Guid dialogId)
{
const string dialogIdClaimName = "i";
return claimsPrincipal.TryGetClaimValue(dialogIdClaimName, out var dialogIdString)
&& Guid.TryParse(dialogIdString, out var dialogIdClaim)
&& dialogId == dialogIdClaim;
}

public static bool VerifyActions(this ClaimsPrincipal claimsPrincipal, params string[] requiredActions)
{
const string actionsClaimName = "a";
const string actionSeparator = ";";
if (requiredActions.Length == 0)
{
return true;
}

if (!claimsPrincipal.TryGetClaimValue(actionsClaimName, out var actions))
{
return false;
}

var requiredActionsLength = requiredActions
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
var intersectionLength = actions
.Split(actionSeparator)
.Intersect(requiredActions, StringComparer.OrdinalIgnoreCase)
.Count();
return intersectionLength == requiredActionsLength;
}

public static bool VerifyExpirationTime(this ClaimsPrincipal claimsPrincipal, IClock clock, TimeSpan clockSkew)
{
const string expirationTimeClaimName = "exp";
var exp = claimsPrincipal.TryGetClaimValue(expirationTimeClaimName, out var exps)
&& long.TryParse(exps, out var expl)
? DateTimeOffset.FromUnixTimeSeconds(expl).Add(clockSkew)
: DateTimeOffset.MinValue;
return clock.UtcNow <= exp;
}

public static bool VerifyNotValidBefore(this ClaimsPrincipal claimsPrincipal, IClock clock, TimeSpan clockSkew)
{
const string notValidBeforeClaimName = "nbf";
var nbf = claimsPrincipal.TryGetClaimValue(notValidBeforeClaimName, out var nbfs)
&& long.TryParse(nbfs, out var nbfl)
? DateTimeOffset.FromUnixTimeSeconds(nbfl).Add(-clockSkew)
: DateTimeOffset.MaxValue;
return nbf <= clock.UtcNow;
}

public static bool TryGetClaimValue(this ClaimsPrincipal claimsPrincipal, string claimType,
[NotNullWhen(true)] out string? value)
{
value = claimsPrincipal.FindFirst(claimType)?.Value;
return value is not null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

<ItemGroup>
<InternalsVisibleTo Include="Digdir.Library.Dialogporten.WebApiClient.Unit.Tests"/>
<InternalsVisibleTo Include="Digdir.Tool.Dialogporten.Benchmarks"/>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2"/>
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ namespace Altinn.ApiClients.Dialogporten;

public interface IDialogTokenValidator
{
IValidationResult Validate(ReadOnlySpan<char> token);
IValidationResult Validate(ReadOnlySpan<char> token,
Guid? dialogId = null,
string[]? requiredActions = null,
DialogTokenValidationParameters? options = null);
}

public class DialogTokenValidationParameters
{
public static DialogTokenValidationParameters Default { get; } = new();
public bool ValidateLifetime { get; set; } = true;
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(10);
}

public interface IValidationResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ public DialogTokenValidator(IEdDsaSecurityKeysCache publicKeysCache, IClock cloc
_clock = clock;
}

public IValidationResult Validate(ReadOnlySpan<char> token)
public IValidationResult Validate(ReadOnlySpan<char> token,
Guid? dialogId = null,
string[]? requiredActions = null,
DialogTokenValidationParameters? options = null)
{
const string tokenPropertyName = "token";
options ??= DialogTokenValidationParameters.Default;
var validationResult = new DefaultValidationResult();
Span<byte> tokenDecodeBuffer = stackalloc byte[Base64Url.GetMaxDecodedLength(token.Length)];

Expand All @@ -39,9 +43,24 @@ public IValidationResult Validate(ReadOnlySpan<char> token)
validationResult.AddError(tokenPropertyName, "Invalid signature");
}

if (!VerifyExpiration(decodedTokenParts))
if (options.ValidateLifetime && !claimsPrincipal.VerifyNotValidBefore(_clock, options.ClockSkew))
{
validationResult.AddError(tokenPropertyName, "Token has expired");
validationResult.AddError(tokenPropertyName, "Invalid nbf");
}

if (options.ValidateLifetime && !claimsPrincipal.VerifyExpirationTime(_clock, options.ClockSkew))
{
validationResult.AddError(tokenPropertyName, "Invalid exp");
}

if (dialogId.HasValue && !validationResult.ClaimsPrincipal.VerifyDialogId(dialogId.Value))
{
validationResult.AddError(tokenPropertyName, "Invalid dialog ID");
}

if (requiredActions is not null && !validationResult.ClaimsPrincipal.VerifyActions(requiredActions))
{
validationResult.AddError(tokenPropertyName, "Invalid actions");
}

return validationResult;
Expand Down Expand Up @@ -145,28 +164,6 @@ private bool VerifySignature(
&& SignatureAlgorithm.Ed25519.Verify(publicKey, signedPart, decodedTokenParts.Signature);
}

private bool VerifyExpiration(JwksTokenParts<byte> decodedTokenParts)
{
const string expiresPropertyName = "exp";
if (!TryGetPropertyValue(decodedTokenParts.Body, expiresPropertyName, out var expiresSpan))
{
return false;
}

if (!Utf8Parser.TryParse(expiresSpan, out long expiresUnixTimeSeconds, out var bytesConsumed))
{
return false;
}

if (bytesConsumed != expiresSpan.Length)
{
return false;
}

var expires = DateTimeOffset.FromUnixTimeSeconds(expiresUnixTimeSeconds);
return expires >= _clock.UtcNow;
}

private static bool TryDecodePart(ReadOnlySpan<char> tokenPart, Span<byte> buffer, out ReadOnlySpan<byte> span, out int length)
{
span = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ internal sealed class DefaultEdDsaSecurityKeysCache : IEdDsaSecurityKeysCache
private List<PublicKeyPair> _publicKeys = [];
public ReadOnlyCollection<PublicKeyPair> PublicKeys => _publicKeys.AsReadOnly();
internal void SetPublicKeys(List<PublicKeyPair> publicKeys) => _publicKeys = publicKeys;

public DefaultEdDsaSecurityKeysCache() { }
internal DefaultEdDsaSecurityKeysCache(IEnumerable<PublicKeyPair> publicKeys)
{
_publicKeys = publicKeys.ToList();
}
}

internal sealed class EdDsaSecurityKeysCacheService : BackgroundService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<ProjectReference Include="..\Digdir.Domain.Dialogporten.Application\Digdir.Domain.Dialogporten.Application.csproj"/>
<ProjectReference Include="..\Digdir.Library.Dialogporten.WebApiClient\Digdir.Library.Dialogporten.WebApiClient.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Digdir.Tool.Dialogporten.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
using BenchmarkDotNet.Running;
using Digdir.Tool.Dialogporten.Benchmarks;
BenchmarkRunner.Run<TokenGenerationBenchmark>();
BenchmarkRunner.Run<TokenValidatorBenchmarks>();
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Buffers.Text;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Altinn.ApiClients.Dialogporten;
using Altinn.ApiClients.Dialogporten.Common;
using Altinn.ApiClients.Dialogporten.Services;
using BenchmarkDotNet.Attributes;
using NSec.Cryptography;

namespace Digdir.Tool.Dialogporten.Benchmarks;

[MemoryDiagnoser]
[SuppressMessage("Performance", "CA1822:Mark members as static")]
public class TokenValidatorBenchmarks
{
private const string ValidTimeStampString = "2025-02-14T09:00:00Z";
private const string DialogToken =
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRwLXN0YWdpbmctMjQwMzIyLW81eW1uIn0.eyJqdGkiOiIzOGNmZGNiOS0zODhiLTQ3YjgtYTFiZi05ZjE1YjI4MTk4OTQiLCJjIjoidXJuOmFsdGlubjpwZXJzb246aWRlbnRpZmllci1ubzoxNDg4NjQ5ODIyNiIsImwiOjMsInAiOiJ1cm46YWx0aW5uOnBlcnNvbjppZGVudGlmaWVyLW5vOjE0ODg2NDk4MjI2IiwicyI6InVybjphbHRpbm46cmVzb3VyY2U6ZGFnbC1jb3JyZXNwb25kZW5jZSIsImkiOiIwMTk0ZmU4Mi05MjgwLTc3YTUtYTdjZC01ZmYwZTZhNmZhMDciLCJhIjoicmVhZCIsImlzcyI6Imh0dHBzOi8vcGxhdGZvcm0udHQwMi5hbHRpbm4ubm8vZGlhbG9ncG9ydGVuL2FwaS92MSIsImlhdCI6MTczOTUyMzM2NywibmJmIjoxNzM5NTIzMzY3LCJleHAiOjE3Mzk1MjM5Njd9.O_f-RJhRPT7B76S7aOGw6jfxKDki3uJQLLC8nVlcNVJWFIOQUsy6gU4bG1ZdqoMBZPvb2K2X4I5fGpHW9dQMAA";
private static readonly PublicKeyPair[] ValidPublicKeyPairs =
[
new("dp-staging-240322-o5ymn", ToPublicKey("zs9hR9oqgf53th2lTdrBq3C1TZ9UlR-HVJOiUpWV63o")),
new("dp-staging-240322-rju3g", ToPublicKey("23Sijekv5ATW4sSEiRPzL_rXH-zRV8MK8jcs5ExCmSU"))
];
private static readonly DialogTokenValidator _sut = new(
new DefaultEdDsaSecurityKeysCache(ValidPublicKeyPairs),
new BenchmarkClock(ValidTimeStampString));


[Benchmark]
public IValidationResult ValidateDialogToken() => _sut.Validate(DialogToken);

private static PublicKey ToPublicKey(string key)
=> PublicKey.Import(SignatureAlgorithm.Ed25519, Base64Url.DecodeFromChars(key), KeyBlobFormat.RawPublicKey);

private sealed class BenchmarkClock(DateTimeOffset utcNow) : IClock
{
public DateTimeOffset UtcNow { get; } = utcNow;
public BenchmarkClock(string input) : this(DateTimeOffset.Parse(input, CultureInfo.InvariantCulture)) { }
}
}

Loading