diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8ab8113..5133cb2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [master] + branches: [ master, v3 ] pull_request: # The branches below must be a subset of the branches above branches: [master] @@ -25,25 +25,25 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: '6.0.x' - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 6d50c47..d8714ca 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -2,7 +2,7 @@ name: .NET Core on: push: - branches: [ master, v2 ] + branches: [ master, v2, v3 ] pull_request: branches: [ master ] @@ -15,11 +15,15 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' - name: Fix Windows VM bug shell: bash if: matrix.os == 'windows-latest' @@ -36,7 +40,7 @@ jobs: run: dotnet pack -c Release - name: Publish artifact if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: nupkg path: '**/*.nupkg' diff --git a/README.md b/README.md index d9caf71..043f47a 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,18 @@ Helper libraries for tokens and cryptography in .NET. - EdDSA support for JWTs (Ed25519 and Ed448) -- Branca tokens with JWT style validation -- PASETO (v1.public & v2.public) with JWT style validation - Base16 (hex) and Base62 encoders - `passwordrule` attribute support for ASP.NET Identity - [Samples](https://github.com/scottbrady91/IdentityModel/tree/master/samples/ScottBrady.IdentityModel.Samples.AspNetCore) in ASP.NET Core +- ~~Branca tokens with JWT style validation~~ (deprecated due to low usage of Branca) +- ~~PASETO (v1.public & v2.public) with JWT style validation~~ (deprecated due to low usage of PASETO) **Feature requests welcome. Please see SECURITY.md for responsible disclosure policy.** ## EdDSA support -EdDSA is a modern signing algorithm, not yet supported out of the box in .NET. This library provides some useful abstractions around the Bouncy Castle (software) implementation of EdDSA. +EdDSA is a modern signing algorithm that is not yet supported out of the box in .NET. +This library provides some useful abstractions around the Bouncy Castle (software) implementation of EdDSA. ```csharp // create EdDSA new key pair @@ -29,9 +30,30 @@ EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) new EdDsaSecurityKey(EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519)) ``` -## Branca Tokens +## Base16 (hex) Encoding + +Base16 allows you to encode and decode hexadecimal strings. + +```csharp +var plaintext = "hello world"; // encoded = 68656c6c6f20776f726c64 +string encoded = Base16.Encode(Encoding.UTF8.GetBytes(plaintext)); +``` -[Branca](https://branca.io/) is token construct suitable for internal systems. The payload is encrypted using XChaCha20-Poly1305, using a 32-byte symmetric key. +## Base62 Encoding + +Base62 encoding uses the `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` character set. + +```csharp +var plaintext = "hello world"; // encoded = AAwf93rvy4aWQVw +string encoded = Base62.Encode(Encoding.UTF8.GetBytes(plaintext)); +``` + +## JWT alternatives (deprecated) + +### Branca Tokens + +[Branca](https://branca.io/) is a token construct suitable for internal systems. +The payload is encrypted using XChaCha20-Poly1305, using a 32-byte symmetric key. This library supports the creation of Branca tokens with an arbitrary payload or using a JWT-style payload. @@ -64,11 +86,17 @@ ClaimsPrincipal principal = handler.ValidateToken( }, out SecurityToken parsedToken); ``` -## PASETO +> [!IMPORTANT] +> Branca support is now deprecated and only supports Microsoft.IdentityModel 6.35.0. +> This is due to the low usage of this library and the Branca project as a whole. + +### PASETO -[PASETO](https://paseto.io/) is a competing standard to JOSE & JWT that offers a versioned ciphersuite. This library currently implements `v1` and `v2` for the `public` purpose, suitable for zero-trust systems such as an OAuth authorization server. +[PASETO](https://paseto.io/) is a competing standard to JOSE & JWT that offers a versioned ciphersuite. +This library currently implements `v1` and `v2` for the `public` purpose, suitable for zero-trust systems such as an OAuth authorization server. -Explicit versioning allows PASETO to side-step [attacks on signature validation](https://www.rfc-editor.org/rfc/rfc8725.html#name-weak-signatures-and-insuffi) found in some JWT libraries. However, it does not mitigate any other attacks. +Explicit versioning allows PASETO to side-step [attacks on signature validation](https://www.rfc-editor.org/rfc/rfc8725.html#name-weak-signatures-and-insuffi) found in some JWT libraries. +However, it does not mitigate any other attacks. If you are considering using PASETO, I recommend reading [RFC 8725 - JWT Best Current Practices](https://www.rfc-editor.org/rfc/rfc8725.html) and deciding if the interoperable JWT format is still wrong for you. @@ -107,7 +135,11 @@ ClaimsPrincipal principal = handler.ValidateToken( }, out SecurityToken parsedToken); ``` -## API Protection with JWT Style Handler +> [!IMPORTANT] +> PASETO support is now deprecated and only supports Microsoft.IdentityModel 6.35.0. +> This is due to the low usage of this library and the PASETO project as a whole. + +### API Protection with JWT Style Handler The Branca and PASETO token handlers can be used with the ASP.NET Core JWT bearer authentication handler. @@ -122,21 +154,3 @@ services.AddAuthentication() options.TokenValidationParameters.ValidAudience = "me"; }) ``` - -## Base16 (hex) Encoding - -Base16 allows you to encode and decode hexidecimal strings.. - -```csharp -var plaintext = "hello world"; // encoded = 68656c6c6f20776f726c64 -string encoded = Base16.Encode(Encoding.UTF8.GetBytes(plaintext)); -``` - -## Base62 Encoding - -Base62 encoding uses the `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` character set. - -```csharp -var plaintext = "hello world"; // encoded = AAwf93rvy4aWQVw -string encoded = Base62.Encode(Encoding.UTF8.GetBytes(plaintext)); -``` diff --git a/ScottBrady.IdentityModel.sln b/ScottBrady.IdentityModel.sln index 5d80c29..d1901ec 100644 --- a/ScottBrady.IdentityModel.sln +++ b/ScottBrady.IdentityModel.sln @@ -17,10 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScottBrady.IdentityModel.Sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScottBrady.IdentityModel.AspNetCore", "src\ScottBrady.IdentityModel.AspNetCore\ScottBrady.IdentityModel.AspNetCore.csproj", "{E2F2D4E3-A732-43FE-B082-9FD5ACBEA89B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScottBrady.IdentityModel.Tokens.Branca", "src\ScottBrady.IdentityModel.Tokens.Branca\ScottBrady.IdentityModel.Tokens.Branca.csproj", "{E27F8536-728B-4855-A8D5-921297CBD58C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScottBrady.IdentityModel.Tokens.Paseto", "src\ScottBrady.IdentityModel.Tokens.Paseto\ScottBrady.IdentityModel.Tokens.Paseto.csproj", "{EA07CA1B-4571-4FD1-9BCE-272A68FB48A6}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,14 +39,6 @@ Global {E2F2D4E3-A732-43FE-B082-9FD5ACBEA89B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2F2D4E3-A732-43FE-B082-9FD5ACBEA89B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2F2D4E3-A732-43FE-B082-9FD5ACBEA89B}.Release|Any CPU.Build.0 = Release|Any CPU - {E27F8536-728B-4855-A8D5-921297CBD58C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E27F8536-728B-4855-A8D5-921297CBD58C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E27F8536-728B-4855-A8D5-921297CBD58C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E27F8536-728B-4855-A8D5-921297CBD58C}.Release|Any CPU.Build.0 = Release|Any CPU - {EA07CA1B-4571-4FD1-9BCE-272A68FB48A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA07CA1B-4571-4FD1-9BCE-272A68FB48A6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA07CA1B-4571-4FD1-9BCE-272A68FB48A6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA07CA1B-4571-4FD1-9BCE-272A68FB48A6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,8 +48,6 @@ Global {B57DDA53-D240-46ED-8275-F19600491681} = {FA25402D-0A81-48F1-9E83-7CA4801E59F9} {B7F49824-C721-4BD1-9BAF-6E442AEAB14D} = {0787B459-DE3E-4296-965C-5C891AE23840} {E2F2D4E3-A732-43FE-B082-9FD5ACBEA89B} = {2CE8E91B-6B6A-4C1F-B6FE-80A1F1199A7A} - {E27F8536-728B-4855-A8D5-921297CBD58C} = {2CE8E91B-6B6A-4C1F-B6FE-80A1F1199A7A} - {EA07CA1B-4571-4FD1-9BCE-272A68FB48A6} = {2CE8E91B-6B6A-4C1F-B6FE-80A1F1199A7A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7ACFEAF2-1A47-419B-989B-75A0A686D710} diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Controllers/HomeController.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Controllers/HomeController.cs index 4d6cb4e..ad0b4f1 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Controllers/HomeController.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Controllers/HomeController.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -10,138 +10,94 @@ using Microsoft.IdentityModel.Tokens; using ScottBrady.IdentityModel.Crypto; using ScottBrady.IdentityModel.Samples.AspNetCore.Models; -using ScottBrady.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Branca; -using ScottBrady.IdentityModel.Tokens.Paseto; -namespace ScottBrady.IdentityModel.Samples.AspNetCore.Controllers +namespace ScottBrady.IdentityModel.Samples.AspNetCore.Controllers; + +public class HomeController : Controller { - public class HomeController : Controller - { - private readonly SampleOptions options; - private readonly UserManager userManager; + private readonly SampleOptions options; + private readonly UserManager userManager; - public HomeController(SampleOptions options, UserManager userManager) - { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - this.userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - } + public HomeController(SampleOptions options, UserManager userManager) + { + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + } - public IActionResult Index() - { - return View(); - } - - [HttpGet] - public IActionResult Branca() - { - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(new SecurityTokenDescriptor - { - Issuer = "me", - Audience = "you", - EncryptingCredentials = options.BrancaEncryptingCredentials - }); - - var parsedToken = handler.DecryptToken(token, ((SymmetricSecurityKey) options.BrancaEncryptingCredentials.Key).Key); - - return View("Index", new TokenModel - { - Type = "Branca", - Token = token, - Payload = Encoding.UTF8.GetString(parsedToken.Payload) - }); - } + public IActionResult Index() + { + return View(); + } + + [HttpGet] + public IActionResult EdDsaJwt() + { + var handler = new JsonWebTokenHandler(); - [HttpGet] - public IActionResult Paseto(string version) + var descriptor = new SecurityTokenDescriptor { - var handler = new PasetoTokenHandler(); - - SigningCredentials signingCredentials; - if (version == PasetoConstants.Versions.V1) - signingCredentials = new SigningCredentials(options.PasetoV1PrivateKey, SecurityAlgorithms.RsaSsaPssSha384); - else if (version == PasetoConstants.Versions.V2) - signingCredentials = new SigningCredentials(options.PasetoV2PrivateKey, ExtendedSecurityAlgorithms.EdDsa); - else - throw new NotSupportedException("Unsupported version"); - - var descriptor = new PasetoSecurityTokenDescriptor(version, PasetoConstants.Purposes.Public) - { - Issuer = "me", - Audience = "you", - SigningCredentials = signingCredentials - }; + Issuer = "me", + Audience = "you", + SigningCredentials = new SigningCredentials(options.EdDsaPrivateKey, ExtendedSecurityAlgorithms.EdDsa) + }; - var token = handler.CreateToken(descriptor); - var payload = descriptor.ToJwtPayload(JwtDateTimeFormat.Iso); - - return View("Index", new TokenModel - { - Type = "PASETO", - Token = token, - Payload = payload - }); - } + var token = handler.CreateToken(descriptor); + var payloadClaims = handler.ReadJsonWebToken(token).Claims; - [HttpGet] - public IActionResult EdDsaJwt() + var claimsJson = new JsonObject(); + foreach (var claim in payloadClaims) { - var handler = new JsonWebTokenHandler(); - - var descriptor = new SecurityTokenDescriptor + if (claim.ValueType.Contains("integer")) { - Issuer = "me", - Audience = "you", - SigningCredentials = new SigningCredentials(options.PasetoV2PrivateKey, ExtendedSecurityAlgorithms.EdDsa) - }; - - var token = handler.CreateToken(descriptor); - var payload = descriptor.ToJwtPayload(JwtDateTimeFormat.Iso); - - return View("Index", new TokenModel + claimsJson.Add(claim.Type, int.Parse(claim.Value)); + } + else { - Type = "EdDSA JWT", - Token = token, - Payload = payload - }); + claimsJson.Add(claim.Type, claim.Value); + } } - [HttpGet] - [Authorize(AuthenticationSchemes = "branca-bearer,paseto-bearer-v1,paseto-bearer-v2,eddsa")] - public IActionResult CallApi() + return View("Index", new TokenModel { - return Ok(); - } + Type = "EdDSA JWT", + Token = token, + Payload = claimsJson.ToString() + }); + } - [HttpGet] - public IActionResult PasswordRules() - { - return View(new PasswordRulesModel()); - } + [HttpGet] + [Authorize(AuthenticationSchemes = "eddsa")] + public IActionResult CallApi() + { + return Ok(); + } - [HttpPost] - public async Task PasswordRules(PasswordRulesModel model) - { - if (!ModelState.IsValid) return View(model); + [HttpGet] + public IActionResult PasswordRules() + { + return View(new PasswordRulesModel()); + } - var errors = new List(); - foreach (var validator in userManager.PasswordValidators) + [HttpPost] + public async Task PasswordRules(PasswordRulesModel model) + { + if (!ModelState.IsValid) return View(model); + + var errors = new List(); + foreach (var validator in userManager.PasswordValidators) + { + var result = await validator.ValidateAsync(userManager, new IdentityUser(), model.Password); + if (!result.Succeeded) { - var result = await validator.ValidateAsync(userManager, new IdentityUser(), model.Password); - if (!result.Succeeded) + if (result.Errors.Any()) { - if (result.Errors.Any()) - { - errors.AddRange(result.Errors.Select(x => x.Description)); - } + errors.AddRange(result.Errors.Select(x => x.Description)); } } - - model.Errors = errors; - model.Message = errors.Any() ? "Password failed server-side validation" : "Password passed server-side validation"; - return View(model); } + + model.Errors = errors; + model.Message = errors.Any() ? "Password failed server-side validation" : "Password passed server-side validation"; + return View(model); } -} +} \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/PasswordRulesModel.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/PasswordRulesModel.cs index 79f0ec9..d9de0ee 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/PasswordRulesModel.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/PasswordRulesModel.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; -namespace ScottBrady.IdentityModel.Samples.AspNetCore.Models +namespace ScottBrady.IdentityModel.Samples.AspNetCore.Models; + +public class PasswordRulesModel { - public class PasswordRulesModel - { - public string Message { get; set; } - public IEnumerable Errors { get; set; } = new List(); - public string Password { get; set; } - } + public string Message { get; set; } + public IEnumerable Errors { get; set; } = new List(); + public string Password { get; set; } } \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/TokenModel.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/TokenModel.cs index 78a9597..e147320 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/TokenModel.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Models/TokenModel.cs @@ -1,9 +1,8 @@ -namespace ScottBrady.IdentityModel.Samples.AspNetCore.Models +namespace ScottBrady.IdentityModel.Samples.AspNetCore.Models; + +public class TokenModel { - public class TokenModel - { - public string Type { get; set; } - public string Token { get; set; } - public string Payload { get; set; } - } + public string Type { get; set; } + public string Token { get; set; } + public string Payload { get; set; } } \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Program.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Program.cs index 59e8264..25f058e 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Program.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Program.cs @@ -1,20 +1,19 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace ScottBrady.IdentityModel.Samples.AspNetCore +namespace ScottBrady.IdentityModel.Samples.AspNetCore; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } -} + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/SampleOptions.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/SampleOptions.cs index 409b4de..57166f5 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/SampleOptions.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/SampleOptions.cs @@ -1,40 +1,14 @@ using System; -using System.Security.Cryptography; -using Microsoft.IdentityModel.Tokens; using ScottBrady.IdentityModel.Crypto; using ScottBrady.IdentityModel.Tokens; -namespace ScottBrady.IdentityModel.Samples.AspNetCore -{ - public class SampleOptions - { - private EncryptingCredentials encryptingCredentials; - - public EncryptingCredentials BrancaEncryptingCredentials - { - get - { - if (encryptingCredentials == null) - { - var key = new byte[32]; - RandomNumberGenerator.Create().GetBytes(key); +namespace ScottBrady.IdentityModel.Samples.AspNetCore; - encryptingCredentials = new EncryptingCredentials( - new SymmetricSecurityKey(key), - ExtendedSecurityAlgorithms.XChaCha20Poly1305); - } - - return encryptingCredentials; - } - } - - public RsaSecurityKey PasetoV1PrivateKey = new RsaSecurityKey(RSA.Create()); - public RsaSecurityKey PasetoV1PublicKey => new RsaSecurityKey(RSA.Create(PasetoV1PrivateKey.Rsa.ExportParameters(false))); - - public readonly EdDsaSecurityKey PasetoV2PublicKey = new EdDsaSecurityKey( - EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X =Convert.FromBase64String("doaS7QILHBdnPULlgs1fX0MWpd1wak14r1yT6ae/b4M=")})); +public class SampleOptions +{ + public readonly EdDsaSecurityKey EdDsaPublicKey = new EdDsaSecurityKey( + EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X =Convert.FromBase64String("doaS7QILHBdnPULlgs1fX0MWpd1wak14r1yT6ae/b4M=")})); - public readonly EdDsaSecurityKey PasetoV2PrivateKey= new EdDsaSecurityKey( - EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {D =Convert.FromBase64String("TYXei5+8Qd2ZqKIlEuJJ3S50WYuocFTrqK+3/gHVH9B2hpLtAgscF2c9QuWCzV9fQxal3XBqTXivXJPpp79vgw==")})); - } + public readonly EdDsaSecurityKey EdDsaPrivateKey= new EdDsaSecurityKey( + EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {D =Convert.FromBase64String("TYXei5+8Qd2ZqKIlEuJJ3S50WYuocFTrqK+3/gHVH9B2hpLtAgscF2c9QuWCzV9fQxal3XBqTXivXJPpp79vgw==")})); } \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/ScottBrady.IdentityModel.Samples.AspNetCore.csproj b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/ScottBrady.IdentityModel.Samples.AspNetCore.csproj index acab5e8..ef444f9 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/ScottBrady.IdentityModel.Samples.AspNetCore.csproj +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/ScottBrady.IdentityModel.Samples.AspNetCore.csproj @@ -1,20 +1,18 @@  - net6.0 + net8.0 - - - - + + + + - - diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Startup.cs b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Startup.cs index e39d481..be098d5 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Startup.cs +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Startup.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -6,94 +5,63 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Logging; using ScottBrady.IdentityModel.AspNetCore.Identity; -using ScottBrady.IdentityModel.Tokens.Branca; -using ScottBrady.IdentityModel.Tokens.Paseto; -namespace ScottBrady.IdentityModel.Samples.AspNetCore +namespace ScottBrady.IdentityModel.Samples.AspNetCore; + +public class Startup { - public class Startup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) - { - IdentityModelEventSource.ShowPII = true; + IdentityModelEventSource.ShowPII = true; - services.AddControllersWithViews() - .AddRazorRuntimeCompilation(); + services.AddControllersWithViews() + .AddRazorRuntimeCompilation(); - var sampleOptions = new SampleOptions(); - services.AddSingleton(sampleOptions); + var sampleOptions = new SampleOptions(); + services.AddSingleton(sampleOptions); - services.AddAuthentication() - .AddJwtBearer("branca-bearer", options => - { - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BrancaTokenHandler()); - options.TokenValidationParameters.TokenDecryptionKey = sampleOptions.BrancaEncryptingCredentials.Key; - options.TokenValidationParameters.ValidIssuer = "me"; - options.TokenValidationParameters.ValidAudience = "you"; - }) - .AddJwtBearer("paseto-bearer-v1", options => - { - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new PasetoTokenHandler( - new Dictionary {{PasetoConstants.Versions.V1, new PasetoVersion1()}})); - - options.TokenValidationParameters.IssuerSigningKey = sampleOptions.PasetoV1PublicKey; - options.TokenValidationParameters.ValidIssuer = "me"; - options.TokenValidationParameters.ValidAudience = "you"; - }) - .AddJwtBearer("paseto-bearer-v2", options => - { - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new PasetoTokenHandler( - new Dictionary {{PasetoConstants.Versions.V2, new PasetoVersion2()}})); - - options.TokenValidationParameters.IssuerSigningKey = sampleOptions.PasetoV2PublicKey; - options.TokenValidationParameters.ValidIssuer = "me"; - options.TokenValidationParameters.ValidAudience = "you"; - }) - .AddJwtBearer("eddsa", options => - { - options.TokenValidationParameters.IssuerSigningKey = sampleOptions.PasetoV2PublicKey; - options.TokenValidationParameters.ValidIssuer = "me"; - options.TokenValidationParameters.ValidAudience = "you"; - }); + services.AddAuthentication() + .AddJwtBearer("eddsa", options => + { + options.TokenValidationParameters.IssuerSigningKey = sampleOptions.EdDsaPublicKey; + options.TokenValidationParameters.ValidIssuer = "me"; + options.TokenValidationParameters.ValidAudience = "you"; + }); - services.AddIdentityCore(options => + services.AddIdentityCore(options => + { + options.Password = new ExtendedPasswordOptions { - options.Password = new ExtendedPasswordOptions - { - RequiredLength = 15, - RequireDigit = true, - RequireLowercase = true, - RequireUppercase = true, - RequireNonAlphanumeric = true, + RequiredLength = 15, + RequireDigit = true, + RequireLowercase = true, + RequireUppercase = true, + RequireNonAlphanumeric = true, - // extended options - MaxLength = 64, - MaxConsecutiveChars = 3 - }; - }) - .AddPasswordValidator>() // Required for max length and consecutive character checks - .AddEntityFrameworkStores(); + // extended options + MaxLength = 64, + MaxConsecutiveChars = 3 + }; + }) + .AddPasswordValidator>() // Required for max length and consecutive character checks + .AddEntityFrameworkStores(); - services.AddDbContext(options => options.UseInMemoryDatabase("test")); - } + services.AddDbContext(options => options.UseInMemoryDatabase("test")); + } - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); - app.UseHttpsRedirection(); + app.UseHttpsRedirection(); - app.UseStaticFiles(); + app.UseStaticFiles(); - app.UseRouting(); + app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); + app.UseAuthentication(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); - } + app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); } -} +} \ No newline at end of file diff --git a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Views/Home/Index.cshtml b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Views/Home/Index.cshtml index 2e1acef..bfef413 100644 --- a/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Views/Home/Index.cshtml +++ b/samples/ScottBrady.IdentityModel.Samples.AspNetCore/Views/Home/Index.cshtml @@ -6,15 +6,6 @@

ScottBrady.IdentityModel

Identity & Crypto helpers

-

- Get Branca token -

-

- Get PASETO v1.public -

-

- Get PASETO v2.public -

Get EdDSA JWT

diff --git a/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordOptions.cs b/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordOptions.cs index ee96bc5..ae22022 100644 --- a/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordOptions.cs +++ b/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordOptions.cs @@ -1,20 +1,19 @@ using Microsoft.AspNetCore.Identity; -namespace ScottBrady.IdentityModel.AspNetCore.Identity +namespace ScottBrady.IdentityModel.AspNetCore.Identity; + +/// +/// Extends to support all passwordrules attribute values. +/// +public class ExtendedPasswordOptions : PasswordOptions { /// - /// Extends to support all passwordrules attribute values. + /// The maximum length of the password. /// - public class ExtendedPasswordOptions : PasswordOptions - { - /// - /// The maximum length of the password. - /// - public int? MaxLength { get; set; } + public int? MaxLength { get; set; } - /// - /// The maximum number of consecutive identical characters allowed in the password. - /// - public int? MaxConsecutiveChars { get; set; } - } + /// + /// The maximum number of consecutive identical characters allowed in the password. + /// + public int? MaxConsecutiveChars { get; set; } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordValidator.cs b/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordValidator.cs index be236e5..35cb032 100644 --- a/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordValidator.cs +++ b/src/ScottBrady.IdentityModel.AspNetCore/Identity/ExtendedPasswordValidator.cs @@ -4,62 +4,61 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -namespace ScottBrady.IdentityModel.AspNetCore.Identity +namespace ScottBrady.IdentityModel.AspNetCore.Identity; + +/// +/// ASP.NET Core Identity PasswordValidator for max length and max consecutive character checks. +/// +public class ExtendedPasswordValidator : IPasswordValidator where TUser : class { /// - /// ASP.NET Core Identity PasswordValidator for max length and max consecutive character checks. + /// Validates the password for max length and max consecutive characters. /// - public class ExtendedPasswordValidator : IPasswordValidator where TUser : class + public Task ValidateAsync(UserManager manager, TUser user, string password) { - /// - /// Validates the password for max length and max consecutive characters. - /// - public Task ValidateAsync(UserManager manager, TUser user, string password) - { - if (manager == null) throw new ArgumentNullException(nameof(manager)); - if (password == null) throw new ArgumentNullException(nameof(password)); + if (manager == null) throw new ArgumentNullException(nameof(manager)); + if (password == null) throw new ArgumentNullException(nameof(password)); - var errors = new List(); + var errors = new List(); - if (manager.Options.Password is ExtendedPasswordOptions options) + if (manager.Options.Password is ExtendedPasswordOptions options) + { + if (options.MaxLength.HasValue && 0 < options.MaxLength && options.MaxLength < password.Length) { - if (options.MaxLength.HasValue && 0 < options.MaxLength && options.MaxLength < password.Length) + errors.Add(new IdentityError { - errors.Add(new IdentityError - { - Code = "PasswordTooLong", - Description = $"Passwords must be no longer than {options.MaxLength} characters" - }); - } + Code = "PasswordTooLong", + Description = $"Passwords must be no longer than {options.MaxLength} characters" + }); + } - if (options.MaxConsecutiveChars.HasValue - && 0 <= options.MaxConsecutiveChars - && HasConsecutiveCharacters(password, options.MaxConsecutiveChars.Value)) + if (options.MaxConsecutiveChars.HasValue + && 0 <= options.MaxConsecutiveChars + && HasConsecutiveCharacters(password, options.MaxConsecutiveChars.Value)) + { + errors.Add(new IdentityError { - errors.Add(new IdentityError - { - Code = "TooManyConsecutiveCharacters", - Description = $"Password must not contain more than {options.MaxConsecutiveChars} consecutive characters" - }); - } + Code = "TooManyConsecutiveCharacters", + Description = $"Password must not contain more than {options.MaxConsecutiveChars} consecutive characters" + }); } - - return Task.FromResult(errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray())); } + + return Task.FromResult(errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray())); + } - /// - /// Checks for consecutive characters using Regex. - /// Does not account for UTF16 surrogate pairs. - /// Adapted from https://codereview.stackexchange.com/questions/102568/checking-if-a-text-contains-n-consecutive-repeating-characters - /// - public virtual bool HasConsecutiveCharacters(string password, int maxConsecutive) - { - if (string.IsNullOrWhiteSpace(password)) throw new ArgumentNullException(nameof(password)); + /// + /// Checks for consecutive characters using Regex. + /// Does not account for UTF16 surrogate pairs. + /// Adapted from https://codereview.stackexchange.com/questions/102568/checking-if-a-text-contains-n-consecutive-repeating-characters + /// + public virtual bool HasConsecutiveCharacters(string password, int maxConsecutive) + { + if (string.IsNullOrWhiteSpace(password)) throw new ArgumentNullException(nameof(password)); - var invalidAmount = 1; - if (1 < maxConsecutive) invalidAmount = maxConsecutive; + var invalidAmount = 1; + if (1 < maxConsecutive) invalidAmount = maxConsecutive; - return Regex.IsMatch(password,"(.)\\1{"+ invalidAmount + "}"); - } + return Regex.IsMatch(password,"(.)\\1{"+ invalidAmount + "}"); } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.AspNetCore/ScottBrady.IdentityModel.AspNetCore.csproj b/src/ScottBrady.IdentityModel.AspNetCore/ScottBrady.IdentityModel.AspNetCore.csproj index 42182f8..71f607b 100644 --- a/src/ScottBrady.IdentityModel.AspNetCore/ScottBrady.IdentityModel.AspNetCore.csproj +++ b/src/ScottBrady.IdentityModel.AspNetCore/ScottBrady.IdentityModel.AspNetCore.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 Scott Brady ASP.NET Core Tag helpers for password rules and validation. icon.png @@ -11,7 +11,7 @@ passwordrules aspnetcore identity true true - 2.0.0 + 2.1.0 Apache-2.0 diff --git a/src/ScottBrady.IdentityModel.AspNetCore/TagHelpers/NewPasswordTagHelper.cs b/src/ScottBrady.IdentityModel.AspNetCore/TagHelpers/NewPasswordTagHelper.cs index ca0c0b5..700d5b3 100644 --- a/src/ScottBrady.IdentityModel.AspNetCore/TagHelpers/NewPasswordTagHelper.cs +++ b/src/ScottBrady.IdentityModel.AspNetCore/TagHelpers/NewPasswordTagHelper.cs @@ -8,75 +8,74 @@ using Microsoft.Extensions.Options; using ScottBrady.IdentityModel.AspNetCore.Identity; -namespace ScottBrady.IdentityModel.AspNetCore.TagHelpers +namespace ScottBrady.IdentityModel.AspNetCore.TagHelpers; + +/// +/// Creates an input element with a type of "password", autocomplete of "new-password", +/// and transforms ASP.NET Identity password validation rules into the passwordrule attribute. +/// +[HtmlTargetElement("newpassword", TagStructure = TagStructure.WithoutEndTag)] +public class NewPasswordTagHelper : InputTagHelper { + internal IdentityOptions Options { get; } + /// - /// Creates an input element with a type of "password", autocomplete of "new-password", - /// and transforms ASP.NET Identity password validation rules into the passwordrule attribute. + /// Creates a new . /// - [HtmlTargetElement("newpassword", TagStructure = TagStructure.WithoutEndTag)] - public class NewPasswordTagHelper : InputTagHelper + /// The . + /// The . + public NewPasswordTagHelper( + IHtmlGenerator generator, + IOptions optionsAccessor) : base(generator) { - internal IdentityOptions Options { get; } - - /// - /// Creates a new . - /// - /// The . - /// The . - public NewPasswordTagHelper( - IHtmlGenerator generator, - IOptions optionsAccessor) : base(generator) - { - Options = optionsAccessor?.Value; - } + Options = optionsAccessor?.Value; + } - /// - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (output == null) throw new ArgumentNullException(nameof(output)); + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (output == null) throw new ArgumentNullException(nameof(output)); - ProcessInputTag(context, output); + ProcessInputTag(context, output); - output.TagName = "input"; - output.Attributes.SetAttribute("type", "password"); - output.Attributes.SetAttribute("autocomplete", "new-password"); - output.Attributes.SetAttribute("autocorrect", "off"); - output.Attributes.SetAttribute("autocapitalize", "off"); + output.TagName = "input"; + output.Attributes.SetAttribute("type", "password"); + output.Attributes.SetAttribute("autocomplete", "new-password"); + output.Attributes.SetAttribute("autocorrect", "off"); + output.Attributes.SetAttribute("autocapitalize", "off"); - ProcessIdentityPasswordRules(Options.Password, output); - } + ProcessIdentityPasswordRules(Options.Password, output); + } - internal virtual void ProcessIdentityPasswordRules(PasswordOptions options, TagHelperOutput output) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - if (output == null) throw new ArgumentNullException(nameof(output)); + internal virtual void ProcessIdentityPasswordRules(PasswordOptions options, TagHelperOutput output) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (output == null) throw new ArgumentNullException(nameof(output)); - var passwordRules = new StringBuilder(); - passwordRules.AppendFormat("minlength: {0};", options.RequiredLength); + var passwordRules = new StringBuilder(); + passwordRules.AppendFormat("minlength: {0};", options.RequiredLength); - if (options.RequireLowercase) passwordRules.Append(" required: lower;"); - if (options.RequireUppercase) passwordRules.Append(" required: upper;"); - if (options.RequireDigit) passwordRules.Append(" required: digit;"); - if (options.RequireNonAlphanumeric) passwordRules.Append(" required: special;"); + if (options.RequireLowercase) passwordRules.Append(" required: lower;"); + if (options.RequireUppercase) passwordRules.Append(" required: upper;"); + if (options.RequireDigit) passwordRules.Append(" required: digit;"); + if (options.RequireNonAlphanumeric) passwordRules.Append(" required: special;"); - if (options is ExtendedPasswordOptions extendedOptions) + if (options is ExtendedPasswordOptions extendedOptions) + { + if (extendedOptions.MaxLength.HasValue && 0 < extendedOptions.MaxLength) { - if (extendedOptions.MaxLength.HasValue && 0 < extendedOptions.MaxLength) - { - passwordRules.AppendFormat(" maxlength: {0};", extendedOptions.MaxLength); - output.Attributes.SetAttribute("maxlength", extendedOptions.MaxLength); - } - if (extendedOptions.MaxConsecutiveChars.HasValue && 0 <= extendedOptions.MaxConsecutiveChars) - passwordRules.AppendFormat(" max-consecutive: {0};", extendedOptions.MaxConsecutiveChars); + passwordRules.AppendFormat(" maxlength: {0};", extendedOptions.MaxLength); + output.Attributes.SetAttribute("maxlength", extendedOptions.MaxLength); } - - output.Attributes.SetAttribute("passwordrules", passwordRules.ToString()); - output.Attributes.SetAttribute("minlength", options.RequiredLength); + if (extendedOptions.MaxConsecutiveChars.HasValue && 0 <= extendedOptions.MaxConsecutiveChars) + passwordRules.AppendFormat(" max-consecutive: {0};", extendedOptions.MaxConsecutiveChars); } - - internal virtual void ProcessInputTag(TagHelperContext context, TagHelperOutput output) - => base.Process(context, output); + + output.Attributes.SetAttribute("passwordrules", passwordRules.ToString()); + output.Attributes.SetAttribute("minlength", options.RequiredLength); } + + internal virtual void ProcessInputTag(TagHelperContext context, TagHelperOutput output) + => base.Process(context, output); } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaSecurityToken.cs b/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaSecurityToken.cs deleted file mode 100644 index b8cfce1..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaSecurityToken.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Text; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Branca -{ - public class BrancaSecurityToken : JwtPayloadSecurityToken - { - public BrancaSecurityToken(BrancaToken token) : base(Encoding.UTF8.GetString(token.Payload)) - { - IssuedAt = token.Timestamp; - } - - public override DateTime IssuedAt { get; } - - public override SecurityKey SecurityKey => throw new NotSupportedException(); - public override SecurityKey SigningKey - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaToken.cs b/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaToken.cs deleted file mode 100644 index b0e4b2f..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaToken.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace ScottBrady.IdentityModel.Tokens.Branca -{ - public class BrancaToken - { - private static readonly DateTime MinDateTime = new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc); - private static readonly DateTime MaxDateTime = new DateTime(2106, 02, 07, 06, 28, 15, DateTimeKind.Utc); - - public BrancaToken(byte[] payload, uint timestamp) - { - Payload = payload ?? throw new ArgumentNullException(nameof(payload)); - Timestamp = GetDateTime(timestamp); - BrancaFormatTimestamp = timestamp; - } - - public byte[] Payload { get; } - public DateTime Timestamp { get; } - public uint BrancaFormatTimestamp { get; } - - public static DateTime GetDateTime(uint timestamp) - { - return DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime; - } - - public static uint GetBrancaTimestamp(DateTimeOffset dateTime) - { - if (dateTime < MinDateTime || MaxDateTime < dateTime) - throw new InvalidOperationException("Timestamp cannot be before 1970 or after 2106 (uint max)"); - - return Convert.ToUInt32(dateTime.ToUniversalTime().ToUnixTimeSeconds()); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaTokenHandler.cs b/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaTokenHandler.cs deleted file mode 100644 index 866d151..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Branca/BrancaTokenHandler.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using NaCl.Core; -using ScottBrady.IdentityModel.Crypto; - -namespace ScottBrady.IdentityModel.Tokens.Branca -{ - public class BrancaTokenHandler : JwtPayloadTokenHandler - { - private const int TagLength = 16; - - public override bool CanReadToken(string token) - { - if (string.IsNullOrWhiteSpace(token)) return false; - if (token.Length > MaximumTokenSizeInBytes) return false; - if (token.Any(x => !Base62.CharacterSet.Contains(x))) return false; - - return true; - } - - /// - /// Branca specification-level token generation. - /// Timestamp set to UtcNow - /// - /// The utf-8 payload to be encrypted into the Branca token - /// 32-byte private key used to encrypt and decrypt the Branca token - /// Base62 encoded Branca Token - public virtual string CreateToken(string payload, byte[] key) - => CreateToken(payload, DateTime.UtcNow, key); - - /// - /// Branca specification-level token generation - /// - /// The utf-8 payload to be encrypted into the Branca token - /// The timestamp included in the Branca token (iat: issued at) - /// 32-byte private key used to encrypt and decrypt the Branca token - /// Base62 encoded Branca Token - public virtual string CreateToken(string payload, DateTimeOffset timestamp, byte[] key) - => CreateToken(payload, BrancaToken.GetBrancaTimestamp(timestamp), key); - - /// - /// Branca specification-level token generation - /// - /// The utf-8 payload to be encrypted into the Branca token - /// The timestamp included in the Branca token (iat: issued at) - /// 32-byte private key used to encrypt and decrypt the Branca token - /// Base62 encoded Branca Token - public virtual string CreateToken(string payload, uint timestamp, byte[] key) - => CreateToken(Encoding.UTF8.GetBytes(payload), timestamp, key); - - /// - /// Branca specification-level token generation - /// - /// The payload bytes to be encrypted into the Branca token - /// The timestamp included in the Branca token (iat: issued at) - /// 32-byte private key used to encrypt and decrypt the Branca token - /// Base62 encoded Branca Token - public virtual string CreateToken(byte[] payload, uint timestamp, byte[] key) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - if (!IsValidKey(key)) throw new InvalidOperationException("Invalid encryption key"); - - var nonce = GenerateNonce(); - - // header - var header = new byte[29]; - using (var stream = new MemoryStream(header)) - { - // version - stream.WriteByte(0xBA); - - // timestamp (big endian uint32) - stream.Write(BitConverter.GetBytes(timestamp).Reverse().ToArray(), 0, 4); - - // nonce - stream.Write(nonce, 0, nonce.Length); - } - - var ciphertext = new byte[payload.Length]; - var tag = new byte[TagLength]; - - new XChaCha20Poly1305(key).Encrypt(nonce, payload, ciphertext, tag, header); - - var tokenBytes = new byte[header.Length + ciphertext.Length + TagLength]; - Buffer.BlockCopy(header, 0, tokenBytes, 0, header.Length); - Buffer.BlockCopy(ciphertext, 0, tokenBytes, header.Length, ciphertext.Length); - Buffer.BlockCopy(tag, 0, tokenBytes, tokenBytes.Length - TagLength, tag.Length); - - return Base62.Encode(tokenBytes); - } - - /// - /// Creates Branca token using JWT rules - /// - /// Token descriptor - /// Base62 encoded Branca Token - public virtual string CreateToken(SecurityTokenDescriptor tokenDescriptor) - { - if (tokenDescriptor == null) throw new ArgumentNullException(nameof(tokenDescriptor)); - - if (!IsValidKey(tokenDescriptor.EncryptingCredentials)) - throw new SecurityTokenEncryptionFailedException( - "Invalid encrypting credentials. Branca tokens require a symmetric key using the XC20P algorithm and no key wrapping"); - - var jwtStylePayload = tokenDescriptor.ToJwtPayload(); - - // Remove iat claim in favour of timestamp - var jObject = JsonSerializer.Deserialize>(jwtStylePayload); - jObject.Remove(JwtRegisteredClaimNames.Iat); - - var symmetricKey = (SymmetricSecurityKey) tokenDescriptor.EncryptingCredentials.Key; - - return CreateToken(JsonSerializer.Serialize(jObject), symmetricKey.Key); - } - - /// - /// Branca specification level token decryption. - /// - /// Base62 encoded Branca token - /// 32-byte private key used to encrypt and decrypt the Branca token - /// Pared and decrypted Branca Token - public virtual BrancaToken DecryptToken(string token, byte[] key) - { - if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(token)); - if (!CanReadToken(token)) throw new InvalidCastException("Unable to read token"); - if (!IsValidKey(key)) throw new InvalidOperationException("Invalid decryption key"); - - var tokenBytes = Base62.Decode(token); - - using (var stream = new MemoryStream(tokenBytes, false)) - { - // header - var header = GuaranteedRead(stream, 29); - - byte[] nonce; - uint timestamp; - using (var headerStream = new MemoryStream(header)) - { - // version - var version = headerStream.ReadByte(); - if (version != 0xBA) throw new SecurityTokenException("Unsupported Branca version"); - - // timestamp (big endian uint32) - var timestampBytes = GuaranteedRead(headerStream, 4).Reverse().ToArray(); - timestamp = BitConverter.ToUInt32(timestampBytes, 0); - - // nonce - nonce = GuaranteedRead(headerStream, 24); - } - - // ciphertext - var ciphertextLength = stream.Length - stream.Position - TagLength; - var ciphertext = GuaranteedRead(stream, (int) ciphertextLength); - var tag = GuaranteedRead(stream, TagLength); - - var plaintext = new byte[ciphertextLength]; - new XChaCha20Poly1305(key).Decrypt(nonce, ciphertext, tag, plaintext, header); - - return new BrancaToken( - plaintext, - timestamp); - } - } - - public override TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) - { - if (string.IsNullOrWhiteSpace(token)) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(token))}; - if (validationParameters == null) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(validationParameters))}; - if (!CanReadToken(token)) return new TokenValidationResult {Exception = new SecurityTokenException("Unable to read token")}; - - // get decryption keys - var securityKeys = GetBrancaDecryptionKeys(token, validationParameters); - - BrancaToken decryptedToken = null; - - foreach (var securityKey in securityKeys) - { - try - { - decryptedToken = DecryptToken(token, securityKey.Key); - if (decryptedToken != null) break; - } - catch (Exception) - { - // ignored - } - } - - if (decryptedToken == null) - return new TokenValidationResult {Exception = new SecurityTokenDecryptionFailedException("Unable to decrypt token")}; - - BrancaSecurityToken brancaToken; - try - { - brancaToken = new BrancaSecurityToken(decryptedToken); - } - catch (Exception e) - { - return new TokenValidationResult {Exception = e}; - } - - var innerValidationResult = ValidateTokenPayload(brancaToken, validationParameters); - if (!innerValidationResult.IsValid) return innerValidationResult; - - var identity = innerValidationResult.ClaimsIdentity; - if (validationParameters.SaveSigninToken) identity.BootstrapContext = token; - - return new TokenValidationResult - { - SecurityToken = brancaToken, - ClaimsIdentity = identity, - IsValid = true - }; - } - - protected virtual IEnumerable GetBrancaDecryptionKeys(string token, TokenValidationParameters validationParameters) - { - var keys = base.GetDecryptionKeys(token, validationParameters); - - return keys.Where(IsValidKey).Select(x => (SymmetricSecurityKey) x).ToList(); - } - - protected virtual bool IsValidKey(byte[] key) => key?.Length == 32; - - protected virtual bool IsValidKey(SecurityKey securityKey) - { - if (securityKey == null) return false; - if (!(securityKey is SymmetricSecurityKey symmetricKey)) return false; - - return IsValidKey(symmetricKey.Key); - } - - protected virtual bool IsValidKey(EncryptingCredentials credentials) - { - if (credentials == null) return false; - if (credentials.Enc != ExtendedSecurityAlgorithms.XChaCha20Poly1305) return false; - if (string.IsNullOrWhiteSpace(credentials.Alg) || credentials.Alg != SecurityAlgorithms.None) - { - return false; - } - - return IsValidKey(credentials.Key); - } - - protected virtual byte[] GenerateNonce() - { - var nonce = new byte[24]; - RandomNumberGenerator.Fill(nonce); - return nonce; - } - - private static byte[] GuaranteedRead(Stream stream, int length) - { - if (!stream.TryRead(length, out var bytes)) throw new SecurityTokenException(""); - return bytes; - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Branca/ScottBrady.IdentityModel.Tokens.Branca.csproj b/src/ScottBrady.IdentityModel.Tokens.Branca/ScottBrady.IdentityModel.Tokens.Branca.csproj deleted file mode 100644 index c8992f6..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Branca/ScottBrady.IdentityModel.Tokens.Branca.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net6.0 - Scott Brady - .NET support for Branca tokens using APIs from Microsoft.IdentityModel.Tokens. - icon.png - https://github.com/scottbrady91/IdentityModel - https://github.com/scottbrady91/IdentityModel/releases - Copyright 2022 (c) Scott Brady - Branca Token - true - true - 3.0.0 - Apache-2.0 - 1591 - - - - - - - - - - - - - - - - diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoConstants.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoConstants.cs deleted file mode 100644 index 095491b..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoConstants.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoConstants - { - public const int MaxPasetoSegmentCount = 4; - - public class Versions - { - public const string V1 = "v1"; - public const string V2 = "v2"; - } - - public class Purposes - { - public const string Local = "local"; - public const string Public = "public"; - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityToken.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityToken.cs deleted file mode 100644 index 221903b..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityToken.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoSecurityToken : JwtPayloadSecurityToken - { - protected PasetoSecurityToken() { } - - public PasetoSecurityToken(PasetoToken token) : base(token.Payload) - { - Version = token.Version; - Purpose = token.Purpose; - - EncodedFooter = token.EncodedFooter; - Footer = token.Footer; - - RawToken = token.RawToken; - } - - public virtual string Version { get; } - public virtual string Purpose { get; } - - public virtual string EncodedFooter { get; } - public virtual string Footer { get; } - - public virtual string RawToken { get; } - - public override DateTime IssuedAt => ParsePasetoDateTimeClaim(JwtRegisteredClaimNames.Iat); - public override DateTime ValidFrom => ParsePasetoDateTimeClaim(JwtRegisteredClaimNames.Nbf); - public override DateTime ValidTo => ParsePasetoDateTimeClaim(JwtRegisteredClaimNames.Exp); - - public override SecurityKey SecurityKey => throw new NotSupportedException(); - public override SecurityKey SigningKey { get; set; } - - protected virtual DateTime ParsePasetoDateTimeClaim(string claimType) - { - if (InnerToken.TryGetPayloadValue(claimType, out var claimValue)) - { - // ISO 8601 format - if (DateTime.TryParse(claimValue, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.RoundtripKind, out var dateTime)) - { - return dateTime.ToUniversalTime(); - } - - throw new SecurityTokenInvalidLifetimeException($"Unable to parse date time from '{claimType}'. Failing value: '{claimValue}'"); - } - - return DateTime.MinValue; - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityTokenDescriptor.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityTokenDescriptor.cs deleted file mode 100644 index bcb2510..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoSecurityTokenDescriptor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoSecurityTokenDescriptor : SecurityTokenDescriptor - { - public PasetoSecurityTokenDescriptor(string version, string purpose) - { - if (string.IsNullOrWhiteSpace(version)) throw new ArgumentNullException(nameof(version)); - if (string.IsNullOrWhiteSpace(purpose)) throw new ArgumentNullException(nameof(purpose)); - - Version = version; - Purpose = purpose; - } - - public string Version { get; } - public string Purpose { get; } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoToken.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoToken.cs deleted file mode 100644 index eb0755b..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoToken.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoToken - { - protected PasetoToken() { } - - public PasetoToken(string token) - { - if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(token)); - - var tokenParts = token.Split(new[] {'.'}, PasetoConstants.MaxPasetoSegmentCount + 1); - if (tokenParts.Length != 3 && tokenParts.Length != 4) throw new ArgumentException("Invalid number of token segments"); - - RawToken = token; - - Version = tokenParts[0]; - Purpose = tokenParts[1]; - EncodedPayload = tokenParts[2]; - if (tokenParts.Length == 4) - { - EncodedFooter = tokenParts[3]; - Footer = Base64UrlEncoder.Decode(EncodedFooter); - } - } - - public string RawToken { get; } - - public string Version { get; } - public string Purpose { get; } - - public string EncodedPayload { get; } - public string Payload { get; protected set; } - - public string EncodedFooter { get; } - public string Footer { get; } - - public void SetPayload(string payload) - { - if (string.IsNullOrWhiteSpace(payload)) throw new ArgumentNullException(nameof(payload)); - - try - { - JsonSerializer.Deserialize>(payload); - Payload = payload; - } - catch (Exception e) - { - throw new ArgumentException("Token does contain valid JSON", e); - } - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoTokenHandler.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoTokenHandler.cs deleted file mode 100644 index b220ff3..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/PasetoTokenHandler.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoTokenHandler : JwtPayloadTokenHandler - { - private readonly Dictionary supportedVersions; - - public PasetoTokenHandler(Dictionary supportedVersions = null) - { - this.supportedVersions = supportedVersions ?? new Dictionary - { - {PasetoConstants.Versions.V1, new PasetoVersion1()}, - {PasetoConstants.Versions.V2, new PasetoVersion2()} - }; - } - - public override bool CanReadToken(string token) - { - if (string.IsNullOrWhiteSpace(token)) return false; - if (token.Length > MaximumTokenSizeInBytes) return false; - - var tokenParts = token.Split(new[] {'.'}, PasetoConstants.MaxPasetoSegmentCount + 1); - if (tokenParts.Length != 3 && tokenParts.Length != 4) return false; - - return true; - } - - public virtual string CreateToken(PasetoSecurityTokenDescriptor tokenDescriptor) - { - if (tokenDescriptor == null) throw new ArgumentNullException(nameof(tokenDescriptor)); - if (!(tokenDescriptor is PasetoSecurityTokenDescriptor pasetoSecurityTokenDescriptor)) - throw new ArgumentException($"Token descriptor must be of type '{typeof(PasetoSecurityTokenDescriptor)}'", nameof(tokenDescriptor)); - - // get strategy for version + purpose - if (!supportedVersions.TryGetValue(pasetoSecurityTokenDescriptor.Version, out var strategy)) - { - throw new SecurityTokenException("Unsupported PASETO version"); - } - - // create payload - var payload = tokenDescriptor.ToJwtPayload(JwtDateTimeFormat.Iso); - - // generate token - string token; - if (pasetoSecurityTokenDescriptor.Purpose == "local") - { - token = strategy.Encrypt(payload, null, pasetoSecurityTokenDescriptor.EncryptingCredentials); - } - else if (pasetoSecurityTokenDescriptor.Purpose == "public") - { - token = strategy.Sign(payload, null, pasetoSecurityTokenDescriptor.SigningCredentials); - } - else - { - throw new SecurityTokenException("Unsupported PASETO purpose"); - } - - return token; - } - - public override TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) - { - if (string.IsNullOrWhiteSpace(token)) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(token))}; - if (validationParameters == null) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(validationParameters))}; - if (!CanReadToken(token)) return new TokenValidationResult {Exception = new SecurityTokenException("Unable to read token")}; - - var pasetoToken = new PasetoToken(token); - - // get strategy for version + purpose - if (!supportedVersions.TryGetValue(pasetoToken.Version, out var strategy)) - { - return new TokenValidationResult {Exception = new SecurityTokenException("Unsupported PASETO version")}; - } - - PasetoSecurityToken pasetoSecurityToken; - try - { - if (pasetoToken.Purpose == "local") - { - var keys = GetDecryptionKeys(token, validationParameters); - pasetoSecurityToken = strategy.Decrypt(pasetoToken, keys); - } - else if (pasetoToken.Purpose == "public") - { - var keys = GetSigningKeys(token, validationParameters); - - // TODO: kid handling (footer?) - - pasetoSecurityToken = strategy.Verify(pasetoToken, keys); - } - else - { - return new TokenValidationResult {Exception = new SecurityTokenException("Unsupported PASETO purpose")}; - } - } - catch (Exception e) - { - return new TokenValidationResult {Exception = e}; - } - - var innerValidationResult = ValidateTokenPayload(pasetoSecurityToken, validationParameters); - if (!innerValidationResult.IsValid) return innerValidationResult; - - var identity = innerValidationResult.ClaimsIdentity; - if (validationParameters.SaveSigninToken) identity.BootstrapContext = token; - - return new TokenValidationResult - { - SecurityToken = pasetoSecurityToken, - ClaimsIdentity = identity, - IsValid = true - }; - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/ScottBrady.IdentityModel.Tokens.Paseto.csproj b/src/ScottBrady.IdentityModel.Tokens.Paseto/ScottBrady.IdentityModel.Tokens.Paseto.csproj deleted file mode 100644 index 5b1e636..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/ScottBrady.IdentityModel.Tokens.Paseto.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net6.0 - Scott Brady - .NET support for PASETO (v1 & v2) using APIs from Microsoft.IdentityModel.Tokens. - icon.png - https://github.com/scottbrady91/IdentityModel - https://github.com/scottbrady91/IdentityModel/releases - Copyright 2022 (c) Scott Brady - PASETO Token - true - true - 3.0.0 - Apache-2.0 - 1591 - - - - - - - - - - - diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion1.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion1.cs deleted file mode 100644 index 05621da..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion1.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoVersion1 : PasetoVersionStrategy - { - private const string PublicHeader = "v1.public."; - - public override string Encrypt(string payload, string footer, EncryptingCredentials encryptingCredentials) - { - throw new NotSupportedException("v1.local not supported"); - } - - public override string Sign(string payload, string footer, SigningCredentials signingCredentials) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - if (signingCredentials == null) throw new ArgumentNullException(nameof(signingCredentials)); - - if (signingCredentials.Key.GetType() != typeof(RsaSecurityKey)) - throw new SecurityTokenInvalidSigningKeyException($"PASETO v1 requires a key of type {typeof(RsaSecurityKey)}"); - if (signingCredentials.Algorithm != SecurityAlgorithms.RsaSsaPssSha384) - throw new SecurityTokenInvalidSigningKeyException($"PASETO v1 requires a key for configured for the '{SecurityAlgorithms.RsaSsaPssSha384}' algorithm"); - - var privateKey = (RsaSecurityKey) signingCredentials.Key; - if (privateKey.PrivateKeyStatus != PrivateKeyStatus.Exists) - throw new SecurityTokenInvalidSigningKeyException($"Missing private key"); - - var payloadBytes = Encoding.UTF8.GetBytes(payload); - - var messageToSign = PreAuthEncode(new[] - { - Encoding.UTF8.GetBytes(PublicHeader), - payloadBytes, - Encoding.UTF8.GetBytes(footer ?? string.Empty) - }); - - var signature = privateKey.Rsa.SignData(messageToSign, HashAlgorithmName.SHA384, RSASignaturePadding.Pss); - - var token = $"{PublicHeader}{Base64UrlEncoder.Encode(payloadBytes.Combine(signature))}"; - if (!string.IsNullOrWhiteSpace(footer)) token += $".{Base64UrlEncoder.Encode(footer)}"; - - return token; - } - - public override PasetoSecurityToken Decrypt(PasetoToken token, IEnumerable decryptionKeys) - { - throw new NotSupportedException("v1.local not supported"); - } - - public override PasetoSecurityToken Verify(PasetoToken token, IEnumerable signingKeys) - { - if (token == null) throw new ArgumentNullException(nameof(token)); - if (signingKeys == null || !signingKeys.Any()) throw new ArgumentNullException(nameof(signingKeys)); - - var keys = signingKeys.OfType().ToList(); - if (!keys.Any()) throw new SecurityTokenInvalidSigningKeyException($"PASETO v1 requires key of type {typeof(RsaSecurityKey)}"); - - if (token.Version != PasetoConstants.Versions.V1) throw new ArgumentException("Invalid PASETO version"); - if (token.Purpose != PasetoConstants.Purposes.Public) throw new ArgumentException("Invalid PASETO purpose"); - - // decode payload - var payload = Base64UrlEncoder.DecodeBytes(token.EncodedPayload); - if (payload.Length < 256) throw new SecurityTokenInvalidSignatureException("Payload does not contain signature"); - - // extract signature from payload (rightmost 64 bytes) - var signature = new byte[256]; - Buffer.BlockCopy(payload, payload.Length - 256, signature, 0, 256); - - // decode payload JSON - var message = new byte[payload.Length - 256]; - Buffer.BlockCopy(payload, 0, message, 0, payload.Length - 256); - token.SetPayload(Encoding.UTF8.GetString(message)); - - // pack - var signedMessage = PreAuthEncode(new[] - { - Encoding.UTF8.GetBytes(PublicHeader), - message, - Base64UrlEncoder.DecodeBytes(token.EncodedFooter ?? string.Empty) - }); - - // verify signature using valid keys - foreach (var publicKey in keys) - { - try - { - var isValidSignature = publicKey.Rsa.VerifyData(signedMessage, signature, HashAlgorithmName.SHA384, RSASignaturePadding.Pss); - if (isValidSignature) return new PasetoSecurityToken(token); - } - catch (Exception) - { - // ignored - } - } - - throw new SecurityTokenInvalidSignatureException("Invalid PASETO signature"); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion2.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion2.cs deleted file mode 100644 index d769b9f..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersion2.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Crypto; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public class PasetoVersion2 : PasetoVersionStrategy - { - private const string PublicHeader = "v2.public."; - - public override string Encrypt(string payload, string footer, EncryptingCredentials encryptingCredentials) - { - throw new NotSupportedException("v2.local not supported"); - } - - public override string Sign(string payload, string footer, SigningCredentials signingCredentials) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - if (signingCredentials == null) throw new ArgumentNullException(nameof(signingCredentials)); - - if (signingCredentials.Key.GetType() != typeof(EdDsaSecurityKey)) - throw new SecurityTokenInvalidSigningKeyException($"PASETO v2 requires a key of type {typeof(EdDsaSecurityKey)}"); - if (signingCredentials.Algorithm != ExtendedSecurityAlgorithms.EdDsa) - throw new SecurityTokenInvalidSigningKeyException($"PASETO v2 requires a key for configured for the '{ExtendedSecurityAlgorithms.EdDsa}' algorithm"); - - var privateKey = (EdDsaSecurityKey) signingCredentials.Key; - if (privateKey.PrivateKeyStatus != PrivateKeyStatus.Exists) - throw new SecurityTokenInvalidSigningKeyException($"Missing private key"); - - var payloadBytes = Encoding.UTF8.GetBytes(payload); - - var messageToSign = PreAuthEncode(new[] - { - Encoding.UTF8.GetBytes(PublicHeader), - payloadBytes, - Encoding.UTF8.GetBytes(footer ?? string.Empty) - }); - - var signature = privateKey.EdDsa.Sign(messageToSign); - - var token = $"{PublicHeader}{Base64UrlEncoder.Encode(payloadBytes.Combine(signature))}"; - if (!string.IsNullOrWhiteSpace(footer)) token += $".{Base64UrlEncoder.Encode(footer)}"; - - return token; - } - - public override PasetoSecurityToken Decrypt(PasetoToken token, IEnumerable decryptionKeys) - { - throw new NotSupportedException("v2.local not supported"); - } - - public override PasetoSecurityToken Verify(PasetoToken token, IEnumerable signingKeys) - { - if (token == null) throw new ArgumentNullException(nameof(token)); - if (signingKeys == null || !signingKeys.Any()) throw new ArgumentNullException(nameof(signingKeys)); - - var keys = signingKeys.OfType().ToList(); - if (!keys.Any()) throw new SecurityTokenInvalidSigningKeyException($"PASETO v2 requires key of type {typeof(EdDsaSecurityKey)}"); - - if (token.Version != PasetoConstants.Versions.V2) throw new ArgumentException("Invalid PASETO version"); - if (token.Purpose != PasetoConstants.Purposes.Public) throw new ArgumentException("Invalid PASETO purpose"); - - // decode payload - var payload = Base64UrlEncoder.DecodeBytes(token.EncodedPayload); - if (payload.Length < 64) throw new SecurityTokenInvalidSignatureException("Payload does not contain signature"); - - // extract signature from payload (rightmost 64 bytes) - var signature = new byte[64]; - Buffer.BlockCopy(payload, payload.Length - 64, signature, 0, 64); - - // decode payload JSON - var message = new byte[payload.Length - 64]; - Buffer.BlockCopy(payload, 0, message, 0, payload.Length - 64); - token.SetPayload(Encoding.UTF8.GetString(message)); - - // pack - var signedMessage = PreAuthEncode(new[] - { - Encoding.UTF8.GetBytes(PublicHeader), - message, - Base64UrlEncoder.DecodeBytes(token.EncodedFooter ?? string.Empty) - }); - - // verify signature using valid keys - foreach (var publicKey in keys) - { - var isValidSignature = publicKey.EdDsa.Verify(signedMessage, signature); - if (isValidSignature) return new PasetoSecurityToken(token); - } - - throw new SecurityTokenInvalidSignatureException("Invalid PASETO signature"); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersionStrategy.cs b/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersionStrategy.cs deleted file mode 100644 index c5ed91e..0000000 --- a/src/ScottBrady.IdentityModel.Tokens.Paseto/VersionStrategies/PasetoVersionStrategy.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens.Paseto -{ - public abstract class PasetoVersionStrategy - { - /// - /// Creates an encrypted local token - /// - public abstract string Encrypt(string payload, string footer, EncryptingCredentials encryptingCredentials); - - /// - /// Creates a signed public token - /// - public abstract string Sign(string payload, string footer, SigningCredentials signingCredentials); - - /// - /// Decrypts a local token - /// - public abstract PasetoSecurityToken Decrypt(PasetoToken token, IEnumerable decryptionKeys); - - /// - /// Verifies the a public token's signature - /// - public abstract PasetoSecurityToken Verify(PasetoToken token, IEnumerable signingKeys); - - protected static byte[] PreAuthEncode(IReadOnlyList pieces) - { - if (pieces == null) throw new ArgumentNullException(nameof(pieces)); - - var output = BitConverter.GetBytes((long) pieces.Count); - - foreach (var piece in pieces) - { - output = output.Combine(BitConverter.GetBytes((long) piece.Length), piece); - } - - return output.ToArray(); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Assembly.cs b/src/ScottBrady.IdentityModel/Assembly.cs deleted file mode 100644 index cfb61cd..0000000 --- a/src/ScottBrady.IdentityModel/Assembly.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly:InternalsVisibleTo("ScottBrady.IdentityModel.Tests")] \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Crypto/ExtendedCryptoProvider.cs b/src/ScottBrady.IdentityModel/Crypto/ExtendedCryptoProvider.cs new file mode 100644 index 0000000..154cc3d --- /dev/null +++ b/src/ScottBrady.IdentityModel/Crypto/ExtendedCryptoProvider.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.IdentityModel.Tokens; +using ScottBrady.IdentityModel.Tokens; + +namespace ScottBrady.IdentityModel.Crypto; + +internal class ExtendedCryptoProvider : ICryptoProvider +{ + public bool IsSupportedAlgorithm(string algorithm, params object[] args) + => algorithm == ExtendedSecurityAlgorithms.EdDsa; + + public object Create(string algorithm, params object[] args) + { + if (algorithm == ExtendedSecurityAlgorithms.EdDsa && args[0] is EdDsaSecurityKey key) + { + return new EdDsaSignatureProvider(key, algorithm); + } + + throw new NotSupportedException(); + } + + public void Release(object cryptoInstance) + { + if (cryptoInstance is IDisposable disposableObject) + disposableObject.Dispose(); + } +} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs b/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs index f420d06..dddd5d8 100644 --- a/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs +++ b/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs @@ -1,25 +1,24 @@ -namespace ScottBrady.IdentityModel.Crypto +namespace ScottBrady.IdentityModel.Crypto; + +public static class ExtendedSecurityAlgorithms { - public static class ExtendedSecurityAlgorithms - { - // https://tools.ietf.org/html/draft-amringer-jose-chacha-02#section-4.1 - public const string ChaCha20Poly1305 = "C20P"; - public const string XChaCha20Poly1305 = "XC20P"; - public const string ChaCha20Poly1305KeyWrap = "C20PKW"; - public const string XChaCha20Poly1305KeyWrap = "XC20PKW"; - public const string EchdEsWithChaCha20Poly1305 = "ECDH-ES+C20PKW"; - public const string EchdEsWithXChaCha20Poly1305 = "ECDH-ES+XC20PKW"; + // https://tools.ietf.org/html/draft-amringer-jose-chacha-02#section-4.1 + public const string ChaCha20Poly1305 = "C20P"; + public const string XChaCha20Poly1305 = "XC20P"; + public const string ChaCha20Poly1305KeyWrap = "C20PKW"; + public const string XChaCha20Poly1305KeyWrap = "XC20PKW"; + public const string EchdEsWithChaCha20Poly1305 = "ECDH-ES+C20PKW"; + public const string EchdEsWithXChaCha20Poly1305 = "ECDH-ES+XC20PKW"; - // https://tools.ietf.org/html/rfc8037#section-5 - public const string EdDsa = "EdDSA"; + // https://tools.ietf.org/html/rfc8037#section-5 + public const string EdDsa = "EdDSA"; - public class Curves - { - // https://tools.ietf.org/html/rfc8037#section-5 - public const string Ed25519 = "Ed25519"; - public const string Ed448 = "Ed448"; - public const string X25519 = "X25519"; - public const string X448 = "X448"; - } + public class Curves + { + // https://tools.ietf.org/html/rfc8037#section-5 + public const string Ed25519 = "Ed25519"; + public const string Ed448 = "Ed448"; + public const string X25519 = "X25519"; + public const string X448 = "X448"; } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Encoding/Base16.cs b/src/ScottBrady.IdentityModel/Encoding/Base16.cs index e89dfdb..e2bcd50 100644 --- a/src/ScottBrady.IdentityModel/Encoding/Base16.cs +++ b/src/ScottBrady.IdentityModel/Encoding/Base16.cs @@ -1,17 +1,17 @@ using System; using System.Text; -namespace ScottBrady.IdentityModel +namespace ScottBrady.IdentityModel; + +/// +/// Base16 (hex) encoder. +/// Encode adapted from https://docs.microsoft.com/en-us/archive/blogs/blambert/blambertcodesnip-fast-byte-array-to-hex-string-conversion. +/// Decode adapted from https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa. +/// Faster alternatives are available. +/// +public static class Base16 { - /// - /// Base16 (hex) encoder. - /// Encode adapted from https://docs.microsoft.com/en-us/archive/blogs/blambert/blambertcodesnip-fast-byte-array-to-hex-string-conversion. - /// Decode adapted from https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa. - /// Faster alternatives are available. - /// - public static class Base16 - { - private static readonly string[] LookupTable = + private static readonly string[] LookupTable = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", @@ -31,8 +31,8 @@ public static class Base16 "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff" }; - public static string Encode(byte[] value) - { + public static string Encode(byte[] value) + { if (value == null) throw new ArgumentNullException(nameof(value)); var stringBuilder = new StringBuilder(); @@ -41,8 +41,8 @@ public static string Encode(byte[] value) return stringBuilder.ToString(); } - public static byte[] Decode(string value) - { + public static byte[] Decode(string value) + { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); var bytes = new byte[value.Length / 2]; @@ -53,5 +53,4 @@ public static byte[] Decode(string value) return bytes; } - } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Encoding/Base62.cs b/src/ScottBrady.IdentityModel/Encoding/Base62.cs index 463c856..c8aecd3 100644 --- a/src/ScottBrady.IdentityModel/Encoding/Base62.cs +++ b/src/ScottBrady.IdentityModel/Encoding/Base62.cs @@ -3,17 +3,17 @@ using System.Linq; using System.Text; -namespace ScottBrady.IdentityModel +namespace ScottBrady.IdentityModel; + +/// +/// Adapted from https://github.com/ghost1face/base62 +/// +public static class Base62 { - /// - /// Adapted from https://github.com/ghost1face/base62 - /// - public static class Base62 - { - public const string CharacterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + public const string CharacterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - public static string Encode(byte[] value) - { + public static string Encode(byte[] value) + { var convertedBytes = BaseConvert(value, 256, 62); var builder = new StringBuilder(); @@ -25,8 +25,8 @@ public static string Encode(byte[] value) return builder.ToString(); } - public static byte[] Decode(string value) - { + public static byte[] Decode(string value) + { var arr = new byte[value.Length]; for (var i = 0; i < arr.Length; i++) { @@ -36,8 +36,8 @@ public static byte[] Decode(string value) return BaseConvert(arr, 62, 256); } - private static byte[] BaseConvert(byte[] source, int sourceBase, int targetBase) - { + private static byte[] BaseConvert(byte[] source, int sourceBase, int targetBase) + { if (source == null) throw new ArgumentNullException(nameof(source)); int count; @@ -62,5 +62,4 @@ private static byte[] BaseConvert(byte[] source, int sourceBase, int targetBase) return result.Select(x => (byte) x).ToArray(); } - } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Extensions/ByteArrayExtensions.cs b/src/ScottBrady.IdentityModel/Extensions/ByteArrayExtensions.cs index a9371c5..7992720 100644 --- a/src/ScottBrady.IdentityModel/Extensions/ByteArrayExtensions.cs +++ b/src/ScottBrady.IdentityModel/Extensions/ByteArrayExtensions.cs @@ -1,16 +1,16 @@ using System; using System.Linq; -namespace ScottBrady.IdentityModel +namespace ScottBrady.IdentityModel; + +public static class ByteArrayExtensions { - public static class ByteArrayExtensions + /// + /// Combines multiple byte arrays. + /// https://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp + /// + public static byte[] Combine(this byte[] source, params byte[][] arrays) { - /// - /// Combines multiple byte arrays. - /// https://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp - /// - public static byte[] Combine(this byte[] source, params byte[][] arrays) - { var output = new byte[source.Length + arrays.Sum(a => a.Length)]; Buffer.BlockCopy(source, 0, output, 0, source.Length); @@ -22,5 +22,4 @@ public static byte[] Combine(this byte[] source, params byte[][] arrays) return output; } - } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Extensions/StreamExtensions.cs b/src/ScottBrady.IdentityModel/Extensions/StreamExtensions.cs index fb2752e..722be27 100644 --- a/src/ScottBrady.IdentityModel/Extensions/StreamExtensions.cs +++ b/src/ScottBrady.IdentityModel/Extensions/StreamExtensions.cs @@ -1,16 +1,15 @@ using System.IO; -namespace ScottBrady.IdentityModel +namespace ScottBrady.IdentityModel; + +public static class StreamExtensions { - public static class StreamExtensions + public static bool TryRead(this Stream stream, int length, out byte[] bytes) { - public static bool TryRead(this Stream stream, int length, out byte[] bytes) - { - bytes = new byte[length]; - var bytesRead = stream.Read(bytes, 0, length); + bytes = new byte[length]; + var bytesRead = stream.Read(bytes, 0, length); - if (bytesRead != length) return false; - return true; - } + if (bytesRead != length) return false; + return true; } } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj b/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj index 310859a..0af5167 100644 --- a/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj +++ b/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 Scott Brady Extensions for Microsoft.IdentityModel, including support for EdDSA signed JWTs. icon.png @@ -11,20 +11,24 @@ IdentityModel Base16 Base62 EdDSA true true - 3.0.0 + 3.1.0 Apache-2.0 1591 - - - + + + + + + + diff --git a/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs b/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs index a76098e..b8cc3ab 100644 --- a/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs +++ b/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs @@ -3,45 +3,44 @@ using Org.BouncyCastle.Crypto.Parameters; using ScottBrady.IdentityModel.Crypto; -namespace ScottBrady.IdentityModel.Tokens -{ - /// - /// A Microsoft.IdentityModel security key for EdDSA. - /// - public class EdDsaSecurityKey : AsymmetricSecurityKey - { - public EdDsa EdDsa { get; } - - private EdDsaSecurityKey() - { - CryptoProviderFactory.CustomCryptoProvider = new ExtendedCryptoProvider(); - } +namespace ScottBrady.IdentityModel.Tokens; - public EdDsaSecurityKey(EdDsa edDsa) : this() - { - EdDsa = edDsa ?? throw new ArgumentNullException(nameof(edDsa)); - } +/// +/// A Microsoft.IdentityModel security key for EdDSA. +/// +public class EdDsaSecurityKey : AsymmetricSecurityKey +{ + public EdDsa EdDsa { get; } - [Obsolete("Deprecated in favor of EdDsa constructor")] - public EdDsaSecurityKey(Ed25519PrivateKeyParameters keyParameters) : this() - { - if (keyParameters == null) throw new ArgumentNullException(nameof(keyParameters)); - EdDsa = EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {D = keyParameters.GetEncoded()}); - } + private EdDsaSecurityKey() + { + CryptoProviderFactory.CustomCryptoProvider = new ExtendedCryptoProvider(); + } - [Obsolete("Deprecated in favor of EdDsa constructor")] - public EdDsaSecurityKey(Ed25519PublicKeyParameters keyParameters) : this() - { - if (keyParameters == null) throw new ArgumentNullException(nameof(keyParameters)); - EdDsa = EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X = keyParameters.GetEncoded()}); - } - - public override int KeySize => EdDsa.KeySize; + public EdDsaSecurityKey(EdDsa edDsa) : this() + { + EdDsa = edDsa ?? throw new ArgumentNullException(nameof(edDsa)); + } - [Obsolete("HasPrivateKey method is deprecated, please use PrivateKeyStatus.")] - public override bool HasPrivateKey => EdDsa.Parameters.D != null; + [Obsolete("Deprecated in favor of EdDsa constructor")] + public EdDsaSecurityKey(Ed25519PrivateKeyParameters keyParameters) : this() + { + if (keyParameters == null) throw new ArgumentNullException(nameof(keyParameters)); + EdDsa = EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {D = keyParameters.GetEncoded()}); + } - public override PrivateKeyStatus PrivateKeyStatus - => EdDsa.Parameters.D != null ? PrivateKeyStatus.Exists : PrivateKeyStatus.DoesNotExist; + [Obsolete("Deprecated in favor of EdDsa constructor")] + public EdDsaSecurityKey(Ed25519PublicKeyParameters keyParameters) : this() + { + if (keyParameters == null) throw new ArgumentNullException(nameof(keyParameters)); + EdDsa = EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X = keyParameters.GetEncoded()}); } + + public override int KeySize => EdDsa.KeySize; + + [Obsolete("HasPrivateKey method is deprecated, please use PrivateKeyStatus.")] + public override bool HasPrivateKey => EdDsa.Parameters.D != null; + + public override PrivateKeyStatus PrivateKeyStatus + => EdDsa.Parameters.D != null ? PrivateKeyStatus.Exists : PrivateKeyStatus.DoesNotExist; } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs b/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs index 76d4865..559f379 100644 --- a/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs +++ b/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs @@ -1,23 +1,20 @@ -using System; -using System.Linq; using Microsoft.IdentityModel.Tokens; -namespace ScottBrady.IdentityModel.Tokens +namespace ScottBrady.IdentityModel.Tokens; + +internal class EdDsaSignatureProvider : SignatureProvider { - internal class EdDsaSignatureProvider : SignatureProvider - { - private readonly EdDsaSecurityKey edDsaKey; + private readonly EdDsaSecurityKey edDsaKey; - public EdDsaSignatureProvider(EdDsaSecurityKey key, string algorithm) - : base(key, algorithm) - { + public EdDsaSignatureProvider(EdDsaSecurityKey key, string algorithm) + : base(key, algorithm) + { edDsaKey = key; } - protected override void Dispose(bool disposing) { } - public override byte[] Sign(byte[] input) => edDsaKey.EdDsa.Sign(input); - public override bool Verify(byte[] input, byte[] signature) => edDsaKey.EdDsa.Verify(input, signature); - public override bool Verify(byte[] input, int inputOffset, int inputLength, byte[] signature, int signatureOffset, int signatureLength) - => edDsaKey.EdDsa.Verify(input, inputOffset, inputLength, signature, signatureOffset, signatureLength); - } + protected override void Dispose(bool disposing) { } + public override byte[] Sign(byte[] input) => edDsaKey.EdDsa.Sign(input); + public override bool Verify(byte[] input, byte[] signature) => edDsaKey.EdDsa.Verify(input, signature); + public override bool Verify(byte[] input, int inputOffset, int inputLength, byte[] signature, int signatureOffset, int signatureLength) + => edDsaKey.EdDsa.Verify(input, inputOffset, inputLength, signature, signatureOffset, signatureLength); } \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs b/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs deleted file mode 100644 index d8820fa..0000000 --- a/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Crypto -{ - internal class ExtendedCryptoProvider : ICryptoProvider - { - public bool IsSupportedAlgorithm(string algorithm, params object[] args) - => algorithm == ExtendedSecurityAlgorithms.EdDsa; - - public object Create(string algorithm, params object[] args) - { - if (algorithm == ExtendedSecurityAlgorithms.EdDsa && args[0] is EdDsaSecurityKey key) - { - return new EdDsaSignatureProvider(key, algorithm); - } - - throw new NotSupportedException(); - } - - public void Release(object cryptoInstance) - { - if (cryptoInstance is IDisposable disposableObject) - disposableObject.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadExtensions.cs b/src/ScottBrady.IdentityModel/Tokens/JwtPayloadExtensions.cs deleted file mode 100644 index b010f80..0000000 --- a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadExtensions.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Text.Json; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens -{ - public static class JwtPayloadExtensions - { - /// - /// Creates a JWT payload from a SecurityTokenDescriptor. - /// Inspired by logic found in Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler - /// - public static string ToJwtPayload(this SecurityTokenDescriptor tokenDescriptor, JwtDateTimeFormat dateTimeFormat = JwtDateTimeFormat.Unix) - { - if (tokenDescriptor == null) throw new ArgumentNullException(nameof(tokenDescriptor)); - - Dictionary payload; - if (tokenDescriptor.Subject != null) - { - payload = ToJwtClaimDictionary(tokenDescriptor.Subject.Claims); - } - else - { - payload = new Dictionary(); - } - - if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) - { - foreach (var pair in tokenDescriptor.Claims) - payload[pair.Key] = pair.Value; - } - - if (tokenDescriptor.Issuer != null) - payload.AddClaimIfNotPresent(JwtRegisteredClaimNames.Iss, tokenDescriptor.Issuer); - if (tokenDescriptor.Audience != null) - payload.AddClaimIfNotPresent(JwtRegisteredClaimNames.Aud, tokenDescriptor.Audience); - - Func dateTimeFormatFunc = null; - if (dateTimeFormat == JwtDateTimeFormat.Unix) dateTimeFormatFunc = GetUnixClaimValueOrDefault; - if (dateTimeFormat == JwtDateTimeFormat.Iso) dateTimeFormatFunc = GetIsoClaimValueOrDefault; - if (dateTimeFormatFunc == null) throw new NotSupportedException("Unsupported DateTime formatting type"); - - var now = DateTime.UtcNow; - - payload.AddClaimIfNotPresent(JwtRegisteredClaimNames.Exp, dateTimeFormatFunc(tokenDescriptor.Expires, now.AddMinutes(60))); - payload.AddClaimIfNotPresent(JwtRegisteredClaimNames.Iat, dateTimeFormatFunc(tokenDescriptor.IssuedAt, now)); - payload.AddClaimIfNotPresent(JwtRegisteredClaimNames.Nbf, dateTimeFormatFunc(tokenDescriptor.NotBefore, now)); - - return JsonSerializer.Serialize(payload, new JsonSerializerOptions{Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}); - } - - /// - /// Handling for serializing claims in a ClaimsIdentity. - /// Adapted from Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.CreateDictionaryFromClaims - /// - public static Dictionary ToJwtClaimDictionary(IEnumerable claims) - { - var payload = new Dictionary(); - - foreach (var claim in claims) - { - if (claim == null) continue; - - if (payload.TryGetValue(claim.Type, out var existingValue)) - { - var existingValues = existingValue as IList; - - if (existingValues == null) - { - existingValues = new List(); - existingValues.Add(existingValue); - } - - existingValues.Add(claim.Value); - payload[claim.Type] = existingValues; - } - else - { - payload[claim.Type] = claim.Value; - } - } - - return payload; - } - - private static void AddClaimIfNotPresent(this Dictionary payload, string type, object value) - { - if (payload.TryGetValue(type, out _)) return; - payload[type] = value; - } - - private static Func GetUnixClaimValueOrDefault - => (value, defaultValue) => value.HasValue - ? EpochTime.GetIntDate(value.Value) - : EpochTime.GetIntDate(defaultValue); - - private static Func GetIsoClaimValueOrDefault - => (value, defaultValue) => value.HasValue - ? value.Value.ToString("yyyy-MM-ddTHH:mm:sszzz") - : defaultValue.ToString("yyyy-MM-ddTHH:mm:sszzz"); - } - - public enum JwtDateTimeFormat - { - Unix, - Iso - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadSecurityToken.cs b/src/ScottBrady.IdentityModel/Tokens/JwtPayloadSecurityToken.cs deleted file mode 100644 index e314bd3..0000000 --- a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadSecurityToken.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens -{ - public abstract class JwtPayloadSecurityToken : SecurityToken - { - protected JwtPayloadSecurityToken() { } - - public JwtPayloadSecurityToken(string payload) - { - try - { - InnerToken = new JsonWebToken("{}", payload); - - using (var hasher = SHA256.Create()) - { - var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(payload)); - TokenHash = Convert.ToBase64String(hash); - } - } - catch (Exception e) - { - throw new ArgumentException("Token does not contain valid JSON", e); - } - } - - public override string Id => InnerToken.Id; - public override string Issuer => InnerToken.Issuer; - public virtual IEnumerable Audiences => InnerToken.Audiences; - public virtual string Subject => InnerToken.Subject; - public virtual string Actor => InnerToken.Actor; - public virtual IEnumerable Claims => InnerToken.Claims; - - public virtual DateTime IssuedAt => InnerToken.IssuedAt; - public override DateTime ValidFrom => InnerToken.ValidFrom; - public override DateTime ValidTo => InnerToken.ValidTo; - - protected JsonWebToken InnerToken { get; } - public virtual string TokenHash { get; } - } -} \ No newline at end of file diff --git a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadTokenHandler.cs b/src/ScottBrady.IdentityModel/Tokens/JwtPayloadTokenHandler.cs deleted file mode 100644 index 3e4af4d..0000000 --- a/src/ScottBrady.IdentityModel/Tokens/JwtPayloadTokenHandler.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace ScottBrady.IdentityModel.Tokens -{ - public abstract class JwtPayloadTokenHandler : TokenHandler, ISecurityTokenValidator - { - public abstract bool CanReadToken(string securityToken); - public bool CanValidateToken => true; - - public virtual ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken) - { - var result = ValidateToken(token, validationParameters); - - if (result.IsValid) - { - validatedToken = result.SecurityToken; - return new ClaimsPrincipal(result.ClaimsIdentity); - } - - throw result.Exception; - } - - public abstract TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters); - - /// - /// Validates a tokens lifetime, audience, and issuer using JWT payload validation rules. - /// Also checks for token replay - /// - protected virtual TokenValidationResult ValidateTokenPayload(JwtPayloadSecurityToken token, TokenValidationParameters validationParameters) - { - if (token == null) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(token))}; - if (validationParameters == null) return new TokenValidationResult {Exception = new ArgumentNullException(nameof(validationParameters))}; - - var expires = token.ValidTo == DateTime.MinValue ? null : new DateTime?(token.ValidTo); - var notBefore = token.ValidFrom == DateTime.MinValue ? null : new DateTime?(token.ValidFrom); - - try - { - ValidateLifetime(notBefore, expires, token, validationParameters); - ValidateAudience(token.Audiences, token, validationParameters); - ValidateIssuer(token.Issuer, token, validationParameters); - ValidateTokenReplay(expires, token.TokenHash, validationParameters); - } - catch (Exception e) - { - return new TokenValidationResult {Exception = e}; - } - - return new TokenValidationResult - { - SecurityToken = token, - ClaimsIdentity = CreateClaimsIdentity(token, validationParameters), - IsValid = true - }; - } - - protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) - => Validators.ValidateLifetime(notBefore, expires, securityToken, validationParameters); - - protected virtual void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) - => Validators.ValidateAudience(audiences, securityToken, validationParameters); - - protected virtual string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) - => Validators.ValidateIssuer(issuer, securityToken, validationParameters); - - protected virtual void ValidateTokenReplay(DateTime? expirationTime, string securityToken, TokenValidationParameters validationParameters) - => Validators.ValidateTokenReplay(expirationTime, securityToken, validationParameters); - - protected virtual void ValidateIssuerSecurityKey(SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters) - => Validators.ValidateIssuerSecurityKey(securityKey, securityToken, validationParameters); - - protected virtual ClaimsIdentity CreateClaimsIdentity(JwtPayloadSecurityToken token, TokenValidationParameters validationParameters) - { - if (token == null) throw LogHelper.LogArgumentNullException(nameof(token)); - if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - var issuer = token.Issuer; - if (string.IsNullOrWhiteSpace(issuer)) issuer = ClaimsIdentity.DefaultIssuer; - - var identity = validationParameters.CreateClaimsIdentity(token, issuer); - foreach (var claim in token.Claims) - { - if (claim.Properties.Count == 0) - { - identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, issuer, issuer, identity)); - } - else - { - var mappedClaim = new Claim(claim.Type, claim.Value, claim.ValueType, issuer, issuer, identity); - - foreach (var kv in claim.Properties) - mappedClaim.Properties[kv.Key] = kv.Value; - - identity.AddClaim(mappedClaim); - } - } - - return identity; - } - - protected virtual IEnumerable GetDecryptionKeys(string token, TokenValidationParameters validationParameters) - { - List keys = null; - - if (validationParameters.TokenDecryptionKeyResolver != null) - { - keys = validationParameters.TokenDecryptionKeyResolver(token, null, null, validationParameters)?.ToList(); - } - - if (keys == null || !keys.Any()) - { - keys = new List(); - if (validationParameters.TokenDecryptionKey != null) - keys.Add(validationParameters.TokenDecryptionKey); - if (validationParameters.TokenDecryptionKeys != null && validationParameters.TokenDecryptionKeys.Any()) - keys.AddRange(validationParameters.TokenDecryptionKeys); - } - - return keys; - } - - protected virtual IEnumerable GetSigningKeys(string token, TokenValidationParameters validationParameters) - { - List keys = null; - - if (validationParameters.IssuerSigningKeyResolver != null) - { - keys = validationParameters.IssuerSigningKeyResolver(token, null, null, validationParameters)?.ToList(); - } - - if (keys == null || !keys.Any()) - { - keys = new List(); - if (validationParameters.IssuerSigningKey != null) - keys.Add(validationParameters.IssuerSigningKey); - if (validationParameters.IssuerSigningKeys != null && validationParameters.IssuerSigningKeys.Any()) - keys.AddRange(validationParameters.IssuerSigningKeys); - } - - return keys; - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/AspNetCore/Identity/ExtendedPasswordValidatorTests.cs b/test/ScottBrady.IdentityModel.Tests/AspNetCore/Identity/ExtendedPasswordValidatorTests.cs index 67e254c..e8ba5c4 100644 --- a/test/ScottBrady.IdentityModel.Tests/AspNetCore/Identity/ExtendedPasswordValidatorTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/AspNetCore/Identity/ExtendedPasswordValidatorTests.cs @@ -7,36 +7,36 @@ using ScottBrady.IdentityModel.AspNetCore.Identity; using Xunit; -namespace ScottBrady.IdentityModel.Tests.AspNetCore.Identity +namespace ScottBrady.IdentityModel.Tests.AspNetCore.Identity; + +public class ExtendedPasswordValidatorTests { - public class ExtendedPasswordValidatorTests - { - private ExtendedPasswordValidator CreateSut() => new ExtendedPasswordValidator(); + private ExtendedPasswordValidator CreateSut() => new ExtendedPasswordValidator(); - private Mock> CreateMockedSut() - { + private Mock> CreateMockedSut() + { var sut = new Mock>() {CallBase = true}; sut.Setup(x => x.HasConsecutiveCharacters(It.IsAny(), It.IsAny())).Returns(false); return sut; } - [Fact] - public async Task ValidateAsync_WhenUserManagerIsNull_ExpectArgumentNullException() - { + [Fact] + public async Task ValidateAsync_WhenUserManagerIsNull_ExpectArgumentNullException() + { var sut = CreateMockedSut(); await Assert.ThrowsAsync(() => sut.Object.ValidateAsync(null, new IdentityUser(), "password")); } - [Fact] - public async Task ValidateAsync_WhenPasswordIsNull_ExpectArgumentNullException() - { + [Fact] + public async Task ValidateAsync_WhenPasswordIsNull_ExpectArgumentNullException() + { var sut = CreateMockedSut(); await Assert.ThrowsAsync(() => sut.Object.ValidateAsync(CreateMockUserManager().Object, new IdentityUser(), null)); } - [Fact] - public async Task ValidateAsync_WhenPasswordOptionsAreNotExtendedPasswordOptions_ExpectSuccess() - { + [Fact] + public async Task ValidateAsync_WhenPasswordOptionsAreNotExtendedPasswordOptions_ExpectSuccess() + { var options = new PasswordOptions(); var sut = CreateMockedSut(); @@ -45,9 +45,9 @@ public async Task ValidateAsync_WhenPasswordOptionsAreNotExtendedPasswordOptions result.Succeeded.Should().BeTrue(); } - [Fact] - public async Task ValidateAsync_WhenPasswordOptionsAreExtendedPasswordOptionsButNotSet_ExpectSuccess() - { + [Fact] + public async Task ValidateAsync_WhenPasswordOptionsAreExtendedPasswordOptionsButNotSet_ExpectSuccess() + { var options = new ExtendedPasswordOptions(); var sut = CreateMockedSut(); @@ -56,11 +56,11 @@ public async Task ValidateAsync_WhenPasswordOptionsAreExtendedPasswordOptionsBut result.Succeeded.Should().BeTrue(); } - [Theory] - [InlineData(1, "123")] - [InlineData(3, "1234")] - public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsTooLong_ExpectError(int maxLength, string password) - { + [Theory] + [InlineData(1, "123")] + [InlineData(3, "1234")] + public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsTooLong_ExpectError(int maxLength, string password) + { var options = new ExtendedPasswordOptions{MaxLength = maxLength}; var sut = CreateMockedSut(); @@ -70,13 +70,13 @@ public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsTooLong_ result.Errors.Should().Contain(x => x.Code == "PasswordTooLong" && x.Description.Contains(options.MaxLength.ToString())); } - [Theory] - [InlineData(-1, "123")] - [InlineData(0, "123")] - [InlineData(4, "123")] - [InlineData(3, "123")] - public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsNotTooLong_ExpectSuccess(int maxLength, string password) - { + [Theory] + [InlineData(-1, "123")] + [InlineData(0, "123")] + [InlineData(4, "123")] + [InlineData(3, "123")] + public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsNotTooLong_ExpectSuccess(int maxLength, string password) + { var options = new ExtendedPasswordOptions{MaxLength = maxLength}; var sut = CreateMockedSut(); @@ -85,12 +85,12 @@ public async Task ValidateAsync_WhenExtendedPasswordOptionsAndPasswordIsNotTooLo result.Succeeded.Should().BeTrue(); } - [Theory] - [InlineData(-1, "123")] - [InlineData(0, "123")] - [InlineData(3, "123")] - public async Task ValidateAsync_WhenExtendedPasswordOptionsAndMaxConsecutiveCharactersValid_ExpectSuccess(int maxConsecutive, string password) - { + [Theory] + [InlineData(-1, "123")] + [InlineData(0, "123")] + [InlineData(3, "123")] + public async Task ValidateAsync_WhenExtendedPasswordOptionsAndMaxConsecutiveCharactersValid_ExpectSuccess(int maxConsecutive, string password) + { var options = new ExtendedPasswordOptions {MaxConsecutiveChars = maxConsecutive}; var sut = CreateMockedSut(); @@ -101,9 +101,9 @@ public async Task ValidateAsync_WhenExtendedPasswordOptionsAndMaxConsecutiveChar result.Succeeded.Should().BeTrue(); } - [Fact] - public async Task ValidateAsync_WhenExtendedPasswordOptionsAndTooManyMaxConsecutiveCharacters_ExpectError() - { + [Fact] + public async Task ValidateAsync_WhenExtendedPasswordOptionsAndTooManyMaxConsecutiveCharacters_ExpectError() + { const string password = "Password123!"; var options = new ExtendedPasswordOptions {MaxConsecutiveChars = 2}; @@ -116,33 +116,33 @@ public async Task ValidateAsync_WhenExtendedPasswordOptionsAndTooManyMaxConsecut result.Errors.Should().Contain(x => x.Code == "TooManyConsecutiveCharacters" && x.Description.Contains(options.MaxConsecutiveChars.ToString())); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void HasConsecutiveCharacters_WhenPasswordIsNullOrWhitespace_ExpectArgumentNullException(string password) - { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void HasConsecutiveCharacters_WhenPasswordIsNullOrWhitespace_ExpectArgumentNullException(string password) + { var sut = CreateSut(); Assert.Throws(() => sut.HasConsecutiveCharacters(password, 42)); } - [Theory] - [InlineData(0, "qwerty")] - [InlineData(1, "qwerty")] - [InlineData(2, "qwerty")] - [InlineData(2, "qqwerty")] - [InlineData(1, "qwertyuiopasdfghjklzxcvbnm")] - public void HasConsecutiveCharacters_WhenNoConsecutiveCharacters_ExpectFalse(int max, string password) - { + [Theory] + [InlineData(0, "qwerty")] + [InlineData(1, "qwerty")] + [InlineData(2, "qwerty")] + [InlineData(2, "qqwerty")] + [InlineData(1, "qwertyuiopasdfghjklzxcvbnm")] + public void HasConsecutiveCharacters_WhenNoConsecutiveCharacters_ExpectFalse(int max, string password) + { var sut = CreateSut(); var hasConsecutiveCharacters = sut.HasConsecutiveCharacters(password, max); hasConsecutiveCharacters.Should().BeFalse(); } - [Fact] - public void HasConsecutiveCharacters_WhenConsecutiveCharactersButUnderLimit_ExpectFalse() - { + [Fact] + public void HasConsecutiveCharacters_WhenConsecutiveCharactersButUnderLimit_ExpectFalse() + { const int maxConsecutiveCharacters = 2; const string password = "qqwweerrttyy"; @@ -152,9 +152,9 @@ public void HasConsecutiveCharacters_WhenConsecutiveCharactersButUnderLimit_Expe hasConsecutiveCharacters.Should().BeFalse(); } - [Fact] - public void HasConsecutiveCharacters_WhenConsecutiveCharactersButDifferentCasing_ExpectFalse() - { + [Fact] + public void HasConsecutiveCharacters_WhenConsecutiveCharactersButDifferentCasing_ExpectFalse() + { const int maxConsecutiveCharacters = 2; const string password = "QqQwerty"; @@ -164,21 +164,20 @@ public void HasConsecutiveCharacters_WhenConsecutiveCharactersButDifferentCasing hasConsecutiveCharacters.Should().BeFalse(); } - [Theory] - [InlineData(1, "qqqwertyy")] - [InlineData(2, "qqqwertyy")] - [InlineData(2, "qwertyyy")] - [InlineData(3, "qwertyyyy")] - public void HasConsecutiveCharacters_WhenConsecutiveCharactersAndOverLimit_ExpectTrue(int max, string password) - { + [Theory] + [InlineData(1, "qqqwertyy")] + [InlineData(2, "qqqwertyy")] + [InlineData(2, "qwertyyy")] + [InlineData(3, "qwertyyyy")] + public void HasConsecutiveCharacters_WhenConsecutiveCharactersAndOverLimit_ExpectTrue(int max, string password) + { var sut = CreateSut(); var hasConsecutiveCharacters = sut.HasConsecutiveCharacters(password, max); hasConsecutiveCharacters.Should().BeTrue(); } - private static Mock> CreateMockUserManager(PasswordOptions options = null) - => new Mock>(new Mock>().Object, - new OptionsWrapper(new IdentityOptions {Password = options ?? new PasswordOptions()}), null, null, null, null, null, null, null); - } + private static Mock> CreateMockUserManager(PasswordOptions options = null) + => new Mock>(new Mock>().Object, + new OptionsWrapper(new IdentityOptions {Password = options ?? new PasswordOptions()}), null, null, null, null, null, null, null); } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/AspNetCore/TagHelpers/NewPasswordTagHelperTests.cs b/test/ScottBrady.IdentityModel.Tests/AspNetCore/TagHelpers/NewPasswordTagHelperTests.cs index c3940c0..a50d70f 100644 --- a/test/ScottBrady.IdentityModel.Tests/AspNetCore/TagHelpers/NewPasswordTagHelperTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/AspNetCore/TagHelpers/NewPasswordTagHelperTests.cs @@ -11,301 +11,300 @@ using ScottBrady.IdentityModel.AspNetCore.TagHelpers; using Xunit; -namespace ScottBrady.IdentityModel.Tests.AspNetCore.TagHelpers +namespace ScottBrady.IdentityModel.Tests.AspNetCore.TagHelpers; + +public class NewPasswordTagHelperTests { - public class NewPasswordTagHelperTests - { - private readonly Mock mockHtmlGenerator = new Mock(); - private Mock> mockOptionsAccessor = new Mock>(); + private readonly Mock mockHtmlGenerator = new Mock(); + private Mock> mockOptionsAccessor = new Mock>(); - private readonly TagHelperContext testContext = new TagHelperContext("newpassword", new TagHelperAttributeList(), new Dictionary(), "123"); - private readonly TagHelperOutput testOutput = new TagHelperOutput("newpassword", new TagHelperAttributeList(), (b, encoder) => Task.FromResult(new DefaultTagHelperContent())); + private readonly TagHelperContext testContext = new TagHelperContext("newpassword", new TagHelperAttributeList(), new Dictionary(), "123"); + private readonly TagHelperOutput testOutput = new TagHelperOutput("newpassword", new TagHelperAttributeList(), (b, encoder) => Task.FromResult(new DefaultTagHelperContent())); - public NewPasswordTagHelperTests() - { - mockOptionsAccessor.Setup(x => x.Value).Returns(new IdentityOptions()); + public NewPasswordTagHelperTests() + { + mockOptionsAccessor.Setup(x => x.Value).Returns(new IdentityOptions()); - } + } - private NewPasswordTagHelper CreateSut() - { - return new NewPasswordTagHelper(mockHtmlGenerator?.Object, mockOptionsAccessor?.Object); - } + private NewPasswordTagHelper CreateSut() + { + return new NewPasswordTagHelper(mockHtmlGenerator?.Object, mockOptionsAccessor?.Object); + } - private Mock CreateMockedSut() - { - var sut = new Mock(mockHtmlGenerator?.Object, mockOptionsAccessor?.Object) {CallBase = true}; - sut.Setup(x => x.ProcessInputTag(It.IsAny(), It.IsAny())); - sut.Setup(x => x.ProcessIdentityPasswordRules(It.IsAny(), It.IsAny())); - return sut; - } - - [Fact] - public void ctor_WhenOptionsAccessorIsNull_ExpectNullOptions() - { - mockOptionsAccessor = null; - var sut = CreateSut(); - sut.Options.Should().BeNull(); - } - - [Fact] - public void ctor_WhenOptionsAccessorReturnsNullIsNull_ExpectNullOptions() - { - mockOptionsAccessor.Setup(x => x.Value).Returns(null); - var sut = CreateSut(); - sut.Options.Should().BeNull(); - } + private Mock CreateMockedSut() + { + var sut = new Mock(mockHtmlGenerator?.Object, mockOptionsAccessor?.Object) {CallBase = true}; + sut.Setup(x => x.ProcessInputTag(It.IsAny(), It.IsAny())); + sut.Setup(x => x.ProcessIdentityPasswordRules(It.IsAny(), It.IsAny())); + return sut; + } + + [Fact] + public void ctor_WhenOptionsAccessorIsNull_ExpectNullOptions() + { + mockOptionsAccessor = null; + var sut = CreateSut(); + sut.Options.Should().BeNull(); + } + + [Fact] + public void ctor_WhenOptionsAccessorReturnsNullIsNull_ExpectNullOptions() + { + mockOptionsAccessor.Setup(x => x.Value).Returns(null); + var sut = CreateSut(); + sut.Options.Should().BeNull(); + } - [Fact] - public void Process_WhenTagHelperContextIsNull_ExpectArgumentNullException() - { - var sut = CreateMockedSut(); - Assert.Throws(() => sut.Object.Process(null, testOutput)); - } - [Fact] - public void Process_WhenTagHelperOutputIsNull_ExpectArgumentNullException() - { - var sut = CreateMockedSut(); - Assert.Throws(() => sut.Object.Process(testContext, null)); - } - - [Fact] - public void Process_ExpectExistingAttributesUnmodified() - { - testOutput.Attributes.SetAttribute("name", "password"); - testOutput.Attributes.SetAttribute("aria-label", "Password"); - testOutput.Attributes.SetAttribute("required", "required"); + [Fact] + public void Process_WhenTagHelperContextIsNull_ExpectArgumentNullException() + { + var sut = CreateMockedSut(); + Assert.Throws(() => sut.Object.Process(null, testOutput)); + } + [Fact] + public void Process_WhenTagHelperOutputIsNull_ExpectArgumentNullException() + { + var sut = CreateMockedSut(); + Assert.Throws(() => sut.Object.Process(testContext, null)); + } + + [Fact] + public void Process_ExpectExistingAttributesUnmodified() + { + testOutput.Attributes.SetAttribute("name", "password"); + testOutput.Attributes.SetAttribute("aria-label", "Password"); + testOutput.Attributes.SetAttribute("required", "required"); - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); - testOutput.Attributes["name"].Value.Should().Be("password"); - testOutput.Attributes["aria-label"].Value.Should().Be("Password"); - testOutput.Attributes["required"].Value.Should().Be("required"); - } - - [Fact] - public void Process_ExpectInputTag() - { - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); - testOutput.TagName.Should().Be("input"); - } - - [Fact] - public void Process_ExpectTypeOfPassword() - { - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); - testOutput.Attributes["type"].Value.Should().Be("password"); - } - - [Fact] - public void Process_WithExistingType_ExpectTypeOfPassword() - { - testOutput.Attributes.SetAttribute("type", "text"); + testOutput.Attributes["name"].Value.Should().Be("password"); + testOutput.Attributes["aria-label"].Value.Should().Be("Password"); + testOutput.Attributes["required"].Value.Should().Be("required"); + } + + [Fact] + public void Process_ExpectInputTag() + { + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); + testOutput.TagName.Should().Be("input"); + } + + [Fact] + public void Process_ExpectTypeOfPassword() + { + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); + testOutput.Attributes["type"].Value.Should().Be("password"); + } + + [Fact] + public void Process_WithExistingType_ExpectTypeOfPassword() + { + testOutput.Attributes.SetAttribute("type", "text"); - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); - testOutput.Attributes["type"].Value.Should().Be("password"); - } - - [Fact] - public void Process_ExpectAutoCompleteOfNewPassword() - { - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); - testOutput.Attributes["autocomplete"].Value.Should().Be("new-password"); - } - - [Fact] - public void Process_ExpectAutoCorrectDisabled() - { - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); - testOutput.Attributes["autocorrect"].Value.Should().Be("off"); - } - - [Fact] - public void Process_ExpectAutoCapitalizeDisabled() - { - var sut = CreateMockedSut(); - sut.Object.Process(testContext, testOutput); - testOutput.Attributes["autocapitalize"].Value.Should().Be("off"); - } - - [Fact] - public void ProcessIdentityPasswordRules_WhenOptionsAreNull_ExpectArgumentNullException() - { - var sut = CreateSut(); - Assert.Throws(() => sut.ProcessIdentityPasswordRules(null, testOutput)); - } - - [Fact] - public void ProcessIdentityPasswordRules_WhenTagHelperOutputIsNull_ExpectArgumentNullException() - { - var sut = CreateSut(); - Assert.Throws(() => sut.ProcessIdentityPasswordRules(new PasswordOptions(), null)); - } - - [Theory] - [InlineData(1)] - [InlineData(6)] - [InlineData(42)] - [InlineData(255)] - public void ProcessIdentityPasswordRules_ExpectCorrectRequiredLengthAttributes(int expectedMinLength) - { - var sut = CreateSut(); - var options = new PasswordOptions {RequiredLength = expectedMinLength}; + testOutput.Attributes["type"].Value.Should().Be("password"); + } + + [Fact] + public void Process_ExpectAutoCompleteOfNewPassword() + { + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); + testOutput.Attributes["autocomplete"].Value.Should().Be("new-password"); + } + + [Fact] + public void Process_ExpectAutoCorrectDisabled() + { + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); + testOutput.Attributes["autocorrect"].Value.Should().Be("off"); + } + + [Fact] + public void Process_ExpectAutoCapitalizeDisabled() + { + var sut = CreateMockedSut(); + sut.Object.Process(testContext, testOutput); + testOutput.Attributes["autocapitalize"].Value.Should().Be("off"); + } + + [Fact] + public void ProcessIdentityPasswordRules_WhenOptionsAreNull_ExpectArgumentNullException() + { + var sut = CreateSut(); + Assert.Throws(() => sut.ProcessIdentityPasswordRules(null, testOutput)); + } + + [Fact] + public void ProcessIdentityPasswordRules_WhenTagHelperOutputIsNull_ExpectArgumentNullException() + { + var sut = CreateSut(); + Assert.Throws(() => sut.ProcessIdentityPasswordRules(new PasswordOptions(), null)); + } + + [Theory] + [InlineData(1)] + [InlineData(6)] + [InlineData(42)] + [InlineData(255)] + public void ProcessIdentityPasswordRules_ExpectCorrectRequiredLengthAttributes(int expectedMinLength) + { + var sut = CreateSut(); + var options = new PasswordOptions {RequiredLength = expectedMinLength}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"minlength: {expectedMinLength};"); - testOutput.Attributes["minlength"].Value.Should().Be(expectedMinLength); - } + testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"minlength: {expectedMinLength};"); + testOutput.Attributes["minlength"].Value.Should().Be(expectedMinLength); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireLowercaseIsTrue_ExpectRequiredLowerAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireLowercase = true}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireLowercaseIsTrue_ExpectRequiredLowerAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireLowercase = true}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: lower;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: lower;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireLowercaseIsFalse_ExpectNoRequiredLowerAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireLowercase = false}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireLowercaseIsFalse_ExpectNoRequiredLowerAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireLowercase = false}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: lower;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: lower;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireUppercaseIsTrue_ExpectRequiredUpperAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireUppercase = true}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireUppercaseIsTrue_ExpectRequiredUpperAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireUppercase = true}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: upper;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: upper;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireUppercaseIsFalse_ExpectNoRequiredUpperAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireUppercase = false}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireUppercaseIsFalse_ExpectNoRequiredUpperAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireUppercase = false}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: upper;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: upper;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireDigitIsTrue_ExpectRequiredDigitAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireDigit = true}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireDigitIsTrue_ExpectRequiredDigitAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireDigit = true}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: digit;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: digit;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireDigitIsFalse_ExpectNoRequiredDigitAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireDigit = false}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireDigitIsFalse_ExpectNoRequiredDigitAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireDigit = false}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: digit;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: digit;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireNonAlphanumericIsTrue_ExpectRequiredCharactersAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireNonAlphanumeric = true}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireNonAlphanumericIsTrue_ExpectRequiredCharactersAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireNonAlphanumeric = true}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: special;"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().Contain("required: special;"); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenRequireNonAlphanumericIsFalse_ExpectNoRequiredCharactersAttribute() - { - var sut = CreateSut(); - var options = new PasswordOptions {RequireNonAlphanumeric = false}; + [Fact] + public void ProcessIdentityPasswordRules_WhenRequireNonAlphanumericIsFalse_ExpectNoRequiredCharactersAttribute() + { + var sut = CreateSut(); + var options = new PasswordOptions {RequireNonAlphanumeric = false}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: ["); - } + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("required: ["); + } - [Fact] - public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithMaxLength_ExpectMaxLengthAttribute() - { - const int expectedMaxLength = 42; + [Fact] + public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithMaxLength_ExpectMaxLengthAttribute() + { + const int expectedMaxLength = 42; - var sut = CreateSut(); - var options = new ExtendedPasswordOptions {MaxLength = expectedMaxLength}; + var sut = CreateSut(); + var options = new ExtendedPasswordOptions {MaxLength = expectedMaxLength}; - sut.ProcessIdentityPasswordRules(options, testOutput); - - testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"maxlength: {expectedMaxLength};"); - testOutput.Attributes["maxlength"].Value.Should().Be(expectedMaxLength); - } - - [Theory] - [InlineData(null)] - [InlineData(0)] - [InlineData(-1)] - public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithInvalidMaxLength_ExpectNoMaxLengthAttribute(int? expectedMaxLength) - { - var sut = CreateSut(); - var options = new ExtendedPasswordOptions {MaxLength = expectedMaxLength}; + sut.ProcessIdentityPasswordRules(options, testOutput); + + testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"maxlength: {expectedMaxLength};"); + testOutput.Attributes["maxlength"].Value.Should().Be(expectedMaxLength); + } + + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(-1)] + public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithInvalidMaxLength_ExpectNoMaxLengthAttribute(int? expectedMaxLength) + { + var sut = CreateSut(); + var options = new ExtendedPasswordOptions {MaxLength = expectedMaxLength}; - sut.ProcessIdentityPasswordRules(options, testOutput); - - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("maxlength"); - testOutput.Attributes["maxlength"].Should().BeNull(); - } - - [Theory] - [InlineData(0)] - [InlineData(2)] - public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithMaxConsecutiveChars_ExpectMaxConsecutiveCharsAttribute(int? expectedMaxConsecutiveChars) - { - var sut = CreateSut(); - var options = new ExtendedPasswordOptions {MaxConsecutiveChars = expectedMaxConsecutiveChars}; + sut.ProcessIdentityPasswordRules(options, testOutput); + + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("maxlength"); + testOutput.Attributes["maxlength"].Should().BeNull(); + } + + [Theory] + [InlineData(0)] + [InlineData(2)] + public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithMaxConsecutiveChars_ExpectMaxConsecutiveCharsAttribute(int? expectedMaxConsecutiveChars) + { + var sut = CreateSut(); + var options = new ExtendedPasswordOptions {MaxConsecutiveChars = expectedMaxConsecutiveChars}; - sut.ProcessIdentityPasswordRules(options, testOutput); - - testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"max-consecutive: {expectedMaxConsecutiveChars};"); - } - - [Theory] - [InlineData(null)] - [InlineData(-1)] - public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithInvalidMaxConsecutiveChars_ExpectNoMaxConsecutiveCharsAttribute(int? expectedMaxConsecutiveChars) - { - var sut = CreateSut(); - var options = new ExtendedPasswordOptions {MaxConsecutiveChars = expectedMaxConsecutiveChars}; + sut.ProcessIdentityPasswordRules(options, testOutput); + + testOutput.Attributes["passwordrules"].Value.As().Should().Contain($"max-consecutive: {expectedMaxConsecutiveChars};"); + } + + [Theory] + [InlineData(null)] + [InlineData(-1)] + public void ProcessIdentityPasswordRules_WhenExtendedOptionsWithInvalidMaxConsecutiveChars_ExpectNoMaxConsecutiveCharsAttribute(int? expectedMaxConsecutiveChars) + { + var sut = CreateSut(); + var options = new ExtendedPasswordOptions {MaxConsecutiveChars = expectedMaxConsecutiveChars}; - sut.ProcessIdentityPasswordRules(options, testOutput); + sut.ProcessIdentityPasswordRules(options, testOutput); - testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("max-consecutive"); - } + testOutput.Attributes["passwordrules"].Value.As().Should().NotContain("max-consecutive"); } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Encoding/Base16Tests.cs b/test/ScottBrady.IdentityModel.Tests/Encoding/Base16Tests.cs index 3e995a9..1cff7b7 100644 --- a/test/ScottBrady.IdentityModel.Tests/Encoding/Base16Tests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Encoding/Base16Tests.cs @@ -3,21 +3,21 @@ using FluentAssertions; using Xunit; -namespace ScottBrady.IdentityModel.Tests +namespace ScottBrady.IdentityModel.Tests; + +public class Base16Tests { - public class Base16Tests + [Theory] + [InlineData("t", "74")] + [InlineData("te", "7465")] + [InlineData("tes", "746573")] + [InlineData("test", "74657374")] + [InlineData("test_", "746573745f")] + [InlineData("test_v", "746573745f76")] + [InlineData("test_va", "746573745f7661")] + [InlineData("test_val", "746573745f76616c")] + public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string expectedResult) { - [Theory] - [InlineData("t", "74")] - [InlineData("te", "7465")] - [InlineData("tes", "746573")] - [InlineData("test", "74657374")] - [InlineData("test_", "746573745f")] - [InlineData("test_v", "746573745f76")] - [InlineData("test_va", "746573745f7661")] - [InlineData("test_val", "746573745f76616c")] - public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string expectedResult) - { var testBytes = Encoding.UTF8.GetBytes(testValue); var result = Base16.Encode(testBytes); @@ -28,11 +28,11 @@ public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string } - [Theory] - [InlineData("ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ", "e19aa0e19b87e19abbe19babe19b92e19ba6e19aa6e19babe19aa0e19ab1e19aa9e19aa0e19aa2e19ab1e19babe19aa0e19b81e19ab1e19aaae19babe19ab7e19b96e19abbe19ab9e19ba6e19b9ae19ab3e19aa2e19b97")] - [InlineData("¥·£·€·$·¢·₡·₢·₣·₤·₥·₦·₧·₨·₩·₪·₫·₭·₮·₯·₹", "c2a5c2b7c2a3c2b7e282acc2b724c2b7c2a2c2b7e282a1c2b7e282a2c2b7e282a3c2b7e282a4c2b7e282a5c2b7e282a6c2b7e282a7c2b7e282a8c2b7e282a9c2b7e282aac2b7e282abc2b7e282adc2b7e282aec2b7e282afc2b7e282b9")] - public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, string expectedResult) - { + [Theory] + [InlineData("ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ", "e19aa0e19b87e19abbe19babe19b92e19ba6e19aa6e19babe19aa0e19ab1e19aa9e19aa0e19aa2e19ab1e19babe19aa0e19b81e19ab1e19aaae19babe19ab7e19b96e19abbe19ab9e19ba6e19b9ae19ab3e19aa2e19b97")] + [InlineData("¥·£·€·$·¢·₡·₢·₣·₤·₥·₦·₧·₨·₩·₪·₫·₭·₮·₯·₹", "c2a5c2b7c2a3c2b7e282acc2b724c2b7c2a2c2b7e282a1c2b7e282a2c2b7e282a3c2b7e282a4c2b7e282a5c2b7e282a6c2b7e282a7c2b7e282a8c2b7e282a9c2b7e282aac2b7e282abc2b7e282adc2b7e282aec2b7e282afc2b7e282b9")] + public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, string expectedResult) + { var testBytes = Encoding.UTF8.GetBytes(testValue); var result = Base16.Encode(testBytes); @@ -42,9 +42,9 @@ public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, stri decodedBytes.Should().BeEquivalentTo(testBytes); } - [Fact] - public void WithRandomBytes_ExpectCorrectValuesEncoded() - { + [Fact] + public void WithRandomBytes_ExpectCorrectValuesEncoded() + { var bytes = new byte[128]; var rng = RandomNumberGenerator.Create(); rng.GetBytes(bytes); @@ -52,5 +52,4 @@ public void WithRandomBytes_ExpectCorrectValuesEncoded() var encodedBytes = Base16.Encode(bytes); Base16.Decode(encodedBytes).Should().BeEquivalentTo(bytes); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Encoding/Base62Tests.cs b/test/ScottBrady.IdentityModel.Tests/Encoding/Base62Tests.cs index 939807f..73f180b 100644 --- a/test/ScottBrady.IdentityModel.Tests/Encoding/Base62Tests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Encoding/Base62Tests.cs @@ -3,21 +3,21 @@ using FluentAssertions; using Xunit; -namespace ScottBrady.IdentityModel.Tests +namespace ScottBrady.IdentityModel.Tests; + +public class Base62Tests { - public class Base62Tests + [Theory] + [InlineData("t", "1s")] + [InlineData("te", "7kb")] + [InlineData("tes", "W0Qd")] + [InlineData("test", "289lyu")] + [InlineData("test_", "8ngM7Ul")] + [InlineData("test_v", "aL8tKx1y")] + [InlineData("test_va", "2Q3IiUVk9J")] + [InlineData("test_val", "9zZdHhz4YSC")] + public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string expectedResult) { - [Theory] - [InlineData("t", "1s")] - [InlineData("te", "7kb")] - [InlineData("tes", "W0Qd")] - [InlineData("test", "289lyu")] - [InlineData("test_", "8ngM7Ul")] - [InlineData("test_v", "aL8tKx1y")] - [InlineData("test_va", "2Q3IiUVk9J")] - [InlineData("test_val", "9zZdHhz4YSC")] - public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string expectedResult) - { var testBytes = Encoding.UTF8.GetBytes(testValue); var result = Base62.Encode(testBytes); @@ -27,11 +27,11 @@ public void WithKnownValues_ExpectCorrectValuesEncoded(string testValue, string decodedBytes.Should().BeEquivalentTo(testBytes); } - [Theory] - [InlineData("ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ", "Z2OiyFrg9j5RTozitQyWlPIpRF9zciF7WUm0omCp8p8amBsp3T7z3XtHx9aAcgG5J5ggJT7mLQP1WonC0PAUF7hrM4KT6dwoUqhBsRWHBS3gZXeCkIbJP")] - [InlineData("¥·£·€·$·¢·₡·₢·₣·₤·₥·₦·₧·₨·₩·₪·₫·₭·₮·₯·₹", "cyHYeZmwVtcfi8uomwZ9VTrEews1tZEkwNsVEOzPtGnuTpxFrKkQykOshm9OCqSa0YkPX13Js2w8QAcKpHsMHzdKzNG9htLkL6Pu6xFSwoSZycE8aUfGRIZTKcX8L")] - public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, string expectedResult) - { + [Theory] + [InlineData("ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ", "Z2OiyFrg9j5RTozitQyWlPIpRF9zciF7WUm0omCp8p8amBsp3T7z3XtHx9aAcgG5J5ggJT7mLQP1WonC0PAUF7hrM4KT6dwoUqhBsRWHBS3gZXeCkIbJP")] + [InlineData("¥·£·€·$·¢·₡·₢·₣·₤·₥·₦·₧·₨·₩·₪·₫·₭·₮·₯·₹", "cyHYeZmwVtcfi8uomwZ9VTrEews1tZEkwNsVEOzPtGnuTpxFrKkQykOshm9OCqSa0YkPX13Js2w8QAcKpHsMHzdKzNG9htLkL6Pu6xFSwoSZycE8aUfGRIZTKcX8L")] + public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, string expectedResult) + { var testBytes = Encoding.UTF8.GetBytes(testValue); var result = Base62.Encode(testBytes); @@ -41,9 +41,9 @@ public void WithUtf8Characters_ExpectCorrectValuesEncoded(string testValue, stri decodedBytes.Should().BeEquivalentTo(testBytes); } - [Fact] - public void WithRandomBytes_ExpectCorrectValuesEncoded() - { + [Fact] + public void WithRandomBytes_ExpectCorrectValuesEncoded() + { var bytes = new byte[128]; var rng = RandomNumberGenerator.Create(); rng.GetBytes(bytes); @@ -51,5 +51,4 @@ public void WithRandomBytes_ExpectCorrectValuesEncoded() var encodedBytes = Base62.Encode(bytes); Base62.Decode(encodedBytes).Should().BeEquivalentTo(bytes); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/ScottBrady.IdentityModel.Tests.csproj b/test/ScottBrady.IdentityModel.Tests/ScottBrady.IdentityModel.Tests.csproj index 927b49a..e2ce262 100644 --- a/test/ScottBrady.IdentityModel.Tests/ScottBrady.IdentityModel.Tests.csproj +++ b/test/ScottBrady.IdentityModel.Tests/ScottBrady.IdentityModel.Tests.csproj @@ -1,21 +1,22 @@ - net6.0 + net6.0;net8.0 - - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaSecurityTokenTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaSecurityTokenTests.cs deleted file mode 100644 index 1ba4142..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaSecurityTokenTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Text; -using FluentAssertions; -using Newtonsoft.Json; -using ScottBrady.IdentityModel.Tokens.Branca; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Branca -{ - public class BrancaSecurityTokenTests - { - [Fact] - public void ctor_ExpectBrancaTokenTimestampUsedForIssuedAt() - { - const uint expectedIssuedAt = 1588341499; - - var jwt = JsonConvert.SerializeObject( - new - { - iss = "me", - aud = "you", - iat = expectedIssuedAt - 1000 - }); - - var token = new BrancaSecurityToken(new BrancaToken(Encoding.UTF8.GetBytes(jwt), expectedIssuedAt)); - - token.IssuedAt.Should().Be(DateTimeOffset.FromUnixTimeSeconds(expectedIssuedAt).UtcDateTime); - } - - [Fact] - public void ctor_WhenPayloadIsNotUtf8_ExpectException() - { - var payload = Encoding.Unicode.GetBytes("������"); - var exception = Assert.Throws(() => new BrancaSecurityToken(new BrancaToken(payload, 0))); - exception.Message.Should().Contain("Token does not contain valid JSON"); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenHandlerTests.cs deleted file mode 100644 index 82a1fda..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenHandlerTests.cs +++ /dev/null @@ -1,516 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using Moq; -using Moq.Protected; -using Newtonsoft.Json.Linq; -using ScottBrady.IdentityModel.Crypto; -using ScottBrady.IdentityModel.Tokens.Branca; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Branca -{ - public class BrancaTokenHandlerTests - { - private const string ValidToken = "5K6fDIqRhrSuqGE3FbuxAPd19P2toAsbBxOn4bgSame9ti6QZUQJkrggCypBJIEXF6tvhgjeMZTV76UkiqXNSvqHebeplccFrhepHkxU1SlSSFoAMKs5TUomcg6ZgDhiaYDs3IlypSxafP4uvKmu0VD"; - private readonly byte[] validKey = Encoding.UTF8.GetBytes("supersecretkeyyoushouldnotcommit"); - private static readonly byte[] ExpectedPayload = Encoding.UTF8.GetBytes("{\"user\":\"scott@scottbrady91.com\",\"scope\":[\"read\",\"write\",\"delete\"]}"); - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void CanReadToken_WhenTokenIsNullOrWhitespace_ExpectFalse(string token) - { - var handler = new BrancaTokenHandler(); - var canReadToken = handler.CanReadToken(token); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenTokenIsTooLong_ExpectFalse() - { - var tokenBytes = new byte[TokenValidationParameters.DefaultMaximumTokenSizeInBytes + 1]; - new Random().NextBytes(tokenBytes); - - var canReadToken = new BrancaTokenHandler().CanReadToken(Convert.ToBase64String(tokenBytes)); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenJwtToken_ExpectFalse() - { - const string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiU2NvdHQgQnJhZHkiLCJpYXQiOjE1ODU3Njc0Mjl9.DcGCOpx19JQzVVeZPHgqB73rbLaCUsx-k6PuFdit6IM"; - - var canReadToken = new BrancaTokenHandler().CanReadToken(jwt); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenTokenContainsNonBase64Characters_ExpectFalse() - { - const string token = "token=="; - - var canReadToken = new BrancaTokenHandler().CanReadToken(token); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenBrancaToken_ExpectTrue() - { - var canReadToken = new BrancaTokenHandler().CanReadToken(ValidToken); - - canReadToken.Should().BeTrue(); - } - - [Fact] - public void CanValidateToken_ExpectTrue() - => new BrancaTokenHandler().CanValidateToken.Should().BeTrue(); - - [Fact] - public void CreateToken_WhenPayloadIsNull_ExpectArgumentNullException() - { - var handler = new BrancaTokenHandler(); - Assert.Throws(() => handler.CreateToken(null, validKey)); - } - - [Fact] - public void CreateToken_WhenKeyIsNull_ExpectInvalidOperationException() - => Assert.Throws(() => new BrancaTokenHandler().CreateToken("test", null)); - - [Fact] - public void CreateToken_WhenKeyIsNot32Bytes_ExpectInvalidOperationException() - => Assert.Throws(() => - new BrancaTokenHandler().CreateToken("test", Encoding.UTF8.GetBytes("iamonly14bytes"))); - - [Fact] - public void CreateToken_WhenTokenGenerated_ExpectBas62EncodedTokenWithCorrectLength() - { - var payload = CreateTestPayload(); - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(payload, 0, validKey); - - token.Any(x => !Base62.CharacterSet.Contains(x)).Should().BeFalse(); - Base62.Decode(token).Length.Should().Be(payload.Length + 29 + 16); - } - - [Fact] - public void CreateToken_WhenSecurityTokenDescriptorIsNull_ExpectArgumentNullException() - => Assert.Throws(() => new BrancaTokenHandler().CreateToken(null)); - - - [Fact] - public void CreateAndDecryptToken_WithSecurityTokenDescriptor_ExpectCorrectBrancaTimestampAndNoIatClaim() - { - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(new SecurityTokenDescriptor - { - EncryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(validKey), ExtendedSecurityAlgorithms.XChaCha20Poly1305) - }); - - var parsedToken = handler.DecryptToken(token, validKey); - var jObject = JObject.Parse(Encoding.UTF8.GetString(parsedToken.Payload)); - jObject["iat"].Should().BeNull(); - - parsedToken.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMilliseconds(1500)); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void DecryptToken_WhenTokenIsNullOrWhitespace_ExpectArgumentNullException(string token) - { - var handler = new BrancaTokenHandler(); - Assert.Throws(() => handler.DecryptToken(token, validKey)); - } - - [Fact] - public void DecryptToken_WhenKeyIsNull_ExpectInvalidOperationException() - => Assert.Throws(() => new BrancaTokenHandler().DecryptToken(ValidToken, null)); - - [Fact] - public void DecryptToken_WhenKeyIsNot32Bytes_ExpectInvalidOperationException() - => Assert.Throws(() => - new BrancaTokenHandler().DecryptToken(ValidToken, Encoding.UTF8.GetBytes("iamonly14bytes"))); - - [Fact] - public void DecryptToken_WhenTokenHasInvalidLength_ExpectSecurityTokenException() - { - var bytes = new byte[20]; - new Random().NextBytes(bytes); - - Assert.Throws(() => - new BrancaTokenHandler().DecryptToken(Base62.Encode(bytes), validKey)); - } - - [Fact] - public void DecryptToken_WhenTokenHasIncorrectVersion_ExpectSecurityTokenException() - { - var bytes = new byte[120]; - new Random().NextBytes(bytes); - bytes[0] = 0x00; - - Assert.Throws(() => - new BrancaTokenHandler().DecryptToken(Base62.Encode(bytes), validKey)); - } - - [Fact] - public void DecryptToken_WhenValidToken_ExpectCorrectPayload() - { - var parsedToken = new BrancaTokenHandler().DecryptToken(ValidToken, validKey); - parsedToken.Payload.Should().BeEquivalentTo(ExpectedPayload); - } - - [Fact] - public void EncryptAndDecryptToken_ExpectCorrectPayloadAndTimestamp() - { - var payload = Guid.NewGuid().ToString(); - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(payload, validKey); - var decryptedPayload = handler.DecryptToken(token, validKey); - - decryptedPayload.Payload.Should().BeEquivalentTo(Encoding.UTF8.GetBytes(payload)); - decryptedPayload.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMilliseconds(1000)); - } - - [Fact] - public void EncryptAndDecryptToken_WithExplicitTimestamp_ExpectCorrectPayloadAndTimestamp() - { - var payload = Guid.NewGuid().ToString(); - var timestamp = new DateTime(2020, 08, 22).ToUniversalTime(); - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(payload, timestamp, validKey); - var decryptedPayload = handler.DecryptToken(token, validKey); - - decryptedPayload.Payload.Should().BeEquivalentTo(Encoding.UTF8.GetBytes(payload)); - decryptedPayload.Timestamp.Should().Be(timestamp); - } - - [Fact] - public void EncryptAndDecryptToken_WithExplicitBrancaTimestamp_ExpectCorrectPayloadAndTimestamp() - { - var payload = CreateTestPayload(); - var timestamp = uint.MinValue; - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(payload, timestamp, validKey); - var decryptedPayload = handler.DecryptToken(token, validKey); - - decryptedPayload.Payload.Should().BeEquivalentTo(payload); - decryptedPayload.Timestamp.Should().Be(new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc)); - decryptedPayload.BrancaFormatTimestamp.Should().Be(timestamp); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void ValidateToken_WhenTokenIsNullOrWhitespace_ExpectFailureWithArgumentNullException(string token) - { - var result = new BrancaTokenHandler().ValidateToken(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenValidationParametersAreNull_ExpectFailureWithArgumentNullException() - { - var result = new BrancaTokenHandler().ValidateToken(ValidToken, null); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenCannotBeRead_ExpectFailureWithSecurityTokenException() - { - var result = new BrancaTokenHandler().ValidateToken("=====", new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenIncorrectDecryptionKey_ExpectFailureWithSecurityTokenDecryptionFailedException() - { - var key = new byte[32]; - new Random().NextBytes(key); - - var result = new BrancaTokenHandler().ValidateToken( - ValidToken, - new TokenValidationParameters {TokenDecryptionKey = new SymmetricSecurityKey(key)}); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenPayloadIsNotJson_ExpectFailureWithArgumentException() - { - const string tokenWithInvalidPayload = "Mvm6wbsyZMgClkmtiBf0lW3rEkvnCK5RgytoerJJex40b9yqh6GbSlfkFJHgFX9ocF"; - - var result = new BrancaTokenHandler().ValidateToken( - tokenWithInvalidPayload, - new TokenValidationParameters {TokenDecryptionKey = new SymmetricSecurityKey(validKey)}); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenValidToken_ExpectSuccessResultWithSecurityTokenAndClaimsIdentity() - { - var expectedIdentity = new ClaimsIdentity("test"); - - var mockHandler = new Mock {CallBase = true}; - mockHandler.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(new TokenValidationResult - { - ClaimsIdentity = expectedIdentity, - IsValid = true - }); - - var result = mockHandler.Object.ValidateToken( - ValidToken, - new TokenValidationParameters {TokenDecryptionKey = new SymmetricSecurityKey(validKey)}); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.Should().Be(expectedIdentity); - result.SecurityToken.Should().NotBeNull(); - } - - [Fact] - public void ValidateToken_WhenSaveSignInTokenIsTrue_ExpectIdentityBootstrapContext() - { - const string expectedToken = ValidToken; - var expectedIdentity = new ClaimsIdentity("test"); - - var mockHandler = new Mock {CallBase = true}; - mockHandler.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(new TokenValidationResult - { - ClaimsIdentity = expectedIdentity, - IsValid = true - }); - - var result = mockHandler.Object.ValidateToken( - expectedToken, - new TokenValidationParameters - { - TokenDecryptionKey = new SymmetricSecurityKey(validKey), - SaveSigninToken = true - }); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.BootstrapContext.Should().Be(expectedToken); - } - - [Fact] - public void CreateAndValidateToken_WithSecurityTokenDescriptor_ExpectCorrectBrancaTimestampAndNoIatClaim() - { - const string issuer = "me"; - const string audience = "you"; - const string subject = "123"; - var expires = DateTime.UtcNow.AddDays(1); - var notBefore = DateTime.UtcNow; - - var handler = new BrancaTokenHandler(); - - var token = handler.CreateToken(new SecurityTokenDescriptor - { - Issuer = issuer, - Audience = audience, - Expires = expires, - NotBefore = notBefore, - Claims = new Dictionary {{"sub", subject}}, - EncryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(validKey), ExtendedSecurityAlgorithms.XChaCha20Poly1305) - }); - - var validatedToken = handler.ValidateToken(token, new TokenValidationParameters - { - ValidIssuer = issuer, - ValidAudience = audience, - TokenDecryptionKey = new SymmetricSecurityKey(validKey) - }); - - validatedToken.IsValid.Should().BeTrue(); - validatedToken.ClaimsIdentity.Claims.Should().Contain( - x => x.Type == "sub" && x.Value == subject); - - var brancaToken = (BrancaSecurityToken) validatedToken.SecurityToken; - brancaToken.Issuer.Should().Be(issuer); - brancaToken.Audiences.Should().Contain(audience); - brancaToken.Subject.Should().Be(subject); - brancaToken.IssuedAt.Should().BeCloseTo(notBefore, TimeSpan.FromSeconds(1)); - brancaToken.ValidFrom.Should().BeCloseTo(notBefore, TimeSpan.FromSeconds(1)); - brancaToken.ValidTo.Should().BeCloseTo(expires, TimeSpan.FromSeconds(1)); - } - - [Fact] - public void GetBrancaDecryptionKeys_WheInvalidKeysInParameters_ExpectInvalidKeysRemoved() - { - var expectedKey = new byte[32]; - new Random().NextBytes(expectedKey); - - var handler = new TestBrancaTokenHandler(); - var keys = handler.GetBrancaDecryptionKeys("test", new TokenValidationParameters - { - TokenDecryptionKeyResolver = (token, securityToken, kid, parameters) => new List(), - TokenDecryptionKey = new SymmetricSecurityKey(expectedKey), - TokenDecryptionKeys = new[] {new RsaSecurityKey(RSA.Create())} - }).ToList(); - - keys.Count.Should().Be(1); - keys.Should().Contain(x => x.Key.SequenceEqual(expectedKey)); - } - - [Fact] - public void IsValidKey_WhenKeyIsNot32Bytes_ExpectFalse() - { - var key = new byte[16]; - new Random().NextBytes(key); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(key); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenKeyIsValid_ExpectTrue() - { - var key = new byte[32]; - new Random().NextBytes(key); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(key); - - isValidKey.Should().BeTrue(); - } - - [Fact] - public void IsValidKey_WhenSecurityKeyIsNot32Bytes_ExpectFalse() - { - var keyBytes = new byte[16]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(key); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenSecurityKeyIsNotSymmetricSecurityKey_ExpectFalse() - { - var key = new RsaSecurityKey(RSA.Create()); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(key); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenSecurityKeyIsValid_ExpectTrue() - { - var keyBytes = new byte[32]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(key); - - isValidKey.Should().BeTrue(); - } - - [Fact] - public void IsValidKey_WhenEcryptingCredentialsKeyIsNot32Bytes_ExpectFalse() - { - var keyBytes = new byte[16]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - var credentials = new EncryptingCredentials(key, ExtendedSecurityAlgorithms.XChaCha20Poly1305); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(credentials); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenEcryptingCredentialsHasKeyWrappingSet_ExpectFalse() - { - var keyBytes = new byte[32]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - var credentials = new EncryptingCredentials( - key, - SecurityAlgorithms.Aes256KeyWrap, - ExtendedSecurityAlgorithms.XChaCha20Poly1305); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(credentials); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenEcryptingCredentialsHasIncorrectEncryptionAlgorithm_ExpectFalse() - { - var keyBytes = new byte[32]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - var credentials = new EncryptingCredentials(key, SecurityAlgorithms.Aes128Encryption); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(credentials); - - isValidKey.Should().BeFalse(); - } - - [Fact] - public void IsValidKey_WhenEcryptingCredentialsIsValid_ExpectTrue() - { - var keyBytes = new byte[32]; - new Random().NextBytes(keyBytes); - var key = new SymmetricSecurityKey(keyBytes); - var credentials = new EncryptingCredentials(key, ExtendedSecurityAlgorithms.XChaCha20Poly1305); - - var isValidKey = new TestBrancaTokenHandler().IsValidKey(credentials); - - isValidKey.Should().BeTrue(); - } - - private static byte[] CreateTestPayload() - { - var payload = new byte[32]; - RandomNumberGenerator.Fill(payload); - return payload; - } - } - - public class TestBrancaTokenHandler : BrancaTokenHandler - { - public new IEnumerable GetBrancaDecryptionKeys(string token, TokenValidationParameters validationParameters) - => base.GetBrancaDecryptionKeys(token, validationParameters); - - public new bool IsValidKey(byte[] key) => base.IsValidKey(key); - public new bool IsValidKey(SecurityKey key) => base.IsValidKey(key); - public new bool IsValidKey(EncryptingCredentials credentials) => base.IsValidKey(credentials); - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenTests.cs deleted file mode 100644 index 7a4efa2..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/BrancaTokenTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Security.Cryptography; -using FluentAssertions; -using ScottBrady.IdentityModel.Tokens.Branca; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Branca -{ - public class BrancaTokenTests - { - [Fact] - public void ctor_ExpectPropertiesSet() - { - var payload = new byte[32]; - RandomNumberGenerator.Fill(payload); - const uint timestamp = uint.MinValue; - - var token = new BrancaToken(payload, timestamp); - - token.Payload.Should().BeEquivalentTo(payload); - token.BrancaFormatTimestamp.Should().Be(timestamp); - token.Timestamp.Should().Be(new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc)); - } - - [Fact] - public void GetDateTime_WhenTimestampIsZero_ExpectUnixTimeStart() - { - const uint timestamp = 0; - - var dateTime = BrancaToken.GetDateTime(timestamp); - - dateTime.Should().Be(new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc)); - } - - [Fact] - public void GetDateTime_WhenTimestampIs27November_ExpectCorrectDateTime() - { - const uint timestamp = 123206400; - - var dateTime = BrancaToken.GetDateTime(timestamp); - - dateTime.Should().Be(new DateTime(1973, 11, 27, 0, 0, 0, DateTimeKind.Utc)); - } - - [Fact] - public void GetDateTime_WhenTimestampIsMaxValue_ExpectCorrectDateTime() - { - const uint timestamp = uint.MaxValue; - - var dateTime = BrancaToken.GetDateTime(timestamp); - - dateTime.Should().Be(new DateTime(2106, 02, 07, 06, 28, 15, DateTimeKind.Utc)); - } - - [Fact] - public void GetBrancaTimestamp_WhenDateBeforeUnixTimeStart_ExpectException() - { - Assert.Throws(() - => BrancaToken.GetBrancaTimestamp(new DateTime(1969, 01, 01))); - } - - [Fact] - public void GetBrancaTimestamp_WhenUnixTimeStart_ExpectZero() - { - var timestamp = BrancaToken.GetBrancaTimestamp(new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc)); - - timestamp.Should().Be(uint.MinValue); - } - - [Fact] - public void GetBrancaTimestamp_When27November_ExpectZero() - { - var timestamp = BrancaToken.GetBrancaTimestamp(new DateTime(1973, 11, 27, 0, 0, 0, DateTimeKind.Utc)); - - timestamp.Should().Be(123206400); - } - - [Fact] - public void GetBrancaTimestamp_WhenMaxTimestamp_ExpectUintMax() - { - var timestamp = BrancaToken.GetBrancaTimestamp(new DateTime(2106, 02, 07, 06, 28, 15, DateTimeKind.Utc)); - - timestamp.Should().Be(uint.MaxValue); - } - - [Fact] - public void GetBrancaTimestamp_WhenAfterMaxTimestamp_ExpectInvalidOperationException() - { - Assert.Throws(() - => BrancaToken.GetBrancaTimestamp(new DateTime(2106, 02, 07, 06, 28, 16, DateTimeKind.Utc))); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/BrancaTestVectors.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/BrancaTestVectors.cs deleted file mode 100644 index d85b658..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/BrancaTestVectors.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text.Json.Nodes; -using FluentAssertions; -using ScottBrady.IdentityModel.Tokens.Branca; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Branca -{ - /// - /// Test vectors from https://github.com/tuupola/branca-spec - /// - public class BrancaTestVectors - { - public static readonly TheoryData EncodingTestVectors = new TheoryData(); - public static readonly TheoryData DecodingTestVectors = new TheoryData(); - - static BrancaTestVectors() - { - var file = File.OpenRead("Tokens/Branca/TestVectors/testvectors.json"); - var data = JsonNode.Parse(file); - if (data == null) throw new Exception("Failed to load test vectors"); - - var testGroups = data["testGroups"].AsArray(); - var encodingTestVectors = testGroups.FirstOrDefault(x => x["testType"]?.GetValue() == "encoding"); - var decodingTestVectors = testGroups.FirstOrDefault(x => x["testType"]?.GetValue() == "decoding"); - - foreach (var testVector in encodingTestVectors?["tests"]?.AsArray() ?? throw new Exception("Failed to load encoding` test vectors")) - { - EncodingTestVectors.Add(new BrancaTestVector(testVector)); - } - foreach (var testVector in decodingTestVectors?["tests"]?.AsArray() ?? throw new Exception("Failed to load decoding test vectors")) - { - DecodingTestVectors.Add(new BrancaTestVector(testVector)); - } - } - - [Theory, MemberData(nameof(EncodingTestVectors))] - public void CreateToken_ExpectCorrectResult(BrancaTestVector testVector) - { - var handler = new TestBrancaTokenHandler {Nonce = testVector.Nonce}; - var token = handler.CreateToken(testVector.Message, testVector.TimeStamp, testVector.Key); - - token.Should().Be(testVector.Token); - } - - [Theory, MemberData(nameof(DecodingTestVectors))] - public void ValidateToken_ExpectCorrectResult(BrancaTestVector testVector) - { - var handler = new BrancaTokenHandler(); - - BrancaToken result = null; - Exception exception = null; - try - { - result = handler.DecryptToken(testVector.Token, testVector.Key); - } - catch (Exception e) - { - exception = e; - } - - if (testVector.IsValid) - { - result.Should().NotBeNull(); - exception.Should().BeNull(); - - result.Payload.Should().BeEquivalentTo(testVector.Message); - result.Timestamp.Should().Be(BrancaToken.GetDateTime(testVector.TimeStamp)); - } - else - { - result.Should().BeNull(); - exception.Should().NotBeNull(); - } - } - - public class TestBrancaTokenHandler : BrancaTokenHandler - { - public byte[] Nonce { get; set; } - protected override byte[] GenerateNonce() => Nonce ?? base.GenerateNonce(); - } - - public class BrancaTestVector - { - public BrancaTestVector(JsonNode data) - { - Id = data["id"]!.GetValue(); - Comment = data["comment"]?.GetValue(); - Token = data["token"]?.GetValue(); - TimeStamp = data["timestamp"]?.GetValue() ?? throw new Exception("Unable to parse timestamp"); - IsValid = data["isValid"]?.GetValue() ?? throw new Exception("Unable to parse isValid"); - - var messageHex = data["msg"]?.GetValue(); - if (!string.IsNullOrWhiteSpace(messageHex)) Message = Base16.Decode(messageHex); - else Message = Array.Empty(); - - var nonceHex = data["nonce"]?.GetValue(); - if (!string.IsNullOrEmpty(nonceHex)) Nonce = Base16.Decode(nonceHex); - - var keyHex = data["key"]?.GetValue() ?? throw new Exception("Failed to find key"); - Key = Base16.Decode(keyHex); - } - - public int Id { get; } - public string Comment { get; } - public byte[] Key { get; } - public byte[] Nonce { get; } - public uint TimeStamp { get; } - public string Token { get; } - public byte[] Message { get; } - public bool IsValid { get; } - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/testvectors.json b/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/testvectors.json deleted file mode 100644 index e7e1bb4..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Branca/TestVectors/testvectors.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "version": "0.3.0", - "numberOfTests": 25, - "testGroups": [ - { - "testType": "encoding", - "tests": [ - { - "id": 0, - "comment": "Hello world with zero timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 0, - "token": "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 1, - "comment": "Hello world with max timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 4294967295, - "token": "89i7YCwu5tWAJNHUDdmIqhzOi5hVHOd4afjZcGMcVmM4enl4yeLiDyYv41eMkNmTX6IwYEFErCSqr", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 2, - "comment": "Hello world with November 27 timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 123206400, - "token": "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 3, - "comment": "Eight null bytes with zero timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 0, - "token": "1jIBheHbDdkCDFQmtgw4RUZeQoOJgGwTFJSpwOAk3XYpJJr52DEpILLmmwYl4tjdSbbNqcF1", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 4, - "comment": "Eight null bytes with max timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 4294967295, - "token": "1jrx6DUu5q06oxykef2e2ZMyTcDRTQot9ZnwgifUtzAphGtjsxfbxXNhQyBEOGtpbkBgvIQx", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 5, - "comment": "Eight null bytes with November 27th timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 123206400, - "token": "1jJDJOEjuwVb9Csz1Ypw1KBWSkr0YDpeBeJN6NzJWx1VgPLmcBhu2SbkpQ9JjZ3nfUf7Aytp", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 6, - "comment": "Empty payload", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 0, - "token": "4sfD0vPFhIif8cy4nB3BQkHeJqkOkDvinI4zIhMjYX4YXZU5WIq9ycCVjGzB5", - "msg": "", - "isValid": true - }, - { - "id": 7, - "comment": "Non-UTF8 payload", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", - "timestamp": 123206400, - "token": "K9u6d0zjXp8RXNUGDyXAsB9AtPo60CD3xxQ2ulL8aQoTzXbvockRff0y1eXoHm", - "msg": "80", - "isValid": true - } - ] - }, - { - "testType": "decoding", - "tests": [ - { - "id": 8, - "comment": "Hello world with zero timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 9, - "comment": "Hello world with max timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 4294967295, - "token": "89i7YCwu5tWAJNHUDdmIqhzOi5hVHOd4afjZcGMcVmM4enl4yeLiDyYv41eMkNmTX6IwYEFErCSqr", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 10, - "comment": "Hello world with November 27 timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 123206400, - "token": "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT", - "msg": "48656c6c6f20776f726c6421", - "isValid": true - }, - { - "id": 11, - "comment": "Eight null bytes with zero timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "1jIBheHbDdkCDFQmtgw4RUZeQoOJgGwTFJSpwOAk3XYpJJr52DEpILLmmwYl4tjdSbbNqcF1", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 12, - "comment": "Eight null bytes with max timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 4294967295, - "token": "1jrx6DUu5q06oxykef2e2ZMyTcDRTQot9ZnwgifUtzAphGtjsxfbxXNhQyBEOGtpbkBgvIQx", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 13, - "comment": "Eight null bytes with November 27th timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 123206400, - "token": "1jJDJOEjuwVb9Csz1Ypw1KBWSkr0YDpeBeJN6NzJWx1VgPLmcBhu2SbkpQ9JjZ3nfUf7Aytp", - "msg": "0000000000000000", - "isValid": true - }, - { - "id": 14, - "comment": "Empty payload", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "4sfD0vPFhIif8cy4nB3BQkHeJqkOkDvinI4zIhMjYX4YXZU5WIq9ycCVjGzB5", - "msg": "", - "isValid": true - }, - { - "id": 15, - "comment": "Non-UTF8 payload", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 123206400, - "token": "K9u6d0zjXp8RXNUGDyXAsB9AtPo60CD3xxQ2ulL8aQoTzXbvockRff0y1eXoHm", - "msg": "80", - "isValid": true - }, - { - "id": 16, - "comment": "Wrong version 0xBB", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "89mvl3RkwXjpEj5WMxK7GUDEHEeeeZtwjMIOogTthvr44qBfYtQSIZH5MHOTC0GzoutDIeoPVZk3w", - "msg": "", - "isValid": false - }, - { - "id": 17, - "comment": "Invalid base62 characters", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 123206400, - "token": "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT_", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - { - "id": 18, - "comment": "Modified version", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "89mvl3S0BE0UCMIY94xxIux4eg1w5oXrhvCEXrDAjusSbO0Yk7AU6FjjTnbTWTqogLfNPJLzecHVb", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - { - "id": 19, - "comment": "Modified first byte of the nonce", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "875GH233SUysT7fQ711EWd9BXpwOjB72ng3ZLnjWFrmOqVy49Bv93b78JU5331LbcY0EEzhLfpmSx", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - - { - "id": 20, - "comment": "Modified timestamp", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "870g1RCk4lW1YInhaU3TP8u2hGtfol16ettLcTOSoA0JIpjCaQRW7tQeP6dQmTvFIB2s6wL5deMXr", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - - { - "id": 21, - "comment": "Modified last byte of the ciphertext", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5Qw6Jpo96myliI3hHD7VbKZBYh", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - { - "id": 22, - "comment": "Modified last byte of the Poly1305 tag", - "key": "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trk0", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - { - "id": 23, - "comment": "Wrong key", - "key": "77726f6e677365637265746b6579796f7573686f756c646e6f74636f6d6d6974", - "nonce": null, - "timestamp": 0, - "token": "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - }, - { - "id": 24, - "comment": "Invalid key", - "key": "746f6f73686f72746b6579", - "nonce": null, - "timestamp": 0, - "token": "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z", - "msg": "48656c6c6f20776f726c6421", - "isValid": false - } - ] - } - ] -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSecurityKeyTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSecurityKeyTests.cs index 86292cf..49f18de 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSecurityKeyTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSecurityKeyTests.cs @@ -9,61 +9,60 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA +namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA; + +public class EdDsaSecurityKeyTests { - public class EdDsaSecurityKeyTests + [Fact] + public void ctor_WhenKeyParametersAreNull_ExpectArgumentNullException() { - [Fact] - public void ctor_WhenKeyParametersAreNull_ExpectArgumentNullException() - { #pragma warning disable CS0618 - Assert.Throws(() => new EdDsaSecurityKey((Ed25519PublicKeyParameters) null)); + Assert.Throws(() => new EdDsaSecurityKey((Ed25519PublicKeyParameters) null)); #pragma warning restore CS0618 - } + } - [Fact] - public void ctor_WhenEd25519PrivateKey_ExpectKeySetAndCorrectCurve() - { - var keyPair = GenerateEd25519KeyPair(); + [Fact] + public void ctor_WhenEd25519PrivateKey_ExpectKeySetAndCorrectCurve() + { + var keyPair = GenerateEd25519KeyPair(); #pragma warning disable CS0618 - var securityKey = new EdDsaSecurityKey((Ed25519PrivateKeyParameters) keyPair.Private); + var securityKey = new EdDsaSecurityKey((Ed25519PrivateKeyParameters) keyPair.Private); #pragma warning restore CS0618 - securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType(); - securityKey.EdDsa.Parameters.D.Should().BeEquivalentTo(((Ed25519PrivateKeyParameters) keyPair.Private).GetEncoded()); - securityKey.EdDsa.Parameters.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519); - securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.Exists); + securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType(); + securityKey.EdDsa.Parameters.D.Should().BeEquivalentTo(((Ed25519PrivateKeyParameters) keyPair.Private).GetEncoded()); + securityKey.EdDsa.Parameters.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519); + securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.Exists); #pragma warning disable 618 - securityKey.HasPrivateKey.Should().BeTrue(); + securityKey.HasPrivateKey.Should().BeTrue(); #pragma warning restore 618 - } + } - [Fact] - public void ctor_WhenEd25519PublicKey_ExpectKeySetAndCorrectCurve() - { - var keyPair = GenerateEd25519KeyPair(); + [Fact] + public void ctor_WhenEd25519PublicKey_ExpectKeySetAndCorrectCurve() + { + var keyPair = GenerateEd25519KeyPair(); #pragma warning disable CS0618 - var securityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public); + var securityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public); #pragma warning restore CS0618 - securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType(); - securityKey.EdDsa.Parameters.X.Should().BeEquivalentTo(((Ed25519PublicKeyParameters) keyPair.Public).GetEncoded()); - securityKey.EdDsa.Parameters.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519); - securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.DoesNotExist); + securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType(); + securityKey.EdDsa.Parameters.X.Should().BeEquivalentTo(((Ed25519PublicKeyParameters) keyPair.Public).GetEncoded()); + securityKey.EdDsa.Parameters.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519); + securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.DoesNotExist); #pragma warning disable 618 - securityKey.HasPrivateKey.Should().BeFalse(); + securityKey.HasPrivateKey.Should().BeFalse(); #pragma warning restore 618 - } + } - private static AsymmetricCipherKeyPair GenerateEd25519KeyPair() - { - var keyPairGenerator = new Ed25519KeyPairGenerator(); - keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); - return keyPairGenerator.GenerateKeyPair(); - } + private static AsymmetricCipherKeyPair GenerateEd25519KeyPair() + { + var keyPairGenerator = new Ed25519KeyPairGenerator(); + keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); + return keyPairGenerator.GenerateKeyPair(); } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSignatureProviderTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSignatureProviderTests.cs index d32647c..7b400a2 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSignatureProviderTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/EdDsaSignatureProviderTests.cs @@ -6,15 +6,15 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA +namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA; + +public class EdDsaSignatureProviderTests { - public class EdDsaSignatureProviderTests - { - // privateKey = "FU1F1QTjYwfB-xkO6aknnBifE_Ywa94U04xpd-XJfBs" + // privateKey = "FU1F1QTjYwfB-xkO6aknnBifE_Ywa94U04xpd-XJfBs" - [Fact] - public void ctor_ExpectPropertiesSet() - { + [Fact] + public void ctor_ExpectPropertiesSet() + { var expectedSecurityKey = new EdDsaSecurityKey(EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519)); var expectedAlgorithm = ExtendedSecurityAlgorithms.EdDsa; @@ -24,9 +24,9 @@ public void ctor_ExpectPropertiesSet() provider.Algorithm.Should().Be(expectedAlgorithm); } - [Fact] - public void Sign_WhenSigningWithEd25519Curve_ExpectCorrectSignature() - { + [Fact] + public void Sign_WhenSigningWithEd25519Curve_ExpectCorrectSignature() + { const string plaintext = "eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhdWQiOiJ5b3UiLCJzdWIiOiJib2IiLCJpc3MiOiJtZSIsImV4cCI6MTU5MDg0MTg4N30"; const string expectedSignature = @@ -43,9 +43,9 @@ public void Sign_WhenSigningWithEd25519Curve_ExpectCorrectSignature() signature.Should().BeEquivalentTo(Base64UrlEncoder.DecodeBytes(expectedSignature)); } - [Fact] - public void Verify_WhenJwtSignedWithEd25519Curve_ExpectTrue() - { + [Fact] + public void Verify_WhenJwtSignedWithEd25519Curve_ExpectTrue() + { const string plaintext = "eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhdWQiOiJ5b3UiLCJzdWIiOiJib2IiLCJpc3MiOiJtZSIsImV4cCI6MTU5MDg0MTg4N30"; const string signature = @@ -64,9 +64,9 @@ public void Verify_WhenJwtSignedWithEd25519Curve_ExpectTrue() isValidSignature.Should().BeTrue(); } - [Fact] - public void VerifyWithOffsets_WhenJwtSignedWithEd25519Curve_ExpectTrue() - { + [Fact] + public void VerifyWithOffsets_WhenJwtSignedWithEd25519Curve_ExpectTrue() + { const string plaintext = "eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhdWQiOiJ5b3UiLCJzdWIiOiJib2IiLCJpc3MiOiJtZSIsImV4cCI6MTU5MDg0MTg4N30"; const string signature = @@ -89,5 +89,4 @@ public void VerifyWithOffsets_WhenJwtSignedWithEd25519Curve_ExpectTrue() isValidSignature.Should().BeTrue(); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/TestVectors/Rfc8037TestVectors.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/TestVectors/Rfc8037TestVectors.cs index 4ffcab4..65426c9 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/TestVectors/Rfc8037TestVectors.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDSA/TestVectors/Rfc8037TestVectors.cs @@ -5,16 +5,16 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA +namespace ScottBrady.IdentityModel.Tests.Tokens.EdDSA; + +public class Rfc8037TestVectors { - public class Rfc8037TestVectors - { - private readonly byte[] privateKey = Base64UrlEncoder.DecodeBytes("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); - private readonly byte[] publicKey = Base64UrlEncoder.DecodeBytes("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); + private readonly byte[] privateKey = Base64UrlEncoder.DecodeBytes("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); + private readonly byte[] publicKey = Base64UrlEncoder.DecodeBytes("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); - [Fact] - public void A_4_Ed25519_Signing() - { + [Fact] + public void A_4_Ed25519_Signing() + { byte[] plaintext = Encoding.UTF8.GetBytes("eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"); byte[] expectedSignature = Base64UrlEncoder.DecodeBytes("hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"); @@ -25,9 +25,9 @@ public void A_4_Ed25519_Signing() signatureProvider.Sign(plaintext).Should().BeEquivalentTo(expectedSignature); } - [Fact] - public void A_5_Ed25519_Validation() - { + [Fact] + public void A_5_Ed25519_Validation() + { byte[] plaintext = Encoding.UTF8.GetBytes("eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"); byte[] signature = Base64UrlEncoder.DecodeBytes("hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"); @@ -37,5 +37,4 @@ public void A_5_Ed25519_Validation() signatureProvider.Verify(plaintext, signature).Should().BeTrue(); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs index 2b9ebc7..dfb7bbb 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs @@ -7,58 +7,58 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens +namespace ScottBrady.IdentityModel.Tests.Tokens; + +public class ExtendedCryptoProviderTests { - public class ExtendedCryptoProviderTests - { - private ExtendedCryptoProvider sut = new ExtendedCryptoProvider(); + private ExtendedCryptoProvider sut = new ExtendedCryptoProvider(); - [Theory] - [InlineData("eddsa")] - [InlineData("RS256")] - [InlineData("EDDSA")] - public void IsSupportedAlgorithm_WhenNotSupportedAlgorithm_ExpectFalse(string algorithm) - => sut.IsSupportedAlgorithm(algorithm); + [Theory] + [InlineData("eddsa")] + [InlineData("RS256")] + [InlineData("EDDSA")] + public void IsSupportedAlgorithm_WhenNotSupportedAlgorithm_ExpectFalse(string algorithm) + => sut.IsSupportedAlgorithm(algorithm); - [Theory] - [InlineData(ExtendedSecurityAlgorithms.EdDsa)] - public void IsSupportedAlgorithm_WhenSupportedAlgorithm_ExpectTrue(string algorithm) - => sut.IsSupportedAlgorithm(algorithm); + [Theory] + [InlineData(ExtendedSecurityAlgorithms.EdDsa)] + public void IsSupportedAlgorithm_WhenSupportedAlgorithm_ExpectTrue(string algorithm) + => sut.IsSupportedAlgorithm(algorithm); - [Fact] - public void Release_WhenObjectImplementsIDisposable_ExpectObjectDisposed() - { + [Fact] + public void Release_WhenObjectImplementsIDisposable_ExpectObjectDisposed() + { var memoryStream = new MemoryStream(); sut.Release(memoryStream); Assert.Throws(() => memoryStream.Read(Span.Empty)); } - [Fact] - public void Release_WhenObjectDoesNotImplementIDisposable_ExpectNoOp() - { + [Fact] + public void Release_WhenObjectDoesNotImplementIDisposable_ExpectNoOp() + { var uri = new Uri("urn:test"); sut.Release(uri); } - [Fact] - public void Create_WhenAlgorithmIsNotEdDsaButHasEdDsaSecurityKey_ExpectNotSupportedException() - { + [Fact] + public void Create_WhenAlgorithmIsNotEdDsaButHasEdDsaSecurityKey_ExpectNotSupportedException() + { var securityKey = new EdDsaSecurityKey(EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519)); Assert.Throws(() => sut.Create(SecurityAlgorithms.RsaSha256, securityKey)); } - [Fact] - public void Create_WhenAlgorithmIsEdDsaButIsNotEdDsaSecurityKey_ExpectNotSupportedException() - { + [Fact] + public void Create_WhenAlgorithmIsEdDsaButIsNotEdDsaSecurityKey_ExpectNotSupportedException() + { var securityKey = new RsaSecurityKey(RSA.Create()); Assert.Throws(() => sut.Create(ExtendedSecurityAlgorithms.EdDsa, securityKey)); } - [Fact] - public void Create_WhenAlgorithmIsEdDsaWithEdDsaSecurityKey_ExpectEdDsaSignatureProvider() - { + [Fact] + public void Create_WhenAlgorithmIsEdDsaWithEdDsaSecurityKey_ExpectEdDsaSignatureProvider() + { var securityKey = new EdDsaSecurityKey(EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519)); var signatureProvider = sut.Create(ExtendedSecurityAlgorithms.EdDsa, securityKey); @@ -67,5 +67,4 @@ public void Create_WhenAlgorithmIsEdDsaWithEdDsaSecurityKey_ExpectEdDsaSignature edDsaSignatureProvider.Algorithm.Should().Be(ExtendedSecurityAlgorithms.EdDsa); edDsaSignatureProvider.Key.Should().Be(securityKey); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs index 4b0f6d4..351daa1 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs @@ -7,33 +7,33 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens +namespace ScottBrady.IdentityModel.Tests.Tokens; + +public class JsonWebTokenHandlerTests { - public class JsonWebTokenHandlerTests + private const string Issuer = "me"; + private const string Audience = "you"; + private const string Subject = "123"; + + private readonly SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Issuer = Issuer, + Audience = Audience, + Expires = DateTime.UtcNow.AddMinutes(30), + Claims = new Dictionary {{"sub", Subject}} + }; + + private readonly TokenValidationParameters tokenValidationParameters = new TokenValidationParameters { - private const string Issuer = "me"; - private const string Audience = "you"; - private const string Subject = "123"; - - private readonly SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor - { - Issuer = Issuer, - Audience = Audience, - Expires = DateTime.UtcNow.AddMinutes(30), - Claims = new Dictionary {{"sub", Subject}} - }; - - private readonly TokenValidationParameters tokenValidationParameters = new TokenValidationParameters - { - ValidIssuer = Issuer, - ValidAudience = Audience, - ValidateLifetime = true, - RequireExpirationTime = true - }; + ValidIssuer = Issuer, + ValidAudience = Audience, + ValidateLifetime = true, + RequireExpirationTime = true + }; - [Fact] - public void WhenEd25519TokenGenerated_ExpectEdDsaTokenVerifiable() - { + [Fact] + public void WhenEd25519TokenGenerated_ExpectEdDsaTokenVerifiable() + { var key = EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519); var handler = new JsonWebTokenHandler(); @@ -48,9 +48,9 @@ public void WhenEd25519TokenGenerated_ExpectEdDsaTokenVerifiable() validationResult.ClaimsIdentity.Claims.Should().Contain(x => x.Type == "sub" && x.Value == Subject); } - [Fact] - public void WhenEd448TokenGenerated_ExpectEdDsaTokenVerifiable() - { + [Fact] + public void WhenEd448TokenGenerated_ExpectEdDsaTokenVerifiable() + { var key = EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed448); var handler = new JsonWebTokenHandler(); @@ -65,9 +65,9 @@ public void WhenEd448TokenGenerated_ExpectEdDsaTokenVerifiable() validationResult.ClaimsIdentity.Claims.Should().Contain(x => x.Type == "sub" && x.Value == Subject); } - [Fact] - public void WhenEd25519SignatureValidatedUsingEs448_ExpectInvalidToken() - { + [Fact] + public void WhenEd25519SignatureValidatedUsingEs448_ExpectInvalidToken() + { var signingKey = EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed25519); var validationKey = EdDsa.Create(ExtendedSecurityAlgorithms.Curves.Ed448); @@ -81,5 +81,4 @@ public void WhenEd25519SignatureValidatedUsingEs448_ExpectInvalidToken() validationResult.IsValid.Should().BeFalse(); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadExtensionsTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadExtensionsTests.cs deleted file mode 100644 index ed1425c..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadExtensionsTests.cs +++ /dev/null @@ -1,320 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; -using ScottBrady.IdentityModel.Tokens; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens -{ - public class JwtPayloadExtensionsTests - { - [Fact] - public void ToJwtPayload_WhenTokenDescriptorIsNull_ExpectArgumentNullException() - { - SecurityTokenDescriptor descriptor = null; - Assert.Throws(() => descriptor.ToJwtPayload()); - } - - [Fact] - public void ToJwtPayload_WhenMultipleSubjectClaimsOfSameType_ExpectJsonArray() - { - const string claimType = "email"; - var claimValues = new[] {"bob@test", "alice@test"}; - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Subject = new ClaimsIdentity(new List - { - new Claim(claimType, claimValues[0]), - new Claim(claimType, claimValues[1]) - }, "test"); - - var jwtPayload = descriptor.ToJwtPayload(); - - var claims = JObject.Parse(jwtPayload)[claimType]; - claims.Values().Should().Contain(claimValues); - } - - [Fact] - public void ToJwtPayload_WhenMultipleClaimsOfSameType_ExpectJsonArray() - { - const string claimType = "email"; - var claimValues = new[] {"bob@test", "alice@test"}; - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary - { - {claimType, claimValues} - }; - - var jwtPayload = descriptor.ToJwtPayload(); - - var claims = JObject.Parse(jwtPayload)[claimType]; - claims.Values().Should().Contain(claimValues); - } - - [Fact] - public void ToJwtPayload_WhenSubjectAndClaimsContainDuplicateTypes_ExpecSubjectClaimsReplaced() - { - var claimType = Guid.NewGuid().ToString(); - var expectedClaimValue = Guid.NewGuid().ToString(); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Subject = new ClaimsIdentity(new List {new Claim(claimType, Guid.NewGuid().ToString())}); - descriptor.Claims = new Dictionary {{claimType, expectedClaimValue}}; - - var jwtPayload = descriptor.ToJwtPayload(); - - var claims = JObject.Parse(jwtPayload)[claimType]; - claims.Value().Should().Contain(expectedClaimValue); - } - - [Fact] - public void ToJwtPayload_WhenIssuerSet_ExpectIssuerClaim() - { - var descriptor = new SecurityTokenDescriptor(); - descriptor.Issuer = "me"; - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuer = JObject.Parse(jwtPayload)["iss"]; - issuer.Value().Should().Be(descriptor.Issuer); - } - - [Fact] - public void ToJwtPayload_WhenIssuerSetInSubject_ExpectSubjectIssuerClaim() - { - var expectedIssuer = Guid.NewGuid().ToString(); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Subject = new ClaimsIdentity(new List {new Claim("iss", expectedIssuer)}); - descriptor.Issuer = Guid.NewGuid().ToString(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuer = JObject.Parse(jwtPayload)["iss"]; - issuer.Value().Should().Be(expectedIssuer); - } - - [Fact] - public void ToJwtPayload_WhenIssuerSetInClaims_ExpectClaimsIssuerClaim() - { - var expectedIssuer = Guid.NewGuid().ToString(); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary {{"iss", expectedIssuer}}; - descriptor.Issuer = Guid.NewGuid().ToString(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuer = JObject.Parse(jwtPayload)["iss"]; - issuer.Value().Should().Be(expectedIssuer); - } - - [Fact] - public void ToJwtPayload_WhenAudienceSet_ExpectAudienceClaim() - { - var descriptor = new SecurityTokenDescriptor(); - descriptor.Audience = "you"; - - var jwtPayload = descriptor.ToJwtPayload(); - - var audience = JObject.Parse(jwtPayload)["aud"]; - audience.Value().Should().Be(descriptor.Audience); - } - - [Fact] - public void ToJwtPayload_WhenAudienceSetInSubject_ExpectSubjectAudienceClaim() - { - var expectedAudience = Guid.NewGuid().ToString(); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Subject = new ClaimsIdentity(new List {new Claim("aud", expectedAudience)}); - descriptor.Audience = Guid.NewGuid().ToString(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var audience = JObject.Parse(jwtPayload)["aud"]; - audience.Value().Should().Be(expectedAudience); - } - - [Fact] - public void ToJwtPayload_WhenAudienceSetInClaims_ExpectClaimsAudienceClaim() - { - var expectedAudience = Guid.NewGuid().ToString(); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary {{"aud", expectedAudience}}; - descriptor.Audience = Guid.NewGuid().ToString(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var audience = JObject.Parse(jwtPayload)["aud"]; - audience.Value().Should().Be(expectedAudience); - } - - [Fact] - public void ToJwtPayload_WhenExpiryNotSet_ExpectExpirySetToOneHour() - { - var descriptor = new SecurityTokenDescriptor(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var expiry = JObject.Parse(jwtPayload)["exp"]; - expiry.Value().Should().BeCloseTo( - (long) (EpochTime.GetIntDate(DateTime.UtcNow) + TimeSpan.FromMinutes(60).TotalSeconds), - 10); - } - - [Fact] - public void ToJwtPayload_WhenExpirySet_ExpectExpiryClaim() - { - var expectedExpiry = DateTime.UtcNow.AddMinutes(5); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Expires = expectedExpiry; - - var jwtPayload = descriptor.ToJwtPayload(); - - var expiry = JObject.Parse(jwtPayload)["exp"]; - expiry.Value().Should().Be(EpochTime.GetIntDate(expectedExpiry)); - } - - [Fact] - public void ToJwtPayload_WhenExpirySetInClaims_ExpectClaimsExpiryClaim() - { - var expectedExpiry = EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary{{"exp", expectedExpiry}}; - descriptor.Expires = DateTime.UtcNow.AddHours(42); - - var jwtPayload = descriptor.ToJwtPayload(); - - var expiry = JObject.Parse(jwtPayload)["exp"]; - expiry.Value().Should().Be(expectedExpiry); - } - - [Fact] - public void ToJwtPayload_WhenIssuedAtNotSet_ExpectIssuedAtSetToNow() - { - var descriptor = new SecurityTokenDescriptor(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuedAt = JObject.Parse(jwtPayload)["iat"]; - issuedAt.Value().Should().BeCloseTo(EpochTime.GetIntDate(DateTime.UtcNow), 10); - } - - [Fact] - public void ToJwtPayload_WhenIssuedAtSet_ExpectIssuedAtClaim() - { - var expectedIssuedAt = DateTime.UtcNow.AddMinutes(5); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.IssuedAt = expectedIssuedAt; - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuedAt = JObject.Parse(jwtPayload)["iat"]; - issuedAt.Value().Should().Be(EpochTime.GetIntDate(expectedIssuedAt)); - } - - [Fact] - public void ToJwtPayload_WhenIssuedAtSetInClaims_ExpectClaimsIssuedAtClaim() - { - var expectedIssuedAt = EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary{{"iat", expectedIssuedAt}}; - descriptor.IssuedAt = DateTime.UtcNow.AddHours(42); - - var jwtPayload = descriptor.ToJwtPayload(); - - var issuedAt = JObject.Parse(jwtPayload)["iat"]; - issuedAt.Value().Should().Be(expectedIssuedAt); - } - - [Fact] - public void ToJwtPayload_WhenNotBeforeNotSet_ExpectNotBeforeSetToNow() - { - var descriptor = new SecurityTokenDescriptor(); - - var jwtPayload = descriptor.ToJwtPayload(); - - var notBefore = JObject.Parse(jwtPayload)["nbf"]; - notBefore.Value().Should().BeCloseTo(EpochTime.GetIntDate(DateTime.UtcNow), 10); - } - - [Fact] - public void ToJwtPayload_WhenNotBeforeSet_ExpectNotBeforeClaim() - { - var expectedNotBefore = DateTime.UtcNow.AddMinutes(5); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.NotBefore = expectedNotBefore; - - var jwtPayload = descriptor.ToJwtPayload(); - - var notBefore = JObject.Parse(jwtPayload)["nbf"]; - notBefore.Value().Should().Be(EpochTime.GetIntDate(expectedNotBefore)); - } - - [Fact] - public void ToJwtPayload_WhenNotBeforeSetInClaims_ExpectClaimsNotBeforeClaim() - { - var expectedNotBefore = EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)); - - var descriptor = new SecurityTokenDescriptor(); - descriptor.Claims = new Dictionary{{"nbf", expectedNotBefore}}; - descriptor.NotBefore = DateTime.UtcNow.AddHours(42); - - var jwtPayload = descriptor.ToJwtPayload(); - - var notBefore = JObject.Parse(jwtPayload)["nbf"]; - notBefore.Value().Should().Be(expectedNotBefore); - } - - [Fact] - public void ToJwtPayload_WhenIsoDateFormat_ExpectNoOverencoding() - { - var descriptor = new SecurityTokenDescriptor(); - - var jwtPayload = descriptor.ToJwtPayload(JwtDateTimeFormat.Iso); - - var expiry = jwtPayload.Should().NotContain(@"\u002B"); - } - - [Fact] - public void ToJwtClaimDictionary_WhenClaimTypeHasSingleValue_ExpectSingleClaim() - { - var claim = new Claim("email", "bob@test"); - - var dictionary = JwtPayloadExtensions.ToJwtClaimDictionary(new List {claim}); - - var values = dictionary[claim.Type]; - values.ToString().Should().Be(claim.Value); - } - - [Fact] - public void ToJwtClaimDictionary_WhenClaimTypeHasMultipleValues_ExpectEntryWithArrayValue() - { - const string claimType = "email"; - const string value1 = "bob@test"; - const string value2 = "alice@test"; - - var dictionary = JwtPayloadExtensions.ToJwtClaimDictionary(new List - { - new Claim(claimType, value1), new Claim(claimType, value2) - }); - - var entry = dictionary[claimType]; - var values = entry as IList; - - values.Should().Contain(value1); - values.Should().Contain(value2); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadSecurityTokenTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadSecurityTokenTests.cs deleted file mode 100644 index 37291b7..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadSecurityTokenTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using ScottBrady.IdentityModel.Tokens; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens -{ - public class JwtPayloadSecurityTokenTests - { - [Fact] - public void ctor_WhenTokenPayloadIsNotJson_ExpectArgumentException() - => Assert.Throws(() => new TestJwtPayloadSecurityToken("")); - - [Fact] - public void ctor_WhenTokenContainsJson_ExpectJwtClaimsParsed() - { - var jwt = new - { - jti = "xyz", - iss = "me", - aud = "you", - sub = "123", - actort = "them", // 🤷‍ - iat = 1588341409, - nbf = 1588341410, - exp = 1588341499 - }; - - var token = new TestJwtPayloadSecurityToken(JsonConvert.SerializeObject(jwt)); - - token.Id.Should().Be(jwt.jti); - token.Issuer.Should().Be(jwt.iss); - token.Audiences.Should().Contain(jwt.aud); - token.Subject.Should().Be(jwt.sub); - token.Actor.Should().Be(jwt.actort); - token.IssuedAt.Should().Be(DateTimeOffset.FromUnixTimeSeconds(jwt.iat).UtcDateTime); - token.ValidFrom.Should().Be(DateTimeOffset.FromUnixTimeSeconds(jwt.nbf).UtcDateTime); - token.ValidTo.Should().Be(DateTimeOffset.FromUnixTimeSeconds(jwt.exp).UtcDateTime); - } - - [Fact] - public void ctor_WhenTokenContainsJson_ExpectClaimsParsed() - { - var jwt = new - { - jti = "xyz", - iss = "me", - aud = "you", - sub = "123", - actort = "them", - iat = 1588341409, - nbf = 1588341410, - exp = 1588341499 - }; - - var token = new TestJwtPayloadSecurityToken(JsonConvert.SerializeObject(jwt)); - - token.Claims.Should().Contain(x => x.Type == "jti" && x.Value == jwt.jti); - token.Claims.Should().Contain(x => x.Type == "iss" && x.Value == jwt.iss); - token.Claims.Should().Contain(x => x.Type == "aud" && x.Value == jwt.aud); - token.Claims.Should().Contain(x => x.Type == "sub" && x.Value == jwt.sub); - token.Claims.Should().Contain(x => x.Type == "actort" && x.Value == jwt.actort); - token.Claims.Should().Contain(x => x.Type == "iat" && x.Value == jwt.iat.ToString()); - token.Claims.Should().Contain(x => x.Type == "nbf" && x.Value == jwt.nbf.ToString()); - token.Claims.Should().Contain(x => x.Type == "exp" && x.Value == jwt.exp.ToString()); - } - } - - internal class TestJwtPayloadSecurityToken : JwtPayloadSecurityToken - { - public TestJwtPayloadSecurityToken(string payload) : base(payload) - { - } - - public override SecurityKey SecurityKey { get; } - public override SecurityKey SigningKey { get; set; } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadTokenHandlerTests.cs deleted file mode 100644 index b1da03e..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtPayloadTokenHandlerTests.cs +++ /dev/null @@ -1,374 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Security.Cryptography; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using Moq; -using Moq.Protected; -using ScottBrady.IdentityModel.Tokens; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens -{ - public class JwtPayloadTokenHandlerTests - { - [Fact] - public void CreateClaimsIdentity_WhenTokenIsNull_ExpectArgumentNullException() - { - var handler = new TestJwtPayloadTokenHandler(); - Assert.Throws(() => handler.TestCreateClaimsIdentity(null, new TokenValidationParameters())); - } - - [Fact] - public void CreateClaimsIdentity_WhenTokenValidationParametersAreNull_ExpectArgumentNullException() - { - var handler = new TestJwtPayloadTokenHandler(); - Assert.Throws(() => handler.TestCreateClaimsIdentity(new MockableJwtPayloadSecurityToken(), null)); - } - - [Fact] - public void CreateClaimsIdentity_WhenTokenHasIssuer_ExpectClaimsToUseTokenIdentity() - { - const string expectedIssuer = "ids"; - var mockToken = new Mock(); - mockToken.Setup(x => x.Issuer).Returns(expectedIssuer); - mockToken.Setup(x => x.Claims).Returns(new List {new Claim("sub", "123")}); - - var handler = new TestJwtPayloadTokenHandler(); - - var identity = handler.TestCreateClaimsIdentity(mockToken.Object, new TokenValidationParameters()); - - identity.Claims.All(x => x.Issuer == expectedIssuer).Should().BeTrue(); - identity.Claims.All(x => x.OriginalIssuer == expectedIssuer).Should().BeTrue(); - } - - [Fact] - public void CreateClaimsIdentity_WhenTokenHasNoIssuer_ExpectClaimsToUseDefaultIssuer() - { - var mockToken = new Mock(); - mockToken.Setup(x => x.Issuer).Returns((string) null); - mockToken.Setup(x => x.Claims).Returns(new List {new Claim("sub", "123")}); - - var handler = new TestJwtPayloadTokenHandler(); - - var identity = handler.TestCreateClaimsIdentity(mockToken.Object, new TokenValidationParameters()); - - identity.Claims.All(x => x.Issuer == ClaimsIdentity.DefaultIssuer).Should().BeTrue(); - identity.Claims.All(x => x.OriginalIssuer == ClaimsIdentity.DefaultIssuer).Should().BeTrue(); - } - - [Fact] - public void CreateClaimsIdentity_WhenTokenHasClaimsWithProperties_ExpectPropertiesPersisted() - { - var expectedProperty = new KeyValuePair("test", "test_val"); - var claimWithProperty = new Claim("sub", "123") {Properties = {expectedProperty}}; - - var mockToken = new Mock(); - mockToken.Setup(x => x.Issuer).Returns((string) null); - mockToken.Setup(x => x.Claims).Returns(new List {claimWithProperty}); - - var handler = new TestJwtPayloadTokenHandler(); - - var identity = handler.TestCreateClaimsIdentity(mockToken.Object, new TokenValidationParameters()); - - var mappedClaim = identity.Claims.Single(x => x.Type == claimWithProperty.Type && x.Value == claimWithProperty.Value); - mappedClaim.Properties.Should().Contain(expectedProperty); - } - - [Fact] - public void ValidateTokenPayload_WhenTokenIsNull_ExpectResultWithArgumentNullException() - { - var result = new TestJwtPayloadTokenHandler().TestValidateTokenPayload(null, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateTokenPayload_WhenTokenValidationParametersAreNull_ExpectResultWithArgumentNullException() - { - var token = CreateMockToken().Object; - - var result = new TestJwtPayloadTokenHandler().TestValidateTokenPayload(token, null); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateTokenPayload_WhenInvalidLifetime_ExpectFailureResultWithCorrectException() - { - var expectedException = new InvalidOperationException("correct error"); - - var token = CreateMockToken().Object; - var mockHandler = CreateMockHandler(); - mockHandler.Protected() - .Setup("ValidateLifetime", - ItExpr.IsAny(), - ItExpr.IsAny(), - ItExpr.IsAny(), - ItExpr.IsAny()) - .Throws(expectedException); - - var result = mockHandler.Object.TestValidateTokenPayload(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateTokenPayload_WhenInvalidAudience_ExpectFailureResultWithCorrectException() - { - var expectedException = new InvalidOperationException("correct error"); - - var token = CreateMockToken().Object; - var mockHandler = CreateMockHandler(); - mockHandler.Protected() - .Setup("ValidateAudience", - ItExpr.IsAny>(), - ItExpr.IsAny(), - ItExpr.IsAny()) - .Throws(expectedException); - - var result = mockHandler.Object.TestValidateTokenPayload(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateTokenPayload_WhenInvalidIssuer_ExpectFailureResultWithCorrectException() - { - var expectedException = new InvalidOperationException("correct error"); - - var token = CreateMockToken().Object; - var mockHandler = CreateMockHandler(); - mockHandler.Protected() - .Setup("ValidateIssuer", - ItExpr.IsAny(), - ItExpr.IsAny(), - ItExpr.IsAny()) - .Throws(expectedException); - - var result = mockHandler.Object.TestValidateTokenPayload(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateTokenPayload_WhenTokenReplay_ExpectFailureResultWithCorrectException() - { - var expectedException = new InvalidOperationException("correct error"); - - var token = CreateMockToken().Object; - var mockHandler = CreateMockHandler(); - mockHandler.Protected() - .Setup("ValidateTokenReplay", - ItExpr.IsAny(), - ItExpr.IsAny(), - ItExpr.IsAny()) - .Throws(expectedException); - - var result = mockHandler.Object.TestValidateTokenPayload(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateTokenPayload_WhenValidToken_ExpectSuccessResult() - { - var expectedIdentity = new ClaimsIdentity("test"); - - var token = CreateMockToken().Object; - var mockHandler = CreateMockHandler(); - mockHandler.Protected() - .Setup("CreateClaimsIdentity", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(expectedIdentity); - - var result = mockHandler.Object.TestValidateTokenPayload(token, new TokenValidationParameters()); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.Should().Be(expectedIdentity); - result.SecurityToken.Should().Be(token); - } - - [Fact] - public void GetDecryptionKeys_WhenKeyResolverReturnsKey_ExpectKeyFromResolver() - { - var expectedKey = new RsaSecurityKey(RSA.Create()); - - var handler = new TestJwtPayloadTokenHandler(); - var keys = handler.TestGetDecryptionKeys("test", new TokenValidationParameters - { - TokenDecryptionKeyResolver = (token, securityToken, kid, parameters) => new[] {expectedKey}, - TokenDecryptionKey = new RsaSecurityKey(RSA.Create()) - }).ToList(); - - keys.Count.Should().Be(1); - keys.Should().Contain(expectedKey); - } - - [Fact] - public void GetDecryptionKeys_WheKeysInParameters_ExpectAllKeys() - { - var expectedKey1 = new RsaSecurityKey(RSA.Create()); - var expectedKey2 = new RsaSecurityKey(RSA.Create()); - - var handler = new TestJwtPayloadTokenHandler(); - var keys = handler.TestGetDecryptionKeys("test", new TokenValidationParameters - { - TokenDecryptionKeyResolver = (token, securityToken, kid, parameters) => new List(), - TokenDecryptionKey = expectedKey1, - TokenDecryptionKeys = new[] {expectedKey2} - }).ToList(); - - keys.Count.Should().Be(2); - keys.Should().Contain(expectedKey1); - keys.Should().Contain(expectedKey2); - } - - [Fact] - public void GetSigningKeys_WhenKeyResolverReturnsKey_ExpectKeyFromResolver() - { - var expectedKey = new RsaSecurityKey(RSA.Create()); - - var handler = new TestJwtPayloadTokenHandler(); - var keys = handler.TestGetSigningKeys("test", new TokenValidationParameters - { - IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => new[] {expectedKey}, - IssuerSigningKey = new RsaSecurityKey(RSA.Create()) - }).ToList(); - - keys.Count.Should().Be(1); - keys.Should().Contain(expectedKey); - } - - [Fact] - public void GetSigningKeys_WheKeysInParameters_ExpectAllKeys() - { - var expectedKey1 = new RsaSecurityKey(RSA.Create()); - var expectedKey2 = new RsaSecurityKey(RSA.Create()); - - var handler = new TestJwtPayloadTokenHandler(); - var keys = handler.TestGetSigningKeys("test", new TokenValidationParameters - { - IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => new List(), - IssuerSigningKey = expectedKey1, - IssuerSigningKeys = new[] {expectedKey2} - }).ToList(); - - keys.Count.Should().Be(2); - keys.Should().Contain(expectedKey1); - keys.Should().Contain(expectedKey2); - } - - [Fact] - public void ValidateToken_ISecurityTokenValidator_WhenSuccess_ExpectInnerTokenAndIdentity() - { - var token = Guid.NewGuid().ToString(); - var validationParameters = new TokenValidationParameters {ValidIssuer = Guid.NewGuid().ToString()}; - - var expectedIdentity = new ClaimsIdentity(new List {new Claim("sub", "123")}, "test"); - var expectedSecurityToken = new MockableJwtPayloadSecurityToken(); - - var mockHandler = new Mock {CallBase = true}; - mockHandler.Setup(x => x.ValidateToken(token, validationParameters)) - .Returns(new TokenValidationResult - { - IsValid = true, - ClaimsIdentity = expectedIdentity, - SecurityToken = expectedSecurityToken - }); - - var claimsPrincipal = mockHandler.Object.ValidateToken(token, validationParameters, out var parsedToken); - - claimsPrincipal.Identity.Should().Be(expectedIdentity); - parsedToken.Should().Be(expectedSecurityToken); - } - - [Fact] - public void ValidateToken_ISecurityTokenValidator_WhenFailure_ExpectInnerException() - { - var token = Guid.NewGuid().ToString(); - var validationParameters = new TokenValidationParameters(); - - var expectedException = new InvalidOperationException("test"); - - var mockHandler = new Mock {CallBase = true}; - mockHandler.Setup(x => x.ValidateToken(token, validationParameters)) - .Returns(new TokenValidationResult - { - IsValid = false, - Exception = expectedException - }); - - SecurityToken parsedToken = null; - var exception = Assert.Throws( - expectedException.GetType(), - () => mockHandler.Object.ValidateToken(token, validationParameters, out parsedToken)); - - parsedToken.Should().BeNull(); - exception.Should().Be(expectedException); - } - - private static Mock CreateMockToken() - { - var mockToken = new Mock(); - - mockToken.Setup(x => x.ValidTo).Returns(null); - mockToken.Setup(x => x.ValidFrom).Returns(null); - mockToken.Setup(x => x.Audiences).Returns(new []{"you"}); - mockToken.Setup(x => x.Issuer).Returns("me"); - mockToken.Setup(x => x.TokenHash).Returns("xyz"); - - return mockToken; - } - - private static Mock CreateMockHandler() - { - var mockHandler = new Mock {CallBase = false}; - mockHandler.Setup(x => x.TestValidateTokenPayload(It.IsAny(), It.IsAny())) - .CallBase(); - mockHandler.Setup(x => x.TestCreateClaimsIdentity(It.IsAny(), It.IsAny())) - .CallBase(); - - return mockHandler; - } - } - - public class TestJwtPayloadTokenHandler : JwtPayloadTokenHandler - { - public virtual TokenValidationResult TestValidateTokenPayload(JwtPayloadSecurityToken token, TokenValidationParameters validationParameters) - => base.ValidateTokenPayload(token, validationParameters); - public virtual ClaimsIdentity TestCreateClaimsIdentity(JwtPayloadSecurityToken jwtToken, TokenValidationParameters validationParameters) - => base.CreateClaimsIdentity(jwtToken, validationParameters); - - public virtual IEnumerable TestGetDecryptionKeys(string token, TokenValidationParameters validationParameters) - => base.GetDecryptionKeys(token, validationParameters); - - public virtual IEnumerable TestGetSigningKeys(string token, TokenValidationParameters validationParameters) - => base.GetSigningKeys(token, validationParameters); - - public override bool CanReadToken(string securityToken) - { - throw new NotImplementedException(); - } - - public override TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) - { - throw new NotImplementedException(); - } - } - - public class MockableJwtPayloadSecurityToken : JwtPayloadSecurityToken - { - public override SecurityKey SecurityKey => throw new NotImplementedException(); - public override SecurityKey SigningKey { get; set; } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtSecurityTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JwtSecurityTokenHandlerTests.cs index fbd68c3..6b5c3b5 100644 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/JwtSecurityTokenHandlerTests.cs +++ b/test/ScottBrady.IdentityModel.Tests/Tokens/JwtSecurityTokenHandlerTests.cs @@ -7,13 +7,13 @@ using ScottBrady.IdentityModel.Tokens; using Xunit; -namespace ScottBrady.IdentityModel.Tests.Tokens +namespace ScottBrady.IdentityModel.Tests.Tokens; + +public class JwtSecurityTokenHandlerTests { - public class JwtSecurityTokenHandlerTests + [Fact] + public void WhenEdDsaTokenGenerated_ExpectEdDsaTokenVerifiable() { - [Fact] - public void WhenEdDsaTokenGenerated_ExpectEdDsaTokenVerifiable() - { const string issuer = "me"; const string audience = "you"; const string subject = "123"; @@ -43,5 +43,4 @@ public void WhenEdDsaTokenGenerated_ExpectEdDsaTokenVerifiable() validationResult.Claims.Should().Contain(x => x.Type == "sub" && x.Value == subject); } - } } \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoSecurityTokenTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoSecurityTokenTests.cs deleted file mode 100644 index 7cf7301..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoSecurityTokenTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using FluentAssertions; -using Newtonsoft.Json; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - public class PasetoSecurityTokenTests - { - [Fact] - public void IssuedAt_WhenIatClaimHasIsoFormat_ExpectDateTime() - { - var expectedDateTime = new DateTime(2038, 03, 17, 01, 02, 03, DateTimeKind.Utc); - - var jwt = new - { - iss = "me", - aud = "you", - iat = "2038-03-17T01:02:03+00:00" - }; - - var innerToken = new TestPasetoToken(); - innerToken.SetPayload(JsonConvert.SerializeObject(jwt)); - var token = new PasetoSecurityToken(innerToken); - - token.IssuedAt.Should().BeCloseTo(expectedDateTime, TimeSpan.FromMilliseconds(10)); - } - - [Fact] - public void ValidFrom_WhenIatClaimHasIsoFormat_ExpectDateTime() - { - var expectedDateTime = new DateTime(2028, 03, 17, 01, 02, 03, DateTimeKind.Utc); - - var jwt = new - { - iss = "me", - aud = "you", - nbf = "2028-03-17T01:02:03+00:00" - }; - - var innerToken = new TestPasetoToken(); - innerToken.SetPayload(JsonConvert.SerializeObject(jwt)); - var token = new PasetoSecurityToken(innerToken); - - token.ValidFrom.Should().BeCloseTo(expectedDateTime, TimeSpan.FromMilliseconds(10)); - } - - [Fact] - public void ValidTo_WhenIatClaimHasIsoFormat_ExpectDateTime() - { - var expectedDateTime = new DateTime(2018, 03, 17, 01, 02, 03, DateTimeKind.Utc); - - var jwt = new - { - iss = "me", - aud = "you", - exp = "2018-03-17T01:02:03+00:00" - }; - - var innerToken = new TestPasetoToken(); - innerToken.SetPayload(JsonConvert.SerializeObject(jwt)); - var token = new PasetoSecurityToken(innerToken); - - token.ValidTo.Should().BeCloseTo(expectedDateTime, TimeSpan.FromMilliseconds(10)); - } - } - - public class TestPasetoToken : PasetoToken - { - public new void SetPayload(string payload) => Payload = payload; - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenHandlerTests.cs deleted file mode 100644 index cc39b4c..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenHandlerTests.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using Moq; -using Moq.Protected; -using ScottBrady.IdentityModel.Crypto; -using ScottBrady.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - public class PasetoTokenHandlerTests - { - private const string TestVersion = "test"; - private readonly Mock mockVersionStrategy = new Mock(); - - private readonly Mock mockedSut; - private readonly PasetoTokenHandler sut; - - public PasetoTokenHandlerTests() - { - mockedSut = new Mock( - new Dictionary{{TestVersion, mockVersionStrategy.Object}}) - { - CallBase = true - }; - - sut = mockedSut.Object; - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void CanReadToken_WhenTokenIsNullOrWhitespace_ExpectFalse(string token) - { - var handler = new PasetoTokenHandler(); - var canReadToken = handler.CanReadToken(token); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenTokenIsTooLong_ExpectFalse() - { - var tokenBytes = new byte[TokenValidationParameters.DefaultMaximumTokenSizeInBytes + 1]; - new Random().NextBytes(tokenBytes); - - var canReadToken = sut.CanReadToken(Convert.ToBase64String(tokenBytes)); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenTokenHasTooManySegments_ExpectFalse() - { - const string invalidToken = "ey.ey.ey.ey.ey.ey"; - - var canReadToken = sut.CanReadToken(invalidToken); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenBrancaToken_ExpectFalse() - { - const string brancaToken = "5K6Oid5pXkASEGvv63CHxpKhSX9passYQ4QhdSdCuOEnHlvBrvX414fWX6zUceAdg3DY9yTVQcmVZn0xr9lsBKBHDzOLNAGVlCs1SHlWIuFDfB8yGXO8EyNPnH9CBMueSEtNmISgcjM1ZmfmcD2EtE6"; - - var canReadToken = sut.CanReadToken(brancaToken); - - canReadToken.Should().BeFalse(); - } - - [Fact] - public void CanReadToken_WhenPasetoToken_ExpectTrue() - { - var canReadToken = sut.CanReadToken(CreateTestToken()); - - canReadToken.Should().BeTrue(); - } - - [Fact] - public void CanValidateToken_ExpectTrue() - => sut.CanValidateToken.Should().BeTrue(); - - [Fact] - public void CreateToken_WhenSecurityTokenDescriptorIsNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.CreateToken(null)); - - [Fact] - public void CreateToken_WhenTokenVersionIsNotSupported_ExpectSecurityTokenException() - { - var tokenDescriptor = new PasetoSecurityTokenDescriptor("v42", PasetoConstants.Purposes.Public); - - Assert.Throws(() => sut.CreateToken(tokenDescriptor)); - } - - [Fact] - public void CreateToken_WhenTokenPurposeNotSupported_ExpectSecurityTokenException() - { - var tokenDescriptor = new PasetoSecurityTokenDescriptor(TestVersion, "external"); - - Assert.Throws(() => sut.CreateToken(tokenDescriptor)); - } - - [Fact] - public void CreateToken_WhenLocalEncryptionThrowsException_ExpectSameException() - { - var expectedException = new ApplicationException("local"); - var tokenDescriptor = new PasetoSecurityTokenDescriptor(TestVersion, PasetoConstants.Purposes.Local); - - mockVersionStrategy.Setup(x => x.Encrypt(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(expectedException); - - var exception = Assert.Throws(expectedException.GetType(), () => sut.CreateToken(tokenDescriptor)); - exception.Should().Be(expectedException); - } - - [Fact] - public void CreateToken_WhenPublicSigningThrowsException_ExpectSameException() - { - var expectedException = new ApplicationException("public"); - var tokenDescriptor = new PasetoSecurityTokenDescriptor(TestVersion, PasetoConstants.Purposes.Public); - - mockVersionStrategy.Setup(x => x.Sign(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(expectedException); - - var exception = Assert.Throws(expectedException.GetType(), () => sut.CreateToken(tokenDescriptor)); - exception.Should().Be(expectedException); - } - - [Fact] - public void CreateToken_WhenLocalEncryptionSucceeds_ExpectLocalToken() - { - const string expectedToken = "local"; - var tokenDescriptor = new PasetoSecurityTokenDescriptor(TestVersion, PasetoConstants.Purposes.Local); - - mockVersionStrategy.Setup(x => x.Encrypt(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(expectedToken); - - var token = sut.CreateToken(tokenDescriptor); - - token.Should().Be(expectedToken); - } - - [Fact] - public void CreateToken_WhenPublicSigningSucceeds_ExpectPublicToken() - { - const string expectedToken = "public"; - var tokenDescriptor = new PasetoSecurityTokenDescriptor(TestVersion, PasetoConstants.Purposes.Public); - - mockVersionStrategy.Setup(x => x.Sign(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(expectedToken); - - var token = sut.CreateToken(tokenDescriptor); - - token.Should().Be(expectedToken); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void ValidateToken_WhenTokenIsNullOrWhitespace_ExpectFailureWithArgumentNullException(string token) - { - var result = sut.ValidateToken(token, new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenValidationParametersAreNull_ExpectFailureWithArgumentNullException() - { - var result = sut.ValidateToken(CreateTestToken(), null); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenCannotBeRead_ExpectFailureWithSecurityTokenException() - { - var result = sut.ValidateToken("ey.ey", new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenVersionIsNotSupported_ExpectSecurityTokenException() - { - var result = sut.ValidateToken(CreateTestToken(version: "v42"), new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenTokenPurposeNotSupported_ExpectSecurityTokenException() - { - var result = sut.ValidateToken(CreateTestToken(purpose: "notapurpose"), new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().BeOfType(); - } - - [Fact] - public void ValidateToken_WhenLocalTokenValidationFails_ExpectFailureResultWithInnerException() - { - var expectedException = new ApplicationException("local"); - - mockVersionStrategy.Setup(x => x.Decrypt(It.IsAny(), It.IsAny>())) - .Throws(expectedException); - - var result = sut.ValidateToken(CreateTestToken(purpose: PasetoConstants.Purposes.Local), new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateToken_WhenPublicTokenValidationFails_ExpectFailureResultWithInnerException() - { - var expectedException = new ApplicationException("public"); - - mockVersionStrategy.Setup(x => x.Verify(It.IsAny(), It.IsAny>())) - .Throws(expectedException); - - var result = sut.ValidateToken(CreateTestToken(purpose: PasetoConstants.Purposes.Public), new TokenValidationParameters()); - - result.IsValid.Should().BeFalse(); - result.Exception.Should().Be(expectedException); - } - - [Fact] - public void ValidateToken_WhenTokenPayloadValidationFails_ExpectPayloadValidationResult() - { - var expectedResult = new TokenValidationResult {IsValid = false, Exception = new ApplicationException("validation")}; - - mockVersionStrategy.Setup(x => x.Verify(It.IsAny(), It.IsAny>())) - .Returns(new TestPasetoSecurityToken()); - mockedSut.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(expectedResult); - - var result = sut.ValidateToken(CreateTestToken(purpose: PasetoConstants.Purposes.Public), new TokenValidationParameters()); - - result.Should().Be(expectedResult); - } - - [Fact] - public void ValidateToken_WhenValidLocalToken_ExpectSuccessResultWithSecurityTokenAndClaimsIdentity() - { - var expectedIdentity = new ClaimsIdentity("test"); - - mockVersionStrategy.Setup(x => x.Decrypt(It.IsAny(), It.IsAny>())) - .Returns(new TestPasetoSecurityToken()); - mockedSut.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(new TokenValidationResult - { - ClaimsIdentity = expectedIdentity, - IsValid = true - }); - - var result = sut.ValidateToken(CreateTestToken(purpose: PasetoConstants.Purposes.Local), new TokenValidationParameters()); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.Should().Be(expectedIdentity); - result.SecurityToken.Should().NotBeNull(); - } - - [Fact] - public void ValidateToken_WhenValidPublicToken_ExpectSuccessResultWithSecurityTokenAndClaimsIdentity() - { - var expectedIdentity = new ClaimsIdentity("test"); - - mockVersionStrategy.Setup(x => x.Verify(It.IsAny(), It.IsAny>())) - .Returns(new TestPasetoSecurityToken()); - mockedSut.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(new TokenValidationResult - { - ClaimsIdentity = expectedIdentity, - IsValid = true - }); - - var result = sut.ValidateToken(CreateTestToken(purpose: PasetoConstants.Purposes.Public), new TokenValidationParameters()); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.Should().Be(expectedIdentity); - result.SecurityToken.Should().NotBeNull(); - } - - [Fact] - public void ValidateToken_WhenSaveSignInTokenIsTrue_ExpectIdentityBootstrapContext() - { - var expectedToken = CreateTestToken(purpose: PasetoConstants.Purposes.Public); - var expectedIdentity = new ClaimsIdentity("test"); - - mockVersionStrategy.Setup(x => x.Verify(It.IsAny(), It.IsAny>())) - .Returns(new TestPasetoSecurityToken()); - mockedSut.Protected() - .Setup("ValidateTokenPayload", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(new TokenValidationResult - { - ClaimsIdentity = expectedIdentity, - IsValid = true - }); - - var result = sut.ValidateToken(expectedToken, new TokenValidationParameters {SaveSigninToken = true}); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.BootstrapContext.Should().Be(expectedToken); - } - - [Fact] - public void CreateAndValidateToken_WhenV2PublicToken_ExpectCorrectClaims() - { - const string expectedClaimType = "name"; - const string expectedClaimValue = "scott"; - const string issuer = "me"; - const string audience = "you"; - - var signingCredentials = new SigningCredentials( - new EdDsaSecurityKey(EdDsa.Create( - new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) - { - D = Convert.FromBase64String("TYXei5+8Qd2ZqKIlEuJJ3S50WYuocFTrqK+3/gHVH9B2hpLtAgscF2c9QuWCzV9fQxal3XBqTXivXJPpp79vgw==") - })), ExtendedSecurityAlgorithms.EdDsa); - var verificationKeys = - new EdDsaSecurityKey(EdDsa.Create( - new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519){X = Convert.FromBase64String("doaS7QILHBdnPULlgs1fX0MWpd1wak14r1yT6ae/b4M=")})); - - var handler = new PasetoTokenHandler(); - var token = handler.CreateToken(new PasetoSecurityTokenDescriptor(PasetoConstants.Versions.V2, PasetoConstants.Purposes.Public) - { - Issuer = issuer, - Audience = audience, - Claims = new Dictionary {{expectedClaimType, expectedClaimValue}}, - SigningCredentials = signingCredentials - }); - - var result = handler.ValidateToken(token, new TokenValidationParameters - { - ValidIssuer = issuer, - ValidAudience = audience, - IssuerSigningKey = verificationKeys - }); - - result.IsValid.Should().BeTrue(); - result.ClaimsIdentity.HasClaim(expectedClaimType, expectedClaimValue).Should().BeTrue(); - } - - private static string CreateTestToken(string version = TestVersion, string purpose = "public", string payload = "ey") - => $"{version}.{purpose}.{payload}"; - } - - internal class TestPasetoSecurityToken : PasetoSecurityToken { } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenTests.cs deleted file mode 100644 index e68e1e9..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/PasetoTokenTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - public class PasetoTokenTests - { - private const string ValidToken = "v2.local.xyz"; - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void ctor_WhenTokenIsNullOrWhitespace_ExpectArgumentNullException(string token) - => Assert.Throws(() => new PasetoToken(token)); - - [Fact] - public void ctor_WhenTokenHasTooManyParts_ExpectArgumentException() - => Assert.Throws(() => new PasetoToken("ey.ey.ey.ey.ey")); - - [Fact] - public void ctor_WhenValidPasetoToken_ExpectCorrectProperties() - { - const string expectedVersion = "v2"; - const string expectedPurpose = "public"; - const string expectedPayload = "fa919c9d3d1248f29213521a40fc2b57"; - var token = $"{expectedVersion}.{expectedPurpose}.{expectedPayload}"; - - var pasetoToken = new PasetoToken(token); - - pasetoToken.RawToken.Should().Be(token); - pasetoToken.Version.Should().Be(expectedVersion); - pasetoToken.Purpose.Should().Be(expectedPurpose); - pasetoToken.EncodedPayload.Should().Be(expectedPayload); - - pasetoToken.Payload.Should().BeNull(); - pasetoToken.EncodedFooter.Should().BeNull(); - } - - [Fact] - public void ctor_WhenValidPasetoTokenWithFooter_ExpectCorrectProperties() - { - const string expectedVersion = "v2"; - const string expectedPurpose = "public"; - const string expectedPayload = "fa919c9d3d1248f29213521a40fc2b57"; - const string expectedFooter = "{test}"; - var token = $"{expectedVersion}.{expectedPurpose}.{expectedPayload}.{Base64UrlEncoder.Encode(expectedFooter)}"; - - var pasetoToken = new PasetoToken(token); - - pasetoToken.RawToken.Should().Be(token); - pasetoToken.Version.Should().Be(expectedVersion); - pasetoToken.Purpose.Should().Be(expectedPurpose); - pasetoToken.EncodedPayload.Should().Be(expectedPayload); - pasetoToken.EncodedFooter.Should().Be(Base64UrlEncoder.Encode(expectedFooter)); - pasetoToken.Footer.Should().Be(expectedFooter); - - pasetoToken.Payload.Should().BeNull(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void SetPayload_WhenPayloadIsNullOrWhitespace_ExpectArgumentNullException(string payload) - => Assert.Throws(() => new PasetoToken(ValidToken).SetPayload(payload)); - - [Fact] - public void SetPayload_WhenPayloadIsNotJson_ExpectArgumentException() - { - const string invalidPayload = "oops"; - var token = new PasetoToken(ValidToken); - - Assert.Throws(() => token.SetPayload(invalidPayload)); - } - - [Fact] - public void SetPayload_WhenValidPayload_ExpectParsedPayload() - { - const string expectedKey = "test"; - const string expectedValue = "test_val"; - - var payload = $"{{ \"{expectedKey}\": \"{expectedValue}\" }}"; - var token = new PasetoToken(ValidToken); - - token.SetPayload(payload); - - token.Payload.Should().Be(payload); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/PasetoTestVectors.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/PasetoTestVectors.cs deleted file mode 100644 index 89a4533..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/PasetoTestVectors.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Security.Cryptography; -using System.Text.Json.Nodes; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Crypto; -using ScottBrady.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - /// - /// Test vectors from https://github.com/paseto-standard/test-vectors - /// - public class PasetoTestVectors - { - public static readonly TheoryData TestVectors = new TheoryData(); - - static PasetoTestVectors() - { - var file = File.OpenRead("Tokens/Paseto/TestVectors/testvectors.json"); - var data = JsonNode.Parse(file); - if (data == null) throw new Exception("Failed to load test vectors"); - - foreach (var testVector in data["v1"]?.AsArray() ?? throw new Exception("Failed to load v1 test vectors")) - { - TestVectors.Add(new PasetoTestVector("v1", testVector)); - } - foreach (var testVector in data["v2"]?.AsArray() ?? throw new Exception("Failed to load v2 test vectors")) - { - TestVectors.Add(new PasetoTestVector("v2", testVector)); - } - } - - [Theory, MemberData(nameof(TestVectors))] - public void ValidateToken_ExpectCorrectResult(PasetoTestVector testVector) - { - var handler = new PasetoTokenHandler(); - var result = handler.ValidateToken(testVector.Token, new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - IssuerSigningKey = testVector.Key - }); - - if (testVector.ShouldFail) - { - result.IsValid.Should().BeFalse(testVector.Name); - result.Claims.Should().BeEmpty(); - result.ClaimsIdentity.Should().BeNull(); - } - else - { - result.IsValid.Should().BeTrue(testVector.Name); - result.Claims.Should().NotBeEmpty(); - result.ClaimsIdentity.Should().NotBeNull(); - - foreach (var claim in testVector.ExpectedPayload.AsObject()) - { - result.Claims.Should().Contain(x => x.Key == claim.Key); - - var value = result.Claims[claim.Key]; - if (value is DateTime) - { - DateTime.TryParse(claim.Value.GetValue(), DateTimeFormatInfo.InvariantInfo, DateTimeStyles.RoundtripKind, out var dateTime).Should().BeTrue(); - value.Should().BeEquivalentTo(dateTime.ToUniversalTime()); - } - else - { - value.Should().BeEquivalentTo(claim.Value.GetValue()); - } - } - } - } - } - - public class PasetoTestVector - { - public PasetoTestVector(string version, JsonNode data) - { - Name = data["name"]?.GetValue(); - ShouldFail = data["expect-fail"]?.GetValue() ?? throw new Exception("Unable to parse expect-fail"); - Token = data["token"]?.GetValue(); - - var payload = data["payload"]?.GetValue(); - if (payload != null) - { - ExpectedPayload = JsonNode.Parse(payload); - } - - var publicKey = data["public-key"]?.GetValue() ?? throw new Exception("Failed to find public key"); - if (version == "v1") - { - var rsaKey = RSA.Create(); - rsaKey.ImportFromPem(publicKey); - Key = new RsaSecurityKey(rsaKey); - } - else if (version == "v2") - { - if (Base16.Decode(publicKey).Length != 32) - { - - } - - Key = new EdDsaSecurityKey(EdDsa.Create( - new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X = Base16.Decode(publicKey)})); - } - } - - public string Name { get; } - public bool ShouldFail { get; } - public SecurityKey Key { get; } - public string Token { get; } - public JsonNode ExpectedPayload { get; } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/testvectors.json b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/testvectors.json deleted file mode 100644 index 6440206..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/TestVectors/testvectors.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "v1": [ - { - "name": "1-S-1", - "expect-fail": false, - "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", - "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", - "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9cIZKahKeGM5kiAS_4D70Qbz9FIThZpxetJ6n6E6kXP_119SvQcnfCSfY_gG3D0Q2v7FEtm2Cmj04lE6YdgiZ0RwA41WuOjXq7zSnmmHK9xOSH6_2yVgt207h1_LphJzVztmZzq05xxhZsV3nFPm2cCu8oPceWy-DBKjALuMZt_Xj6hWFFie96SfQ6i85lOsTX8Kc6SQaG-3CgThrJJ6W9DC-YfQ3lZ4TJUoY3QNYdtEgAvp1QuWWK6xmIb8BwvkBPej5t88QUb7NcvZ15VyNw3qemQGn2ITSdpdDgwMtpflZOeYdtuxQr1DSGO2aQyZl7s0WYn1IjdQFx6VjSQ4yfw", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "", - "implicit-assertion": "" - }, - { - "name": "1-S-2", - "expect-fail": false, - "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", - "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", - "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", - "implicit-assertion": "" - }, - { - "name": "1-S-3", - "expect-fail": false, - "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", - "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", - "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", - "implicit-assertion": "discarded-anyway" - }, - { - "name": "1-F-1", - "expect-fail": true, - "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", - "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", - "token": "v1.local.uLSnLZApN_-wP2fM5d-0DgQ4RoR8zO6Sp4p5VbYkSW_KuWt_1sbtDalN3HBtGR8PAtU5M_-bes-iSqppFtmykiznxvxvz30K8yXJ2aXGbS-upJKXjO5cjWXBGJLNBPzzdpUo85Iv-gW8bbZ9e4tfnNdsRcJZ-g.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", - "payload": null, - "footer": "arbitrary-string-that-isn't-json", - "implicit-assertion": "{\"test-vector\":\"1-F-1\"}" - } - ], - "v2": [ - { - "name": "2-S-1", - "expect-fail": false, - "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGntTu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_DjJK2ZXC2SUYuOFM-Q_5Cw", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "", - "implicit-assertion": "" - }, - { - "name": "2-S-2", - "expect-fail": false, - "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - "implicit-assertion": "" - }, - { - "name": "2-S-3", - "expect-fail": false, - "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", - "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - "implicit-assertion": "discarded-anyway" - }, - { - "name": "2-F-1", - "expect-fail": true, - "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - "token": "v2.local.pN9Y9kTFKnCskKr7B13IoceBabSTMS0LkUg3SeAqONg6EJsq9h-CLWdWaA_rMZX4MhGsOQn5I0EsIgYeOA2NPJZU0uulsahH-k871PBq.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", - "payload": null, - "footer": "arbitrary-string-that-isn't-json", - "implicit-assertion": "{\"test-vector\":\"2-F-1\"}" - } - ] -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion1Tests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion1Tests.cs deleted file mode 100644 index befbad3..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion1Tests.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - public class PasetoVersion1Tests - { - private const string ValidVersion = "v1"; - private const string ValidPublicPurpose = "public"; - private const string ValidPublicPayload = "eyJzdWIiOiIxMjMiLCJleHAiOiIyMDIwLTA1LTA3VDE3OjExOjM4LjM3MTE3MTJaIn1HginqDCa4m01vI75OaWrFyAYCA1k9_sx36XVDEcHosOkk6coBDwDfoOaSFA_wE3nkfyuy3fTr7g6BpdzPbIb5qhI4Wpdy_zhhyEz7kC8ZSaDNN0tnBT0sL1c6hSuWKGh3tT6qPmjUmJwIv2ZjosozSmRF7bhWKJDsvTzQN6EFBddcvQpPQok9Ekdgzd70_Yxjl9YlUizF7WOiDm-R6m3xy_Mk8IRGQwiArYGmJRmR82W97ajqdBUJD8kbaFQglDxwEMcX-T4AqXCttjhdQi-JcXX34SDTyxE-8m02X8eNrKg64L6ZAFDAzbaa2bz3EUo5ULW2XaG4DW2zZ4nFd9m2"; - private readonly string validToken = $"{ValidVersion}.{ValidPublicPurpose}.{ValidPublicPayload}"; - - private const string ValidSigningPrivateKey = - "PFJTQUtleVZhbHVlPjxNb2R1bHVzPm9VQ002dEdieFc5eGZqWm1mbmQzTThLQkFzd0ZmbHVLY2IwV1RNMXMzeVh2c3dZci9MbkFVVGVhNjF4QWRma3BSbVVuT3VrODRwZUN5OEVkMzdZcXVHL05WMldsT0dRckxMRkh3UklIMGdmU0IzMjFpdlVzN2xqbDc5S25TRmpEU0ZqcUJNTEJTSS93ZlhobCtYTGZrTjczaGJmeTNSRzVTUDU4Vm5UUEQveWFRczlmNVdVVHhCSFBKNWx4Ump3cVpTemJjZE05cHNtcVFHWGcveUVCejFsMlJQaCtTK0R1aEw5TU1iRWdTb0lXanFKaWEzUFllRDF5WEt3RjlPdjlaa3V4L21ZZjRkRW9pWUZXV05jS3ZSSGFTVFBjTFd0NnZpUXZsekREd09FUG9HdlI4SkNreUJ2a1J0Q1VBeVAyMEpkYzFGK0xqdEp3dkIyNTRBTjU1UT09PC9Nb2R1bHVzPjxFeHBvbmVudD5BUUFCPC9FeHBvbmVudD48UD54Mlp3Y1N1Ni9KeTB5UFRqcVJXTlA1OVhtMk5hNDFEajdQeThsNHBXbWZKWkExTWxBMzRMNUpLVEpJMHZEWStyMTBhN1JRcVJSeHpseEVnQStvMHQwbW5uckRZbGJYbDB4OGlON04vb2w2OHZ0NEJtWFZCWGdxYURpNUJoaWtvLzVzd3EvOVhBd1ZKYm1zVFBCSjVGdi9DQWxUSytNbStacUt6MzlHVTYvMHM9PC9QPjxRPnp3WU80MVdGRUFyTS80R3hDOW1HZEhJeEFKR2RBaWoyZWJ0NjMrQk9QRUxpelZySjdudVR0ZGtxZnlhRHR5eXpGTWxVRHV2VVpIMU1YNnlsUldsQWQrSlhZcUFRZVlML2poYXZYL0llL2NsRE5YRnlRbTNoeitrOFRZZDV2KzFId0RjVTNWVUJlQnVTa3d3bmgrSXBHMU5HeWk4b0RJZldYZThIUXRsdVBZOD08L1E+PERQPllPbGN4T1FvSVJaWWwwTE9VeU55WHZXbXNwTDdYWGUzRHp0V3ZhQXlydWVtYzRNNWZoVUkycktTYVRWbEpRWXEwcHBCOGpCTW8yOWNES1dpTkNQaG5WNXpock5hUlhhK1Ywc1dENFpUbVVVL3Y4UGIvSVpMd2VnRUR4VEJFMkU2NVlWZGNMSUcyTzZhTHdKd1N5SlJiQlFMcW5mYkVOQkVza0krMEwxU2l6az08L0RQPjxEUT5XRTJyT0FpWVV6bG9LMnYwU3F1a0VETk05NE1reDNFVmdPTVpERGt1NWNGWjRHSGpWQmZkNzJrTUdXUWlOcFdZWlR0aTRXSnlHOUxlS3NrSFRjNFJNNUdWMkhtUnpXSzFBclJtWmJSdXg2MTdQMlorYUJ0YWdFWnA5Ri9lN0tDWFJFTzZZSllMcEdHT2FhNTdoaGhQbEZvM0RiS0RrS1M0S1NUMW9ld0FlNzA9PC9EUT48SW52ZXJzZVE+SVpvNFVZRzVOVWt6TlA3LzVXTzhkVW9BUFJJU2htVithNTh5dWZWRjZtMUl3NTRJVDNTVWhXOVh5dFdMa0ZsWmJZakhxMmZlaUhDOUw0OGFUTG1SMmlIejVKUjJKN2pENnJGbis1SFNZR2l0OGpZbnBvNGpvVjZwaDAraWhOVnMzbkk3OEZVcFhmVDdWd0hhZDF4SXJ0QzA0VHBPODFjSjZpbi91TTRBVDI4PTwvSW52ZXJzZVE+PEQ+bEtQSEFmR2prRlJSSHRHUW13VU9pVlRDelV3NXlDY2pzQUpuMnZZRlpKRTRxaUtIUzVnQ0VodWFuMWZUUjZ3Y2d2cGROaTJuWlF2YWttMTZWeXc1cHZmUUpiN1psT2lvNzdLZS9QYmM1SnMyM0piaFVLejk5TnRYWVVFaDJFdVIvMCtPc0VMQ0hsd29oOUFDMS9VdTVnRFIwNTRqcmVwWGpGU2hVcVNyOWdRaG5sejhreHlhdUpaK3hKQWorSWVnd3lzRkp0ZWVLUldJZjdDZ3ZESFZrMk8rcjBJcTZldTBWTFMxbHladlNNOWJyWDlxOVRYUzMvODJFQ2M2UW9PbUk5NEFTOXhmcGZWeGIyZjJEQ0dSN0dZL1M2WWJTRGpJMXpMQWxQRzZiWVJRTFlsR1FVd3NvdHkrYzFZK1ZCc1R3VEk4WTFneTVOemdES2x6S3I3ZXhRPT08L0Q+PC9SU0FLZXlWYWx1ZT4="; - private const string ValidSigningPublicKey = - "PFJTQUtleVZhbHVlPjxNb2R1bHVzPm9VQ002dEdieFc5eGZqWm1mbmQzTThLQkFzd0ZmbHVLY2IwV1RNMXMzeVh2c3dZci9MbkFVVGVhNjF4QWRma3BSbVVuT3VrODRwZUN5OEVkMzdZcXVHL05WMldsT0dRckxMRkh3UklIMGdmU0IzMjFpdlVzN2xqbDc5S25TRmpEU0ZqcUJNTEJTSS93ZlhobCtYTGZrTjczaGJmeTNSRzVTUDU4Vm5UUEQveWFRczlmNVdVVHhCSFBKNWx4Ump3cVpTemJjZE05cHNtcVFHWGcveUVCejFsMlJQaCtTK0R1aEw5TU1iRWdTb0lXanFKaWEzUFllRDF5WEt3RjlPdjlaa3V4L21ZZjRkRW9pWUZXV05jS3ZSSGFTVFBjTFd0NnZpUXZsekREd09FUG9HdlI4SkNreUJ2a1J0Q1VBeVAyMEpkYzFGK0xqdEp3dkIyNTRBTjU1UT09PC9Nb2R1bHVzPjxFeHBvbmVudD5BUUFCPC9FeHBvbmVudD48L1JTQUtleVZhbHVlPg=="; - - private readonly SigningCredentials validSigningCredentials; - private readonly List validVerificationKeys; - - private readonly PasetoVersion1 sut = new PasetoVersion1(); - - public PasetoVersion1Tests() - { - var privateKey = RSA.Create(); - privateKey.FromXmlString(Encoding.UTF8.GetString(Convert.FromBase64String(ValidSigningPrivateKey))); - - var publicKey = RSA.Create(); - publicKey.FromXmlString(Encoding.UTF8.GetString(Convert.FromBase64String(ValidSigningPublicKey))); - - validSigningCredentials = new SigningCredentials(new RsaSecurityKey(privateKey), SecurityAlgorithms.RsaSsaPssSha384); - validVerificationKeys = new List {new RsaSecurityKey(publicKey)}; - } - - [Fact] - public void Sign_WhenPayloadIsNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Sign(null, null, validSigningCredentials)); - - [Fact] - public void Sign_WhenSigningCredentialsAreNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Sign("test", null, null)); - - [Fact] - public void Sign_WhenSigningCredentialsDoNotContainRsaSecurityKey_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(new ECDsaSecurityKey(ECDsa.Create()), SecurityAlgorithms.EcdsaSha256); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenSigningCredentialsNotConfiguredForPs384_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(validSigningCredentials.Key, SecurityAlgorithms.RsaSha384); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenSigningCredentialsDoNotContainPrivateKey_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(validVerificationKeys.First(), SecurityAlgorithms.RsaSsaPssSha384); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenTokenGenerated_ExpectThreeParts() - { - var token = sut.Sign("payload", null, validSigningCredentials); - - token.Split('.').Length.Should().Be(3); - } - - [Fact] - public void Sign_WhenTokenGeneratedWithFooter_ExpectFourParts() - { - var token = sut.Sign("payload", "footer", validSigningCredentials); - - token.Split('.').Length.Should().Be(4); - } - - [Fact] - public void Verify_WhenTokenIsNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(null, validVerificationKeys)); - - [Fact] - public void Verify_WhenSecurityKeysAreNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(new PasetoToken(validToken), null)); - - [Fact] - public void Verify_WhenSecurityKeysAreEmpty_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(new PasetoToken(validToken), new List())); - - [Fact] - public void Verify_WhenNoRsaSecurityKeysPresent_ExpectSecurityTokenInvalidSigningKeyException() - { - var keys = new List {new ECDsaSecurityKey(ECDsa.Create())}; - - Assert.Throws(() => sut.Verify(new PasetoToken(validToken), keys)); - } - - [Fact] - public void Verify_WhenIncorrectVersion_ExpectArgumentException() - { - var token = new PasetoToken($"v42.{ValidPublicPurpose}.{ValidPublicPayload}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenIncorrectPurpose_ExpectArgumentException() - { - var token = new PasetoToken($"{ValidVersion}.local.{ValidPublicPayload}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenPayloadDoesNotContainEnoughBytes_ExpectSecurityTokenInvalidSignatureException() - { - var payloadBytes = new byte[32]; - new Random().NextBytes(payloadBytes); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payloadBytes)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenPayloadDoesNotContainJson_ExpectSecurityTokenException() - { - var payloadValue = "test"; - var payloadValueBytes = Encoding.UTF8.GetBytes(payloadValue); - - var signature = new byte[256]; - new Random().NextBytes(signature); - - var payload = new byte[payloadValueBytes.Length + signature.Length]; - Buffer.BlockCopy(payloadValueBytes, 0, payload, 0, payloadValueBytes.Length); - Buffer.BlockCopy(signature, 0, payload, payloadValueBytes.Length, signature.Length); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payload)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenSignatureInvalid_ExpectSecurityTokenInvalidSignatureException() - { - var payloadValue = "{ \"test\": \"test\" }"; - var payloadValueBytes = Encoding.UTF8.GetBytes(payloadValue); - - var signature = new byte[256]; - new Random().NextBytes(signature); - - var payload = new byte[payloadValueBytes.Length + signature.Length]; - Buffer.BlockCopy(payloadValueBytes, 0, payload, 0, payloadValueBytes.Length); - Buffer.BlockCopy(signature, 0, payload, payloadValueBytes.Length, signature.Length); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payload)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenSignatureIsValid_ExpectCorrectSecurityToken() - { - var token = new PasetoToken(validToken); - - var securityToken = sut.Verify(token, validVerificationKeys); - - securityToken.Should().NotBeNull(); - securityToken.RawToken.Should().Be(token.RawToken); - } - - [Fact] - public void SignAndVerify_WhenKeysAreValid_ExpectCorrectClaimsFromPayload() - { - const string expectedClaimType = "test"; - const string expectedClaimValue = "test_val"; - var expectedPayload = $"{{ \"{expectedClaimType}\": \"{expectedClaimValue}\" }}"; - - var token = sut.Sign(expectedPayload, null, validSigningCredentials); - var parsedToken = sut.Verify(new PasetoToken(token), validVerificationKeys); - - parsedToken.Claims.Should().Contain(x => x.Type == expectedClaimType && x.Value == expectedClaimValue); - parsedToken.RawToken.Should().Be(token); - } - - [Fact] - public void SignAndVerify_WhenKeysAreValidAndWithFooter_ExpectCorrectClaimsFromPayloadAndCorrectFooter() - { - const string expectedClaimType = "test"; - const string expectedClaimValue = "test_val"; - const string expectedFooter = "{\"kid\": \"123\"}"; - var expectedPayload = $"{{ \"{expectedClaimType}\": \"{expectedClaimValue}\" }}"; - - var token = sut.Sign(expectedPayload, expectedFooter, validSigningCredentials); - var parsedToken = sut.Verify(new PasetoToken(token), validVerificationKeys); - - parsedToken.Claims.Should().Contain(x => x.Type == expectedClaimType && x.Value == expectedClaimValue); - parsedToken.RawToken.Should().Be(token); - parsedToken.Footer.Should().Be(expectedFooter); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion2Tests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion2Tests.cs deleted file mode 100644 index 46ee685..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersion2Tests.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Crypto; -using ScottBrady.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - public class PasetoVersion2Tests - { - private const string ValidVersion = "v2"; - private const string ValidPublicPurpose = "public"; - private const string ValidPublicPayload = "eyJzdWIiOiIxMjMiLCJleHAiOiIyMDIwLTA1LTAyVDE2OjIzOjQwLjI1Njg1MTVaIn08nP0mX2YJvYOcMLBpiFbFs1C2gyNAJg_kpuniow671AfrEZWRDZWmLAQbuKRQNiJ2gIrXVeC-tO20zrVQ58wK"; - private readonly string validToken = $"{ValidVersion}.{ValidPublicPurpose}.{ValidPublicPayload}"; - - private const string ValidSigningPrivateKey = "TYXei5+8Qd2ZqKIlEuJJ3S50WYuocFTrqK+3/gHVH9B2hpLtAgscF2c9QuWCzV9fQxal3XBqTXivXJPpp79vgw=="; - private const string ValidSigningPublicKey = "doaS7QILHBdnPULlgs1fX0MWpd1wak14r1yT6ae/b4M="; - - private readonly SigningCredentials validSigningCredentials = new SigningCredentials( - new EdDsaSecurityKey(EdDsa.Create( - new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {D = Convert.FromBase64String(ValidSigningPrivateKey)})), - ExtendedSecurityAlgorithms.EdDsa); - - private readonly List validVerificationKeys = new List - { - new EdDsaSecurityKey(EdDsa.Create( - new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X = Convert.FromBase64String(ValidSigningPublicKey)})) - }; - - private readonly PasetoVersion2 sut = new PasetoVersion2(); - - [Fact] - public void Sign_WhenPayloadIsNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Sign(null, null, validSigningCredentials)); - - [Fact] - public void Sign_WhenSigningCredentialsAreNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Sign("test", null, null)); - - [Fact] - public void Sign_WhenSigningCredentialsDoNotContainEdDsaSecurityKey_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(new RsaSecurityKey(RSA.Create()), ExtendedSecurityAlgorithms.EdDsa); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenSigningCredentialsNotConfigureForEdDSA_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(validSigningCredentials.Key, ExtendedSecurityAlgorithms.XChaCha20Poly1305); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenSigningCredentialsDoNotContainPrivateKey_ExpectSecurityTokenInvalidSigningKeyException() - { - var signingCredentials = new SigningCredentials(validVerificationKeys.First(), ExtendedSecurityAlgorithms.EdDsa); - - Assert.Throws(() => sut.Sign("payload", null, signingCredentials)); - } - - [Fact] - public void Sign_WhenTokenGenerated_ExpectThreeParts() - { - var token = sut.Sign("payload", null, validSigningCredentials); - - token.Split('.').Length.Should().Be(3); - } - - [Fact] - public void Sign_WhenTokenGeneratedWithFooter_ExpectFourParts() - { - var token = sut.Sign("payload", "footer", validSigningCredentials); - - token.Split('.').Length.Should().Be(4); - } - - [Fact] - public void Verify_WhenTokenIsNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(null, validVerificationKeys)); - - [Fact] - public void Verify_WhenSecurityKeysAreNull_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(new PasetoToken(validToken), null)); - - [Fact] - public void Verify_WhenSecurityKeysAreEmpty_ExpectArgumentNullException() - => Assert.Throws(() => sut.Verify(new PasetoToken(validToken), new List())); - - [Fact] - public void Verify_WhenNoEdDsaSecurityKeysPresent_ExpectSecurityTokenInvalidSigningKeyException() - { - var keys = new List {new RsaSecurityKey(RSA.Create())}; - - Assert.Throws(() => sut.Verify(new PasetoToken(validToken), keys)); - } - - [Fact] - public void Verify_WhenIncorrectVersion_ExpectArgumentException() - { - var token = new PasetoToken($"v42.{ValidPublicPurpose}.{ValidPublicPayload}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenIncorrectPurpose_ExpectArgumentException() - { - var token = new PasetoToken($"{ValidVersion}.local.{ValidPublicPayload}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenPayloadIsNotBase64UrlEncodedValue_ExpectFormatException() - { - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.ey!!"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenPayloadDoesNotContainEnoughBytes_ExpectSecurityTokenInvalidSignatureException() - { - var payloadBytes = new byte[32]; - new Random().NextBytes(payloadBytes); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payloadBytes)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenPayloadDoesNotContainJson_ExpectSecurityTokenException() - { - var payloadValue = "test"; - var payloadValueBytes = Encoding.UTF8.GetBytes(payloadValue); - - var signature = new byte[64]; - new Random().NextBytes(signature); - - var payload = new byte[payloadValueBytes.Length + signature.Length]; - Buffer.BlockCopy(payloadValueBytes, 0, payload, 0, payloadValueBytes.Length); - Buffer.BlockCopy(signature, 0, payload, payloadValueBytes.Length, signature.Length); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payload)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenSignatureInvalid_ExpectSecurityTokenInvalidSignatureException() - { - var payloadValue = "{ \"test\": \"test\" }"; - var payloadValueBytes = Encoding.UTF8.GetBytes(payloadValue); - - var signature = new byte[64]; - new Random().NextBytes(signature); - - var payload = new byte[payloadValueBytes.Length + signature.Length]; - Buffer.BlockCopy(payloadValueBytes, 0, payload, 0, payloadValueBytes.Length); - Buffer.BlockCopy(signature, 0, payload, payloadValueBytes.Length, signature.Length); - - var token = new PasetoToken($"{ValidVersion}.{ValidPublicPurpose}.{Base64UrlEncoder.Encode(payload)}"); - - Assert.Throws(() => sut.Verify(token, validVerificationKeys)); - } - - [Fact] - public void Verify_WhenSignatureIsValid_ExpectCorrectSecurityToken() - { - // "wxFZtnkkIXbcNh4WTYbTS8WgEyWaYRhfT1603kN6SdQ=" - // "v2.public.eyJzdWIiOiIxMjMiLCJleHAiOiIyMDIwLTA1LTAzVDEzOjE0OjE0LjE5MDA1OFoiff5U7ni0Bd5yame3wT41v26UMyH56JA4Un077FPn_UkGpx78fVgbegW0FEMLw0J61ms0OJHarRzyRrX4dWn6LgA" - var token = new PasetoToken(validToken); - - var securityToken = sut.Verify(token, validVerificationKeys); - - securityToken.Should().NotBeNull(); - securityToken.RawToken.Should().Be(token.RawToken); - } - - [Fact] - public void Verify_WhenFooterPresentAndSignatureIsValid_ExpectCorrectSecurityToken() - { - var tokenWithFooter = new PasetoToken( - "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"); - var publicKey = Convert.FromBase64String("Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI="); - - var securityToken = sut.Verify(tokenWithFooter, - new[] {new EdDsaSecurityKey(EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) {X = publicKey}))}); - - securityToken.Should().NotBeNull(); - securityToken.RawToken.Should().Be(tokenWithFooter.RawToken); - } - - [Fact] - public void SignAndVerify_WhenKeysAreValid_ExpectCorrectClaimsFromPayload() - { - const string expectedClaimType = "test"; - const string expectedClaimValue = "test_val"; - var expectedPayload = $"{{ \"{expectedClaimType}\": \"{expectedClaimValue}\" }}"; - - var token = sut.Sign(expectedPayload, null, validSigningCredentials); - var parsedToken = sut.Verify(new PasetoToken(token), validVerificationKeys); - - parsedToken.Claims.Should().Contain(x => x.Type == expectedClaimType && x.Value == expectedClaimValue); - parsedToken.RawToken.Should().Be(token); - } - - [Fact] - public void SignAndVerify_WhenKeysAreValidAndWithFooter_ExpectCorrectClaimsFromPayloadAndCorrectFooter() - { - const string expectedClaimType = "test"; - const string expectedClaimValue = "test_val"; - const string expectedFooter = "{'kid': '123'}"; - var expectedPayload = $"{{ \"{expectedClaimType}\": \"{expectedClaimValue}\" }}"; - - var token = sut.Sign(expectedPayload, expectedFooter, validSigningCredentials); - var parsedToken = sut.Verify(new PasetoToken(token), validVerificationKeys); - - parsedToken.Claims.Should().Contain(x => x.Type == expectedClaimType && x.Value == expectedClaimValue); - parsedToken.RawToken.Should().Be(token); - parsedToken.Footer.Should().Be(expectedFooter); - } - } -} \ No newline at end of file diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersionStrategyTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersionStrategyTests.cs deleted file mode 100644 index fdbc200..0000000 --- a/test/ScottBrady.IdentityModel.Tests/Tokens/Paseto/VersionStrategies/PasetoVersionStrategyTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using FluentAssertions; -using Microsoft.IdentityModel.Tokens; -using ScottBrady.IdentityModel.Tokens.Paseto; -using Xunit; - -namespace ScottBrady.IdentityModel.Tests.Tokens.Paseto -{ - /// - /// https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md - /// - public class PasetoVersionStrategyTests - { - [Fact] - public void PreAuthEncode_WhenPiecesIsNull_ExpectArgumentNullException() - => Assert.Throws(() => TestPasetoVersionStrategy.PreAuthEncodeSpy(null)); - - [Fact] - public void PreAuthEncodeSpy_WhenEmptyCollection_ExpectKnownResponse() - { - var encodedValue = TestPasetoVersionStrategy.PreAuthEncodeSpy(new List()); - encodedValue.Should().BeEquivalentTo(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}); - } - - [Fact] - public void PreAuthEncodeSpy_WhenEmptyString_ExpectKnownResponse() - { - var encodedValue = TestPasetoVersionStrategy.PreAuthEncodeSpy(new[] {Encoding.UTF8.GetBytes(string.Empty)}); - encodedValue.Should().BeEquivalentTo(new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); - } - - [Fact] - public void PreAuthEncodeSpy_WhenTestString_ExpectKnownResponse() - { - var testBytes = Encoding.UTF8.GetBytes("test"); - - var encodedValue = TestPasetoVersionStrategy.PreAuthEncodeSpy(new[] {testBytes}); - encodedValue.Should().BeEquivalentTo(new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0}.Concat(testBytes)); - } - } - - public class TestPasetoVersionStrategy : PasetoVersionStrategy - { - public override string Encrypt(string payload, string footer, EncryptingCredentials encryptingCredentials) => throw new NotImplementedException(); - public override string Sign(string payload, string footer, SigningCredentials signingCredentials) => throw new NotImplementedException(); - public override PasetoSecurityToken Decrypt(PasetoToken token, IEnumerable decryptionKeys) => throw new System.NotImplementedException(); - public override PasetoSecurityToken Verify(PasetoToken token, IEnumerable signingKeys) => throw new System.NotImplementedException(); - - public static byte[] PreAuthEncodeSpy(IReadOnlyList pieces) => PreAuthEncode(pieces); - } -} \ No newline at end of file