Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Overview
lastUpdated: 2026-06-20
lastUpdated: 2026-07-02
description: Release notes and version history for fullstackhero.
sidebar:
order: 1
Expand All @@ -11,6 +11,10 @@ seo:

Notable changes to the kit, newest first.

## 2026-07-02

- **Identity: password-reset and e-mail-confirmation links now resolve the correct front-end per request.** The reset link was built from a single configured `OriginOptions.OriginUrl` — which points at the API and ships empty in production, so `forgot-password` threw `Origin URL is not configured` — and the confirmation link was built from the request host and pointed straight at the API's `GET /confirm-email` route. Neither could target the right SPA when the kit serves more than one front-end (the admin console and the tenant dashboard on different origins). Link resolution now goes through a dedicated **`FrontendOptions`** (`AllowedOrigins` + `DefaultOrigin`), kept separate from CORS. **Self-service** flows (`forgot-password`, `self-register`) build the link from the request `Origin` header, validated against `FrontendOptions:AllowedOrigins` and returned as the canonical entry — so each user gets a link back to the app they started from; because forgot-password is anonymous a forged/unlisted `Origin` is rejected with **`400`**, and a request with no `Origin` (curl, Scalar, mobile, server-to-server) falls back to `DefaultOrigin`. **Operator-driven** flows (`register`, `resend-confirmation-email`) target `DefaultOrigin` — the recipient's app — so a tenant user provisioned from the admin console gets a link into the tenant app, not the console. The confirmation e-mail now lands on the SPA `/confirm-email` page (which then calls the API) instead of the raw API route. **Action for deployments:** set `FrontendOptions:AllowedOrigins` + `FrontendOptions:DefaultOrigin` — the dev config ships `http://localhost:5173`/`5174` with `DefaultOrigin` the tenant app; both ship empty in production and startup validation now **fails the boot** until you configure them. `CorsOptions:AllowedOrigins` and `OriginOptions:OriginUrl` keep their own roles (browser CORS; the API's public base for avatar URLs). See [#1323](https://github.com/fullstackhero/dotnet-starter-kit/pull/1323).

## 2026-06-20

- **The `fsh` CLI and `dotnet new` template are now on NuGet as stable `10.0.0`.** The two distribution packages that 10.0.0 had been waiting on have shipped: `FullStackHero.CLI` (install with `dotnet tool install -g FullStackHero.CLI` — no more `--prerelease`) and `FullStackHero.NET.StarterKit` (`dotnet new install FullStackHero.NET.StarterKit`). Because `fsh new` scaffolds *from* that template, the one-command flow is now end-to-end: `dotnet tool install -g FullStackHero.CLI && fsh new MyApp` produces a fully renamed project — unique JWT signing key, generated Docker secrets, `npm install` run, initial commit on `main`. The [Install](/docs/getting-started/install/) and [CLI](/docs/cli/) pages now lead with the CLI as the recommended path; `git clone` and the GitHub template remain available for reading the source or zero-install runs. See the [10.0.0 release](https://github.com/fullstackhero/dotnet-starter-kit/releases/tag/10.0.0).
Expand Down
6 changes: 5 additions & 1 deletion src/content/docs/modules/identity.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Identity module
lastUpdated: 2026-06-11
lastUpdated: 2026-07-02
description: JWT bearer + refresh tokens, ASP.NET Identity with roles + permissions, user groups, operator impersonation, two-factor TOTP, sessions, and password-policy enforcement.
sidebar:
label: Identity
Expand Down Expand Up @@ -149,6 +149,10 @@ endpoints.MapPost("/users", handler)

All 51 endpoints are under `/api/v1/identity/`. The rate-limited `auth` policy covers `POST /token/issue`, `POST /token/refresh`, `GET /confirm-email`, `POST /users/{id}/resend-confirmation-email`, `POST /forgot-password`, `POST /reset-password`, and `POST /self-register`. Full table:

<Callout type="note" title="Where reset & confirmation e-mails point">
Auth e-mail links resolve through `FrontendOptions` (a dedicated config, separate from CORS). **Self-service** flows (`forgot-password`, `self-register`) link back to the front-end that made the request — the base URL comes from the request `Origin` header, validated against `FrontendOptions:AllowedOrigins` — so with more than one SPA each user gets a link to the app they started from; a forged/unlisted origin is rejected with `400`, and a request with no `Origin` (non-browser callers) falls back to `FrontendOptions:DefaultOrigin`. **Operator-driven** flows (`register`, `resend-confirmation-email`) target `DefaultOrigin` (the recipient's app), so a tenant user provisioned from the admin console gets a link into the tenant app, not the console. The confirmation link lands on the SPA `/confirm-email` page (which then calls `GET /confirm-email`), not the API route directly. Configure `FrontendOptions` for these flows to work — see [CORS & headers](/docs/security/cors-and-headers/).
</Callout>

| Verb | Route | What it does |
|---|---|---|
| POST | `/token/issue` | Login |
Expand Down
21 changes: 19 additions & 2 deletions src/content/docs/security/cors-and-headers.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: CORS & security headers
lastUpdated: 2026-06-11
lastUpdated: 2026-07-02
description: CORS-before-HTTPS-redirect ordering, the SignalR-credentialed-CORS gotcha, and the production security headers the kit emits by default.
sidebar:
label: CORS & headers
Expand Down Expand Up @@ -54,6 +54,23 @@ Pipeline order (relevant slice):
6. ...
```

## Front-end origin for auth e-mail links

Links that land on a front-end SPA — the password-reset and e-mail-confirmation e-mails — are **not** built from the CORS list. They resolve through a dedicated `FrontendOptions`, kept separate from CORS on purpose: the CORS allowlist governs which browsers may *call* the API, while this list governs which origins may appear *inside an outbound link*. The two often overlap but carry different duties, and coupling them breaks same-origin / reverse-proxy topologies (SPA + API on one domain need no CORS entries, yet the browser still sends `Origin` on the POST).

```jsonc
"FrontendOptions": {
"AllowedOrigins": [ "http://localhost:5173", "http://localhost:5174" ],
"DefaultOrigin": "http://localhost:5174" // the tenant SPA
}
```

- **Self-service flows** (`forgot-password`, `self-register`) build the link from the request `Origin` header, validated against `AllowedOrigins` and returned as the canonical list entry — so with more than one SPA each user gets a link back to the app they started from. Because forgot-password is anonymous this is a security boundary: a **forged/unlisted `Origin` is rejected with `400`** rather than turned into a link. A request with **no** `Origin` header (curl, the Scalar try-it UI, mobile, server-to-server) falls back to `DefaultOrigin` instead of failing.
- **Operator-driven flows** (`register`, `resend-confirmation-email`) target `DefaultOrigin` — the recipient's app — not the calling operator's origin, so a tenant user provisioned from the admin console gets a link into the tenant app, not the console.
- Matching is component-wise (scheme + host + port, port exact). Startup validation **fails the boot** if neither `AllowedOrigins` nor `DefaultOrigin` is set, so a missing config surfaces loudly instead of 500-ing on the first reset. `appsettings.Production.json` ships both empty — you must configure them (see the [production checklist](/docs/security/production-checklist/)).

(`OriginOptions:OriginUrl` is unrelated: it's the API's own public base for back-end-served assets such as avatar URLs, exposed via `IRequestContext.Origin`.)

## Why not AllowAnyOrigin for SignalR

CORS spec says: when a response has `Access-Control-Allow-Credentials: true`, the `Access-Control-Allow-Origin` must be an explicit origin, not `*`. SignalR's negotiate request is credentialed (it carries `Cookie` or the JWT via `accessTokenFactory`'s query-param fallback). With `AllowAnyOrigin()`, the server emits `Allow-Origin: *`, which violates the spec — the browser silently refuses to use the response, and SignalR's `HubConnection` fails to start with a confusing CORS error.
Expand Down Expand Up @@ -129,7 +146,7 @@ The cookie should be HttpOnly (no JS access — limits XSS impact), Secure (HTTP
## Common mistakes

- **Setting `AllowAll = true` in production.** CORS exists to give browsers a sanity check on cross-origin calls. Opening to the world removes the check (it doesn't directly compromise auth — auth still gates the request — but it removes the browser-enforced "is this site allowed to call you?" layer).
- **Forgetting to fill `AllowedOrigins` in production.** With `AllowAll: false` and no origins, CORS isn't mounted — your React apps on other origins will get blocked by the browser. The symptom is "works in Postman, fails in the browser".
- **Forgetting to fill `AllowedOrigins` in production.** With `AllowAll: false` and no origins, CORS isn't mounted — your React apps on other origins will get blocked by the browser. The symptom is "works in Postman, fails in the browser". (Auth e-mail links are a *separate* concern now — they use `FrontendOptions`, see above — but that list ships empty in production too, and its startup validation fails the boot until you set it.)
- **Missing HSTS.** Without HSTS, an attacker on the network can downgrade to HTTP for the first request. The kit emits it on HTTPS responses automatically; verify your proxy doesn't strip it.
- **CSP that breaks the UI.** If a third-party widget breaks after tightening CSP, look at the browser console — CSP violations are logged. Add the needed origins to `ScriptSources`/`StyleSources`, don't disable the middleware.

Expand Down
4 changes: 3 additions & 1 deletion src/content/docs/security/production-checklist.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Production security checklist
lastUpdated: 2026-06-11
lastUpdated: 2026-07-02
description: Ten configuration items you must check before shipping fullstackhero to production. Skip none.
sidebar:
label: Production checklist
Expand Down Expand Up @@ -59,6 +59,8 @@ Adjust for your industry. Healthcare (HIPAA) and finance (PCI-DSS) tend to requi

`CorsOptions:AllowAll = true` (and the `SetIsOriginAllowed(_ => true)` policy it enables) is **dev only**. Production needs the explicit lists — and note that `appsettings.Production.json` ships `AllowedOrigins` empty, which means **no CORS middleware mounts at all** until you fill it in; your front-ends on other origins will be blocked by the browser. See [CORS & security headers](/docs/security/cors-and-headers/).

Separately, set **`FrontendOptions`** (`AllowedOrigins` + `DefaultOrigin`) — the allowlist and fallback the Identity module uses to build password-reset and e-mail-confirmation links. It also ships empty in production, and its startup validation **fails the boot** if neither is set, so a missing config surfaces immediately rather than 500-ing on the first reset. `DefaultOrigin` is the tenant SPA; it's the fallback for non-browser callers and the target for operator-driven register/resend links.

```jsonc
{
"CorsOptions": {
Expand Down