Skip to content

docs(identity): document 2FA verification code mechanics and customization#25314

Merged
maliming merged 2 commits intoabpframework:devfrom
bsogulcan:dev
Apr 28, 2026
Merged

docs(identity): document 2FA verification code mechanics and customization#25314
maliming merged 2 commits intoabpframework:devfrom
bsogulcan:dev

Conversation

@bsogulcan
Copy link
Copy Markdown
Contributor

Description

Adds two new sections to the Two Factor Authentication guide
(docs/en/modules/identity/two-factor-authentication.md) covering an
area the document did not previously address: how the Email/SMS
verification codes are actually produced, and how to replace the
provider when the default behaviour is not sufficient
.

New sections (appended after "2FA From the End Users' Perspective"):

  1. How the Verification Code Is Generated

    • Clarifies that the Email and Phone providers are ASP.NET Core
      Identity's built-in EmailTokenProvider<TUser> /
      PhoneNumberTokenProvider<TUser>, registered via
      AddDefaultTokenProviders() in AbpIdentityAspNetCoreModule and
      not replaced by ABP, while Authenticator is overridden by
      AbpAuthenticatorTokenProvider.
    • Documents the RFC 6238 TOTP derivation
      (HMAC-SHA1(SecurityStamp, timestep || modifier), 3-minute step).
    • Spells out five behavioural consequences of the stateless design
      that users regularly ask about on the forums / GitHub (same code
      returned within a timestep, codes not being single-use, a new
      request not invalidating the previous code, effective 3–6 minute
      window, security-stamp rotation as the only natural invalidation
      — including the caveat that rotating mid-login breaks the
      RequiresTwoFactor token produced by SignInManager).
  2. Customizing the Verification Code Provider

    • Explains that stronger single-use / replay-resistant semantics
      can be obtained by implementing
      IUserTwoFactorTokenProvider<IdentityUser> and registering it
      under the existing Email / Phone keys, which overrides the
      previous descriptor in TokenOptions.ProviderMap.
    • Provides a self-contained IDistributedCache-backed example
      (cryptographically-secure 6-digit code, overwrite on regenerate,
      removed on successful validation, 3-minute TTL).
    • Notes that AccountAppService.SendTwoFactorCodeAsync and
      SignInManager.TwoFactorSignInAsync dispatch through
      UserManager.GenerateTwoFactorTokenAsync /
      VerifyTwoFactorTokenAsync, so no further wiring is required,
      and that the RequiresTwoFactor sentinel (using
      TokenOptions.DefaultProvider) is unaffected.

Placement: the new sections are appended at the end of the document
so it now flows from administrator configuration → provider overview →
end-user experience → developer customization, which is the same
progression the existing Identity.TwoFactorRememberMe cookie
override example already establishes on its closing paragraph.

Not a breaking change. Documentation-only; no source code or
public API is touched.

Checklist

  • I fully tested it as developer / designer and created unit / integration tests
    (documentation-only change; no code paths modified, so no tests apply)
  • I documented it (or no need to document or I will create a separate documentation issue)

How to test it?

  • Render docs/en/modules/identity/two-factor-authentication.md and
    confirm the two new ## sections appear after
    2FA From the End Users' Perspective, the heading hierarchy is
    correct, and the csharp / text code blocks render.
  • Cross-check the technical claims against source:
    • modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs
      AddDefaultTokenProviders() registration.
    • AbpAuthenticatorTokenProvider (in the commercial Identity Pro
      module) → the only provider ABP overrides out of the default three.
    • TokenOptions.ProviderMap replacement semantics via
      IdentityBuilder.AddTokenProvider(name, type) in the ASP.NET Core
      Identity source.
  • Optional sanity check of the example: drop the two provider classes
    into any ABP Identity-enabled app, register them as shown, then log
    in with a 2FA-enabled user — requesting a code twice should yield
    two different values, and submitting an already-validated code
    should fail on the second attempt.

@maliming maliming self-requested a review April 24, 2026 00:40
@maliming
Copy link
Copy Markdown
Member

Hi @bsogulcan, thanks for the PR — the "2FA verification code is not single-use" behavior is something we see asked about repeatedly, and putting it in the docs is useful.

After discussing internally, we'd like to address the underlying issue in the framework itself, similar to AbpSingleActiveTokenProvider which we recently added for password reset / email confirmation / change email tokens (#24926). The plan:

  • Add AbpEmailTwoFactorTokenProvider and AbpPhoneNumberTwoFactorTokenProvider in Volo.Abp.Identity.AspNetCore
  • Store a hash of the 6-digit code in UserTokens (consistent with the existing single-active pattern — no new Redis dependency)
  • Remove the entry on successful validation (true single-use), with a configurable TokenLifespan via Options
  • Register as defaults for TokenOptions.DefaultEmailProvider / DefaultPhoneProvider

We'll open a separate PR for the framework change. Could you keep this PR open in the meantime? Once ours is merged, you could update the docs here to align with the new built-in behavior — probably something much shorter like "ABP's 2FA email/phone codes are single-use by default, configure lifespan via AbpEmailTwoFactorTokenProviderOptions", without the custom-provider walkthrough.

Two small corrections on the current text, so they don't get lost:

  • The phone modifier is "PhoneNumber:{purpose}:{phoneNumber}", not "Phone:..." (see PhoneNumberTokenProvider<TUser>.GetUserModifierAsync).
  • Point 5 about security stamp rotation — the RequiresTwoFactor token uses DataProtectorTokenProvider (TokenOptions.DefaultProvider), not a stamp-keyed HMAC, so the stamp invalidation path goes through SignInManager's stamp validation rather than the HMAC key changing.

I'll link the framework PR here once it's up. Thanks again for raising this!

@bsogulcan
Copy link
Copy Markdown
Contributor Author

Hi @maliming, this sounds like a solid approach. Handling it at the framework level with single-use tokens will definitely make things cleaner.

I’ll keep this PR open for now and wait for the framework changes.

Also, thanks for pointing out the corrections regarding the phone modifier and the security stamp, I’ll make sure those are fixed in the meantime.

Looking forward to the update. Thank again!

@maliming
Copy link
Copy Markdown
Member

maliming commented Apr 27, 2026

hi

#25316 merged.

You can now update your PR.

Thanks.

pull Bot pushed a commit to nagyist/abp that referenced this pull request Apr 27, 2026
Replaces the TOTP-based Email/Phone 2FA providers under
TokenOptions.DefaultEmailProvider / DefaultPhoneProvider with
DataProtector-backed single-use equivalents.

- Encrypt the 6-digit code via IDataProtector (purpose chain isolated per
  provider + token purpose), store ciphertext + absolute UTC expiration
  (unix seconds) in the user token table
- Remove the stored entry on successful validation (true single-use)
- Concurrency race (ConcurrencyStamp failure) returns false instead of 500
- Configurable TokenLifespan (default 3 minutes) via Options

AbpSingleActiveTokenProvider.GenerateAsync now checks the IdentityResult
from UserManager.UpdateAsync so a silent persistence failure no longer
returns a token that was not saved.

Related to abpframework#25314.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 27, 2026

CLA assistant check
All committers have signed the CLA.

@bsogulcan
Copy link
Copy Markdown
Contributor Author

Hi @maliming, thanks for landing #25316!

Updated the PR to follow the new built-in providers — rewrote the verification-code section, swapped the custom IDistributedCache example for TokenLifespan/CodeLength configuration and a short provider-replacement note.

Let me know what you think when you get a chance!

Copy link
Copy Markdown
Member

@maliming maliming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thoughtful rewrite, @bsogulcan! The content is accurate, concise and aligned with the new providers. Really appreciate the quick turnaround.

@maliming maliming merged commit 29c1cfa into abpframework:dev Apr 28, 2026
1 check passed
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.

3 participants