Sencilla.Component.Users.Auth
10.0.32
dotnet add package Sencilla.Component.Users.Auth --version 10.0.32
NuGet\Install-Package Sencilla.Component.Users.Auth -Version 10.0.32
<PackageReference Include="Sencilla.Component.Users.Auth" Version="10.0.32" />
<PackageVersion Include="Sencilla.Component.Users.Auth" Version="10.0.32" />
<PackageReference Include="Sencilla.Component.Users.Auth" />
paket add Sencilla.Component.Users.Auth --version 10.0.32
#r "nuget: Sencilla.Component.Users.Auth, 10.0.32"
#:package Sencilla.Component.Users.Auth@10.0.32
#addin nuget:?package=Sencilla.Component.Users.Auth&version=10.0.32
#tool nuget:?package=Sencilla.Component.Users.Auth&version=10.0.32
Sencilla.Component.Users.Auth
Authentication component for the Sencilla Framework. It issues JWT access tokens, manages rotating refresh tokens, and handles registration / login on top of the Sencilla.Component.Users identity model.
- Namespace:
Sencilla.Component.Users.Auth - Package:
Sencilla.Component.Users.Auth - Depends on:
Sencilla.Component.Users,Sencilla.Core,System.IdentityModel.Tokens.Jwt
Table of Contents
- Installation
- What the component provides
- Quick start
- Using
IAuthService - API reference
- How it works
- Behavioral notes & limitations
- Security guidance
- See also
Installation
dotnet add package Sencilla.Component.Users.Auth
This package transitively pulls in Sencilla.Component.Users and Sencilla.Core. The assembly is marked with [assembly: AutoDiscovery], so once it is referenced, AddSencilla discovers and registers everything automatically — no manual AddScoped<IAuthService, AuthService>() is required.
Tip — force the assembly to load. If nothing in your app references a type from this package directly, the assembly may not be loaded at startup and auto-discovery will skip it. Call the no-op marker once in
Program.csto guarantee the reference:builder.Services.AddSencillaUsersRegistration();
What the component provides
| Type | Kind | Purpose |
|---|---|---|
IAuthService / AuthService |
Service | Registration, login, and refresh-token rotation. Auto-registered as transient. |
AuthOptions |
Options | JWT signing key, issuer, audience, access-token lifetime. |
RegisterRequest |
DTO | Input for RegisterAsync. |
LoginRequest |
DTO | Input for LoginAsync. |
TokenResponse |
DTO | Output of all three operations (AccessToken, RefreshToken, ExpiresAt). |
UserRefreshToken |
Entity | Persisted refresh token (sec.UserRefreshToken). |
UserRefreshTokenFilter |
Filter | Query refresh tokens by token value. |
PasswordHashExtensions |
Extensions | HashPassword() / VerifyPassword() helpers (ASP.NET Core Identity PasswordHasher). |
The component issues and stores tokens. It does not configure the ASP.NET Core authentication middleware that validates incoming tokens — you wire that up yourself (see step 3) so the access/validation sides share the same key, issuer, and audience.
Quick start
1. Configure AuthOptions
AuthService is constructed with IOptions<AuthOptions>, so you must bind the options. Add a section to appsettings.json:
{
"Auth": {
"SecretKey": "REPLACE-WITH-A-LONG-RANDOM-SECRET-AT-LEAST-32-CHARS",
"Issuer": "https://your-app.example.com",
"Audience": "https://your-app.example.com",
"JwtExpiresMinutes": 60
}
}
Bind it in Program.cs:
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Auth"));
SecretKeyis used withHmacSha256, which requires a key of at least 256 bits (32 bytes). Use a long, random value and keep it out of source control (user-secrets, environment variables, or a secrets vault).
2. Register Sencilla
using Sencilla.Core;
var builder = WebApplication.CreateBuilder(args);
// Bind JWT options
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Auth"));
// Core DI + auto-discovery (registers IAuthService and all repositories)
builder.Services.AddSencilla(builder.Configuration);
// EF Core repositories — discovers User, UserPassword, UserRefreshToken, etc.
builder.Services.AddSencillaRepositoryForEF(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Ensure the Auth assembly is referenced so auto-discovery sees it
builder.Services.AddSencillaUsersRegistration();
AuthService depends on three repositories that the EF integration auto-registers from the entity definitions:
ICreateRepository<User>IUpdateRepository<UserPassword>ICreateRepository<UserRefreshToken>
3. Validate the issued JWTs (JWT Bearer)
To let the tokens this component issues protect your endpoints, configure the bearer middleware with the same secret/issuer/audience:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var auth = builder.Configuration.GetSection("Auth").Get<AuthOptions>()!;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = auth.Issuer,
ValidAudience = auth.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(auth.SecretKey))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Requires the
Microsoft.AspNetCore.Authentication.JwtBearerpackage in the host application.
4. Create the database tables
The component persists refresh tokens to sec.UserRefreshToken. The DDL ships with the package at
Database/Tables/UserRefreshToken.sql:
CREATE TABLE [sec].[UserRefreshToken]
(
[Id] INT IDENTITY NOT NULL,
[UserId] INT NOT NULL,
[Token] NVARCHAR(2000) NOT NULL,
[ExpiresAt] DATETIME2(7) NOT NULL,
[CreatedAt] DATETIME2(7) NOT NULL,
[CreatedByIp] NVARCHAR(45) NULL,
[RevokedAt] DATETIME2(7) NULL,
[RevokedByIp] NVARCHAR(45) NULL,
[ReplacedByToken] NVARCHAR(2000) NULL,
CONSTRAINT [PK_UserRefreshToken] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_UserRefreshToken_UserId] FOREIGN KEY ([UserId]) REFERENCES [sec].[User]([Id])
)
You also need the sec.User and the user-password storage from Sencilla.Component.Users.
Using IAuthService
Inject IAuthService anywhere in the DI container — a controller, a minimal-API handler, or another service.
public class AccountService(IAuthService auth)
{
public Task<TokenResponse> SignUp(string email, string password, long phone) =>
auth.RegisterAsync(new RegisterRequest
{
Email = email,
Password = password,
Phone = phone
});
}
Register
Creates (or upserts) the user, stores the hashed password, assigns the User role, and returns a fresh token pair.
var tokens = await auth.RegisterAsync(new RegisterRequest
{
Email = "jane@example.com",
Password = "S3cur3P@ssw0rd!",
Phone = 15551234567
});
// tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt
Throws Exception("User already registered with that email") if the email already exists.
Login
Validates the password against the stored hash and returns a new token pair.
var tokens = await auth.LoginAsync(new LoginRequest
{
Email = "jane@example.com",
Password = "S3cur3P@ssw0rd!"
});
Throws Exception("No user with that email") or Exception("Invalid password") on failure.
Refresh
Exchanges a valid, non-revoked, non-expired refresh token for a new token pair. The presented token is revoked (rotated) as part of the call.
var tokens = await auth.RefreshTokenAsync(oldRefreshToken);
Throws Exception("Invalid refresh token") if the token is missing, expired, or already revoked; Exception("User no longer exists") if the owning user was deleted.
Wiring up endpoints
Minimal API example:
app.MapPost("/auth/register", async (RegisterRequest req, IAuthService auth, CancellationToken ct) =>
Results.Ok(await auth.RegisterAsync(req, ct)));
app.MapPost("/auth/login", async (LoginRequest req, IAuthService auth, CancellationToken ct) =>
Results.Ok(await auth.LoginAsync(req, ct)));
app.MapPost("/auth/refresh", async (string refreshToken, IAuthService auth, CancellationToken ct) =>
Results.Ok(await auth.RefreshTokenAsync(refreshToken, ct)));
// Protected endpoint — requires a valid access token from step 3
app.MapGet("/me", (ClaimsPrincipal user) =>
Results.Ok(new { Id = user.FindFirstValue(ClaimTypes.NameIdentifier) }))
.RequireAuthorization();
API reference
IAuthService
public interface IAuthService
{
Task<TokenResponse> RegisterAsync(RegisterRequest request, CancellationToken ct = default);
Task<TokenResponse> LoginAsync(LoginRequest request, CancellationToken ct = default);
Task<TokenResponse> RefreshTokenAsync(string refreshToken, CancellationToken ct = default);
}
| Method | Description |
|---|---|
RegisterAsync |
Creates the user + password hash, assigns the User role, returns a token pair. |
LoginAsync |
Verifies credentials and returns a token pair. |
RefreshTokenAsync |
Rotates a refresh token and returns a new token pair. |
AuthOptions
public class AuthOptions
{
public string SecretKey { get; set; } // HMAC-SHA256 signing key (>= 32 bytes)
public string Issuer { get; set; } // "iss" claim
public string Audience { get; set; } // "aud" claim
public int JwtExpiresMinutes { get; set; } = 60; // access-token lifetime
}
| Property | Default | Notes |
|---|---|---|
SecretKey |
— | Symmetric key for HmacSha256. Must be ≥ 256 bits. |
Issuer |
— | Written to the token and validated by the bearer middleware. |
Audience |
— | Written to the token and validated by the bearer middleware. |
JwtExpiresMinutes |
60 |
Access-token lifetime in minutes. The refresh token lifetime is fixed at 7 days (see below). |
Request / response DTOs
public class RegisterRequest
{
public string Email { get; set; }
public string Password { get; set; }
public long Phone { get; set; }
public string FirstName { get; set; } // see Behavioral notes
public string LastName { get; set; } // see Behavioral notes
}
public class LoginRequest
{
public string Email { get; set; }
public string Password { get; set; }
}
public class TokenResponse
{
public string AccessToken { get; set; } // signed JWT
public string RefreshToken { get; set; } // opaque GUID string ("N" format)
public DateTime ExpiresAt { get; set; } // access-token expiry (UTC)
}
UserRefreshToken entity
[Table(nameof(UserRefreshToken), Schema = "sec")]
public class UserRefreshToken : IEntity
{
public int Id { get; set; }
public int UserId { get; set; }
public string Token { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public string? CreatedByIp { get; set; }
public DateTime? RevokedAt { get; set; }
public string? RevokedByIp { get; set; }
public string? ReplacedByToken { get; set; }
}
Query it with UserRefreshTokenFilter:
var token = (await tokenRepo.GetAll(UserRefreshTokenFilter.ByToken(value))).FirstOrDefault();
PasswordHashExtensions
String extension helpers built on ASP.NET Core Identity's PasswordHasher<User>:
string hash = "myPassword".HashPassword(); // hash a plaintext password
bool isValid = storedHash.VerifyPassword("myPassword"); // verify against a stored hash
AuthService uses these internally; you rarely call them directly.
How it works
RegisterAsync / LoginAsync / RefreshTokenAsync
│
▼
GenerateTokenResponse(user)
│
├─ Build claims: NameIdentifier = user.Id, Email = user.Email
├─ Sign JWT: HmacSha256 with AuthOptions.SecretKey
│ iss = Issuer, aud = Audience, exp = now + JwtExpiresMinutes
├─ Create refresh token: GUID ("N"), ExpiresAt = now + 7 days
├─ Persist refresh token (sec.UserRefreshToken)
└─ Return TokenResponse { AccessToken, RefreshToken, ExpiresAt }
- Register — rejects duplicate emails, upserts the
User(matched onEmail), stores the hashed password inUserPassword, assignsRoleType.User, then issues tokens. - Login — loads the password record by email, verifies the hash, loads the user, then issues tokens.
- Refresh — looks up the presented refresh token; rejects it if missing, expired (
ExpiresAt < now), or revoked (RevokedAtset); marks it revoked, then issues a new pair (token rotation).
Behavioral notes & limitations
These reflect the current implementation — keep them in mind when integrating:
- Roles are not yet placed in the JWT. The token carries only
NameIdentifierandEmailclaims. If you authorize by role, add the role claim when issuing or enrich the principal from the database. FirstName/LastNameonRegisterRequestare accepted but not persisted byRegisterAsync— onlyEmailandPhoneare written to theUser. Update the profile separately via the Users component if you need these stored.- Refresh-token lifetime is hard-coded to 7 days and is not configurable through
AuthOptions. RefreshTokenAsyncsetsReplacedByTokento a new random GUID, not to the value of the newly issued refresh token, so the rotation chain is not directly linkable.CreatedByIp/RevokedByIpare never populated by the service; capture and set them yourself if you need IP auditing.- Errors are thrown as plain
Exceptionwith English messages. Catch and translate them into the appropriate HTTP responses (e.g.400/401) at your API boundary.
Security guidance
- Store
SecretKeyin user-secrets, environment variables, or a secrets manager — never commit it. - Use HTTPS so access and refresh tokens are never sent in clear text.
- Treat the refresh token as a credential: store it in an
HttpOnly,Securecookie or secure client storage, and rotate on every use (the component already revokes the old one). - Keep access-token lifetimes short (
JwtExpiresMinutes) and rely on refresh for longevity. - Consider a background job to purge expired/revoked rows from
sec.UserRefreshToken.
See also
- Components overview
- Users component — the
User,UserPassword, and role model this package builds on - Dependency Injection — how auto-discovery registers
IAuthService - Repositories — the repository interfaces
AuthServicedepends on
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Sencilla.Component.Users (>= 10.0.32)
- Sencilla.Core (>= 10.0.32)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
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 |
|---|---|---|
| 10.0.32 | 40 | 6/4/2026 |
| 10.0.31 | 42 | 6/4/2026 |
| 10.0.30 | 46 | 6/4/2026 |
| 10.0.29 | 43 | 6/4/2026 |
| 10.0.28 | 44 | 6/3/2026 |
| 10.0.27 | 38 | 6/3/2026 |
| 10.0.26 | 38 | 6/2/2026 |
| 10.0.19 | 92 | 6/1/2026 |
| 10.0.18 | 94 | 6/1/2026 |
| 10.0.17 | 93 | 5/13/2026 |
| 10.0.16 | 94 | 5/12/2026 |
| 10.0.15 | 94 | 5/12/2026 |
| 10.0.14 | 91 | 5/1/2026 |
| 10.0.13 | 110 | 3/29/2026 |
| 10.0.12 | 105 | 3/2/2026 |
| 10.0.11 | 103 | 2/27/2026 |
| 10.0.10 | 100 | 2/26/2026 |
| 10.0.9 | 102 | 2/26/2026 |
| 10.0.8 | 100 | 2/25/2026 |
| 10.0.7 | 103 | 2/24/2026 |