Skip to content

DHI.Services for Security — Internal Developer Guide

This guide explains the Security pieces in DHI.Services used across our products: accounts, authentication (with lockout & throttling), refresh tokens, two-factor (OTP), authorization via user-groups, password history / expiry, and the small mail building blocks used for registration and password reset. It follows the same structure as the Documents guide.


What the Security module does

At a glance:

  • User accounts (create/update/remove) with PBKDF2 password hashing (legacy SHA-1 supported for back-compat).
  • Authentication service that validates credentials, throttles by client IP, and locks accounts after repeated failures.
  • Registration & password reset with email templates and activation/reset tokens.
  • Refresh tokens (issue, exchange, revoke) for session management.
  • Two-factor (OTP) framework with pluggable authenticators and CIDR IP whitelisting.
  • Authorization via user-groups (roles are obsolete).
  • Password history & expiry (prevent reuse; configurable policy).
  • Small helpers: claims utilities, mail sender/templates.

Core layers follow the same Repository ↔ Service pattern as other domains.


Key concepts

Accounts & passwords

  • Account (BaseNamedEntity<string>) carries Id, Name, Email, Company, PhoneNumber, flags (Activated, Enabled, AllowMePasswordChange, Locked), lockout fields (NoOfUnsuccessfulLoginAttempts, LastLoginAttemptedDate, LockedDateEnd), plus metadata.
  • Password storage: EncryptedPassword holds either
    • PBKDF2: 36 bytes = 16-byte salt + 20-byte hash (10,000 iterations), or
    • legacy SHA-1: 20-byte hash (supported only to validate existing repos).
  • SetPassword(string) hashes with PBKDF2. Never set EncryptedPassword directly.

Authentication flow (validate + protect)

  • AuthenticationService delegates password checks to an authentication provider (IAuthenticationProvider, implemented by IAccountRepository).
  • On failures it uses progressive delays per client IP (1s → 2s → 4s → …) and updates login attempt counters.
  • Lockout: after MaxNumberOfLoginAttempts within the ResetInterval, account is locked for LockedPeriod. A correct login after lock expiry unlocks.

Tokens

  • Activation/reset tokens live on the Account (Token, TokenExpiration) and are managed by RegistrationService.
  • Refresh tokens (RefreshToken) are separate entities with their own repository/service. Key points:
    • Token id is accountId or accountId-clientIp (if a client IP is included).
    • Each token has a string token, UTC expiration, and IsExpired guard.
    • Exchange replaces a valid token with a new one; revoke by id or by account.

Two-factor (OTP)

  • OtpService hosts one or more IOtpAuthenticators keyed by name (e.g., TotpAuthenticator), and implements:
    • ValidateOtp(otp, twoFactorAuthenticators, key) for verification.
    • GetOtpConfiguration(twoFactorAuthenticators, ipWhitelistMatch) to decide whether OTP is required, supports a special CIDR: rule to whitelist IPs.
    • GenerateOtpAuthenticatorSetupCode(accountName, twoFactorAuthenticators, key) for TOTP provisioning (manual code + QR).

Authorization (user-groups, not roles)

  • Use groups: UserGroup + UserGroupService provide membership and queries.
  • ClaimsPrincipalExtensions:
    • GetUserId() → current subject id.
    • GetPrincipals(){userId + all ClaimTypes.GroupSid values} (handy for permission checks).
  • Permission is a small immutable struct: principals set + operation + Allowed/Denied.

Account.Roles / GetRoles() are obsolete. Use user-groups.

Password history & expiry

  • Policy (PasswordExpirationPolicy): defaults PasswordExpiryDurationInDays = 3, PreviousPasswordsReUseLimit = 3.
  • PasswordHistory stores the encrypted password and its expiry date.
  • PasswordHistoryService:
    • ValidateCurrentPassword(...) also throttles by client IP.
    • AddPasswordHistoryAsync(...) on change; prevents reuse of the last N passwords.
    • Comparison (PasswordHistoryComparer) supports both PBKDF2 and legacy SHA-1.

Core types overview

Entities

  • Account — user profile, status, token fields, password hash.
  • RefreshToken — token string, accountId, expiration, optional clientIp; IsExpired.
  • UserGroup — named group with HashSet<string> Users.
  • Permission / PermissionType — immutable permission tuple.
  • PasswordHistory — accountId, encrypted password, expiry.

Repositories (JSON implementations provided)

  • IAccountRepository : IAuthenticationProvider, IDiscreteRepository<Account,string>, IUpdatableRepository<…>
    • JSON impl: AccountRepository
    • Extra lookups: GetByEmail(), GetByToken()
    • Also exposes lock/unlock/reset helpers used by auth.
  • IRefreshTokenRepository : JSON impl RefreshTokenRepository
    • GetByAccount(), GetByToken()
  • IUserGroupRepository : JSON impl UserGroupRepository
  • IPasswordHistoryRepository : JSON impl PasswordHistoryRepository
    • GetMostRecentByAccountId, GetRecentByAccountId, GetByAccountId
  • IMailTemplateRepository : JSON impl MailTemplateRepository

Services

  • AccountService — CRUD with guards:
    • Add/TryAdd/AddOrUpdate require EncryptedPassword != null, and set Activated = true on creation.
    • Cannot remove admin account.
    • UpdateMe(Account) lets a user change their own contact data and, if allowed, password.
  • RegistrationServiceRegister → email, Activate, ResetPassword → email, UpdatePassword.
    • Uses MailTemplate and an IMailSender (e.g., SmtpMailSender) to send activation/reset emails.
    • Tokens are GUID “N” strings; expiry defaults to 1 day.
    • On password updates it optionally writes password history and enforces reuse policy.
  • AuthenticationServiceValidate(accountId, password, clientIp) with throttling and lockout; also exposes provider discovery helpers.
  • RefreshTokenServiceCreate, Exchange, GetByToken, GetByAccount, RemoveByAccount, ContainsAccount.
  • OtpServiceValidateOtp, GetOtpConfiguration (CIDR/IP whitelist), GenerateOtpAuthenticatorSetupCode.
  • UserGroupServiceAddUser / RemoveUser, membership checks, “all groups for user”.
  • MailTemplateService — trivial wrapper around template repo.

ServiceConnection” types exist for dynamic creation but are marked obsolete; prefer standard DI.


Guarding inputs & obsolete bits

  • Use Guard.Against.* (e.g., NullOrEmpty) at public boundaries.
  • Obsolete (back-compat only):
    • Account.HashPassword (SHA-1) and Account.ValidatePassword — use PBKDF2 via SetPassword() and provider validation.
    • Role APIs: Account.Roles, Account.GetRoles(), and AccountService.GetRoles().
    • AuthenticationServiceConnection, AccountServiceConnection, RefreshTokenServiceConnection (prefer DI).
    • IAuthentication.IAccessTokenProvider.

Typical tasks (with code)

Bootstrap JSON-backed repos/services

var appData = Path.Combine(builder.Environment.ContentRootPath, "App_Data");

// Accounts
var accountsRepo = new AccountRepository(Path.Combine(appData, "accounts.json"));
var accounts = new AccountService(accountsRepo);

// Refresh tokens
var rtRepo = new RefreshTokenRepository(Path.Combine(appData, "refreshTokens.json"));
var refreshTokens = new RefreshTokenService(rtRepo);

// User groups
var groupsRepo = new UserGroupRepository(Path.Combine(appData, "groups.json"));
var groups = new UserGroupService(groupsRepo);

// Password history (optional but recommended)
var pwdHistRepo = new PasswordHistoryRepository(Path.Combine(appData, "passwordHistory.json"));
var pwdHistory = new PasswordHistoryService(pwdHistRepo);

// Mail templates + sender
var mailTemplates = new MailTemplateService(new MailTemplateRepository(Path.Combine(appData, "mailTemplates.json")));
var mailSender = new SmtpMailSender("smtp.yourdomain.local"); // or host,port,user,pass

// Registration flows
var registration = new RegistrationService(
    accountsRepo, mailSender,
    activationEmailTemplate: mailTemplates.Get("activation-template"),
    passwordResetEmailTemplate: mailTemplates.Get("passwordreset-template"));

Create an account (hashing via PBKDF2)

var acc = new Account("alice", "Alice Jensen")
{
    Email = "alice@acme.com",
    Company = "ACME",
};
acc.SetPassword("S7rong-P@ss!");
accounts.Add(acc);
// registration sends an activation mail using template placeholders: {0}=name, {1}=link
registration.Register(acc, activationUri: "https://example.com/activate");

// later, when the user clicks the link:
var ok = registration.Activate(activationToken);

Login with throttling & lockout

var auth = new AuthenticationService(accountsRepo);
var ok = await auth.Validate("alice", "S7rong-P@ss!", clientIp: httpContext.Connection.RemoteIpAddress.ToString());

Password reset (email) → update

registration.ResetPassword("alice", "https://example.com/reset", mailBodyName: "default");
// in reset handler:
var success = registration.UpdatePassword(resetToken, "N3wS7rongP@ss!");

Enforce password history / expiry (optional)

// Validate at login time against expiry:
var historyOk = await pwdHistory.ValidateCurrentPassword("alice", "N3wS7rongP@ss!", DateTime.UtcNow, clientIp);
// Add a record after password change:
await pwdHistory.AddPasswordHistoryAsync(acc, "N3wS7rongP@ss!", DateTime.UtcNow);

Two-factor (OTP): require, validate, provision

// Suppose you have a TOTP authenticator implementing IOtpAuthenticator:
var otp = new OtpService(new Dictionary<string, IOtpAuthenticator> {
  { "Totp", new TotpAuthenticator(/* secrets/config */) }
});

// Account’s 2FA metadata (examples)
var twoFA = new[] {
  "CIDR:10.0.0.0/8",           // if matches request IP, OTP not required
  "Totp:issuer=ACME;digits=6"  // otherwise require TOTP via this provider
};

// At login UI step 1:
var cfg = otp.GetOtpConfiguration(twoFA, ipWhitelistMatch: cidrs => CidrHelper.IsClientWhitelisted(cidrs, clientIp));
if (cfg.OtpRequired) {
  // Ask user for OTP and show available providers in cfg.OtpAuthenticatorIds
}

// Validate OTP:
var ok2 = otp.ValidateOtp(inputOtp, twoFA, otpAuthenticatorKey: "Totp");

// Provision (for UI that sets up TOTP):
var (manual, qr) = otp.GenerateOtpAuthenticatorSetupCode("alice", twoFA, "Totp");

Groups (authorization)

groups.Add(new UserGroup("admins", "Administrators", new HashSet<string>()));
groups.AddUser("admins", "alice");
var isInAny = groups.AnyContainsUser(new[] { "admins", "editors" }, "alice"); // true

Refresh tokens

// Issue
var token = refreshTokens.CreateRefreshToken("alice", TimeSpan.FromDays(7), clientIp);

// Exchange (rotate)
var rotated = refreshTokens.ExchangeRefreshToken(token.Token, "alice", TimeSpan.FromDays(7), clientIp);

// Revoke
var removed = refreshTokens.RemoveByAccount("alice");

Policies & configuration

  • LoginAttemptPolicy (used by AuthenticationService and AccountRepository):
    • MaxNumberOfLoginAttempts — lock when reached within ResetInterval.
    • LockedPeriod — how long the account stays locked.
    • ResetInterval — window for counting attempts; attempts reset when exceeded.
  • PasswordExpirationPolicy:
    • PasswordExpiryDurationInDays (default 3)
    • PreviousPasswordsReUseLimit (default 3)
  • OTP:
    • twoFactorAuthenticators is a string list: each item "type:configuration".
    • Special type CIDR holds IP/net strings; if client matches, OTP may be skipped.
    • OtpService throws clear errors for missing provider or invalid configuration.

Events & interception

All services derive from the standard base services and raise Adding/Added, Updating/Updated, Removing/Removed events with CancelEventArgs<T> on the “before” hooks (you can cancel). FilterService (if you use real-time filters) additionally exposes Adding/Added/Deleting/Deleted transport connection events.


Provider discovery & connections

  • Discovery helpers list compatible implementations from loaded assemblies:
    • AuthenticationService.GetAuthenticationProviderTypes(...)
    • AccountService.GetRepositoryTypes(...)
    • UserGroupService.GetRepositoryTypes(...)
    • RefreshTokenService.GetRepositoryTypes(...)
    • PasswordHistoryService.GetRepositoryTypes(...)
  • ServiceConnection classes exist (Account/Authentication/RefreshToken/PasswordHistory/UserGroup), but are marked obsolete in favor of ASP.NET DI. Prefer normal DI/constructor injection.

Errors & edge cases

  • AccountService.Add* throws if EncryptedPassword is null.
  • Admin account cannot be removed.
  • Back-compat hashing: if an existing EncryptedPassword is 20 bytes, it’s treated as legacy SHA-1 and still validated.
  • Activation / reset fail when token is unknown or expired.
  • OTP: if the OTP provider key is unknown or not allowed by the account’s config, you get an explicit exception.
  • Refresh token exchange throws when the supplied token/id is invalid or expired.
  • UpdateMe respects AllowMePasswordChange; if false, password changes are ignored.
  • Lock/unlock: unlocking occurs automatically on a valid login after LockedDateEnd.

Quick reference

Need… Use… Notes
Create account + secure password Account.SetPassword + AccountService.Add PBKDF2, sets Activated=true on creation
Login with throttling & lockout AuthenticationService.Validate(id, pwd, clientIp) Uses LoginAttemptPolicy; doubles delay on failures
Register & email activation RegistrationService.RegisterActivate(token) Templates via MailTemplate + IMailSender
Password reset via email ResetPassword(id | email, resetUri, body)UpdatePassword(token,pwd) Writes password history if configured
Enforce password expiry/reuse PasswordHistoryService.ValidateCurrentPassword / AddPasswordHistoryAsync Policy defaults: 3 days / last 3
Manage refresh tokens RefreshTokenService.Create/Exchange/RemoveByAccount Scoping by accountId or accountId-clientIp
Two-factor (OTP) OtpService.GetOtpConfiguration / ValidateOtp / GenerateSetupCode Supports CIDR whitelist
Group-based authorization UserGroupService.AddUser/RemoveUser/AnyContainsUser Use ClaimsPrincipalExtensions to extract principals

Minimal wiring (Program.cs)

// 1) App_Data convenience
AppDomain.CurrentDomain.SetData("DataDirectory",
    Path.Combine(builder.Environment.ContentRootPath, "App_Data"));

// 2) Repos & services (see §5.1 for full example)

// 3) ASP.NET DI example
builder.Services.AddSingleton<IAccountRepository>(_ => new AccountRepository("[AppData]accounts.json".Resolve()));
builder.Services.AddSingleton<IRefreshTokenRepository>(_ => new RefreshTokenRepository("[AppData]refreshTokens.json".Resolve()));
builder.Services.AddSingleton<IUserGroupRepository>(_ => new UserGroupRepository("[AppData]groups.json".Resolve()));
builder.Services.AddSingleton<IPasswordHistoryRepository>(_ => new PasswordHistoryRepository("[AppData]passwordHistory.json".Resolve()));
builder.Services.AddSingleton<IMailTemplateRepository>(_ => new MailTemplateRepository("[AppData]mailTemplates.json".Resolve()));
builder.Services.AddSingleton<IMailSender>(_ => new SmtpMailSender("smtp.host.local"));

builder.Services.AddSingleton<AuthenticationService>();
builder.Services.AddSingleton<RefreshTokenService>();
builder.Services.AddSingleton<UserGroupService>();
builder.Services.AddSingleton<PasswordHistoryService>();
builder.Services.AddSingleton<MailTemplateService>();
builder.Services.AddSingleton<RegistrationService>();

Final notes

  • Keep roles out of new code—user-groups replace them.
  • If you migrate old JSON stores, leave legacy hashes in place; the provider will keep validating them, and any new password set will be stored as PBKDF2.
  • When you build UI flows, surface clear messages for lockout and token expiry, and always require client IP for auth throttling.