Conversation
Update: RavenDB version
… audit trail - Add Duende IdentityServer with RavenDB user store, FAPI 2.0 (PAR + DPoP), login/register/logout Razor pages, and RavenEventSink writing auth events to SecurityEvents collection in RavenDB - Add Duende BFF proxying API calls and frontend through a single origin, handling token lifecycle transparently via session cookies - Add AuthMiddleware (IFunctionsWorkerMiddleware) + JWT Bearer validation on Azure Functions backend; protect write endpoints with [Authorize] - Add GET /api/security/events endpoint and /security frontend page showing auth events stored in RavenDB alongside financial data - Add migration 006 seeding demo users (alice, bob, carol, dave, eve) - Add IDENTITY.md documenting why IdentityServer, BFF pattern, and FAPI 2.0 make sense for a financial audit platform
Resolves the 4 merge conflicts by keeping main's standalone "seeder" worker (Setup project) as the migration runner and layering the identity/auth feature on top. - Program.cs (App): use simple AddRavenDBClient; drop in-app MigrationStartup/MigrationContext/AddRavenDbMigrations (now handled by the seeder). Keep JWT auth + AuthMiddleware. - Api.cs (App): adopt main's Api(session, store, edgar) constructor and drop the POST /api/migrate endpoint (superseded by the seeder). Keep the GetSubjectFromBearer/DecodeJwtClaims helpers used by auth endpoints. - AppHost.csproj: keep IdentityServer + Bff references; reference Setup as a normal Aspire resource (the seeder), not IsAspireProjectResource=false. - Setup.csproj: keep both main's Exe/CommunityToolkit RavenDB client and the AspNetCore FrameworkReference needed by 006_SeedDemoUsers (PasswordHasher). The seeder scans the Setup assembly, so 006_SeedDemoUsers runs automatically alongside migrations 001-005. echo "---"; git log --oneline -3; echo "---"; git status
- AuthMiddleware: enforce role claims from [Authorize(Roles=...)] attributes, return 403 on mismatch
- Program.cs: configure JWT with MapInboundClaims=false, RoleClaimType="role", JsonStringEnumConverter
- Api.cs: rename admin/* routes to manage/* (admin is reserved by Azure Functions host), filter null SubjectId in GetAllUsers
- VerityAgentApi.cs: fix GenerateAuditNotes — load user document as users/{userId} not {userId}
- BFF: Duende BFF with FAPI 2.0 / DPoP, role and company_id claims forwarded to frontend
- IdentityServer: FAPI 2.0 security profile, registration endpoint, role claim in tokens
- Frontend: admin panel (/admin route) for role and company assignment, role-gated UI on company/report pages, BFF auth integration
- Migration 006: seed demo users (alice=Admin, bob=Analyst, carol/dave=Viewer, eve=Analyst)
- Remove accidentally committed IdentityServer signing key, add keys/ to .gitignore
There was a problem hiding this comment.
Pull request overview
Adds an Identity & Security layer to Verity by introducing a Duende IdentityServer authority, a Duende.BFF-based reverse-proxy entry point (BFF pattern), role/company-based authorization for the Azure Functions API, and a RavenDB-backed security audit trail surfaced in the frontend.
Changes:
- Introduces new IdentityServer and BFF projects wired into the Aspire AppHost to provide OIDC authentication and server-side token management.
- Adds role/company authorization across API endpoints plus an audit trail (
SecurityEvents) persisted from IdentityServer events with 90-day expiry. - Updates the SvelteKit frontend to route API calls through the BFF and adds auth UI (AuthBar/AuthModal), admin/security pages, and access-gating.
Reviewed changes
Copilot reviewed 50 out of 51 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| src/RavenDB.Samples.Verity.slnx | Adds IdentityServer and BFF projects to the solution. |
| src/RavenDB.Samples.Verity.Setup/RavenDB.Samples.Verity.Setup.csproj | Adds ASP.NET Core framework reference for password hashing in migrations. |
| src/RavenDB.Samples.Verity.Setup/Migrations/006_SeedDemoUsers.cs | Seeds demo users with hashed passwords and company/role assignments. |
| src/RavenDB.Samples.Verity.Setup/Constants.cs | Adds Identity URL env var constant and formatting adjustments. |
| src/RavenDB.Samples.Verity.Model/UserRole.cs | Introduces role enum used across Identity/API/frontend. |
| src/RavenDB.Samples.Verity.Model/User.cs | Expands user model for identity (subject, username, roles, company list, password hash). |
| src/RavenDB.Samples.Verity.Model/SecurityEvent.cs | Adds security event document model for audit trail. |
| src/RavenDB.Samples.Verity.IdentityServer/UserStore.cs | Implements RavenDB-backed user lookup/registration and claims emission. |
| src/RavenDB.Samples.Verity.IdentityServer/RavenEventSink.cs | Persists Duende auth events to RavenDB with expiration metadata. |
| src/RavenDB.Samples.Verity.IdentityServer/RavenDB.Samples.Verity.IdentityServer.csproj | New IdentityServer project with Duende + RavenDB references. |
| src/RavenDB.Samples.Verity.IdentityServer/Properties/launchSettings.json | Local launch profile for IdentityServer. |
| src/RavenDB.Samples.Verity.IdentityServer/Program.cs | Configures IdentityServer, cookie policy, key management, and clients/scopes. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Register/Index.cshtml.cs | Implements registration handler and redirect to BFF login. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Register/Index.cshtml | Registration UI markup/styling. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Login/Index.cshtml.cs | Implements login flow and cookie issuance. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Login/Index.cshtml | Login UI markup/styling. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Logout/Index.cshtml.cs | Implements logout and post-logout redirect. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/Account/Logout/Index.cshtml | Minimal logout page. |
| src/RavenDB.Samples.Verity.IdentityServer/Pages/_ViewImports.cshtml | Adds Razor tag helpers/imports. |
| src/RavenDB.Samples.Verity.IdentityServer/IdentityConfig.cs | Defines identity resources/scopes/api resources and BFF client. |
| src/RavenDB.Samples.Verity.IdentityServer/Endpoints/RegisterEndpoints.cs | Adds registration API endpoint for the BFF to proxy. |
| src/RavenDB.Samples.Verity.Frontend/vite.config.ts | Stops exposing an API base URL; assumes relative paths via BFF. |
| src/RavenDB.Samples.Verity.Frontend/static/samples-ui-wrapper.js | Persists welcome toast dismissal in sessionStorage. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/security/+page.svelte | Adds admin-only security events page UI. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/register/+page.svelte | Adds register route redirecting into login/register flow. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/login/+page.svelte | Adds register-in-login flow and redirects to BFF login when appropriate. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/companies/[cik]/reports/[accession]/+page.svelte | Adds auth gating + role/company gating for audit edit/restore actions. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/companies/[cik]/+page.svelte | Adds auth gating for report access and fetch controls. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/admin/+page.svelte | Adds admin UI for role/company assignments. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/+page.svelte | Integrates AuthBar and gates “Add Company” behind authentication. |
| src/RavenDB.Samples.Verity.Frontend/src/routes/+layout.svelte | Adds header auth actions, security link, and auth modal. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/stores/authModal.ts | New store to manage auth modal state. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/services/users.ts | Expands user API surface for admin management endpoints. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/services/security.ts | Adds API client for security events paging. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/components/AuthModal.svelte | Adds login/register modal UX. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/components/AuthBar.svelte | Adds top-bar auth state UX and admin link. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/auth.ts | Adds BFF session claim parsing and BFF login/register URL helpers. |
| src/RavenDB.Samples.Verity.Frontend/src/lib/api.ts | Forces relative API base and redirects to BFF login on 401. |
| src/RavenDB.Samples.Verity.Bff/RavenDB.Samples.Verity.Bff.csproj | New BFF project (Duende.BFF.Yarp). |
| src/RavenDB.Samples.Verity.Bff/Properties/launchSettings.json | Local launch profile for BFF. |
| src/RavenDB.Samples.Verity.Bff/Program.cs | Configures BFF auth, DPoP key, token forwarding, and proxying to API/frontend. |
| src/RavenDB.Samples.Verity.AppHost/RavenDB.Samples.Verity.AppHost.csproj | References new IdentityServer and BFF projects. |
| src/RavenDB.Samples.Verity.AppHost/AppHost.cs | Wires IdentityServer+BFF+Frontend into Aspire and injects required URLs/license. |
| src/RavenDB.Samples.Verity.App/RavenDB.Samples.Verity.App.csproj | Adds JwtBearer auth package for API token validation. |
| src/RavenDB.Samples.Verity.App/Program.cs | Configures JWT bearer auth, enum serialization, and registers auth middleware. |
| src/RavenDB.Samples.Verity.App/Middleware/AuthMiddleware.cs | Adds Functions middleware enforcing [Authorize]/roles based on JWT validation. |
| src/RavenDB.Samples.Verity.App/Infrastructure/VerityAgentApi.cs | Adjusts user loading for agent endpoints. |
| src/RavenDB.Samples.Verity.App/Api.cs | Adds authorization attributes and role/company scoping logic + new admin/security endpoints. |
| IDENTITY.md | Adds architecture/rationale documentation for Identity/BFF/FAPI/events/roles. |
| .gitignore | Ignores generated IdentityServer signing keys. |
| .claude/settings.local.json | Updates local Claude permissions (currently broken JSON). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
EventSource cannot send custom headers, so /api/audit/stream and /api/report/stream need to bypass AsBffApiEndpoint()'s X-CSRF requirement while still enforcing authentication via RequireAuthorization(). Without this, the SSE connections opened by +layout.svelte fail. Also remove a stale comment in api.ts.
- Register: drop the unused/misleading Role selector (account always registers as Viewer regardless of the form value) - CreateAudit: take auditor identity from the authenticated user instead of trusting client-supplied AuditorName/Surname/Email - 006_SeedDemoUsers: implement Down() to remove the seeded demo users - VerityAgentApi: fix pre-existing build error (GenerateAuditRequest has no UserId; use the loaded user's id) - Adopt Directory.Packages.props (matching main's CPM setup) across all projects, adding Duende.IdentityServer, Duende.BFF.Yarp and Microsoft.AspNetCore.Authentication.JwtBearer, and bump shared package versions to match main
SEC-imported auditor placeholder users have neither field set, and declaring them non-nullable misrepresents documents that may lack them after deserialization.
# Conflicts: # src/Directory.Packages.props # src/RavenDB.Samples.Verity.App/RavenDB.Samples.Verity.App.csproj # src/RavenDB.Samples.Verity.ServiceDefaults/RavenDB.Samples.Verity.ServiceDefaults.csproj # src/RavenDB.Samples.Verity.Setup/RavenDB.Samples.Verity.Setup.csproj
… metadata, atomic username uniqueness - User.PasswordHash is now [JsonIgnore]d so it never leaks in API responses - GetAuditAgentContext derives the auditor from the authenticated user instead of a caller-supplied userId, closing an IDOR that exposed arbitrary users' PII - Login page defaults ReturnUrl to empty and only resolves the IdentityServer authorization context when a return URL is actually present - App's JWT bearer requires HTTPS metadata outside Development - UserStore.RegisterAsync reserves the username via a RavenDB compare-exchange key to prevent a race between two concurrent registrations
| .header-auth { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.75rem; | ||
| } |
There was a problem hiding this comment.
Fixed in d4a1df5 — .header-auth removed together with the slot block it styled.
| <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Http" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" /> | ||
| <PackageReference Include="Microsoft.Extensions.Http.Resilience" /> |
There was a problem hiding this comment.
AddStandardResilienceHandler() is called on line 32 of Extensions.cs inside ConfigureHttpClientDefaults, so the package is actively used. No change needed.
…ncellation tokens - callApi: use new Headers() to merge options.headers correctly — object spread silently drops entries when options.headers is a Headers instance - layout.svelte: remove slot="header-actions" block (samples-ui-wrapper has no such named slot) and the now-dead .header-auth CSS; AuthBar import also removed - Register: validate BffReturnUrl is a relative path before using it in a redirect, preventing an open-redirect to attacker-supplied URLs - /bff/register: pass req.HttpContext.RequestAborted to PostAsync and ReadAsStringAsync so the upstream request is cancelled when the client disconnects
- GetMe now reads name/role/email/company_id claims from the validated HttpContext.User instead of re-decoding the raw JWT, so it can't be fooled if the JwtBearer/AuthMiddleware ordering or config ever changes - callApi guards the 401 redirect behind typeof window !== 'undefined', throwing instead of crashing with a ReferenceError outside the browser
| var report = await session.LoadAsync<Report>(body.ReportId, req.HttpContext.RequestAborted); | ||
| if (report is null) | ||
| return new NotFoundObjectResult($"Report '{body.ReportId}' not found."); | ||
|
|
||
| var user = await session.LoadAsync<User>(body.UserId, req.HttpContext.RequestAborted); | ||
| var user = await session.LoadAsync<User>(User.BuildId(sub), req.HttpContext.RequestAborted); | ||
| if (user is null) | ||
| return new NotFoundObjectResult($"User '{body.UserId}' not found."); | ||
| return new UnauthorizedResult(); | ||
|
|
There was a problem hiding this comment.
Fixed in a1576da — added the same CanAccessCompany check used by GetAuditAgentContext and AgentSaveAudit, so an Analyst can no longer generate notes for a report outside their CompanyIds.
| var principal = req.HttpContext.User; | ||
| var fullName = principal.FindFirst("name")?.Value ?? ""; | ||
| var parts = fullName.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); | ||
| var roleString = principal.FindFirst("role")?.Value ?? "Viewer"; | ||
|
|
||
| user = new User | ||
| { | ||
| Id = id, | ||
| SubjectId = sub, | ||
| Name = parts.ElementAtOrDefault(0) ?? fullName, | ||
| Surname = parts.ElementAtOrDefault(1) ?? "", | ||
| Email = principal.FindFirst("email")?.Value ?? "", | ||
| Role = Enum.TryParse<UserRole>(roleString, out var r) ? r : UserRole.Viewer, | ||
| CompanyIds = [.. principal.FindAll("company_id").Select(c => c.Value)], | ||
| }; |
There was a problem hiding this comment.
Good catch — fixed in a1576da. GetMe now reads given_name/family_name claims directly instead of splitting name (which is the login username per UserStore.GetClaims).
| options.OnAppendCookie = ctx => | ||
| { | ||
| if (ctx.CookieOptions.SameSite == SameSiteMode.None) | ||
| { | ||
| ctx.CookieOptions.SameSite = SameSiteMode.Lax; | ||
| ctx.CookieOptions.Secure = false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Fixed in a1576da — added the same SameSite=None→Lax rewrite to OnDeleteCookie.
…x name claims, cookie delete - GenerateAuditNotes now enforces CanAccessCompany like the other agent endpoints, closing a gap where an Analyst could generate notes for reports outside their companies - GetSubjectFromBearer relies solely on the validated HttpContext.User; removed the unvalidated DecodeJwtClaims fallback and the now-dead method along with it - GetMe now reads given_name/family_name claims instead of splitting the name claim, which UserStore.GetClaims sets to the login username, not the display name - IdentityServer's dev-only SameSite=None rewrite now also applies to OnDeleteCookie, so logout doesn't fail in dev for the same reason login cookies needed the rewrite
Reading builder.Configuration["Parameters:duende-license"] directly bypasses Aspire's parameter resolution, so unlike the other parameters it never shows up in the dashboard. Registering it as a regular (empty-default, secret) parameter keeps it optional while making it visible/editable like ravendb-license, openai-api-key, etc.

Identity & Security layer
Adds authentication, authorization, and a security-audit trail to Verity using
Duende IdentityServer + the BFF pattern, hardened with FAPI 2.0.
The Azure Functions backend only validates tokens — it never issues them.
Full rationale in
IDENTITY.md.What's included
verity-bffclientAuthMiddlewarevalidates JWT + enforces[Authorize]on write endpointsSecurityEventscollection (90-day expiry) + a/securitypageUser,UserRole,SecurityEvent; migration006_SeedDemoUsersseeds demo accountsmain— migrations now run via the standaloneseederworkerSetup: requires a Duende license key (free Community Edition):