Summary
Add a phone number confirmation flow that mirrors the existing email confirmation flow. Users can already set a phone number on their profile, but the value is never verified — PhoneNumberConfirmed stays false and there is no way for the system (or other modules) to trust that the number actually belongs to the user.
Why we need this
ASP.NET Identity exposes a full phone confirmation surface (GenerateChangePhoneNumberTokenAsync, ChangePhoneNumberAsync, VerifyChangePhoneNumberTokenAsync, IsPhoneNumberConfirmedAsync, PhoneNumberConfirmed) and we use almost none of it. Without confirmation:
- We cannot use SMS as a 2FA channel (Identity will refuse to send tokens to an unconfirmed number).
- We cannot rely on the phone number for account recovery, support contact, or transactional alerts.
- Compliance frameworks that require a verified second contact channel (SOC 2, some financial regulations) treat unverified phone numbers as worthless.
- Users who mistype their number have no signal that they did so.
The email flow already exists end-to-end (Pages/Account/Manage/EmailEndpoint.cs, ConfirmEmailEndpoint, ConfirmEmailChangeEndpoint, ResendEmailConfirmationEndpoint). This issue brings phone numbers up to parity.
How the user will use it
End user flow (in Manage/Index or a new Manage/Phone page):
- User enters or edits a phone number, presses "Send verification code".
- Backend calls
UserManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber) and dispatches the token through an ISmsSender abstraction (start with ConsoleSmsSender for dev, mirror ConsoleEmailSender).
- User enters the 6-digit code, backend calls
UserManager.ChangePhoneNumberAsync(user, phoneNumber, token) which atomically updates the number and sets PhoneNumberConfirmed = true.
- UI shows a green "Verified" badge next to the phone number, similar to the existing email confirmed badge.
- If user changes the number later, the badge resets and they must reconfirm.
Admin flow: in the user edit page, show phone number with a "Verified / Unverified" indicator. Admin can clear the confirmation (force re-verification) the same way ForceEmailReverificationAsync works today.
Implementation notes
- New endpoints in
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/:
SendPhoneVerificationCodeEndpoint (POST) — generates and sends token.
ConfirmPhoneNumberEndpoint (POST) — verifies token, calls ChangePhoneNumberAsync.
RemovePhoneNumberEndpoint (POST) — clears number + confirmation flag.
- New
ISmsSender abstraction in SimpleModule.Users.Contracts + ConsoleSmsSender default implementation (logs token to console, like ConsoleEmailSender). Real SMS providers (Twilio, etc.) plug in via DI replacement.
- Update
Manage/Index.tsx (or split into a dedicated Manage/Phone.tsx page) to show verification UI and badge.
- Admin tab
UserSecurityTab should display phone confirmed status and a "Force phone re-verification" button (mirrors ForceEmailReverificationAsync).
- Remember to register
Manage/Phone (if added) in Pages/index.ts per the Pages Registry rule.
- Tests: integration tests for happy path, wrong code, expired token, changing number invalidates previous confirmation.
Benefits
- Unblocks SMS-based 2FA as a future feature (the user manager will then accept it).
- Verified phone is usable for account recovery / support workflows.
- Closes a meaningful piece of the ASP.NET Identity surface area we already pay the schema cost for.
- Establishes the
ISmsSender abstraction other modules can reuse (notifications, alerts).
Acceptance criteria
Summary
Add a phone number confirmation flow that mirrors the existing email confirmation flow. Users can already set a phone number on their profile, but the value is never verified —
PhoneNumberConfirmedstaysfalseand there is no way for the system (or other modules) to trust that the number actually belongs to the user.Why we need this
ASP.NET Identity exposes a full phone confirmation surface (
GenerateChangePhoneNumberTokenAsync,ChangePhoneNumberAsync,VerifyChangePhoneNumberTokenAsync,IsPhoneNumberConfirmedAsync,PhoneNumberConfirmed) and we use almost none of it. Without confirmation:The email flow already exists end-to-end (
Pages/Account/Manage/EmailEndpoint.cs,ConfirmEmailEndpoint,ConfirmEmailChangeEndpoint,ResendEmailConfirmationEndpoint). This issue brings phone numbers up to parity.How the user will use it
End user flow (in
Manage/Indexor a newManage/Phonepage):UserManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber)and dispatches the token through anISmsSenderabstraction (start withConsoleSmsSenderfor dev, mirrorConsoleEmailSender).UserManager.ChangePhoneNumberAsync(user, phoneNumber, token)which atomically updates the number and setsPhoneNumberConfirmed = true.Admin flow: in the user edit page, show phone number with a "Verified / Unverified" indicator. Admin can clear the confirmation (force re-verification) the same way
ForceEmailReverificationAsyncworks today.Implementation notes
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/:SendPhoneVerificationCodeEndpoint(POST) — generates and sends token.ConfirmPhoneNumberEndpoint(POST) — verifies token, callsChangePhoneNumberAsync.RemovePhoneNumberEndpoint(POST) — clears number + confirmation flag.ISmsSenderabstraction inSimpleModule.Users.Contracts+ConsoleSmsSenderdefault implementation (logs token to console, likeConsoleEmailSender). Real SMS providers (Twilio, etc.) plug in via DI replacement.Manage/Index.tsx(or split into a dedicatedManage/Phone.tsxpage) to show verification UI and badge.UserSecurityTabshould display phone confirmed status and a "Force phone re-verification" button (mirrorsForceEmailReverificationAsync).Manage/Phone(if added) inPages/index.tsper the Pages Registry rule.Benefits
ISmsSenderabstraction other modules can reuse (notifications, alerts).Acceptance criteria
Manage/.PhoneNumberConfirmedflips totrueonly after a valid code is entered.ISmsSenderabstraction exists with a console default; real provider can be plugged in via DI.Pages/index.tsregisters any new view endpoints;npm run validate-pagespasses.