Guide for implementing SMS-based phone number verification using CIBA (Client-Initiated Backchannel Authentication) with an OTP code.
- What is the SMS flow?
- Quick Setup
- How to Use
- Configuration
- Frontend Integration
- Technical Details
- API Endpoints
- Troubleshooting
- Related Documentation
The SMS flow uses CIBA with channel sms so the auth server sends an OTP to the user’s phone. The user enters the code in the app to complete verification.
- SMS OTP – Auth server sends a one-time code via SMS
- CIBA – Same OpenID Connect CIBA grant as other flows, with
channel: 'sms' - Callback – Backend submits the user-entered code to the auth server callback before token exchange
- Scope – Typically
openid ip:phone_verifyfor phone verification
Use for: Phone number verification (PVN), login with SMS OTP, KYC.
In config/default.json:
{
"auth_servers": [
{
"id": "stage",
"url": "https://api.stage.ipification.com/auth"
}
]
}Add an SMS client:
{
"clients": [
{
"user_flow": "pvn_sms",
"title": "SMS Phone Number Verification",
"scope": "openid ip:phone_verify"
}
]
}The app shows an SMS button when pvn_sms is in the PNV flows. User enters phone number, starts the flow, receives SMS, then enters OTP in the popup.
- User selects auth server (if multiple).
- User enters phone number and clicks the SMS button.
- Backend starts CIBA with
channel: 'sms'; auth server sends OTP via SMS. - User receives SMS with OTP (e.g. “Your verification PIN is: 123456”).
- App shows “Enter OTP code” popup; user types the code and confirms.
- Backend sends the code to the auth server callback, then exchanges for tokens and loads user info.
- User is redirected to
/user/info.
1. POST /sms/auth → start CIBA (channel sms), get auth_req_id + nonce
2. User gets SMS and enters OTP
3. POST /sms/token → send code to auth callback, then token + userinfo
4. Optional: POST /sms/log → log flow data
- auth_servers: At least one server (see CONFIG_DEFAULT_JSON_SAMPLE.md).
- client_id and client_secret: OAuth client used for CIBA.
- realm: Usually
"ipification". - clients: One entry with
user_flow: "pvn_sms"(and optionaltitle,scope).
SMS verification typically uses:
openid ip:phone_verify
The message template (e.g. “Your verification PIN is: {{code}}”) is set in the backend when calling CIBA auth (see Technical Details).
The SMS button is rendered when pvn_sms is in the PNV flows and a client has user_flow: "pvn_sms":
button#pvn_sms.btn_sms(data-user-flow='pvn_sms' data-client-id=client.client_id) SMS-
start_pvn_sms(client_id, phone_number)
Builds payload withlogin_hint(phone),client_id,scope,server_idand callsstart_sms_flow(data). -
start_sms_flow(data)
POST /sms/authwithdata→ getauth_req_id,nonce.- Show SweetAlert2 “Enter OTP code” dialog.
- On confirm,
POST /sms/tokenwithcode,auth_req_id,client_id,nonce,server_id. - On success, redirect to
/user/info. Optionally calllog_sms_data().
-
log_sms_data(data)
POST /sms/logwith{ data }for logging.
Phone number is validated (e.g. intl-tel-input) before starting the flow.
The app only talks to the showcase backend. The backend talks to the IPification auth server.
- Client → Backend:
POST /sms/auth(phone aslogin_hint, client_id, scope, server_id). - Backend → Auth server: CIBA auth with
channel: 'sms'and message template. - Auth server: Sends SMS with OTP to the phone.
- Backend → Client: Returns
auth_req_id,nonce, andauth_serverinfo. - User: Enters OTP in the popup.
- Client → Backend:
POST /sms/tokenwith OTPcode,auth_req_id,client_id,nonce,server_id. - Backend → Auth server:
POST .../ext/bc/sms/callbackwith{ code },Authorization: Bearer {auth_req_id}. - Backend → Auth server:
POST .../tokenwith CIBA grant → getaccess_token. - Backend → Auth server:
GET .../userinfowithaccess_token. - Backend → Client: User info JSON; backend sets session and returns response.
Backend calls the auth server CIBA auth endpoint with form body including:
client_id,client_secretscope(e.g.openid ip:phone_verify)login_hint: user phone numberchannel:'sms'message: template, e.g.'Your verification PIN is: {{code}}'
Response contains auth_req_id, which is used in the callback and token exchange.
Before token exchange, the backend must submit the user-entered OTP to the auth server so it can complete the CIBA consent:
- URL:
{authServerUrl}/realms/{realm}/protocol/openid-connect/ext/bc/sms/callback - Method: POST
- Headers:
Authorization: Bearer {auth_req_id},Content-Type: application/json - Body:
{ "code": "<otp>" }
After a successful callback, the backend uses the same auth_req_id in the token endpoint.
Same as other CIBA flows:
- URL:
{authServerUrl}/realms/{realm}/protocol/openid-connect/token - Method: POST
- Body:
client_id,client_secret,grant_type: 'urn:openid:params:grant-type:ciba',auth_req_id
Then userinfo is fetched with the returned access_token.
POST /sms/auth
Starts the CIBA flow with SMS channel. Auth server sends OTP to the given phone.
- Headers:
Content-Type: application/json - Body:
{
"client_id": "your_client_id",
"server_id": "stage",
"login_hint": "+1234567890",
"scope": "openid ip:phone_verify"
}| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | OAuth2 client ID |
server_id |
string | No | Auth server ID; defaults to first server |
login_hint |
string | Yes | User phone number (E.164) |
scope |
string | No | Scopes; defaults to client’s configured scope |
{
"auth_server": { "id": "stage", "url": "https://api.stage.ipification.com/auth" },
"auth_req_id": "<auth_req_id>",
"nonce": "<uuid>"
}| Field | Type | Description |
|---|---|---|
auth_server |
object | Selected auth server |
auth_req_id |
string | CIBA auth request ID |
nonce |
string | Session/correlation id |
- 400: Invalid or missing
server_id, or no auth servers configured. - 401: Client not found (invalid
client_id). - 5xx: Auth server error; body may include
error,status,data(upstream response).
POST /sms/token
Sends the user-entered OTP to the auth server callback, then performs CIBA token exchange and returns user info.
- Headers:
Content-Type: application/json - Body:
{
"code": "123456",
"auth_req_id": "<auth_req_id_from_sms_auth>",
"client_id": "your_client_id",
"nonce": "<nonce_from_sms_auth>",
"server_id": "stage"
}| Field | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | OTP from SMS |
auth_req_id |
string | Yes | From /sms/auth response |
client_id |
string | Yes | OAuth2 client ID |
nonce |
string | Yes | From /sms/auth response |
server_id |
string | No | Auth server ID |
{
"auth_server": { "id": "stage", "url": "..." },
"sub": "<subject>",
"phone_number": "+1234567890",
"phone_number_verified": true
}User info fields depend on scope and auth server. Session is set for /user/info.
- 400: Invalid or missing
server_id, or no auth servers configured. - 401: Client not found.
- 4xx/5xx: Callback or token/userinfo error; body includes
error,status, and optionallydata. Session may still be updated with error payload for display.
POST /sms/log
Optional endpoint to log client-side flow data (e.g. token response) for debugging or analytics.
- Headers:
Content-Type: application/json - Body:
{ "data": { ... } }
- 200: Body
"OK". Logging is implementation-specific (e.g. server logs).
- Ensure
auth_serversin config has at least one entry and thatserver_id(if sent) matches one of theidvalues.
- Check that
client_idmatches a configured client and that credentials (e.g.client_secret) are correct for that client.
- Confirm auth server is configured to send SMS and that the number is in E.164 format.
- Check auth server logs and SMS provider/configuration (rate limits, number format, country).
- User must enter the exact code from the SMS before it expires.
- Ensure the backend calls the SMS callback with the same
auth_req_idbefore calling the token endpoint. - Check auth server callback and token endpoints for detailed error in response
data.
- On token error, the backend may still set session with error payload; check response body and session handling in
routes/sms.jsand/user/info.
- TS43 SIM Verification – SIM-based verification (no SMS).
- API Reference – Other endpoints.
- Architecture – System overview.
- Configuration – Auth servers and clients.