Skip to content

Commit

Permalink
Implemented One-Time Passwords. (#40)
Browse files Browse the repository at this point in the history
* Implementing One-Time Passwords.

* Created a repository interface and added the TenantId property.

* Implemented OTP persistence and unit tests.

* Fixed typos.

* Implemented unit tests.

* Implemented unit tests.

* Refactored demo project.

* Refactored password management.

* NuGet Upgrade.

* Completed password management.

* Fixed namespaces.

* Removed the PasswordMock.
  • Loading branch information
Utar94 authored Jan 22, 2024
1 parent 04206b3 commit 95787ab
Show file tree
Hide file tree
Showing 64 changed files with 3,242 additions and 111 deletions.
7 changes: 7 additions & 0 deletions src/Logitar.Identity.Demo/Constants/Api.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Logitar.Identity.Demo.Constants;

internal static class Api
{
public const string Title = "Identity API";
public static readonly Version Version = new(1, 0, 0);
}
13 changes: 13 additions & 0 deletions src/Logitar.Identity.Demo/Controllers/IndexController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Logitar.Identity.Demo.Models.Index;
using Microsoft.AspNetCore.Mvc;

namespace Logitar.Identity.Demo.Controllers;

[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[Route("")]
public class IndexController : ControllerBase
{
[HttpGet]
public ActionResult<ApiVersion> Get() => Ok(new ApiVersion());
}
23 changes: 23 additions & 0 deletions src/Logitar.Identity.Demo/Models/Index/ApiVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Logitar.Identity.Demo.Constants;

namespace Logitar.Identity.Demo.Models.Index;

public record ApiVersion
{
public string Title { get; set; }
public string Version { get; set; }

public ApiVersion() : this(Api.Title, Api.Version)
{
}

public ApiVersion(string title, Version version) : this(title, version.ToString())
{
}

public ApiVersion(string title, string version)
{
Title = title;
Version = version;
}
}
8 changes: 4 additions & 4 deletions src/Logitar.Identity.Demo/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
Expand All @@ -13,7 +13,7 @@
"https": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
Expand All @@ -23,15 +23,15 @@
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "8081",
"ASPNETCORE_HTTP_PORTS": "8080"
Expand Down
1 change: 0 additions & 1 deletion src/Logitar.Identity.Demo/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"EnableMigrations": true,
"EnableOpenApi": true,
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
2 changes: 1 addition & 1 deletion src/Logitar.Identity.Domain/ApiKeys/ApiKeyAggregate.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using FluentValidation;
using Logitar.EventSourcing;
using Logitar.Identity.Domain.ApiKeys.Events;
using Logitar.Identity.Domain.ApiKeys.Validators;
using Logitar.Identity.Domain.Passwords;
using Logitar.Identity.Domain.Roles;
using Logitar.Identity.Domain.Shared;
using Logitar.Identity.Domain.Shared.Validators;

namespace Logitar.Identity.Domain.ApiKeys;

Expand Down
2 changes: 1 addition & 1 deletion src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<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.3" />
<PackageReference Include="Logitar.Security" Version="6.1.0" />
<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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Logitar.EventSourcing;
using Logitar.Identity.Domain.Shared;
using MediatR;

namespace Logitar.Identity.Domain.Passwords.Events;

/// <summary>
/// The event raised when a new One-Time Password (OTP) is created.
/// </summary>
public record OneTimePasswordCreatedEvent : DomainEvent, INotification
{
/// <summary>
/// Gets the tenant identifier of the One-Time Password (OTP).
/// </summary>
public TenantId? TenantId { get; }

/// <summary>
/// Gets the encoded value of the One-Time Password (OTP).
/// </summary>
public Password Password { get; }

/// <summary>
/// Gets the expiration date and time of the One-Time Password (OTP).
/// </summary>
public DateTime? ExpiresOn { get; }
/// <summary>
/// Gets the maximum number of attempts of the One-Time Password (OTP).
/// </summary>
public int? MaximumAttempts { get; }

/// <summary>
/// Initializes a new instance of the <see cref="OneTimePasswordCreatedEvent"/> class.
/// </summary>
/// <param name="actorId">The actor identifier.</param>
/// <param name="expiresOn">The expiration date and time of the One-Time Password (OTP).</param>
/// <param name="maximumAttempts">The maximum number of attempts of the One-Time Password (OTP).</param>
/// <param name="password">The encoded value of the One-Time Password (OTP).</param>
/// <param name="tenantId">The tenant identifier of the One-Time Password (OTP).</param>
public OneTimePasswordCreatedEvent(ActorId actorId, DateTime? expiresOn, int? maximumAttempts, Password password, TenantId? tenantId)
{
ActorId = actorId;
ExpiresOn = expiresOn;
MaximumAttempts = maximumAttempts;
Password = password;
TenantId = tenantId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Domain.Passwords.Events;

/// <summary>
/// The event raised when a One-Time Password (OTP) is deleted.
/// </summary>
public record OneTimePasswordDeletedEvent : DomainEvent, INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="OneTimePasswordDeletedEvent"/> class.
/// </summary>
/// <param name="actorId">The actor identifier.</param>
public OneTimePasswordDeletedEvent(ActorId actorId)
{
ActorId = actorId;
IsDeleted = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Domain.Passwords.Events;

/// <summary>
/// The event raised when an existing One-Time Password (OTP) is modified.
/// </summary>
public record OneTimePasswordUpdatedEvent : DomainEvent, INotification
{
/// <summary>
/// Gets or sets the custom attribute modifications of the One-Time Password (OTP).
/// </summary>
public Dictionary<string, string?> CustomAttributes { get; } = [];

/// <summary>
/// Gets a value indicating whether or not the One-Time Password (OTP) is being modified.
/// </summary>
public bool HasChanges => CustomAttributes.Count > 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Domain.Passwords.Events;

/// <summary>
/// The event raised when a One-Time Password (OTP) validation failed.
/// </summary>
public record OneTimePasswordValidationFailedEvent : DomainEvent, INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="OneTimePasswordValidationFailedEvent"/> class.
/// </summary>
/// <param name="actorId">The actor identifier.</param>
public OneTimePasswordValidationFailedEvent(ActorId actorId)
{
ActorId = actorId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Domain.Passwords.Events;

/// <summary>
/// The event raised when a One-Time Password (OTP) is successfully validated.
/// </summary>
public record OneTimePasswordValidationSucceededEvent : DomainEvent, INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="OneTimePasswordValidationSucceededEvent"/> class.
/// </summary>
/// <param name="actorId">The actor identifier.</param>
public OneTimePasswordValidationSucceededEvent(ActorId actorId)
{
ActorId = actorId;
}
}
103 changes: 103 additions & 0 deletions src/Logitar.Identity.Domain/Passwords/IOneTimePasswordRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Logitar.Identity.Domain.Shared;

namespace Logitar.Identity.Domain.Passwords;

/// <summary>
/// Defines methods to retrieve and store One-Time Passwords (OTP) to an event store.
/// </summary>
public interface IOneTimePasswordRepository
{
/// <summary>
/// Loads a One-Time Password (OTP) by the specified unique identifier.
/// </summary>
/// <param name="id">The unique identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The One-Time Password (OTP), if found.</returns>
Task<OneTimePasswordAggregate?> LoadAsync(OneTimePasswordId id, CancellationToken cancellationToken = default);
/// <summary>
/// Loads a One-Time Password (OTP) by the specified unique identifier.
/// </summary>
/// <param name="id">The unique identifier.</param>
/// <param name="version">The version at which to load the One-Time Password (OTP).</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The One-Time Password (OTP), if found.</returns>
Task<OneTimePasswordAggregate?> LoadAsync(OneTimePasswordId id, long? version, CancellationToken cancellationToken = default);
/// <summary>
/// Loads a One-Time Password (OTP) by the specified unique identifier.
/// </summary>
/// <param name="id">The unique identifier.</param>
/// <param name="includeDeleted">A value indicating whether or not to load the One-Time Password (OTP) if it is deleted.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The One-Time Password (OTP), if found.</returns>
Task<OneTimePasswordAggregate?> LoadAsync(OneTimePasswordId id, bool includeDeleted, CancellationToken cancellationToken = default);
/// <summary>
/// Loads a One-Time Password (OTP) by the specified unique identifier.
/// </summary>
/// <param name="id">The unique identifier.</param>
/// <param name="version">The version at which to load the One-Time Password (OTP).</param>
/// <param name="includeDeleted">A value indicating whether or not to load the One-Time Password (OTP) if it is deleted.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The One-Time Password (OTP), if found.</returns>
Task<OneTimePasswordAggregate?> LoadAsync(OneTimePasswordId id, long? version, bool includeDeleted, CancellationToken cancellationToken = default);

/// <summary>
/// Loads the One-Time Password (OTP)s from the event store.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Loads the One-Time Password (OTP)s from the event store.
/// </summary>
/// <param name="includeDeleted">A value indicating whether or not to load deleted One-Time Passwords (OTP).</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(bool includeDeleted, CancellationToken cancellationToken = default);

/// <summary>
/// Loads the One-Time Password (OTP)s by the specified list of unique identifiers.
/// </summary>
/// <param name="ids">The unique identifiers.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(IEnumerable<OneTimePasswordId> ids, CancellationToken cancellationToken = default);
/// <summary>
/// Loads the One-Time Password (OTP)s by the specified list of unique identifiers.
/// </summary>
/// <param name="ids">The unique identifiers.</param>
/// <param name="includeDeleted">A value indicating whether or not to load deleted One-Time Passwords (OTP).</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(IEnumerable<OneTimePasswordId> ids, bool includeDeleted, CancellationToken cancellationToken = default);

/// <summary>
/// Loads the One-Time Password (OTP)s in the specified tenant.
/// </summary>
/// <param name="tenantId">The identifier of the tenant.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(TenantId? tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// Loads the One-Time Password (OTP)s in the specified tenant.
/// </summary>
/// <param name="tenantId">The identifier of the tenant.</param>
/// <param name="includeDeleted">A value indicating whether or not to load deleted One-Time Passwords (OTP).</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found One-Time Passwords (OTP).</returns>
Task<IEnumerable<OneTimePasswordAggregate>> LoadAsync(TenantId? tenantId, bool includeDeleted, CancellationToken cancellationToken = default);

/// <summary>
/// Saves the specified One-Time Password (OTP) into the store.
/// </summary>
/// <param name="oneTimePassword">The One-Time Password (OTP) to save.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task SaveAsync(OneTimePasswordAggregate oneTimePassword, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the specified One-Time Passwords (OTP) into the store.
/// </summary>
/// <param name="oneTimePasswords">The One-Time Password (OTP)s to save.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task SaveAsync(IEnumerable<OneTimePasswordAggregate> oneTimePasswords, CancellationToken cancellationToken = default);
}
38 changes: 33 additions & 5 deletions src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public interface IPasswordManager
{
/// <summary>
/// Creates a password from the specified string.
/// Creates a password from the specified string. This method should not be used to create strong user passwords.
/// </summary>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
Expand All @@ -18,10 +18,38 @@ public interface IPasswordManager
/// <returns>The password instance.</returns>
Password Decode(string password);
/// <summary>
/// Generates a new password of the specified length.
/// Generates a password from a cryptographically strong random sequence of characters, selecting characters randomly in the following character set:
/// <br />!"#$%&amp;'()*+,-./0123456789:;&lt;=&gt;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
/// </summary>
/// <param name="length">The length of the password, in bytes.</param>
/// <param name="password">The password bytes.</param>
/// <param name="length">The length of the password, in number of characters.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
Password Generate(int length, out string password);
/// <summary>
/// Generates a password from a cryptographically strong random sequence of bytes selected from the specified character set.
/// </summary>
/// <param name="characters">The list of characters to pick from.</param>
/// <param name="length">The length of the password, in number of characters.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
Password Generate(string characters, int length, out string password);
/// <summary>
/// Generates a password from the specified number of bytes, then converts it to Base64 and creates a password from the generated string.
/// </summary>
/// <param name="length">The length of the password, in number of bytes.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
Password GenerateBase64(int length, out string password);
/// <summary>
/// Validates the specified password string.
/// </summary>
/// <param name="password">The password string.</param>
void Validate(string password);
/// <summary>
/// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise.
/// </summary>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
Password Generate(int length, out byte[] password);
/// <exception cref="FluentValidation.ValidationException">The password is too weak.</exception>
Password ValidateAndCreate(string password);
}
Loading

0 comments on commit 95787ab

Please sign in to comment.