Skip to content
GitHub

Passwordless Authentication

Complete guide for implementing passwordless authentication using Guardian’s /v2/passwordless endpoint.

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.

The passwordless authentication flow consists of two phases:

  1. Init: User initiates authentication by providing their phone number or email address → Guardian sends an OTP code

  2. 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.

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

All configuration is tenant-specific and stored in database tables. Configure the following in your tenant’s database tables before using passwordless authentication:

  1. OTP Settings - OTP behavior (length, validity, retry limits)
  2. SMS Service - SMS provider endpoint for OTP delivery via sms
  3. Email Service - Email provider endpoint for OTP delivery via email
  4. User Service - User management service endpoint

Refer Configuration Guide for complete setup details

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:

ParameterTypeRequiredDescription
client_idstringYesGuardian OAuth client ID. This must be created in Guardian before use.
scopesarray[string]NoOAuth scopes to request (e.g., [“openid”, “email”, “profile”])
flowstringNoAuthentication flow type: “signin”, “signup”, or “signinup” (default: “signinup”)
response_typestringNoResponse type: “token” or “code” (default: “token”)
contactsarray[object]YesArray of contact objects for OTP delivery
contacts[].channelstringYesContact channel: “sms” or “email”
contacts[].identifierstringYesPhone number (SMS) or email address (email)
contacts[].templateobjectNoTemplate override (uses tenant default if not provided)
contacts[].template.namestringNoTemplate name
contacts[].template.paramsobjectNoTemplate parameters as key-value pairs
statestringNoState key from previous /init response. Required only when resending OTP. Omit for initial request
meta_infoobjectNoMetadata object
meta_info.ipstringNoClient IP address
meta_info.locationstringNoGeographic location
meta_info.device_namestringNoDevice identifier
meta_info.sourcestringNoApplication 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:

ParameterTypeDescription
statestringUnique state for subsequent requests. Store this for the complete endpoint.
triesintegerNumber of OTP verification attempts made
retries_leftintegerRemaining verification attempts
resendsintegerNumber of times OTP has been resent
resends_leftintegerRemaining resend attempts
resend_afterintegerUnix timestamp after which resend is allowed
is_new_userbooleanBoolean indicating whether the user was created during the authentication

Error Responses:

CodeStatusMessageWhenResolution
invalid_state400”Invalid state”State not found/expiredStart new flow
resends_exhausted400”Resends exhausted”resends >= maxResendsStart new flow
resends_not_allowed400”Resend triggered too quick”Resend before intervalWait until resendAfter

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:

ParameterTypeRequiredDescription
statestringYesState key from init response
otpstringYesOTP 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:

ParameterTypeDescription
access_tokenstringJWT access token for API authentication
refresh_tokenstring32-character alphanumeric refresh token for obtaining new access tokens
id_tokenstringJWT ID token containing user information (if OIDC enabled)
sso_tokenstring15-character alphanumeric Single Sign-On token
token_typestringToken type. Always “Bearer”
expires_inintegerAccess token expiration time in seconds
is_new_userbooleanBoolean indicating if user was created in this flow

Error Responses:

CodeStatusMessageWhenResolution
invalid_state400”Invalid state”State not found/expiredStart new flow
incorrect_otp400”Incorrect otp”OTP mismatchRetry (check metadata.otp_retries_left)
retries_exhausted400”Retries exhausted”tries >= maxTriesStart new flow
flow_blocked403”API is blocked for this userIdentifier”User identifier is blockedCheck 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
  }
}

Key schemas:

  • V2PasswordlessInitRequestBody - Init endpoint request
  • V1PasswordlessCompleteRequestBody - Complete endpoint request
  • V2PasswordlessInitResponse - Init endpoint response
  • V2TokenResponse - Complete endpoint response (tokens)

For complete request and response schemas, refer to the Guardian API Specification.

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"
}'
┌─────────┐                    ┌──────────┐                    ┌─────────────┐                    ┌───────────┐
│ 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}│                               │                                   │
     │                              │                               │                                   │

Guardian enforces rate limiting to prevent abuse and ensure security:

TypeDefaultEnforcementAction on Exhaustion
OTP Verification5 attemptsPer stateState deleted, flow terminated
OTP Resend5 attemptsPer stateState deleted, flow terminated
Resend Interval30 secondsTime-basedresendAfter = currentTime + interval
OTP Validity900 seconds (15 min)Redis TTLState 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

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/init again
  • 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

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_config or email_config table
  • 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: true for testing (returns static OTP 999999)

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_left in error response to see remaining attempts

Problem: Maximum OTP verification attempts reached

Solutions:

  • Start a new authentication flow by calling /v2/passwordless/init again
  • Inform user that they’ve exceeded maximum attempts
  • Consider implementing account lockout for security

Problem: Maximum OTP resend attempts reached

Solutions:

  • Start a new authentication flow by calling /v2/passwordless/init again
  • Wait for the resend interval before requesting again
  • Check resend_after timestamp in response to know when resend is allowed

Problem: Guardian cannot communicate with user service

Solutions:

  • Check user service is accessible and running
  • Verify user service configuration in user_config table
  • 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

Problem: Guardian cannot communicate with Email/Sms service

Solutions:

  • Check Email/Sms service is accessible and running
  • Verify Email/Sms service configuration in email_config or sms_config table
  • 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

Problem: Authentication is blocked for user identifier

Solutions:

  • Check error response metadata for retryAfter timestamp
  • Wait until the retry time before attempting again
  • This is a security feature to prevent abuse
  • Contact Guardian administrator if blocking persists
  1. Set appropriate OTP validity period (default: 15 minutes)

  2. Enforce rate limiting to prevent brute force attacks

  3. Use HTTPS for all API calls in production

  4. Store state securely on client side

  5. Don’t expose state in URLs or logs

  6. Use state only once - don’t reuse states

  7. Store tokens securely (HttpOnly cookies or secure storage)

  8. Never expose tokens in client-side code or URLs

  9. Implement token refresh mechanism

  10. Clear tokens on logout