astrolabe-local-users
.NET library for local user authentication with email verification, MFA, password resets, and account management. Use when implementing username/password authentication instead of external OAuth providers.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Astrolabe.LocalUsers - User Management Library
Overview
Astrolabe.LocalUsers provides comprehensive abstractions and base classes for implementing robust local user management in .NET applications. It handles account creation, authentication, multi-factor authentication (MFA), password resets, and profile management.
When to use: Use this library when you need local user authentication (username/password) with email verification, MFA support, and password management features. Not needed if using external authentication providers (OAuth, Azure AD, etc.).
Package: Astrolabe.LocalUsers
Dependencies: FluentValidation, Astrolabe.Common
TypeScript Client: @astroapps/client-localusers
Target Framework: .NET 7-8
Key Concepts
1. User Service Abstraction
AbstractLocalUserService<TNewUser, TUserId> provides the core logic for user management operations. You implement abstract methods to integrate with your database and email service.
2. Controller Abstraction
AbstractLocalUserController<TNewUser, TUserId> provides standard REST API endpoints for user operations. Extend it and implement GetUserId() to extract the current user from claims.
3. Password Security
Built-in IPasswordHasher interface with SaltedSha256PasswordHasher implementation. Can be replaced with bcrypt or other hashing algorithms.
4. Multi-Factor Authentication
Optional MFA support via SMS/phone verification codes, integrated into the authentication flow.
5. Email Verification
New accounts start unverified and require email confirmation before full access.
Common Patterns
Basic Setup - User Model
using Astrolabe.LocalUsers;
// 1. Define your new user model
public class NewUser : ICreateNewUser
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Confirm { get; set; } = string.Empty;
// Additional fields as needed
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
// 2. Define your user entity (database model)
public class User
{
public Guid Id { get; set; }
public string Email { get; set; } = string.Empty;
public string HashedPassword { get; set; } = string.Empty;
public string? VerificationCode { get; set; }
public bool EmailVerified { get; set; }
public string? MfaNumber { get; set; }
public string? MfaCode { get; set; }
public DateTime? MfaCodeExpiry { get; set; }
public string? ResetCode { get; set; }
public DateTime? ResetCodeExpiry { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
Implementing the User Service
using Astrolabe.LocalUsers;
using Astrolabe.Email;
using Microsoft.EntityFrameworkCore;
public class UserService : AbstractLocalUserService<NewUser, Guid>
{
private readonly AppDbContext _context;
private readonly IEmailService _emailService;
private readonly TokenGenerator _tokenGenerator;
public UserService(
AppDbContext context,
IEmailService emailService,
TokenGenerator tokenGenerator,
IPasswordHasher passwordHasher,
LocalUserMessages? messages = null)
: base(passwordHasher, messages)
{
_context = context;
_emailService = emailService;
_tokenGenerator = tokenGenerator;
}
// Send verification email with code
protected override async Task SendVerificationEmail(NewUser newUser, string verificationCode)
{
await _emailService.SendEmail(new EmailMessage
{
To = newUser.Email,
Subject = "Verify your email",
Body = $"Your verification code is: {verificationCode}"
});
}
// Create unverified account
protected override async Task CreateUnverifiedAccount(
NewUser newUser,
string hashedPassword,
string verificationCode)
{
var user = new User
{
Id = Guid.NewGuid(),
Email = newUser.Email,
FirstName = newUser.FirstName,
LastName = newUser.LastName,
HashedPassword = hashedPassword,
VerificationCode = verificationCode,
EmailVerified = false,
CreatedAt = DateTime.UtcNow
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
}
// Check if email already exists
protected override async Task<int> CountExistingForEmail(string email)
{
return await _context.Users.CountAsync(u => u.Email == email);
}
// Verify email with code and return JWT token
protected override async Task<string?> VerifyAccountCode(string code)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.VerificationCode == code);
if (user == null) return null;
user.EmailVerified = true;
user.VerificationCode = null;
await _context.SaveChangesAsync();
return GenerateToken(user);
}
// Authenticate with username/password
protected override async Task<string?> AuthenticatedHashed(
AuthenticateRequest request,
string hashedPassword)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Email == request.Username && u.HashedPassword == hashedPassword);
if (user == null || !user.EmailVerified) return null;
return GenerateToken(user);
}
// Helper method
private string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
new Claim("firstName", user.FirstName),
new Claim("lastName", user.LastName)
};
return _tokenGenerator(claims, 3600); // 1 hour
}
}
Implementing the Controller
using Astrolabe.LocalUsers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[ApiController]
[Route("api/users")]
public class UserController : AbstractLocalUserController<NewUser, Guid>
{
public UserController(ILocalUserService<NewUser, Guid> userService)
: base(userService)
{
}
protected override Guid GetUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Guid.Parse(userIdClaim ?? throw new UnauthorizedAccessException());
}
// Optional: Add custom endpoints
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = GetUserId();
return Ok(new { UserId = userId });
}
}
API Endpoints
The controller provides these standard endpoints:
| Endpoint | Method | Description | Auth Required |
|---|---|---|---|
/create |
POST | Create new account | No |
/verify |
POST | Verify email with code | No |
/authenticate |
POST | Login with username/password | No |
/forgotPassword |
POST | Initiate password reset | No |
/changePassword |
POST | Change password (authenticated) | Yes |
/resetPassword |
POST | Reset password with code | No |
Best Practices
1. Use Strong Password Hashing
// ✅ DO - Use bcrypt or similar
builder.Services.AddSingleton<IPasswordHasher, BcryptPasswordHasher>();
// ⚠️ CAUTION - SaltedSha256 is provided but bcrypt is stronger
builder.Services.AddSingleton<IPasswordHasher, SaltedSha256PasswordHasher>();
2. Set Reasonable Code Expiration Times
// ✅ DO - Short expiration for codes
user.MfaCodeExpiry = DateTime.UtcNow.AddMinutes(10); // 10 minutes
user.ResetCodeExpiry = DateTime.UtcNow.AddHours(24); // 24 hours
// ❌ DON'T - Codes that never expire or last too long
user.MfaCodeExpiry = DateTime.UtcNow.AddDays(365); // Way too long!
Troubleshooting
Common Issues
Issue: Users can't verify their email
- Cause: Verification code not being saved or email not sent
-
Solution: Check
CreateUnverifiedAccountsaves the code andSendVerificationEmailis working
Issue: JWT token null after authentication
- Cause: Token generator not configured or claims incorrect
-
Solution: Verify
TokenGeneratoris registered in DI and check claim generation
Issue: Password reset not working
- Cause: Reset code not saved or expired
-
Solution: Check
SetResetCodeAndEmailsaves code with appropriate expiry
Project Structure Location
-
Path:
Astrolabe.LocalUsers/ -
Project File:
Astrolabe.LocalUsers.csproj -
Namespace:
Astrolabe.LocalUsers
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!