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.