Skip to content

fix(identity): make the two-factor issuer name configurable - #1443

Open
marcelo-maciel wants to merge 3 commits into
fullstackhero:mainfrom
marcelo-maciel:fix/totp-issuer-configurable
Open

marcelo-maciel wants to merge 3 commits into
fullstackhero:mainfrom
marcelo-maciel:fix/totp-issuer-configurable

Conversation

@marcelo-maciel

@marcelo-maciel marcelo-maciel commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1440

What

The issuer of the two-factor otpauth:// URI was a hardcoded "FullStackHero" in EnrollTwoFactorCommandHandler, so every product built on the kit appeared under that name in the user's authenticator app. It now comes from configuration:

"TwoFactorOptions": {
  "Issuer": "FullStackHero"
}
  • New TwoFactorOptions (Identity module root, next to TenantGraceOptions), bound with AddOptions<TwoFactorOptions>().BindConfiguration(nameof(TwoFactorOptions)).ValidateDataAnnotations().ValidateOnStart(), the same idiom as JwtOptions. No existing Identity options class covers TOTP (JwtOptions:Issuer is the token issuer, fsh.local, a different concept), so a small dedicated class seemed cleaner than overloading one.
  • Issuer defaults to FullStackHero, so nothing changes for current deployments. It is [Required]: a blank issuer would produce otpauth://totp/:user?...&issuer=, which authenticator apps reject, so it fails at startup instead.
  • The issuer stays percent-encoded with UrlEncoder.Default in both the label and the issuer= parameter, so Canal Correto becomes Canal%20Correto, Ação becomes A%C3%A7%C3%A3o, and a : or & in the name cannot break the label or the query.
  • appsettings.json documents the key next to PasswordPolicy.
  • Projects scaffolded with fsh new / dotnet new fsh already got their own display name here, because the template replaces FullStackHero with it; that still applies to both the default and the appsettings.json value. The default-issuer test derives its expectation from TwoFactorOptions for that reason (the brand can contain a space, as in the scaffold smoke job's Smoke Api). What this PR adds is the ability to change it after scaffolding, and for clones and forks of the kit.

Existing enrollments

Only new enrollments get the new issuer. Users who enrolled earlier keep the old label in their app, and their codes keep working because the shared secret is not touched. No migration is needed. A user who wants the new label disables and enrolls again.

Tests

  • Identity.Tests/Handlers/EnrollTwoFactorCommandHandlerTests (new): the default is FullStackHero; exact URI with the default issuer, with a custom issuer, and with Canal Correto, Ação and A:B & C (encoded in both positions, and Uri.TryCreate accepts it); unauthenticated caller throws UnauthorizedException; a blank or whitespace issuer fails validation.
  • Mutation gate: with the handler mutated to ignore the option ("FullStackHero" hardcoded again, field still read), 4 tests fail (custom issuer + the three encoding cases); with the issuer passed unencoded, the 3 encoding cases fail. Both green with the fix.

Note on the first commit

The first commit (build(deps): move the .NET 10 packages to 10.0.13 for the October advisories) is byte-for-byte the same as fd6e9b0b in #1426. main currently fails restore on NuGet audit (System.Security.Cryptography.Pkcs/Xml and SignalR.Common 10.0.12 advisories under TreatWarningsAsErrors), so this PR carries it to build. It drops out on rebase once #1426 merges.

Docs: fullstackhero/docs#271

…isories

Advisories published on 2026-10-13 cover every 10.0.x up to 10.0.12 of
System.Security.Cryptography.Xml, System.Security.Cryptography.Pkcs and
Microsoft.AspNetCore.SignalR.Common, and NuGetAudit under
TreatWarningsAsErrors fails restore for every project, main included.
SignalR.Common 10.0.13 requires Microsoft.Extensions.* 10.0.13, so a
transitive pin alone does not resolve; the whole 10.0.12 line moves to
10.0.13 and Xml/Pkcs stay pinned.
EnrollTwoFactorCommandHandler hardcoded "FullStackHero" as the issuer of
the otpauth URI, so every product built on the kit showed that name in
the user's authenticator app. The issuer now comes from
TwoFactorOptions:Issuer, defaulting to "FullStackHero" and validated on
start so a blank value fails fast. It stays percent-encoded in both the
label and the issuer parameter.

Only new enrollments pick up a changed issuer; existing enrollments keep
working because the shared secret does not change.

Fixes fullstackhero#1440
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-14T21:28:36.068538Z e946a68 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The template replaces "FullStackHero" with the project's display name,
which can contain spaces, so a literal expectation breaks the scaffolded
build. The default-issuer test now derives the expected label from
TwoFactorOptions, and the integration assertion goes back to the prefix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two-factor: the authenticator app always shows "FullStackHero" as the issuer

1 participant