Authentication API
Role-aware authentication with metadata-driven navigation instructions
Overview
The Authentication API provides a secure, role-aware authentication flow. The startAuth
mutation processes device metadata, matches the application to a role, and returns navigation instructions
to guide the client through the appropriate authentication path.
Key Features:
- Role-based authentication using app_name from metadata
- Three navigation paths: ENTER_PASSWORD, VERIFY_OTP, TOAST_INFO
- Automatic metadata processing via MetadataService
- Phone number masking for privacy
- ApiCallLogger integration for audit trail
Quick Start
Copy both sections below into the GraphQL Playground to test immediately.
Mutation (paste in the query panel):
mutation StartAuth($input: StartAuthInput!) {
startAuth(input: $input) {
data {
error_status
error_message
auth {
status
message
phone
username
role
nav
is_verified
user_status
}
url
metadata {
appName
deviceType
deviceOS
ip_address
}
}
}
}
Query Variables (paste in the Variables panel):
{
"input": {
"phone": "+911234567890",
"meta": {
"user_id": "test-user-001",
"userId": "test-user-001",
"device_id": "device-abc-123",
"deviceId": "device-abc-123",
"session_id": "session-xyz-789",
"usageTrackerSessionId": "session-xyz-789",
"appName": "rider_app",
"app_version": "2.1.0",
"appVersion": "2.1.0",
"environment": "prod",
"deviceLocale": "en_US",
"deviceOS": "android",
"deviceOSVersion": "14",
"deviceModel": "Pixel 8",
"deviceManufacturer": "Google",
"deviceType": "mobile",
"deviceNetworkType": "wifi",
"wifi": true,
"is_emulator": false,
"sdk_env": "prod"
}
}
}
Tip: Change appName to match a roles.slug
in your database (e.g., driver_app, admin_app). Change phone to an existing user's
phone to trigger ENTER_PASSWORD, or a new number for VERIFY_OTP.
Authentication Flow
The startAuth mutation follows a decision tree to determine the appropriate navigation instruction:
Decision Logic:
Process Metadata
MetadataService::processMetadata() extracts core, device, network, location, performance, and app data
Extract app_name
Read metadata['app']['name'] to identify the requesting application
Match Role
Role::where('slug', $appName) matches the app to a role (e.g., rider_app → rider)
Check User
Look up phone in AppUser table. If found, verify role assignment
Return Navigation
Respond with the appropriate nav instruction for the client to follow
Flow Diagram:
startAuth(input)
│
├─ processMetadata(meta)
│
├─ phone empty? → TOAST_INFO (MISSING_PHONE)
│
├─ app_name empty/unknown? → TOAST_INFO (ROLE_MISMATCH)
│
├─ role not found by slug? → TOAST_INFO (ROLE_MISMATCH)
│
├─ user exists?
│ ├─ has role? → ENTER_PASSWORD (USER_EXISTS)
│ └─ no role? → TOAST_INFO (ROLE_MISMATCH)
│
└─ new user? → VERIFY_OTP (NEW_USER)
Input Parameters
StartAuthInput
| Field | Type | Required | Description |
|---|---|---|---|
phone |
String! | Required | Phone number with country code (e.g., +911234567890) |
meta |
MetaInput! | Required | Device and application metadata (see below) |
MetaInput Required Fields
| Field Path | Type | Required | Description |
|---|---|---|---|
meta.core.device_id |
String | Required | Unique device identifier/fingerprint |
meta.core.session_id |
String | Required | Current session UUID |
meta.core.request_id |
String | Required | Unique request identifier (UUID) |
meta.app.app_name |
String | Critical | Used for role matching (e.g., rider_app, driver_app) |
Important: The app_name must match a roles.slug value in the database.
If no matching role exists, the mutation returns TOAST_INFO with ROLE_MISMATCH.
Response Structure
The startAuth mutation returns a consistent response structure with navigation instructions:
{
"data": {
"error_status": false,
"error_message": null,
"auth": {
"status": true,
"message": "user Exists",
"phone": "*******7890",
"username": "john_doe",
"role": "Rider",
"nav": "ENTER_PASSWORD",
"is_verified": true,
"user_status": "STATUS_ACTIVE"
},
"url": "USER_EXISTS",
"metadata": { /* raw MetaInput */ }
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
data.error_status |
Boolean | true if there was an error (ROLE_MISMATCH, MISSING_PHONE), false for USER_EXISTS/NEW_USER |
data.error_message |
String | Error description when error_status is true, null otherwise |
data.auth.status |
Boolean | Authentication status (true for valid flow directions) |
data.auth.message |
String | Human-readable status message |
data.auth.username |
String | User's username when user exists, null for new users |
data.auth.phone |
String | Masked phone number (only last 4 digits visible) |
data.auth.role |
String | Matched role name (e.g., Rider, Driver, Admin) |
data.auth.nav |
AuthNavType! | Navigation instruction: ENTER_PASSWORD, VERIFY_OTP, or TOAST_INFO |
data.auth.is_verified |
Boolean | Verification status of the user (true if email AND phone verified), null for new users |
data.auth.user_status |
String | Account status constant: STATUS_INACTIVE, STATUS_ACTIVE, or STATUS_SUSPENDED. null for new users |
data.url |
String | Reason code: USER_EXISTS, NEW_USER, ROLE_MISMATCH, MISSING_PHONE, INTERNAL_ERROR |
data.metadata |
Meta | Echoed back raw metadata from the request |
Navigation Instructions
The nav field tells the client which authentication screen to show:
ENTER_PASSWORD
USER_EXISTS
User exists and has the correct role. Show the password screen.
VERIFY_OTP
NEW_USER
New user. Show the OTP verification screen to register.
TOAST_INFO
ROLE_MISMATCH / Error
Error or role mismatch. Show an informational toast message.
Response Examples
ENTER_PASSWORD (User Exists)
{
"data": {
"error_status": false,
"error_message": null,
"auth": {
"status": true,
"message": "user Exists",
"phone": "*******7890",
"username": "john_doe",
"role": "Rider",
"nav": "ENTER_PASSWORD",
"is_verified": true,
"user_status": "STATUS_ACTIVE"
},
"url": "USER_EXISTS",
"metadata": { ... }
}
}
VERIFY_OTP (New User)
{
"data": {
"error_status": false,
"error_message": null,
"auth": {
"status": true,
"message": "New User",
"phone": "*******7890",
"username": null,
"role": "Rider",
"nav": "VERIFY_OTP",
"is_verified": null,
"user_status": null
},
"url": "NEW_USER",
"metadata": { ... }
}
}
TOAST_INFO (Role Mismatch)
{
"data": {
"error_status": true,
"error_message": "Role mismatch",
"auth": {
"status": false,
"message": "Role mismatch",
"phone": "*******7890",
"username": "john_doe",
"role": "Rider",
"nav": "TOAST_INFO",
"is_verified": true,
"user_status": "STATUS_ACTIVE"
},
"url": "ROLE_MISMATCH",
"metadata": { ... }
}
}
TOAST_INFO (Missing Phone)
{
"data": {
"error_status": true,
"error_message": "Phone number is required",
"auth": {
"status": false,
"message": "Phone number is required",
"phone": null,
"username": null,
"role": null,
"nav": "TOAST_INFO",
"is_verified": null,
"user_status": null
},
"url": "MISSING_PHONE",
"metadata": { ... }
}
}
Metadata Processing
Every startAuth call processes metadata through MetadataService::processMetadata(),
which extracts and normalizes the following data categories:
Client-Side (Must Send)
- meta.core.device_id (Required)
- meta.core.session_id (Required)
- meta.core.request_id (Required)
- meta.app.app_name (Critical for role matching)
- meta.app.app_version (Recommended)
- meta.app.environment (Recommended)
Server-Side (Automatic)
- Metadata processing & normalization
- App-to-role resolution
- Phone existence check
- Role assignment verification
- ApiCallLogger audit logging
Note: The mutation logs all authentication attempts via ApiCallLogger::log()
for audit trail and analytics purposes.
Error Handling
The mutation handles errors gracefully with descriptive reason codes:
| Reason Code | Nav Instruction | Description |
|---|---|---|
| USER_EXISTS | ENTER_PASSWORD | User found and has the correct role assignment |
| NEW_USER | VERIFY_OTP | Phone not found, new user registration flow |
| ROLE_MISMATCH | TOAST_INFO | App name doesn't match any role, or user doesn't have the required role |
| MISSING_PHONE | TOAST_INFO | Phone number was empty or not provided |
| INTERNAL_ERROR | TOAST_INFO | Unexpected server-side error occurred |
Send OTP
The sendOtp mutation sends an OTP to the provided phone number
via the MSG91 gateway. It processes device metadata through MetadataService, stores it in all
respective tables (api_devices, api_sessions, api_app_metadata,
api_network_metadata), and logs the request via ApiCallLogger.
Key Features:
- MSG91 OTP gateway integration with configurable template ID
- Full metadata processing pipeline (device, session, app, network)
- ApiCallLogger audit trail with response payload capture
- Phone number masking for privacy (
+1234****90) - Environment-aware:
sdk_env: "prod"sends real SMS via MSG91
Mutation
mutation SendOtp($phone: String!, $meta: MetaInput) {
sendOtp(phone: $phone, meta: $meta) {
error_status
error_message
auth {
status
message
phone
nav
}
url
metadata {
appName
deviceType
deviceOS
ip_address
}
msg91_request_id
}
}
Query Variables
{
"phone": "+911234567890",
"meta": {
"user_id": "test-user-001",
"userId": "test-user-001",
"device_id": "device-abc-123",
"deviceId": "device-abc-123",
"session_id": "session-xyz-789",
"usageTrackerSessionId": "session-xyz-789",
"appName": "com.rptpl.roadpilot",
"app_version": "2.1.0",
"appVersion": "2.1.0",
"sdk_env": "dev",
"deviceLocale": "en_US",
"deviceOS": "android",
"deviceOSVersion": "14",
"deviceModel": "Pixel 8",
"deviceManufacturer": "Google",
"deviceType": "mobile",
"deviceNetworkType": "wifi",
"wifi": true,
"ip_address": "192.168.1.100",
"lat": 19.0760,
"lng": 72.8777,
"location_accuracy": 25,
"deviceLocationCountry": "IN",
"glocal": "Asia/Kolkata",
"battery_level": 85,
"low_power_mode": false,
"has_nfc": true,
"nfc_enabled": true,
"is_GPS_enabled": true,
"is_network_enabled": true,
"has_biometric": true,
"biometric_type": "fingerprint",
"endpoint": "sendOtp",
"method": "POST",
"request_id": "req-{{timestamp}}",
"client_start_time": {{client_start_time}}
}
}
Response
{
"data": {
"sendOtp": {
"error_status": false,
"error_message": null,
"auth": {
"status": true,
"message": "OTP sent successfully",
"phone": "+9112****90",
"nav": "VERIFY_OTP"
},
"url": "https://server.roadpilot.co.in/sendOtp",
"metadata": {
"appName": "com.rptpl.roadpilot",
"deviceType": "mobile",
"deviceOS": "android",
"ip_address": "192.168.1.100"
},
"msg91_request_id": "msg91_req_abc123xyz"
}
}
}
Send OTP Flow
Execution Steps:
Process Metadata
MetadataService::processMetadata() stores device, session, app metadata, network metadata in their respective tables
Validate Phone
Validates phone is 7-17 digits. Returns INVALID_PHONE if empty or invalid format
Send OTP via MSG91
POSTs to https://control.msg91.com/api/v5/otp with template_id, mobile, and authkey from config('services.msg91')
Check Response
If MSG91 returns type: "success", return VERIFY_OTP nav. Otherwise, return TOAST_INFO with the error message
Log API Call
ApiCallLogger::log() creates ApiRequestLog, stores response payload, and links to device/session metadata
Return Response
Returns StartAuthData with masked phone, MSG91 message, nav instruction, and echoed metadata
Response Codes
| Reason Code | Nav Instruction | HTTP Code | Description |
|---|---|---|---|
| OTP_SENT | VERIFY_OTP | 200 | MSG91 accepted the request and sent OTP |
| OTP_FAILED | TOAST_INFO | 200 | MSG91 returned an error (invalid template, missing auth key, etc.) |
| INVALID_PHONE | TOAST_INFO | 400 | Phone number is empty or doesn't match digit pattern |
| INTERNAL_ERROR | TOAST_INFO | 500 | Unexpected server-side error (MSG91 unreachable, exception, etc.) |
Note: In sdk_env: "dev" mode, the mutation still calls MSG91 and sends a real SMS.
To test without sending real messages, configure the MSG91 template to a test template or use a test phone
number registered in the MSG91 dashboard.