diff --git a/src/Digdir.Library.Dialogporten.WebApiClient/Common/ClaimsPrincipalExtensions.cs b/src/Digdir.Library.Dialogporten.WebApiClient/Common/ClaimsPrincipalExtensions.cs
new file mode 100644
index 000000000..5a6818763
--- /dev/null
+++ b/src/Digdir.Library.Dialogporten.WebApiClient/Common/ClaimsPrincipalExtensions.cs
@@ -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;
+ }
+}
diff --git a/src/Digdir.Library.Dialogporten.WebApiClient/Digdir.Library.Dialogporten.WebApiClient.csproj b/src/Digdir.Library.Dialogporten.WebApiClient/Digdir.Library.Dialogporten.WebApiClient.csproj
index 79016e963..77306e2d1 100644
--- a/src/Digdir.Library.Dialogporten.WebApiClient/Digdir.Library.Dialogporten.WebApiClient.csproj
+++ b/src/Digdir.Library.Dialogporten.WebApiClient/Digdir.Library.Dialogporten.WebApiClient.csproj
@@ -34,6 +34,7 @@
+
diff --git a/src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs b/src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs
index 1b175f10e..ebcfadbb2 100644
--- a/src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs
+++ b/src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs
@@ -5,7 +5,17 @@ namespace Altinn.ApiClients.Dialogporten;
public interface IDialogTokenValidator
{
- IValidationResult Validate(ReadOnlySpan token);
+ IValidationResult Validate(ReadOnlySpan 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
diff --git a/src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs b/src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs
index 9d89ccd91..ac1968498 100644
--- a/src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs
+++ b/src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs
@@ -21,9 +21,13 @@ public DialogTokenValidator(IEdDsaSecurityKeysCache publicKeysCache, IClock cloc
_clock = clock;
}
- public IValidationResult Validate(ReadOnlySpan token)
+ public IValidationResult Validate(ReadOnlySpan token,
+ Guid? dialogId = null,
+ string[]? requiredActions = null,
+ DialogTokenValidationParameters? options = null)
{
const string tokenPropertyName = "token";
+ options ??= DialogTokenValidationParameters.Default;
var validationResult = new DefaultValidationResult();
Span tokenDecodeBuffer = stackalloc byte[Base64Url.GetMaxDecodedLength(token.Length)];
@@ -39,9 +43,24 @@ public IValidationResult Validate(ReadOnlySpan 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;
@@ -145,28 +164,6 @@ private bool VerifySignature(
&& SignatureAlgorithm.Ed25519.Verify(publicKey, signedPart, decodedTokenParts.Signature);
}
- private bool VerifyExpiration(JwksTokenParts 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 tokenPart, Span buffer, out ReadOnlySpan span, out int length)
{
span = default;
diff --git a/src/Digdir.Library.Dialogporten.WebApiClient/Services/EdDsaSecurityKeysCacheService.cs b/src/Digdir.Library.Dialogporten.WebApiClient/Services/EdDsaSecurityKeysCacheService.cs
index be771d660..817c387bd 100644
--- a/src/Digdir.Library.Dialogporten.WebApiClient/Services/EdDsaSecurityKeysCacheService.cs
+++ b/src/Digdir.Library.Dialogporten.WebApiClient/Services/EdDsaSecurityKeysCacheService.cs
@@ -20,6 +20,12 @@ internal sealed class DefaultEdDsaSecurityKeysCache : IEdDsaSecurityKeysCache
private List _publicKeys = [];
public ReadOnlyCollection PublicKeys => _publicKeys.AsReadOnly();
internal void SetPublicKeys(List publicKeys) => _publicKeys = publicKeys;
+
+ public DefaultEdDsaSecurityKeysCache() { }
+ internal DefaultEdDsaSecurityKeysCache(IEnumerable publicKeys)
+ {
+ _publicKeys = publicKeys.ToList();
+ }
}
internal sealed class EdDsaSecurityKeysCacheService : BackgroundService
diff --git a/src/Digdir.Tool.Dialogporten.Benchmarks/Digdir.Tool.Dialogporten.Benchmarks.csproj b/src/Digdir.Tool.Dialogporten.Benchmarks/Digdir.Tool.Dialogporten.Benchmarks.csproj
index 7dbde8269..5a9df0be2 100644
--- a/src/Digdir.Tool.Dialogporten.Benchmarks/Digdir.Tool.Dialogporten.Benchmarks.csproj
+++ b/src/Digdir.Tool.Dialogporten.Benchmarks/Digdir.Tool.Dialogporten.Benchmarks.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/Digdir.Tool.Dialogporten.Benchmarks/Program.cs b/src/Digdir.Tool.Dialogporten.Benchmarks/Program.cs
index 587aee869..e098ce13d 100644
--- a/src/Digdir.Tool.Dialogporten.Benchmarks/Program.cs
+++ b/src/Digdir.Tool.Dialogporten.Benchmarks/Program.cs
@@ -1,3 +1,3 @@
using BenchmarkDotNet.Running;
using Digdir.Tool.Dialogporten.Benchmarks;
-BenchmarkRunner.Run();
+BenchmarkRunner.Run();
diff --git a/src/Digdir.Tool.Dialogporten.Benchmarks/TokenValidatorBenchmarks.cs b/src/Digdir.Tool.Dialogporten.Benchmarks/TokenValidatorBenchmarks.cs
new file mode 100644
index 000000000..84fb50930
--- /dev/null
+++ b/src/Digdir.Tool.Dialogporten.Benchmarks/TokenValidatorBenchmarks.cs
@@ -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)) { }
+ }
+}
+
diff --git a/tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs b/tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs
index 8ab884276..023d83ee4 100644
--- a/tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs
+++ b/tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
+using Altinn.ApiClients.Dialogporten;
using Altinn.ApiClients.Dialogporten.Common;
using Altinn.ApiClients.Dialogporten.Services;
using NSec.Cryptography;
@@ -12,7 +13,7 @@ namespace Digdir.Library.Dialogporten.WebApiClient.Unit.Tests;
public class DialogTokenValidatorTests
{
- private const string ValidTimeStampString = "2025-02-14T09:00:00Z";
+ private static readonly DateTimeOffset ValidTimeStamp = DateTimeOffset.Parse("2025-02-14T09:00:00Z", CultureInfo.InvariantCulture);
private const string DialogToken =
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRwLXN0YWdpbmctMjQwMzIyLW81eW1uIn0.eyJqdGkiOiIzOGNmZGNiOS0zODhiLTQ3YjgtYTFiZi05ZjE1YjI4MTk4OTQiLCJjIjoidXJuOmFsdGlubjpwZXJzb246aWRlbnRpZmllci1ubzoxNDg4NjQ5ODIyNiIsImwiOjMsInAiOiJ1cm46YWx0aW5uOnBlcnNvbjppZGVudGlmaWVyLW5vOjE0ODg2NDk4MjI2IiwicyI6InVybjphbHRpbm46cmVzb3VyY2U6ZGFnbC1jb3JyZXNwb25kZW5jZSIsImkiOiIwMTk0ZmU4Mi05MjgwLTc3YTUtYTdjZC01ZmYwZTZhNmZhMDciLCJhIjoicmVhZCIsImlzcyI6Imh0dHBzOi8vcGxhdGZvcm0udHQwMi5hbHRpbm4ubm8vZGlhbG9ncG9ydGVuL2FwaS92MSIsImlhdCI6MTczOTUyMzM2NywibmJmIjoxNzM5NTIzMzY3LCJleHAiOjE3Mzk1MjM5Njd9.O_f-RJhRPT7B76S7aOGw6jfxKDki3uJQLLC8nVlcNVJWFIOQUsy6gU4bG1ZdqoMBZPvb2K2X4I5fGpHW9dQMAA";
private static readonly PublicKeyPair[] ValidPublicKeyPairs =
@@ -25,9 +26,7 @@ public class DialogTokenValidatorTests
public void ShouldReturnIsValid_GivenValidToken()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
// Act
var result = sut.Validate(DialogToken);
@@ -39,10 +38,8 @@ public void ShouldReturnIsValid_GivenValidToken()
[Fact]
public void ShouldThrowException_GivenNoPublicKeys()
{
-
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture));
+ var sut = GetSut(ValidTimeStamp);
// Assert
Assert.Throws(() => sut.Validate(DialogToken));
@@ -52,9 +49,7 @@ public void ShouldThrowException_GivenNoPublicKeys()
public void ShouldReturnError_GivenMalformedToken()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
// Act
var result = sut.Validate("This.TokenIsMalformed....");
@@ -69,9 +64,7 @@ public void ShouldReturnError_GivenMalformedToken()
public void ShouldReturnError_GivenInvalidToken()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
// Act
var result = sut.Validate("This.TokenIs.Invalid");
@@ -81,13 +74,12 @@ public void ShouldReturnError_GivenInvalidToken()
Assert.True(result.Errors.ContainsKey("token"));
Assert.Contains("Invalid token format", result.Errors["token"]);
}
+
[Fact]
public void ShouldReturnError_GivenNoPublicKeyWithCorrectKeyId()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var token = UpdateTokenHeader(DialogToken, "kid", "dp-testing-fake-kid");
// Act
@@ -113,17 +105,32 @@ public void ShouldReturnError_GivenExpiredToken()
// Assert
Assert.False(result.IsValid);
Assert.True(result.Errors.ContainsKey("token"));
- Assert.Contains("Token has expired", result.Errors["token"]);
+ Assert.Contains("Invalid exp", result.Errors["token"]);
}
[Fact]
- public void ShouldReturnError_GivenEmptyToken()
+ public void ShouldReturnError_WhenUsedBeforeNbf()
{
// Arrange
var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
+ DateTimeOffset.Parse("2025-02-14T08:50:00Z", CultureInfo.InvariantCulture),
ValidPublicKeyPairs);
+ // Act
+ var result = sut.Validate(DialogToken);
+
+ // Assert
+ Assert.False(result.IsValid);
+ Assert.True(result.Errors.ContainsKey("token"));
+ Assert.Contains("Invalid nbf", result.Errors["token"]);
+ }
+
+ [Fact]
+ public void ShouldReturnError_GivenEmptyToken()
+ {
+ // Arrange
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
+
// Act
var result = sut.Validate("");
@@ -137,9 +144,7 @@ public void ShouldReturnError_GivenEmptyToken()
public void ShouldReturnError_GivenTokenWithWrongSignature()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var token = UpdateTokenPayload(DialogToken, "l", "4");
@@ -156,9 +161,7 @@ public void ShouldReturnError_GivenTokenWithWrongSignature()
public void ShouldReturnError_GivenTokenWithWrongAlg()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var token = UpdateTokenHeader(DialogToken, "alg", "RS512");
@@ -181,9 +184,7 @@ public void ShouldReturnError_GivenMalformedJsonHeader()
"kid": "dp-staging-240322-o5ymn"
"""u8;
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var tokenParts = DialogToken.Split('.');
tokenParts[0] = Base64Url.EncodeToString(invalidHeader);
@@ -207,9 +208,7 @@ public void ShouldReturnError_GivenMalformedJsonBody()
"c": "urn:altinn:person:identifier-no:14886498226",
"""u8;
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var tokenParts = DialogToken.Split('.');
tokenParts[1] = Base64Url.EncodeToString(invalidBody);
@@ -225,12 +224,10 @@ public void ShouldReturnError_GivenMalformedJsonBody()
}
[Fact]
- public void ShouldContainClaims_GivenValidTokenWithClaims()
+ public void ShouldReturnClaims_GivenValidTokenWithClaims()
{
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
// Act
var result = sut.Validate(DialogToken);
@@ -260,9 +257,7 @@ public void ClaimsShouldBeNull_GivenInvalidTokenFormat()
"c": "urn:altinn:person:identifier-no:14886498226",
"""u8;
// Arrange
- var sut = GetSut(
- DateTimeOffset.Parse(ValidTimeStampString, CultureInfo.InvariantCulture),
- ValidPublicKeyPairs);
+ var sut = GetSut(ValidTimeStamp, ValidPublicKeyPairs);
var tokenParts = DialogToken.Split('.');
tokenParts[1] = Base64Url.EncodeToString(invalidBody);
var token = string.Join(".", tokenParts);
@@ -275,8 +270,42 @@ public void ClaimsShouldBeNull_GivenInvalidTokenFormat()
Assert.Null(result.ClaimsPrincipal);
}
- private static DialogTokenValidator GetSut(DateTimeOffset simulatedNow, params PublicKeyPair[] publicKeyPairs)
+ [Fact]
+ public void ShouldReturnError_GivenTokenWithInvalidDialogId()
+ {
+ // Arrange
+ var wrongDialogId = new Guid("329491ca-a4e9-4460-8988-f2dc80ea39fe");
+ var sut = GetSut(ValidTimeStamp, publicKeyPairs: ValidPublicKeyPairs);
+
+ // Act
+ var result = sut.Validate(DialogToken, dialogId: wrongDialogId);
+
+ // Assert
+ Assert.False(result.IsValid);
+ Assert.True(result.Errors.ContainsKey("token"));
+ Assert.Contains("Invalid dialog ID", result.Errors["token"]);
+ }
+
+ [Fact]
+ public void ShouldReturnError_GivenTokenWithInvalidActions()
+ {
+ // Arrange
+ var sut = GetSut(ValidTimeStamp, publicKeyPairs: ValidPublicKeyPairs);
+
+ // Act
+ var result = sut.Validate(DialogToken, requiredActions: ["write"]);
+
+ // Assert
+ Assert.False(result.IsValid);
+ Assert.True(result.Errors.ContainsKey("token"));
+ Assert.Contains("Invalid actions", result.Errors["token"]);
+ }
+
+ private static DialogTokenValidator GetSut(
+ DateTimeOffset simulatedNow,
+ params PublicKeyPair[] publicKeyPairs)
{
+ DialogTokenValidationParameters.Default.ClockSkew = TimeSpan.Zero;
var keyCache = Substitute.For();
var clock = Substitute.For();
keyCache.PublicKeys.Returns(new ReadOnlyCollection(publicKeyPairs));