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 point at the front-end that made the 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). The Identity module now resolves the link's base URL from the request `Origin` header, validated against `CorsOptions.AllowedOrigins`: each user gets a link back to the app they started from, and — because forgot-password is anonymous — a forged or unlisted `Origin` is rejected rather than turned into a link inside an e-mail. 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:** list your SPA origins in `CorsOptions:AllowedOrigins` — the dev config now ships `http://localhost:5173` and `http://localhost:5174`; with the list empty these flows are rejected. `OriginOptions:OriginUrl` keeps its role as the API's own public base (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">
The `forgot-password` and registration e-mails link back to the **front-end that made the request**: the Identity module resolves the link's base URL from the request `Origin` header, validated against `CorsOptions.AllowedOrigins`. With more than one SPA each user gets a link to the app they started from, and a forged or unlisted origin is rejected rather than e-mailed. The confirmation link lands on the SPA `/confirm-email` page (which then calls `GET /confirm-email`), not the API route directly. List your SPA origins in `AllowedOrigins` 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
10 changes: 8 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,12 @@ Pipeline order (relevant slice):
6. ...
```

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

`AllowedOrigins` does double duty. Besides the CORS policy, it is the allowlist the Identity module validates the request `Origin` header against when it builds a link that lands on a front-end — the password-reset and e-mail-confirmation e-mails. Each link is built from the origin the request actually came from, so when you serve more than one SPA (say an admin console on one origin and a tenant app on another) every user gets a link back to the app they started from, not a single hard-coded URL.

Because forgot-password is anonymous, this validation is a security boundary: a forged `Origin` header that is not in `AllowedOrigins` is rejected rather than turned into a link inside an e-mail. The check always runs against `AllowedOrigins`, independently of `AllowAll` — so for these links to resolve you must list your SPA origins explicitly, even in dev. A request whose `Origin` isn't allow-listed fails instead of e-mailing a link to the wrong host. (`OriginOptions:OriginUrl` remains the API's own public base, used for API-served assets such as avatar URLs — it is not the front-end link base.)

## 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 +135,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". It also breaks password-reset and e-mail-confirmation links: the Identity module validates the request `Origin` against this same list to pick which front-end the e-mailed link points to, so an origin that isn't listed makes forgot-password and registration reject the request instead of sending the e-mail.
- **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: 2 additions & 2 deletions 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 @@ -57,7 +57,7 @@ Adjust for your industry. Healthcare (HIPAA) and finance (PCI-DSS) tend to requi

## 3. Allowlist CORS origins

`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/).
`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. `AllowedOrigins` is also the allowlist the Identity module validates the request `Origin` against to build password-reset and e-mail-confirmation links, so if it doesn't list your SPA origins those flows are rejected and no e-mail is sent. See [CORS & security headers](/docs/security/cors-and-headers/).

```jsonc
{
Expand Down