Skip to content

Commit

Permalink
WIP: Implemented token management. (#39)
Browse files Browse the repository at this point in the history
* Implementing token management.

* Implemented token creation & unit tests.

* Completed unit tests.

* Implementing token validation.

* Implemented the token blacklist.

* Added a migration.

* Implemented unit tests.

* Implemented unit tests.

* Completed token management.
  • Loading branch information
Utar94 authored Jan 21, 2024
1 parent 64b679d commit 04206b3
Show file tree
Hide file tree
Showing 37 changed files with 2,277 additions and 13 deletions.
16 changes: 15 additions & 1 deletion Identity.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BCB8A692
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Identity.Domain.UnitTests", "tests\Logitar.Identity.Domain.UnitTests\Logitar.Identity.Domain.UnitTests.csproj", "{9A27A378-14E7-4D3C-B847-27D0D4EFA8F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Identity.EFCore.SqlServer.IntegrationTests", "tests\Logitar.Identity.EFCore.SqlServer.IntegrationTests\Logitar.Identity.EFCore.SqlServer.IntegrationTests.csproj", "{FA9AB722-026B-4842-B888-E9824568CBC1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Identity.EFCore.SqlServer.IntegrationTests", "tests\Logitar.Identity.EFCore.SqlServer.IntegrationTests\Logitar.Identity.EFCore.SqlServer.IntegrationTests.csproj", "{FA9AB722-026B-4842-B888-E9824568CBC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Identity.Infrastructure.UnitTests", "tests\Logitar.Identity.Infrastructure.UnitTests\Logitar.Identity.Infrastructure.UnitTests.csproj", "{04C669C6-0B63-45A1-8F0F-16A7E7FC023E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Identity.Tests", "tests\Logitar.Identity.Tests\Logitar.Identity.Tests.csproj", "{D0781AC3-5827-4DAE-BBAD-481634FF3C0F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -64,13 +68,23 @@ Global
{FA9AB722-026B-4842-B888-E9824568CBC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA9AB722-026B-4842-B888-E9824568CBC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA9AB722-026B-4842-B888-E9824568CBC1}.Release|Any CPU.Build.0 = Release|Any CPU
{04C669C6-0B63-45A1-8F0F-16A7E7FC023E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04C669C6-0B63-45A1-8F0F-16A7E7FC023E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04C669C6-0B63-45A1-8F0F-16A7E7FC023E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04C669C6-0B63-45A1-8F0F-16A7E7FC023E}.Release|Any CPU.Build.0 = Release|Any CPU
{D0781AC3-5827-4DAE-BBAD-481634FF3C0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0781AC3-5827-4DAE-BBAD-481634FF3C0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0781AC3-5827-4DAE-BBAD-481634FF3C0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0781AC3-5827-4DAE-BBAD-481634FF3C0F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9A27A378-14E7-4D3C-B847-27D0D4EFA8F9} = {BCB8A692-DF88-4E50-91A7-AD91E466559C}
{FA9AB722-026B-4842-B888-E9824568CBC1} = {BCB8A692-DF88-4E50-91A7-AD91E466559C}
{04C669C6-0B63-45A1-8F0F-16A7E7FC023E} = {BCB8A692-DF88-4E50-91A7-AD91E466559C}
{D0781AC3-5827-4DAE-BBAD-481634FF3C0F} = {BCB8A692-DF88-4E50-91A7-AD91E466559C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {45FD7647-C5AB-4CE1-A93C-59A73FDD2196}
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ services:
context: .
dockerfile: /src/Logitar.Identity.Demo/Dockerfile
image: identity_demo
container_name: Logitar.Identity.Demo
container_name: Logitar.Identity_demo
depends_on:
- identity_mssql
restart: unless-stopped
Expand Down
4 changes: 3 additions & 1 deletion src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,18 @@
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="libphonenumber-csharp" Version="8.13.27" />
<PackageReference Include="Logitar.EventSourcing" Version="5.0.1" />
<PackageReference Include="Logitar.Security" Version="6.0.1" />
<PackageReference Include="Logitar.Security" Version="6.0.3" />
<PackageReference Include="MediatR.Contracts" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.2.0" />
<PackageReference Include="NodaTime" Version="3.1.10" />
</ItemGroup>

<ItemGroup>
<Using Include="System.Collections.Immutable" />
<Using Include="System.Globalization" />
<Using Include="System.Security.Claims" />
<Using Include="System.Text" />
<Using Include="System.Text.Json.Serialization" />
</ItemGroup>
Expand Down
40 changes: 40 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/CreateTokenOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.IdentityModel.Tokens;

namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Represents token creation options.
/// </summary>
public record CreateTokenOptions
{
/// <summary>
/// Gets or sets the token type. This defaults to 'JWT'.
/// </summary>
public string Type { get; set; } = "JWT";
/// <summary>
/// Gets or sets the signing algorithm. This defaults to 'HS256'.
/// </summary>
public string SigningAlgorithm { get; set; } = SecurityAlgorithms.HmacSha256;

/// <summary>
/// Gets or sets the token audience.
/// </summary>
public string? Audience { get; set; }
/// <summary>
/// Gets or sets the token issuer.
/// </summary>
public string? Issuer { get; set; }

/// <summary>
/// Gets or sets the token expiration date and time. Unspecified date time kinds will be treated as UTC.
/// </summary>
public DateTime? Expires { get; set; }
/// <summary>
/// Gets or sets the date and time when the token was issued. Unspecified date time kinds will be treated as UTC.
/// </summary>
public DateTime? IssuedAt { get; set; }
/// <summary>
/// Gets or sets the date and time from when the token is valid. Unspecified date time kinds will be treated as UTC.
/// </summary>
public DateTime? NotBefore { get; set; }
}
54 changes: 54 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/CreateTokenParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Represents token creation parameters.
/// </summary>
public record CreateTokenParameters : CreateTokenOptions
{
/// <summary>
/// Gets or sets the token subject.
/// </summary>
public ClaimsIdentity Subject { get; set; }
/// <summary>
/// Gets or sets the signing secret.
/// </summary>
public string Secret { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="CreateTokenParameters"/> class.
/// </summary>
public CreateTokenParameters() : this(new(), string.Empty)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CreateTokenParameters"/> class.
/// </summary>
/// <param name="subject">The token subject.</param>
/// <param name="secret">The signing secret.</param>
public CreateTokenParameters(ClaimsIdentity subject, string secret)
{
Subject = subject;
Secret = secret;
}

/// <summary>
/// Initializes a new instance of the <see cref="CreateTokenParameters"/> class.
/// </summary>
/// <param name="subject">The token subject.</param>
/// <param name="secret">The signing secret.</param>
/// <param name="options">The token creation options.</param>
public CreateTokenParameters(ClaimsIdentity subject, string secret, CreateTokenOptions? options) : this(subject, secret)
{
if (options != null)
{
Type = options.Type;
SigningAlgorithm = options.SigningAlgorithm;
Audience = options.Audience;
Issuer = options.Issuer;
Expires = options.Expires;
IssuedAt = options.IssuedAt;
NotBefore = options.NotBefore;
}
}
}
29 changes: 29 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/CreatedToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.IdentityModel.Tokens;

namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Represents a created token.
/// </summary>
public record CreatedToken
{
/// <summary>
/// Gets the created security token.
/// </summary>
public SecurityToken SecurityToken { get; }
/// <summary>
/// Gets a string representation of the created token.
/// </summary>
public string TokenString { get; }

/// <summary>
/// Initializes a new instance of the <see cref="CreatedToken"/> class.
/// </summary>
/// <param name="securityToken">The created security token.</param>
/// <param name="tokenString">A string representation of the created token.</param>
public CreatedToken(SecurityToken securityToken, string tokenString)
{
SecurityToken = securityToken;
TokenString = tokenString;
}
}
36 changes: 36 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/ITokenBlacklist.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Defines a token blacklist.
/// </summary>
public interface ITokenBlacklist
{
/// <summary>
/// Blacklists the specified list of token identifiers.
/// </summary>
/// <param name="tokenIds">The token identifiers to blacklist.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task BlacklistAsync(IEnumerable<string> tokenIds, CancellationToken cancellationToken = default);
/// <summary>
/// Blacklists the specified list of token identifiers.
/// </summary>
/// <param name="tokenIds">The token identifiers to blacklist.</param>
/// <param name="expiresOn">The expiration date and time of the token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task BlacklistAsync(IEnumerable<string> tokenIds, DateTime? expiresOn, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the blacklisted token identifiers from the specified list of token identifiers.
/// </summary>
/// <param name="tokenIds">The list of token identifiers.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The list of blacklisted token identifiers.</returns>
Task<IEnumerable<string>> GetBlacklistedAsync(IEnumerable<string> tokenIds, CancellationToken cancellationToken = default);
/// <summary>
/// Removes expired token identifiers from the blacklist.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task PurgeAsync(CancellationToken cancellationToken = default);
}
57 changes: 57 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/ITokenManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Defines methods to manage tokens.
/// </summary>
public interface ITokenManager
{
/// <summary>
/// Creates a token for the specified subject, using the specified signing secret.
/// </summary>
/// <param name="subject">The subject of the token.</param>
/// <param name="secret">The signing secret.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created token.</returns>
Task<CreatedToken> CreateAsync(ClaimsIdentity subject, string secret, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a token for the specified subject, using the specified signing secret and creation options.
/// </summary>
/// <param name="subject">The subject of the token.</param>
/// <param name="secret">The signing secret.</param>
/// <param name="options">The creation options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created token.</returns>
Task<CreatedToken> CreateAsync(ClaimsIdentity subject, string secret, CreateTokenOptions? options, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a token with the specified parameters.
/// </summary>
/// <param name="parameters">The creation parameters.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created token.</returns>
Task<CreatedToken> CreateAsync(CreateTokenParameters parameters, CancellationToken cancellationToken = default);

/// <summary>
/// Validates a token using the specified signing secret.
/// </summary>
/// <param name="token">The token to validate.</param>
/// <param name="secret">The signing secret.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The validated token.</returns>
Task<ValidatedToken> ValidateAsync(string token, string secret, CancellationToken cancellationToken = default);
/// <summary>
/// Validates a token using the specified signing secret and validation options.
/// </summary>
/// <param name="token">The token to validate.</param>
/// <param name="secret">The signing secret.</param>
/// <param name="options">The validation options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The validated token.</returns>
Task<ValidatedToken> ValidateAsync(string token, string secret, ValidateTokenOptions? options, CancellationToken cancellationToken = default);
/// <summary>
/// Validates a token with the specified parameters.
/// </summary>
/// <param name="parameters">The validation parameters.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The validated token.</returns>
Task<ValidatedToken> ValidateAsync(ValidateTokenParameters parameters, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.IdentityModel.Tokens;

namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// The exception raised when a validated security token is blacklisted.
/// </summary>
public class SecurityTokenBlacklistedException : SecurityTokenValidationException
{
/// <summary>
/// A generic error message for this exception.
/// </summary>
public const string ErrorMessage = "The security token is blacklisted.";

/// <summary>
/// Gets or sets the list of blacklisted token identifiers.
/// </summary>
public IEnumerable<string> BlacklistedIds
{
get => (IEnumerable<string>)Data[nameof(BlacklistedIds)]!;
private set => Data[nameof(BlacklistedIds)] = value;
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenBlacklistedException"/> class.
/// </summary>
/// <param name="blacklistedIds">The list of the blacklisted token identifiers.</param>
public SecurityTokenBlacklistedException(IEnumerable<string> blacklistedIds) : base(BuildMessage(blacklistedIds))
{
BlacklistedIds = blacklistedIds;
}

private static string BuildMessage(IEnumerable<string> blacklistedIds)
{
StringBuilder message = new();

message.AppendLine(ErrorMessage);
message.AppendLine("BlacklistedIds:");
foreach (string blacklistedId in blacklistedIds)
{
message.Append(" - ").Append(blacklistedId).AppendLine();
}

return message.ToString();
}
}
26 changes: 26 additions & 0 deletions src/Logitar.Identity.Domain/Tokens/ValidateTokenOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Logitar.Identity.Domain.Tokens;

/// <summary>
/// Represents token validation options.
/// </summary>
public record ValidateTokenOptions
{
/// <summary>
/// Gets or sets the list of valid token types.
/// </summary>
public List<string> ValidTypes { get; set; } = [];

/// <summary>
/// Gets or sets the list of valid audiences.
/// </summary>
public List<string> ValidAudiences { get; set; } = [];
/// <summary>
/// Gets or sets the list of valid audiences.
/// </summary>
public List<string> ValidIssuers { get; set; } = [];

/// <summary>
/// Gets or sets a value indicating whether or not to blacklist the token identifiers if the token is valid. This should be set to true for one-time use tokens.
/// </summary>
public bool Consume { get; set; }
}
Loading

0 comments on commit 04206b3

Please sign in to comment.