Astrolabe.LocalUsers 5.0.1

dotnet add package Astrolabe.LocalUsers --version 5.0.1
                    
NuGet\Install-Package Astrolabe.LocalUsers -Version 5.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Astrolabe.LocalUsers" Version="5.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Astrolabe.LocalUsers" Version="5.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Astrolabe.LocalUsers" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Astrolabe.LocalUsers --version 5.0.1
                    
#r "nuget: Astrolabe.LocalUsers, 5.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Astrolabe.LocalUsers@5.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Astrolabe.LocalUsers&version=5.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Astrolabe.LocalUsers&version=5.0.1
                    
Install as a Cake Tool

Astrolabe.LocalUsers

NuGet License: MIT

A comprehensive library for implementing local user management in .NET 8+ applications using Minimal APIs. Part of the Astrolabe Apps library stack.

Overview

Astrolabe.LocalUsers provides abstractions and base classes for implementing robust user management, including:

  • Account creation with email verification
  • Secure password authentication
  • Multi-factor authentication (MFA) via SMS/phone
  • Password reset functionality
  • User profile management (email and MFA number changes)

Installation

dotnet add package Astrolabe.LocalUsers

Features

User Management

  • Account Creation: Create new user accounts with email verification
  • Authentication: Authenticate users with username/password
  • Multi-Factor Authentication: MFA support using verification codes
  • Password Management: Change and reset password flows
  • Profile Management: Update email and MFA phone number

Security Features

  • Password Hashing: Secure password storage with salted SHA-256 hashing (customizable)
  • Validation: Comprehensive validation for user inputs using FluentValidation
  • MFA Support: Two-factor authentication with verification codes

Getting Started

Step 1: Define Your User Model

Create a class that implements ICreateNewUser:

public class CreateUserRequest : ICreateNewUser
{
    public string Email { get; set; }
    public string Password { get; set; }
    public string Confirm { get; set; }

    // Additional properties as needed
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MobileNumber { get; set; }
}

Step 2: Implement a User Service

Extend the AbstractLocalUserService<TNewUser, TUserId> class:

public class MyUserService : AbstractLocalUserService<CreateUserRequest, Guid>
{
    private readonly AppDbContext _context;
    private readonly IEmailService _emailService;

    public MyUserService(
        IPasswordHasher passwordHasher,
        AppDbContext context,
        IEmailService emailService,
        LocalUserMessages? messages = null)
        : base(passwordHasher, messages)
    {
        _context = context;
        _emailService = emailService;
    }

    // Implement abstract methods (see implementation section below)
}

Step 3: Implement a User ID Provider

Create a class that extracts the user ID from the HTTP context:

public class ClaimsUserIdProvider : ILocalUserIdProvider<Guid>
{
    public Guid GetUserId(HttpContext context)
    {
        var claim = context.User.FindFirst(ClaimTypes.NameIdentifier);
        if (claim == null)
            throw new UnauthorizedException("User not authenticated");
        return Guid.Parse(claim.Value);
    }
}

Step 4: Create Your Endpoints Class

Extend the LocalUserEndpoints<TNewUser, TUserId> class:

public class MyUserEndpoints : LocalUserEndpoints<CreateUserRequest, Guid>
{
    public MyUserEndpoints(
        ILocalUserIdProvider<Guid> userIdProvider,
        LocalUserEndpointOptions? options = null)
        : base(userIdProvider, options) { }
}

Step 5: Register Services in Program.cs

// Register the password hasher
builder.Services.AddSingleton<IPasswordHasher, SaltedSha256PasswordHasher>();

// Register your user service
builder.Services.AddScoped<ILocalUserService<CreateUserRequest, Guid>, MyUserService>();

// Register the user ID provider
builder.Services.AddScoped<ILocalUserIdProvider<Guid>, ClaimsUserIdProvider>();

// Register the endpoints
builder.Services.AddLocalUserEndpoints<MyUserEndpoints, CreateUserRequest, Guid>();

Step 6: Map the Endpoints

// Map endpoints with a route prefix
app.MapLocalUserEndpoints<MyUserEndpoints, CreateUserRequest, Guid>("api/auth");

Or with additional configuration:

app.MapGroup("api/auth")
   .MapLocalUserEndpoints<MyUserEndpoints, CreateUserRequest, Guid>()
   .WithTags("Authentication");

Implementation Guide

Implementing AbstractLocalUserService

You must implement these abstract methods:

// Send verification email with code
protected abstract Task SendVerificationEmail(TNewUser newUser, string verificationCode);

// Create unverified account in your database
protected abstract Task CreateUnverifiedAccount(TNewUser newUser, string hashedPassword, string verificationCode);

// Count existing users with the same email
protected abstract Task<int> CountExistingForEmail(string email);

// Verify email with code and return JWT token (or MFA token if MFA required)
protected abstract Task<string?> VerifyAccountCode(string code);

// MFA verification for account creation
protected abstract Task<string?> VerifyAccountWithMfaForUserId(MfaAuthenticateRequest mfaAuthenticateRequest);

// Authenticate with username/password and return JWT token (or MFA token)
protected abstract Task<string?> AuthenticatedHashed(AuthenticateRequest authenticateRequest, string hashedPassword);

// Send an MFA code via SMS/phone
protected abstract Task<bool> SendCode(MfaCodeRequest mfaCodeRequest);
protected abstract Task<bool> SendCode(TUserId userId, string? number = null);

// Verify MFA code and return JWT token
protected abstract Task<string?> VerifyMfaCode(string token, string code, string? number);
protected abstract Task<bool> VerifyMfaCode(TUserId userId, string code, string? number);

// Set reset code for password reset
protected abstract Task SetResetCodeAndEmail(string email, string resetCode);

// Change email for a user
protected abstract Task<bool> EmailChangeForUserId(TUserId userId, string hashedPassword, string newEmail);

// Change password
protected abstract Task<(bool, Func<string, Task<string>>?)> PasswordChangeForUserId(TUserId userId, string hashedPassword);
protected abstract Task<Func<string, Task>?> PasswordResetForResetCode(string resetCode);

// Change MFA phone number
protected abstract Task<bool> ChangeMfaNumberForUserId(TUserId userId, string hashedPassword, string newNumber);

Customization

Disabling Specific Endpoints

Use LocalUserEndpointOptions to disable endpoints you don't need:

builder.Services.AddLocalUserEndpoints<MyUserEndpoints, CreateUserRequest, Guid>(options =>
{
    options.EnableSendMfaCodeToNumber = false;
    options.EnableInitiateMfaNumberChange = false;
    options.EnableCompleteMfaNumberChange = false;
});

Customizing Endpoint Routes

Override the mapping methods in your endpoints class:

public class MyUserEndpoints : LocalUserEndpoints<CreateUserRequest, Guid>
{
    // Change the route path
    protected override RouteHandlerBuilder MapCreateAccount(RouteGroupBuilder group) =>
        group
            .MapPost("register", (CreateUserRequest newUser, HttpContext context) => HandleCreateAccount(newUser, context))
            .WithName("RegisterUser");

    // Add custom authorization policy
    protected override RouteHandlerBuilder MapChangePassword(RouteGroupBuilder group) =>
        base.MapChangePassword(group)
            .RequireAuthorization("MustBeVerified");
}

Customizing Handler Logic

Override handler methods to add custom logic:

public class MyUserEndpoints : LocalUserEndpoints<CreateUserRequest, Guid>
{
    private readonly ILogger<MyUserEndpoints> _logger;

    public MyUserEndpoints(
        ILocalUserIdProvider<Guid> userIdProvider,
        ILogger<MyUserEndpoints> logger,
        LocalUserEndpointOptions? options = null)
        : base(userIdProvider, options)
    {
        _logger = logger;
    }

    protected override async Task HandleCreateAccount(CreateUserRequest newUser, HttpContext context)
    {
        _logger.LogInformation("Creating account for {Email}", newUser.Email);
        await base.HandleCreateAccount(newUser, context);
        _logger.LogInformation("Account created successfully for {Email}", newUser.Email);
    }
}

Custom Password Requirements

Override the ApplyPasswordRules method in your service implementation:

protected override void ApplyPasswordRules<T>(AbstractValidator<T> validator)
    where T : IPasswordHolder
{
    validator.RuleFor(x => x.Password)
        .MinimumLength(10)
        .WithMessage("Password must be at least 10 characters")
        .Matches(@"[A-Z]+")
        .WithMessage("Password must contain an uppercase letter")
        .Matches(@"[0-9]+")
        .WithMessage("Password must contain a number");
}

Custom Error Messages

Provide a custom LocalUserMessages instance:

var messages = new LocalUserMessages
{
    AccountExists = "This email is already registered",
    PasswordMismatch = "Passwords do not match",
    PasswordWrong = "The password you entered is incorrect",
    EmailInvalid = "Please enter a valid email address"
};

builder.Services.AddSingleton(messages);

Custom Email Validation Rules

Override the ApplyCreationRules method:

protected override Task ApplyCreationRules(CreateUserRequest user, AbstractValidator<CreateUserRequest> validator)
{
    validator.RuleFor(x => x.Email)
        .Must(x => x.EndsWith("@mycompany.com"))
        .WithMessage("Only company email addresses are allowed");

    return Task.CompletedTask;
}

Secure Password Storage

The library includes a SaltedSha256PasswordHasher implementation, but you can create your own by implementing the IPasswordHasher interface:

public class BcryptPasswordHasher : IPasswordHasher
{
    public string Hash(string password)
    {
        return BCrypt.Net.BCrypt.HashPassword(password);
    }
}

API Endpoints

The endpoints are organized by resource with sensible OpenAPI operation IDs:

Account Management (account/*)

Method Path Operation ID Auth Required Description
POST /account CreateAccount No Create a new account
POST /account/verify VerifyAccount No Verify account with email code
POST /account/verify/mfa VerifyAccountWithMfa No Complete verification with MFA
POST /account/email ChangeEmail Yes Change email address
POST /account/password ChangePassword Yes Change password
POST /account/mfa-number InitiateMfaNumberChange No Start MFA number change
POST /account/mfa-number/complete CompleteMfaNumberChange Yes Complete MFA number change
POST /account/mfa-number/send-code SendMfaCodeToNumber No Send MFA code to number

Authentication (auth/*)

Method Path Operation ID Auth Required Description
POST /auth Authenticate No Authenticate with credentials
POST /auth/mfa/send SendAuthenticationMfaCode No Send MFA code for auth
POST /auth/mfa/complete CompleteAuthentication No Complete MFA authentication

Password Reset (password/*)

Method Path Operation ID Auth Required Description
POST /password/forgot ForgotPassword No Initiate password reset
POST /password/reset ResetPassword No Reset password with code

Migration Guide (v4.x to v5.x)

This version introduces breaking changes to improve API clarity and migrate from MVC controllers to Minimal APIs.

Breaking Changes

1. AbstractLocalUserController Removed

The AbstractLocalUserController<TNewUser, TUserId> class has been removed. Replace it with the new LocalUserEndpoints<TNewUser, TUserId> class and Minimal APIs approach.

Before (v4.x):

[ApiController]
[Route("api/users")]
public class UserController : AbstractLocalUserController<NewUser, Guid>
{
    public UserController(ILocalUserService<NewUser, Guid> userService)
        : base(userService) { }

    protected override Guid GetUserId() =>
        Guid.Parse(User.FindFirst("userId")?.Value ?? "");
}

After (v4.x):

// 1. Create a user ID provider
public class ClaimsUserIdProvider : ILocalUserIdProvider<Guid>
{
    public Guid GetUserId(HttpContext context) =>
        Guid.Parse(context.User.FindFirst("userId")?.Value ?? "");
}

// 2. Create an endpoints class
public class UserEndpoints : LocalUserEndpoints<NewUser, Guid>
{
    public UserEndpoints(
        ILocalUserIdProvider<Guid> userIdProvider,
        LocalUserEndpointOptions? options = null)
        : base(userIdProvider, options) { }
}

// 3. Register in Program.cs
builder.Services.AddScoped<ILocalUserIdProvider<Guid>, ClaimsUserIdProvider>();
builder.Services.AddLocalUserEndpoints<UserEndpoints, NewUser, Guid>();

// 4. Map endpoints
app.MapLocalUserEndpoints<UserEndpoints, NewUser, Guid>("api/users");
2. ILocalUserService Method Renames

The following methods have been renamed for clarity:

Old Name (v4.x) New Name (v5.x) Description
MfaVerifyAccount VerifyAccountWithMfa Complete account verification with MFA
SendMfaCode(MfaCodeRequest) SendAuthenticationMfaCode Send MFA code during authentication
MfaAuthenticate CompleteAuthentication Complete MFA authentication flow
ChangeMfaNumber InitiateMfaNumberChange Start MFA number change process
MfaChangeMfaNumber CompleteMfaNumberChange Complete MFA number change with code
SendMfaCode(string, Func) SendMfaCodeToNumber Send MFA code to specific number
3. AbstractLocalUserService Abstract Method Rename

If you extend AbstractLocalUserService, rename this method:

Old Name (v4.x) New Name (v5.x)
MfaVerifyAccountForUserId VerifyAccountWithMfaForUserId

Before:

protected override Task<string?> MfaVerifyAccountForUserId(
    MfaAuthenticateRequest mfaAuthenticateRequest)
{
    // ...
}

After:

protected override Task<string?> VerifyAccountWithMfaForUserId(
    MfaAuthenticateRequest mfaAuthenticateRequest)
{
    // ...
}
4. API Route Changes

The default API routes have changed to be more RESTful:

Old Route (v3.x) New Route (v4.x)
/create /account
/verify /account/verify
/mfaVerify /account/verify/mfa
/authenticate /auth
/mfaCode/authenticate /auth/mfa/send
/mfaAuthenticate /auth/mfa/complete
/forgotPassword /password/forgot
/resetPassword /password/reset
/changeEmail /account/email
/changePassword /account/password
/changeMfaNumber /account/mfa-number
/mfaChangeMfaNumber /account/mfa-number/complete
/mfaCode/number /account/mfa-number/send-code

If you need to maintain backwards compatibility with existing clients, override the mapping methods to use the old routes:

public class UserEndpoints : LocalUserEndpoints<NewUser, Guid>
{
    protected override RouteHandlerBuilder MapCreateAccount(RouteGroupBuilder group) =>
        group.MapPost("create", (NewUser newUser, HttpContext context) => HandleCreateAccount(newUser, context))
            .WithName("CreateAccount");

    // Override other methods as needed...
}
5. Target Framework

The library now targets .NET 8.0 (previously .NET 7.0).

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.1 38 1/29/2026
5.0.0 35 1/29/2026
4.0.0 509 5/29/2025
3.0.0 279 10/7/2024
2.0.0 260 3/21/2024
1.0.0 253 1/28/2024