Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e6769a2
Add: identity and bff
Vienias Apr 29, 2026
f286eba
Merge
Vienias Apr 29, 2026
9be0542
Update: prevent automatic login after application restart
Vienias Apr 29, 2026
b97435f
Add: identity and bff
Vienias Apr 29, 2026
61c3b5b
Update: prevent automatic login after application restart
Vienias Apr 29, 2026
0085b78
Merge with main
Vienias May 6, 2026
ebe3bf5
Merge with main
Vienias May 6, 2026
0d35e3e
Fix: duplicated code
Vienias May 6, 2026
af537c8
Update: Correct user Id building in indentity server
Vienias May 6, 2026
5e70656
Add: FAPI 2.0 Security Profile
Vienias May 6, 2026
6bbc559
Add Duende IdentityServer + BFF integration with FAPI 2.0 and RavenDB…
Mateosssss Jun 3, 2026
d735bbd
Merge origin/main into identity: adopt seeder migration architecture
Mateosssss Jun 5, 2026
92647ac
Add 3-role RBAC system (Admin/Analyst/Viewer) and fix auth issues
Mateosssss Jun 5, 2026
77d2089
Apply fixes from comments
Mateosssss Jun 10, 2026
4c8410c
Apply requested fixes
Mateosssss Jun 10, 2026
49cd03f
Potential fix for pull request finding
Mateosssss Jun 10, 2026
f45ef15
Potential fix for pull request finding
Mateosssss Jun 10, 2026
36232bd
Potential fix for pull request finding
Scooletz Jun 11, 2026
8dac2ae
Fix BFF SSE streaming endpoints for audit/report notifications
Mateosssss Jun 11, 2026
507da00
Address Copilot review feedback and adopt central package management
Mateosssss Jun 11, 2026
387e681
Make User.SubjectId and User.Username nullable
Mateosssss Jun 11, 2026
9226e5e
Merge remote-tracking branch 'origin/main' into identity
Mateosssss Jun 11, 2026
40f2d74
Address Copilot review: hide PasswordHash, fix IDOR, ReturnUrl, HTTPS…
Mateosssss Jun 11, 2026
d4a1df5
Address Copilot review: headers merge, layout slot, open redirect, ca…
Mateosssss Jun 11, 2026
53d3cdf
Address Copilot review: trust validated principal, guard window in SSR
Mateosssss Jun 18, 2026
a1576da
Address Copilot review: company scoping, drop JWT fallback decode, fi…
Mateosssss Jun 18, 2026
9bcca83
Wire duende-license through Aspire AddParameter instead of raw config
Mateosssss Jun 30, 2026
5da99b2
Merge main into identity: add TLS gate, cert env vars, EndpointAnnota…
Mateosssss Jul 17, 2026
bd6dde5
Fix IdentityServer: inject RavenDB client cert path and DOTNET_ENVIRO…
Mateosssss Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:api.github.com)",
"WebSearch",
"PowerShell(Get-ChildItem *)"
"PowerShell(Get-ChildItem *)",
"Bash(npm run *)"
]
}
}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# Dev TLS certificates — never commit private keys
certs/

# Duende IdentityServer signing keys — generated at runtime, must not be committed
**/IdentityServer/keys/

# User-specific files
*.rsuser
*.suo
Expand All @@ -14,6 +17,10 @@ certs/
*.sln.docstates
*.env

# IdentityServer auto-generated signing keys (env-specific, protected by Data Protection)
src/RavenDB.Samples.Verity.IdentityServer/keys/


# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

Expand Down
124 changes: 124 additions & 0 deletions IDENTITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Identity & Security in Verity

Verity is a financial audit platform — auditors review SEC filings, generate AI-assisted reports, and sign off on findings. That context shapes every identity decision made here.

## Why Duende IdentityServer

Verity has multiple companies, multiple auditors per company, and three distinct roles: Viewer, Analyst, and Admin. A single shared login or ad-hoc JWT generation would not scale here. Duende IdentityServer provides a dedicated OAuth 2.0 / OpenID Connect authority that:

- Owns all user credentials and claims (name, email, `role`, one `company_id` claim per assigned company)
- Issues short-lived access tokens scoped to the `verity-api` resource
- Manages refresh tokens with one-time-use rotation, so a stolen refresh token is immediately invalidated on use
- Keeps auth logic out of the application — the Azure Functions backend only validates tokens, never issues them

For a financial platform with auditor accountability, having a central, auditable identity authority is not optional. It is the foundation that makes the audit trail meaningful.

## Why the BFF Pattern

The Verity frontend is a SvelteKit SPA. SPAs that store OAuth tokens in `localStorage` or JavaScript memory are vulnerable to XSS — any injected script can exfiltrate tokens silently.

The Backend-for-Frontend (BFF) pattern solves this by keeping tokens on the server:

```
Browser ──── cookie ────► BFF ──── Bearer token ────► Azure Functions
└──── OIDC ────────────────► IdentityServer
```

The browser never sees an access token. It authenticates using an `httpOnly` session cookie managed by `Duende.BFF`. Every `/api/*` call is forwarded by the BFF, which injects the current access token transparently. Token refresh happens automatically in the background via `Duende.AccessTokenManagement` — the frontend never has to think about token expiry.

The BFF also acts as the single external entry point: it proxies both the API and the Vite frontend dev server, so the browser always talks to one origin.

## Why FAPI 2.0

FAPI 2.0 (Financial-grade API Security Profile) is the security baseline required by Open Banking standards worldwide (UK, EU PSD2, AU CDR). For a platform that handles financial filings, applying FAPI 2.0 is the right posture — not because it is required here, but because it demonstrates what a real production deployment would need.

Two mechanisms are enabled on the `verity-bff` client:

**Pushed Authorization Requests (PAR)**
The browser never carries authorization parameters in the URL. Instead, the BFF pushes the full authorization request to IdentityServer's PAR endpoint first, receives a `request_uri`, and only that opaque reference appears in the browser redirect. This prevents parameter tampering and leakage via the referrer header or browser history.

**Demonstrating Proof-of-Possession (DPoP)**
Access tokens are issued as DPoP-bound to the BFF's RSA key pair. This sample demonstrates DPoP at the client/issuer; the Azure Functions API validates JWTs but does not validate per-request DPoP proofs.

## Identity Events in RavenDB

Every significant authentication action — login, logout, token issuance, client authentication failure — is written to the `SecurityEvents` collection in RavenDB by a custom `IEventSink` (`RavenEventSink`).

| Event | What it records |
| ----------------------------- | ----------------------------------------------- |
| `UserLoginSuccess` | who logged in, from which IP, via which client |
| `UserLoginFailure` | attempted username, failure reason |
| `UserLogoutSuccess` | who logged out |
| `TokenIssuedSuccess` | subject, client, grant type |
| `TokenIssuedFailure` | client, error reason |
| `ClientAuthenticationFailure` | client ID, error (potential brute-force signal) |

Documents expire automatically after 90 days via RavenDB's built-in `@expires` metadata.

This matters for financial applications because compliance frameworks (SOC 2, ISO 27001, FAPI 2.0 itself) require evidence of _who authenticated and when_, not just _who changed what_. Storing these events in the same database as the audit records — queryable with RQL, visible in RavenDB Studio — creates a unified compliance picture: financial operations and the access history surrounding them, in one place.

## Architecture Overview

```
Browser
BFF (Duende.BFF + YARP)
│ httpOnly session cookie
│ DPoP-bound access token forwarded to API
├──── /bff/login → IdentityServer /authorize (PAR + PKCE + DPoP)
├──── /bff/user → session claims
├──── /api/* → Azure Functions (Bearer token injected)
└──── /* → Vite dev server (frontend assets)

IdentityServer (Duende IdentityServer 7.x)
│ RavenDB user store
│ In-memory clients & scopes
└──── SecurityEvents → RavenDB (RavenEventSink)

Azure Functions (backend API)
│ JWT Bearer validation (Authority = IdentityServer)
│ [Authorize(Roles = "...")] on all non-public endpoints
│ Analyst queries scoped to their CompanyIds at the DB level
└──── RavenDB (Verity database)
```

## Setup

In addition to the prerequisites listed in the main README, Verity's identity layer requires a **Duende license key**.

Duende offers a free [Community Edition](https://duendesoftware.com/products/communityedition) for qualifying open-source projects. Once you have a key, store it as a user secret in the AppHost project:

```bash
cd src/RavenDB.Samples.Verity.AppHost
dotnet user-secrets set "Parameters:duende-license" "<your-license-key>"
```

The same key is forwarded to both the IdentityServer and BFF projects by Aspire at startup.

## Roles

| Role | What they can do |
| --------- | ----------------------------------------------------------------------------------------------------------------- |
| `Viewer` | Browse all companies and reports — read-only, no audit access |
| `Analyst` | Read and write audits, fetch 10-Q filings — scoped to their assigned companies only |
| `Admin` | Full access to all companies, reports, and audits; manages user roles and company assignments via the Admin Panel |

Roles and company assignments are managed in the Admin Panel (`/admin`), visible in the navbar when logged in as Admin. New accounts created via the Register form always start as `Viewer`.

When an Analyst holds multiple company assignments, IdentityServer emits one `company_id` claim per company. The backend enforces the scope at query level — an Analyst calling `/api/companies` receives only their assigned companies, not the full list.

## Demo Credentials

After running the Setup migrations (`POST /api/migrate`), the following accounts are available:

| Username | Password | Role | Companies |
| -------- | ----------- | ------- | --------------------------------------- |
| `alice` | `Demo1234!` | Admin | — |
| `bob` | `Demo1234!` | Analyst | Apple (companies[0] alphabetically) |
| `carol` | `Demo1234!` | Analyst | Microsoft (companies[1] alphabetically) |
| `dave` | `Demo1234!` | Analyst | Microsoft (companies[1] alphabetically) |
| `eve` | `Demo1234!` | Viewer | — |

To create additional accounts, use the **Register** link in the top-right corner of the application. New accounts start as `Viewer` — use the Admin Panel to promote them to Analyst or Admin and assign companies.
5 changes: 5 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@
<PackageVersion Include="Spectre.Console" Version="0.56.1-alpha.0.2" />
<PackageVersion Include="System.Reactive" Version="6.1.0" />
</ItemGroup>
<ItemGroup Label="Identity">
<PackageVersion Include="Duende.IdentityServer" Version="7.4.7" />
<PackageVersion Include="Duende.BFF.Yarp" Version="4.1.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
</ItemGroup>
</Project>
Loading