Skip to content

Commit

Permalink
chore: enrich validator api (#1958)
Browse files Browse the repository at this point in the history
  • Loading branch information
MagnusSandgren authored Feb 26, 2025
1 parent c2de39d commit 739380a
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 66 deletions.
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

0 comments on commit 739380a

Please sign in to comment.