diff --git a/src/Paseto/Handlers/PasetoPurposeHandler.cs b/src/Paseto/Handlers/PasetoPurposeHandler.cs index 7da50a2..8b22245 100644 --- a/src/Paseto/Handlers/PasetoPurposeHandler.cs +++ b/src/Paseto/Handlers/PasetoPurposeHandler.cs @@ -46,6 +46,7 @@ public virtual PasetoTokenValidationResult ValidateTokenPayload(PasetoToken toke ValidateLifetime(token, validationParameters); ValidateAudience(token, validationParameters); ValidateIssuer(token, validationParameters); + ValidateSubject(token, validationParameters); } catch (Exception ex) { @@ -87,4 +88,13 @@ protected virtual void ValidateIssuer(PasetoToken token, PasetoTokenValidationPa if (token.Payload.HasIssuer()) new EqualValidator(token.Payload, PasetoRegisteredClaimNames.Issuer).Validate(validationParameters.ValidIssuer); } + + protected virtual void ValidateSubject(PasetoToken token, PasetoTokenValidationParameters validationParameters) + { + if (!validationParameters.ValidateSubject && !string.IsNullOrWhiteSpace(validationParameters.ValidSubject)) + return; + + if (token.Payload.HasSubject()) + new EqualValidator(token.Payload, PasetoRegisteredClaimNames.Subject).Validate(validationParameters.ValidSubject); + } } diff --git a/src/Paseto/PasetoTokenValidationParameters.cs b/src/Paseto/PasetoTokenValidationParameters.cs index 294b3ca..77749b5 100644 --- a/src/Paseto/PasetoTokenValidationParameters.cs +++ b/src/Paseto/PasetoTokenValidationParameters.cs @@ -22,6 +22,11 @@ public class PasetoTokenValidationParameters /// public bool ValidateIssuer { get; set; } + /// + /// Gets or sets a value for comparing the subject of the payload. + /// + public bool ValidateSubject { get; set; } + /// /// Gets or sets the valid audience for comparing against the payload-provided aud. /// @@ -31,4 +36,9 @@ public class PasetoTokenValidationParameters /// Gets or sets the valid issuer for comparing against the payload-provided iss. /// public string ValidIssuer { get; set; } + + /// + /// Gets or sets the valid subject for comparing against the payload-provided sub. + /// + public string ValidSubject { get; set; } } \ No newline at end of file diff --git a/tests/Paseto.Tests/PasetoValidationTest.cs b/tests/Paseto.Tests/PasetoValidationTest.cs index d6472d2..bf5ecce 100644 --- a/tests/Paseto.Tests/PasetoValidationTest.cs +++ b/tests/Paseto.Tests/PasetoValidationTest.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Linq; using FluentAssertions; using Paseto.Builder; @@ -22,7 +23,7 @@ public void TokenWithValidIssuerValidationSucceeds(ProtocolVersion version, Purp ValidIssuer = "valid-issuer", }; - var (token, decodeKey) = GenerateToken(version, purpose, "valid-issuer"); + var (token, decodeKey) = GenerateToken(version, purpose, PasetoRegisteredClaimNames.Issuer, "valid-issuer"); var decoded = new PasetoBuilder() .Use(version, purpose) .WithKey(decodeKey) @@ -44,7 +45,7 @@ public void TokenWithInValidIssuerValidationFails(ProtocolVersion version, Purpo ValidIssuer = "valid-issuer", }; - var (token, decodeKey) = GenerateToken(version, purpose, "invalid-issuer"); + var (token, decodeKey) = GenerateToken(version, purpose, PasetoRegisteredClaimNames.Issuer, "invalid-issuer"); var decoded = new PasetoBuilder() .Use(version, purpose) .WithKey(decodeKey) @@ -53,9 +54,64 @@ public void TokenWithInValidIssuerValidationFails(ProtocolVersion version, Purpo decoded.IsValid.Should().BeFalse(); } - private static (string token, PasetoKey decodeKey) GenerateToken(ProtocolVersion version, Purpose purpose, string issuer) + [Theory(DisplayName = "Should succeed on token with valid subject")] + [InlineData(ProtocolVersion.V3, Purpose.Local)] + [InlineData(ProtocolVersion.V3, Purpose.Public)] + [InlineData(ProtocolVersion.V4, Purpose.Local)] + [InlineData(ProtocolVersion.V4, Purpose.Public)] + public void TokenWithValidSubjectValidationSucceeds(ProtocolVersion version, Purpose purpose) + { + var validationParameters = new PasetoTokenValidationParameters() + { + ValidateSubject = true, + ValidSubject = "valid-subject", + }; + + var (token, decodeKey) = GenerateToken(version, purpose, PasetoRegisteredClaimNames.Subject, "valid-subject"); + var decoded = new PasetoBuilder() + .Use(version, purpose) + .WithKey(decodeKey) + .Decode(token, validationParameters); + + decoded.IsValid.Should().BeTrue(); + } + + [Theory(DisplayName = "Should fail on token with invalid subject")] + [InlineData(ProtocolVersion.V3, Purpose.Local)] + [InlineData(ProtocolVersion.V3, Purpose.Public)] + [InlineData(ProtocolVersion.V4, Purpose.Local)] + [InlineData(ProtocolVersion.V4, Purpose.Public)] + public void TokenWithInValidSubjectValidationFails(ProtocolVersion version, Purpose purpose) + { + var validationParameters = new PasetoTokenValidationParameters() + { + ValidateSubject = true, + ValidSubject = "valid-subject", + }; + + var (token, decodeKey) = GenerateToken(version, purpose, PasetoRegisteredClaimNames.Subject, "invalid-subject"); + var decoded = new PasetoBuilder() + .Use(version, purpose) + .WithKey(decodeKey) + .Decode(token, validationParameters); + + decoded.IsValid.Should().BeFalse(); + } + + private static (string token, PasetoKey decodeKey) GenerateToken(ProtocolVersion version, Purpose purpose, string claimName, string claimValue) { var builder = new PasetoBuilder().Use(version, purpose); + switch (claimName) + { + case PasetoRegisteredClaimNames.Issuer: + builder.Issuer(claimValue); + break; + case PasetoRegisteredClaimNames.Subject: + builder.Subject(claimValue); + break; + default: + throw new NotImplementedException(); + } switch (purpose) { case Purpose.Local: @@ -63,7 +119,6 @@ private static (string token, PasetoKey decodeKey) GenerateToken(ProtocolVersion var key = builder.GenerateSymmetricKey(); var token = builder .WithKey(key) - .Issuer(issuer) .Encode(); return (token, key); } @@ -72,7 +127,6 @@ private static (string token, PasetoKey decodeKey) GenerateToken(ProtocolVersion var keyPair = builder.GenerateAsymmetricKeyPair(Enumerable.Repeat((byte)0x00, 32).ToArray()); var token = builder .WithKey(keyPair.SecretKey) - .Issuer(issuer) .Encode(); return (token, keyPair.PublicKey); }