Skip to main content

Flows

Happy path — change username

parent iframe auth-central API
│ │ │
│ load iframe ────▶│ │
│ ◀── INIT ─────────│ │
│ save connId │ │
│ │ │
│ ─ UPDATE_USERNAME▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, username } ─│
│ │ ── POST /users/exists▶│
│ │ ◀── { isExistsUsername:false }
│ │ ── POST /users/{id}/setUsername ▶│
│ │ ◀── { status:'ok' } ──│
│ ◀ USERNAME_UPDATED│ │

Local validation failure

The iframe never calls the backend if the trimmed value is empty or fails userNameValidation.

parent iframe auth-central API
│ ─ UPDATE_USERNAME▶│ │
│ │ (validate locally) │
│ ◀ VALIDATION_ERROR│ reason: 'required' │
│ │ | 'invalid' │

Same username (no-op success)

If the submitted username (after trim, case-insensitive) is identical to the user's current one, the iframe skips the exists and setUsername calls and replies with USERNAME_UPDATED right away. The GET /users call still runs (to compare), so a 401 at that step still wins and is surfaced as AUTH_TOKEN_401.

parent iframe auth-central API
│ ─ UPDATE_USERNAME▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, username } ─│
│ │ (same — skip update) │
│ ◀ USERNAME_UPDATED│ │

Username already taken

parent iframe auth-central API
│ ─ UPDATE_USERNAME▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, username } ─│
│ │ ── POST /users/exists▶│
│ │ ◀── { isExistsUsername:true }
│ ◀ VALIDATION_ERROR│ reason: 'exist' │

Happy path — change email + verify

parent iframe auth-central API
│ ── UPDATE_EMAIL ─▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, email } ────│
│ │ ── POST /users/exists▶│
│ │ ◀── { isExistsEmail:false }
│ │ ── POST /users/{id}/setEmail ▶│
│ │ ◀── { status:'ok' } ──│
│ ◀── EMAIL_UPDATED│ (verification email sent)
│ │ │
│ user copies code from inbox │
│ │ │
│ ── CONFIRM_EMAIL ▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id } ───────────│
│ │ ── POST /verification/confirm/{id} ▶│
│ │ ◀── { token, refreshToken, email } │
│ ◀── EMAIL_CONFIRMED │
│ store new JWTs │ │

Same email (no-op success)

parent iframe auth-central API
│ ── UPDATE_EMAIL ─▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, email } ────│
│ │ (same — skip update) │
│ ◀── EMAIL_UPDATED│ │

Useful when the host wants to ask the backend to re-send the verification code without distinguishing "new email" vs "current email" in its UI — follow up with RESEND_EMAIL_CODE.

Email already taken / limit reached

parent iframe auth-central API
│ ── UPDATE_EMAIL ─▶│ │
│ │ ── POST /users/exists▶│
│ │ ◀── { isExistsEmail:true }
│ ◀ VALIDATION_ERROR│ reason: 'exist' │
parent iframe auth-central API
│ ── UPDATE_EMAIL ─▶│ │
│ │ ── POST /setEmail ───▶│
│ │ ◀── 400 ──────────────│
│ ◀ VALIDATION_ERROR│ reason: 'limitReached'

Invalid verification code

The iframe applies the same rules as the standalone EmailVerificationForm: trim → not empty → ≤ 6 chars → digits only. Each rule maps to its own reason so the host can render the matching message.

parent iframe auth-central API
│ ── CONFIRM_EMAIL ▶│ │
│ │ (local format check) │
│ ◀ CONFIRMATION_ERROR│ reason: 'required' │
│ │ | 'max' │
│ │ | 'invalid' │
│ │ │
│ ── CONFIRM_EMAIL ▶│ (valid format) │
│ │ ── POST /confirm ────▶│
│ │ ◀── 400 ──────────────│
│ ◀ CONFIRMATION_ERROR│ reason: 'invalidCode'

Resend verification code

parent iframe auth-central API
│ ── RESEND_EMAIL_CODE ▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id } ───────────│
│ │ ── POST /resendEmail/{id} ▶│
│ │ ◀── { status:'ok' } ──│
│ ◀── EMAIL_CODE_RESENT │

Rate-limit response (400) is surfaced as EMAIL_VALIDATION_ERROR { reason: 'limitReached' }.

Happy path — change phone + verify

Mirror image of the email flow. The only differences are the endpoint paths (setPhone, resendSms), the inbound message types (PHONE_*), and the field name in the action payload (phoneNumber instead of email). Phone numbers are submitted in international format with a + prefix.

parent iframe auth-central API
│ ── UPDATE_PHONE ─▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id, phone } ────│
│ │ ── POST /users/exists▶│
│ │ ◀── { isExistsPhoneNumber:false }
│ │ ── POST /users/{id}/setPhone ▶│
│ │ ◀── { status:'ok' } ──│
│ ◀── PHONE_UPDATED│ (SMS sent)
│ │ │
│ user copies code from SMS │
│ │ │
│ ── CONFIRM_PHONE ▶│ │
│ │ ── GET /users ───────▶│
│ │ ◀── { id } ───────────│
│ │ ── POST /verification/confirm/{id} ▶│
│ │ ◀── { token, refreshToken, email, phone } │
│ ◀── PHONE_CONFIRMED │
│ store new JWTs │ │

RESEND_PHONE_CODE and the same-number no-op behave exactly like the email counterparts, but post to /verification/resendSms/{id} and emit PHONE_CODE_RESENT / PHONE_UPDATED { phoneNumber } respectively.

Happy path — change password

Single-step. No verification code, no token rotation.

parent iframe auth-central API
│ ── UPDATE_PASSWORD ─▶│ │
│ │ (local strength check)│
│ │ ── POST /users/changePassword ▶│
│ │ ◀── { status:'ok' } ──│
│ ◀── PASSWORD_UPDATED │

Strength rules run inside the iframe in this order — only the first failing one is reported back:

parent iframe
│ ── UPDATE_PASSWORD ─▶│
│ │ requiredCurrent? → reason: 'requiredCurrent'
│ │ requiredNew? → reason: 'requiredNew'
│ │ length < 6 → reason: 'min'
│ │ no uppercase → reason: 'uppercase'
│ │ no special → reason: 'special'
│ │ no digit → reason: 'number'
│ ◀ PASSWORD_VALIDATION_ERROR

Wrong current password (400 from the backend) is surfaced as PASSWORD_VALIDATION_ERROR { reason: 'invalidCurrent' }. The iframe never sees the confirmation field — match it on the host before posting.

Token expired (401)

A 401 from any backend call (regardless of which action triggered it) is collapsed into a single outbound AUTH_TOKEN_401 event. The host is expected to refresh the token (via the standard refresh-token flow) and re-send the same action message with the fresh value.

parent iframe auth-central API
│ ── <ACTION> ─────▶│ │
│ │ ── any call ─────────▶│
│ │ ◀── 401 ──────────────│
│ ◀── AUTH_TOKEN_401│ │
│ refresh token │ │
│ ── <ACTION> ─────▶│ (with fresh token) │

Generic failures

Any other failure — network errors, unexpected 400/500 bodies, an empty id returned from GET /users — is reported as the action's validation event (USERNAME_VALIDATION_ERROR, EMAIL_VALIDATION_ERROR, or EMAIL_CONFIRMATION_ERROR) with reason: 'unknown' and the originating error message in payload.message (best effort). The host should show a generic error and offer a retry.

Re-running an action

A given iframe instance can run any number of action cycles for any supported action. The connectionId stays the same for the lifetime of the iframe; only when the iframe is remounted is a new one generated — discard the previous one.