Passwordless Authentication
Complete guide for implementing passwordless authentication using Guardian’s /v2/passwordless endpoint.
Overview
Section titled “Overview”Guardian implements passwordless authentication using OTP (One-Time Password) delivery via SMS or Email. This authentication method eliminates the need for users to remember passwords by sending a time-limited OTP code to their registered phone number or email address.
How It Works
Section titled “How It Works”The passwordless authentication flow consists of two phases:
-
Init: User initiates authentication by providing their phone number or email address → Guardian sends an OTP code
-
Complete: User verifies the received OTP code → Guardian returns access tokens, refresh tokens, and ID tokens
Supported Flows:
-
SIGNIN: User must already exist in your user service. If user doesn’t exist, authentication fails. -
SIGNUP: User must not exist. If user already exists, authentication fails. -
SIGNINUP(default): Works for both existing and new users. Automatically creates user if they don’t exist.
Prerequisites
Section titled “Prerequisites”Before implementing passwordless authentication, ensure you have:
- Guardian Tenant: A configured tenant ID
- OAuth Client: A registered client ID for your application
- User Service: External service for user management (lookup and creation)
- Communication Service: External service for SMS/Email OTP delivery
Configuration
Section titled “Configuration”All configuration is tenant-specific and stored in database tables. Configure the following in your tenant’s database tables before using passwordless authentication:
- OTP Settings - OTP behavior (length, validity, retry limits)
- SMS Service - SMS provider endpoint for OTP delivery via sms
- Email Service - Email provider endpoint for OTP delivery via email
- User Service - User management service endpoint
Refer Configuration Guide for complete setup details
API Endpoints
Section titled “API Endpoints”Init Endpoint
Section titled “Init Endpoint”Endpoint: POST /v2/passwordless/init
Purpose: Initiates the passwordless authentication flow and sends an OTP to the user. Also used to resend OTP by including the state from a previous response.
Headers:
-
Content-Type: application/json -
tenant-id: <your-tenant-id>(required)
Request Body:
{
"client_id": "my-client-id",
"scopes": ["openid", "email"],
"flow": "signinup",
"response_type": "token",
"contacts": [{
"channel": "sms",
"identifier": "9999999999",
"template": {
"name": "templateName",
"params": {
"variable-1": "value-1"
}
}
}],
"state": "string",
"meta_info": {
"ip": "string",
"location": "string",
"device_name": "string",
"source": "string"
}
}Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_id | string | Yes | Guardian OAuth client ID. This must be created in Guardian before use. |
| scopes | array[string] | No | OAuth scopes to request (e.g., [“openid”, “email”, “profile”]) |
| flow | string | No | Authentication flow type: “signin”, “signup”, or “signinup” (default: “signinup”) |
| response_type | string | No | Response type: “token” or “code” (default: “token”) |
| contacts | array[object] | Yes | Array of contact objects for OTP delivery |
| contacts[].channel | string | Yes | Contact channel: “sms” or “email” |
| contacts[].identifier | string | Yes | Phone number (SMS) or email address (email) |
| contacts[].template | object | No | Template override (uses tenant default if not provided) |
| contacts[].template.name | string | No | Template name |
| contacts[].template.params | object | No | Template parameters as key-value pairs |
| state | string | No | State key from previous /init response. Required only when resending OTP. Omit for initial request |
| meta_info | object | No | Metadata object |
| meta_info.ip | string | No | Client IP address |
| meta_info.location | string | No | Geographic location |
| meta_info.device_name | string | No | Device identifier |
| meta_info.source | string | No | Application source (e.g., “mobile_app”, “web”) |
Response: 200 OK
{
"state": "aB3dE5fG7h",
"tries": 0,
"retries_left": 5,
"resends": 0,
"resends_left": 5,
"resend_after": 1640995230,
"is_new_user": true
}Response Parameters:
| Parameter | Type | Description |
|---|---|---|
| state | string | Unique state for subsequent requests. Store this for the complete endpoint. |
| tries | integer | Number of OTP verification attempts made |
| retries_left | integer | Remaining verification attempts |
| resends | integer | Number of times OTP has been resent |
| resends_left | integer | Remaining resend attempts |
| resend_after | integer | Unix timestamp after which resend is allowed |
| is_new_user | boolean | Boolean indicating whether the user was created during the authentication |
Error Responses:
| Code | Status | Message | When | Resolution |
|---|---|---|---|---|
| invalid_state | 400 | ”Invalid state” | State not found/expired | Start new flow |
| resends_exhausted | 400 | ”Resends exhausted” | resends >= maxResends | Start new flow |
| resends_not_allowed | 400 | ”Resend triggered too quick” | Resend before interval | Wait until resendAfter |
Complete Endpoint
Section titled “Complete Endpoint”Endpoint: POST /v2/passwordless/complete
Purpose: Verifies OTP and completes authentication
Headers:
-
Content-Type: application/json -
tenant-id: <your-tenant-id>(required)
Request Body:
{
"state": "aB3dE5fG7h",
"otp": "123456"
}Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| state | string | Yes | State key from init response |
| otp | string | Yes | OTP code received by user |
Response: 200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1v",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"sso_token": "aB3dE5fG7hI9jK",
"token_type": "Bearer",
"expires_in": 900,
"is_new_user": false
}Response Parameters:
| Parameter | Type | Description |
|---|---|---|
| access_token | string | JWT access token for API authentication |
| refresh_token | string | 32-character alphanumeric refresh token for obtaining new access tokens |
| id_token | string | JWT ID token containing user information (if OIDC enabled) |
| sso_token | string | 15-character alphanumeric Single Sign-On token |
| token_type | string | Token type. Always “Bearer” |
| expires_in | integer | Access token expiration time in seconds |
| is_new_user | boolean | Boolean indicating if user was created in this flow |
Error Responses:
| Code | Status | Message | When | Resolution |
|---|---|---|---|---|
| invalid_state | 400 | ”Invalid state” | State not found/expired | Start new flow |
| incorrect_otp | 400 | ”Incorrect otp” | OTP mismatch | Retry (check metadata.otp_retries_left) |
| retries_exhausted | 400 | ”Retries exhausted” | tries >= maxTries | Start new flow |
| flow_blocked | 403 | ”API is blocked for this userIdentifier” | User identifier is blocked | Check error response metadata.retryAfter for unblock time |
Error Response Example (for flow_blocked):
{
"error": "flow_blocked",
"error_description": "API is blocked for this userIdentifier",
"metadata": {
"retryAfter": 1640995230
}
}API Specification
Section titled “API Specification”Key schemas:
V2PasswordlessInitRequestBody- Init endpoint requestV1PasswordlessCompleteRequestBody- Complete endpoint requestV2PasswordlessInitResponse- Init endpoint responseV2TokenResponse- Complete endpoint response (tokens)
For complete request and response schemas, refer to the Guardian API Specification.
Examples
Section titled “Examples”cURL Examples
Section titled “cURL Examples”Init Request:
curl --location 'http://localhost:8080/v2/passwordless/init' \
--header 'Content-Type: application/json' \
--header 'tenant-id: tenant1' \
--data '{
"client_id": "my-client-id",
"scopes": ["openid", "email"],
"flow": "signinup",
"response_type": "token",
"contacts": [{
"channel": "sms",
"identifier": "9999999999"
}],
"meta_info": {
"ip": "127.0.0.1",
"location": "localhost",
"device_name": "Chrome Browser",
"source": "web"
}
}'Complete Request:
curl --location 'http://localhost:8080/v2/passwordless/complete' \
--header 'Content-Type: application/json' \
--header 'tenant-id: tenant1' \
--data '{
"state": "aB3dE5fG7h",
"otp": "123456"
}'Resend OTP (using state from previous init response):
curl --location 'http://localhost:8080/v2/passwordless/init' \
--header 'Content-Type: application/json' \
--header 'tenant-id: tenant1' \
--data '{
"state": "aB3dE5fG7h"
}'Flow Diagram
Section titled “Flow Diagram”Complete Passwordless Authentication Flow
Section titled “Complete Passwordless Authentication Flow”┌─────────┐ ┌──────────┐ ┌─────────────┐ ┌───────────┐
│ Client │ │ Guardian │ │ User Service│ │OTP Service│
│ │ │ │ │ │ │ │
└────┬────┘ └────┬─────┘ └────┬────────┘ └─────┬─────┘
│ │ │ │
│ 1. POST /v2/passwordless/init│ │ │
│─────────────────────────────>│ │ │
│ {client_id, contacts, ...} │ │ │
│ │ │ │
│ │ 2. GET /user?email=... │ │
│ │ OR ?phoneNumber=... │ │
│ │──────────────────────────────>│ │
│ │ │ │
│ │ 3. User lookup response │ │
│ │<──────────────────────────────│ │
│ │ │ │
│ │ 4. Generate OTP │ │
│ │ │ │
│ │ 5. POST /sendSms or /sendEmail│ │
│ │──────────────────────────────────────────────────────────────────>│
│ │ │ │
│ │ 6. OTP sent confirmation │ │
│ │<──────────────────────────────────────────────────────────────────│
│ │ │ │
│ │ 7. Store state in Redis │ │
│ │ │ │
│ 8. Return state & metadata │ │ │
│<─────────────────────────────│ │ │
│ {state, tries, resends, ...} │ │ │
│ │ │ │
│ │ │ │
│ 9. POST /v2/passwordless/ │ │ │
│ complete │ │ │
│─────────────────────────────>│ │ │
│ {state, otp} │ │ │
│ │ │ │
│ │ 10. Get state from Redis │ │
│ │ │ │
│ │ 11. Verify OTP │ │
│ │ │ │
│ │ 12. POST /user (if new user) │ │
│ │──────────────────────────────>│ │
│ │ │ │
│ │ 13. User created response │ │
│ │<──────────────────────────────│ │
│ │ │ │
│ │ 14. Generate & store tokens │ │
│ │ │ │
│ 15. Return tokens │ │ │
│<─────────────────────────────│ │ │
│ {access_token, refresh_token}│ │ │
│ │ │ │Rate Limiting
Section titled “Rate Limiting”Guardian enforces rate limiting to prevent abuse and ensure security:
| Type | Default | Enforcement | Action on Exhaustion |
|---|---|---|---|
| OTP Verification | 5 attempts | Per state | State deleted, flow terminated |
| OTP Resend | 5 attempts | Per state | State deleted, flow terminated |
| Resend Interval | 30 seconds | Time-based | resendAfter = currentTime + interval |
| OTP Validity | 900 seconds (15 min) | Redis TTL | State auto-deleted on expiry |
Important Notes:
- All rate limits are configurable per tenant. Refer to the Configuration Guide for details.
- Rate limits are enforced per authentication state
- Once limits are exhausted, users must start a new authentication flow
- OTP state expires after 15 minutes (default), after which a new OTP must be requested
- Resend interval prevents users from requesting too many OTPs in a short time
Troubleshooting
Section titled “Troubleshooting””Invalid state” Error
Section titled “”Invalid state” Error”Problem: Request fails with “Invalid state” error
Possible Causes:
- State has expired (default: 15 minutes)
- State was already used
- State doesn’t exist
Solutions:
- Start a new authentication flow by calling
/v2/passwordless/initagain - Store the state securely and use it within the validity period
- Don’t reuse states - each complete request should use a fresh state from init
”OTP not received” Issue
Section titled “”OTP not received” Issue”Problem: User doesn’t receive OTP code
Possible Causes:
- Email/Sms service is not configured correctly
- Email/Sms service is unavailable or timing out
- Invalid phone number or email address
- SMS/Email delivery issues
Solutions:
- Verify Email/Sms service configuration in
sms_configoremail_configtable - Check Email/Sms service logs for delivery errors
- Test Email/Sms service endpoints directly
- Verify phone number/email format is correct
- Check if Email/Sms service provider has delivery issues
- Use
is_otp_mocked: truefor testing (returns static OTP999999)
“Incorrect OTP” Error
Section titled ““Incorrect OTP” Error”Problem: OTP verification fails even with correct code
Possible Causes:
- OTP has expired
- OTP was already used
- Wrong OTP entered
- Case sensitivity issues
Solutions:
- Request a new OTP if the current one has expired
- Ensure OTP is entered correctly (check for typos)
- OTP codes are case-sensitive - enter exactly as received
- Check
retries_leftin error response to see remaining attempts
”Retries exhausted” Error
Section titled “”Retries exhausted” Error”Problem: Maximum OTP verification attempts reached
Solutions:
- Start a new authentication flow by calling
/v2/passwordless/initagain - Inform user that they’ve exceeded maximum attempts
- Consider implementing account lockout for security
”Resends exhausted” Error
Section titled “”Resends exhausted” Error”Problem: Maximum OTP resend attempts reached
Solutions:
- Start a new authentication flow by calling
/v2/passwordless/initagain - Wait for the resend interval before requesting again
- Check
resend_aftertimestamp in response to know when resend is allowed
”User service error”
Section titled “”User service error””Problem: Guardian cannot communicate with user service
Solutions:
- Check user service is accessible and running
- Verify user service configuration in
user_configtable - Check network connectivity between Guardian and user service
- Verify SSL settings if using HTTPS
- Check user service logs for errors
- Ensure user service endpoints return correct response format
”OTP service error”
Section titled “”OTP service error””Problem: Guardian cannot communicate with Email/Sms service
Solutions:
- Check Email/Sms service is accessible and running
- Verify Email/Sms service configuration in
email_configorsms_configtable - Check network connectivity between Guardian and Email/Sms service
- Verify SSL settings if using HTTPS
- Check Email/Sms service logs for errors
- Test Email/Sms service endpoints directly
”Flow blocked” Error
Section titled “”Flow blocked” Error”Problem: Authentication is blocked for user identifier
Solutions:
- Check error response metadata for
retryAftertimestamp - Wait until the retry time before attempting again
- This is a security feature to prevent abuse
- Contact Guardian administrator if blocking persists
Best Practices
Section titled “Best Practices”-
Set appropriate OTP validity period (default: 15 minutes)
-
Enforce rate limiting to prevent brute force attacks
-
Use HTTPS for all API calls in production
-
Store state securely on client side
-
Don’t expose state in URLs or logs
-
Use state only once - don’t reuse states
-
Store tokens securely (HttpOnly cookies or secure storage)
-
Never expose tokens in client-side code or URLs
-
Implement token refresh mechanism
-
Clear tokens on logout
Related Documentation
Section titled “Related Documentation”- Post Authentication - Session management and Logout
- Configuration - Guardian configuration options