Confirming Identity
How the starter asks an account to prove who it is again before a sensitive action, and which proof each kind of account gives.
On this page
Introduction#
A few actions are sensitive enough that the backend asks the caller to prove who they are again: turning two-factor on or off, viewing or regenerating recovery codes, revoking a browser session, deleting the account, linking a provider and setting a first password. An account with a password types it. An account created through a provider has none, so the backend takes a different proof.
Which Proof an Account Gives#
| Account | Proof | Request fields |
|---|---|---|
| Has a password | The password, in a password dialog | password |
| No password, not a guest | A step-up dialog (below) | code, or confirmation_token |
Guest (is_guest, no password) |
None: the backend lets it through and no dialog is shown | none |
has_password and is_guest come from the user resource. A proof is minted per call and never kept: a confirmation token is single use, so a retry asks again. The proof reaches the controller as a Map<String, String> that is spread into the request body.
Gated Controller Methods#
These methods on MagicStarterProfileController take proof:, not password:. Where the route is a DELETE, the controller sends a POST with _method: DELETE in the body (Laravel method spoofing) and the proof fields beside it:
| Method | Request |
|---|---|
doEnableTwoFactor(proof:) |
POST /two-factor-authentication |
doDisableTwoFactor(proof:) |
POST /two-factor-authentication with _method: DELETE |
getRecoveryCodes(proof:) |
POST /two-factor-recovery-codes/show |
doRegenerateRecoveryCodes(proof:) |
POST /two-factor-recovery-codes |
doRevokeSession(tokenId:, proof:) |
POST /sessions/{token} with _method: DELETE |
doRevokeOtherSessions(proof:) |
POST /sessions/other with _method: DELETE |
doDeleteAccount(proof:) |
POST /user with _method: DELETE (schedules the deletion) |
doSetPassword(password:, passwordConfirmation:, proof:) |
POST /user/password/set (see Set a Password) |
Connecting a provider carries the same proof through beginSocialConnect(provider, proof:) to the bridge's beginConnect. To pass a password account's proof by hand, use {'password': password}.
The Step-Up Dialog#
MagicStarterStepUpDialog asks a password-less account for what it can send:
- A code field, when two-factor is confirmed on the account (
two_factor_enabled): a TOTP code, sent ascode. - One "Confirm with " button per linked provider (an entry of
social_accountswithrevoked_atnull that the bridge also offers): it re-authenticates throughMagicStarterSocialAuth.confirm(provider)and sends the resulting token asconfirmation_token. A provider row asks the bridge again on every tap. - With neither, only the
social.step_up_requiredsentence ("Please confirm your identity to continue.").
It opens proactively for a password-less account. A refusal keeps it open and shows the error inline so the user can retry with a fresh proof. When the backend answers 422 step_up_required, its accepts list (code, confirmation_token) narrows the dialog to the proofs the server takes; MagicStarterProfileController.stepUpAccepts exposes it and is cleared with the errors.
The web rule applies here too: the bridge's confirm is called before anything is awaited in the tap, so the provider popup opens. Cancel stays available while a provider confirm is pending, since on the web a closed popup only fails after a long timeout; a token that arrives after the dialog closed is discarded.
Refusals#
Gated calls report a refusal by its code, never by its message:
| Code | Shown as |
|---|---|
step_up_required |
social.step_up_required; the dialog narrows to accepts. |
password_already_set, password_not_set |
The cached user was stale: it is restored first, and the page rebuilds with the other password form. |
last_login_method |
social.last_login_method. |
owns_shared_teams, team_has_active_subscription, subscription_active |
Their own social.* sentences (account deletion refused). |
| anything else | The standard API error handling with the call's fallback sentence. |
Helpers#
The starter's screens use two helpers from lib/src/support/confirms_identity.dart. The package barrel exports them, along with MagicStarterStepUpDialog, so a host that overrides a registry view can confirm a password-less account the same way.
| Helper | Does |
|---|---|
confirmIdentity(context, {variant, title, description, accepts, attempt}) |
Picks the proof by the rules above and answers it as request fields, or null when the user cancels. With attempt, each proof is handed to it before the dialog closes: a returned error string is shown inline and the dialog stays open for the next, fresh proof. |
confirmAndRun(context, controller, {action, variant, title, description}) |
Confirms identity, then runs a gated action(proof) inside the dialog, so a refusal is shown inline and the user retries without reopening it. The dialog stays open only while the server refuses the proof itself (step_up_required, or a field error on password, code or confirmation_token); any other refusal closes it and leaves the controller's error on the page. Resolves to true when the action succeeded, false when the user cancelled or the action was refused for another reason. |
final success = await confirmAndRun(
context,
controller,
title: trans('profile.set_password'),
action: (proof) => controller.doDeleteAccount(proof: proof),
);
Gotchas#
| Mistake | Fix |
|---|---|
| Keeping a proof between attempts | A proof is minted per call and a confirmation token is single use; ask again on every retry. |
| Showing a dialog to a guest | confirmIdentity answers {} for a guest (no password, is_guest) without a dialog; the backend lets it through. |
| Switching on a refusal's message | Read its code (step_up_required, last_login_method, ...); the message is localised and not stable. |
Calling the provider bridge's confirm after an await on the web |
The popup is blocked. The step-up dialog calls confirm before anything is awaited in the tap; a bridge of your own must open the provider in that same call. |
Passing password: to a gated method |
They take proof:; use {'password': value} for a password account or let confirmIdentity produce it. |
Treating has_password as present on every backend |
MagicStarterAuthUser.hasPassword defaults to true when the field is missing, so a backend that never sends it shows the password dialog. |
Related Documentation#
- Authentication: the social bridge contract and
confirm. - Connected Accounts: the page that asks for a proof before it links a provider.
- Set a Password: the first-password form for a provider account.
- Profile: two-factor, sessions and account deletion, the gated screens.
- Social Auth Flows: how the bridge's
confirmandbeginConnectrun.