Security Providers – Guide & Recipes¶
A compact, developer-friendly map to wire Security into your service. Pick a provider, drop in the code, and ship.
See also (deep dives)¶
Which provider should I use?¶
| Provider | Use when | Where data/api lives | Creates storage? | Highlights |
|---|---|---|---|---|
DS AccountRepository (DHI.Services.Provider.DS) |
Your app should call a remote Security Web API (accounts + tokens) | Another DS host exposing /api/accounts & /api/tokens |
N/A | Bearer via IAccessTokenProvider, retries (Polly), login/refresh/CRUD |
PostgreSQL Security repos (DHI.Services.Provider.PostgreSQL) |
You want to store Security data in Postgres | Your PostgreSQL DB | Yes (auto-DDL + minimal seed) | Separate repos for Accounts, UserGroups, RefreshTokens, PasswordHistory; PBKDF2 password validation |
5-minute wiring¶
A) Consume a remote Security API (DS client)¶
using DHI.Services.Provider.DS;
// BaseUrl **must** point to /api/accounts (tokens/refresh are auto-derived)
var accounts = new AccountRepository(
"baseUrl=https://security.example.com/api/accounts;provider=YourAccessTokenProvider;retries=5",
logger);
// 1) Login (JWT + refresh)
var tokens = await accounts.CreateTokenAsync("jdoe", "S3cure!passw0rd", otp: null, otpAuthenticator: null);
// 2) Refresh
var renewed = await accounts.RefreshTokenAsync(tokens.RefreshToken.Token);
// 3) Validate credentials (no bearer)
var ok = await accounts.ValidatePassword("jdoe", "candidate");
// 4) Admin CRUD (requires repo’s bearer via IAccessTokenProvider)
var all = await accounts.GetAllAsync();
var one = await accounts.GetAsync("jdoe");
if (one.HasValue) { var acc = one.Value; acc.Enabled = true; await accounts.UpdateAsync(acc); }
await accounts.AddAsync(new Account("alice", "Alice") { Email = "alice@example.com", Activated = true });
await accounts.RemoveAsync("alice");
DS behavior notes
- Auth headers: Bearer added only to
/api/accounts/**calls; token/validation endpoints are no-bearer by design. - Retries: default 5, backoff
2^nseconds; retries 404 and 5xx > 500 (not 500 exactly). - Sync wrappers exist, but prefer
Asyncin web/UI code.
B) Host your own Security data (PostgreSQL)¶
using DHI.Services.Provider.PostgreSQL;
// One repo per table (all auto-create tables if missing)
var accRepo = new AccountRepository("Host=pg;Database=security;Username=svc;Password=***", logger);
var grpRepo = new UserGroupRepository("Host=pg;Database=security;Username=svc;Password=***", logger);
var rtRepo = new RefreshTokenRepository("Host=pg;Database=security;Username=svc;Password=***", logger);
var pwdRepo = new PasswordHistoryRepository("Host=pg;Database=security;Username=svc;Password=***", logger);
// Seed caution: first run seeds a demo admin + default groups. Remove in real envs.
var acc = new Account("jdoe", "John Doe") { Email = "jdoe@example.com", Activated = true, Enabled = true };
acc.SetPassword("S3cure!passw0rd"); // sets PBKDF2 salt+hash
accRepo.Add(acc);
// Validate
var ok = await accRepo.ValidatePassword("jdoe", "S3cure!passw0rd");
// Groups
grpRepo.Add(new UserGroup("Operators", "Ops team", new HashSet<string>{ "jdoe" }));
// Lock/unlock helpers
var maybe = accRepo.Get("jdoe");
if (maybe.HasValue) { accRepo.LockAccount(maybe.Value, TimeSpan.FromMinutes(15)); accRepo.UnlockAccount(maybe.Value); }
// Refresh tokens
var rt = rtRepo.GetByToken("rTok123");
if (rt.HasValue) { var e = rt.Value; e.Expiration = DateTime.UtcNow.AddDays(90); rtRepo.Update(e); }
PostgreSQL behavior notes
- Tables auto-created:
accounts,usergroups,refreshtokens,passwordhistory(+ indexes). - Password hashing: validates legacy 20-byte SHA-1, writes/validates PBKDF2 (salt+hash, 10k iters).
- Seeding (first run): a demo admin user + default groups (remove immediately in non-test envs).
- Table override: each repo supports a
Table=override via the underlying PostgreSQL provider infrastructure.
DS API mapping (mental model)¶
| You call (DS repo) | It does (HTTP) |
|---|---|
CreateToken* |
POST {root}/api/tokens |
RefreshToken* |
POST {root}/api/tokens/refresh |
ValidatePassword* |
POST {root}/api/accounts/validation |
Get/GetAll/Add/Update/Remove/Count |
REST on {root}/api/accounts |
You pass
BaseUrl = {root}/api/accounts; token endpoints are auto-derived.
Configuration cheatsheet¶
DS client connection string
baseUrl(required):https://…/api/accountsprovider(required): identifies yourIAccessTokenProviderretries(optional): default 5
PostgreSQL connection string
- Standard Npgsql; each repo targets one table in public by default.
- Optional
Table=myschema.mytableper repository if you want custom names.
Security checklist (read me before prod)¶
- TLS everywhere (API & DB).
- Rotate & store secrets in a secret manager; never in code.
- Remove seed user
demo_admin_to_be_deletedand fix default groups on first deploy. - Least privilege DB role (CRUD on these tables only).
- UTC timestamps (mixed timestamptz/without tz columns—normalize in app).
- Don’t log request bodies for login/refresh endpoints.
- Use PBKDF2 on new password writes (automatic via
SetPassword).
Foot-guns & fixes¶
| Symptom | Likely cause | Fix |
|---|---|---|
401/403 on DS account CRUD |
IAccessTokenProvider issues a token without admin scope/role |
Issue a service-to-service token accepted by the Security API’s admin policy |
DS retries keep happening on 404 |
Remote service still warming up/behind gateway | Let retries complete; verify baseUrl → /api/accounts |
Login fails with 400 |
Wrong password, account locked/disabled, or missing/invalid OTP | Read the response; if OTP required, prompt and pass otp + otpAuthenticator |
| Password validates locally but fails after change | Legacy SHA-1 record not upgraded | Ensure users change passwords → stored as PBKDF2; validation covers both |
| Seed admin still present | First-run seeding not cleaned | Delete demo_admin_to_be_deleted and remove from groups |
Quick reference¶
| Need… | Use… | Notes |
|---|---|---|
| Call a remote Security API | Provider.DS.AccountRepository |
baseUrl=/api/accounts; tokens/refresh auto-derived |
| Store security data yourself | Provider.PostgreSQL repos |
Auto-DDL; PBKDF2; seed admin/group on empty DB |
| Lock/unlock accounts | LockAccount/UnlockAccount/ResetAccount (PG) |
Helpers mutate fields then Update |
| Validate password | ValidatePassword on either provider |
DS hits /validation; PG checks PBKDF2/SHA-1 |
| Refresh JWT | RefreshToken* (DS) |
Posts to /api/tokens/refresh |
Bottom line:
- Use DS when Security lives elsewhere and you just need a robust client (login/refresh/admin).
- Use PostgreSQL when you own the Security store and want simple, code-first tables with solid password hashing.