Appearance
Authentication System
Upvendo uses a custom JWT-based authentication system (not Laravel Sanctum/Passport). Three distinct user types share the same JWT infrastructure: Users (backoffice), Customers (online ordering), and Devices (kiosk/KDS).
Key Files
| File | Purpose |
|---|---|
app/Http/Middleware/JwtAuthenticate.php | Core JWT validation middleware |
app/Http/Middleware/Authenticate.php | Laravel auth middleware override |
app/Http/Middleware/TokenType.php | Validates user type matches route |
app/Http/Middleware/OptionalJwtAuth.php | Optional auth (for guest + auth routes) |
app/Http/Middleware/CheckPermission.php | RBAC permission checking |
app/Http/Middleware/SetTenantDatabase.php | Multi-tenant database routing |
app/Http/Middleware/CapacitorApiKey.php | API key auth for Capacitor apps |
app/Services/JwtService.php | JWT token creation and validation |
app/Services/AuthService.php | BackOffice auth business logic |
app/Services/AuthCustomerService.php | Customer auth business logic |
app/Services/AuthDeviceService.php | Device auth business logic |
app/Services/PasskeyService.php | WebAuthn passkey management |
JWT Token Structure
Tokens are generated by JwtService and contain:
Header: { alg: HS256, typ: JWT }
Payload: {
sub: <user_id>, // MongoDB ObjectId as string
type: <user_type>, // "user", "customer", "kiosk", "kds"
tenant_database: <db>, // Tenant MongoDB database name
iat: <timestamp>, // Issued at
exp: <timestamp> // Expiration (absent for device tokens)
}Token Lifetimes
- User tokens: Have expiration (configurable via
JWT_TTLenv variable) - Device tokens: No expiration (
expfield is absent) -- devices stay authenticated until explicitly logged out - Customer tokens: Have expiration
Token Validation Flow
Request -> JwtAuthenticate middleware
1. Extract Bearer token from Authorization header
2. Validate token signature and expiration via JwtService::validateToken()
3. Resolve model from token via JwtService::getModelFromToken()
- Returns User, Device, or Customer model based on token type
4. Set user resolver on Request object
5. Set tenant database from token payload if present
6. Optionally check user type against route requirementsAuthentication Flows
1. Email/Password Login (BackOffice)
Route: POST /api/loginController: AuthController::login()Service: AuthService::loginWithDeviceCheck()
Flow:
- Client sends
{ email, password } LoginRequestvalidates inputAuthService::loginWithDeviceCheck()verifies credentials- If the user has a passkey registered, the response includes
requires_challenge: true- Client must complete a second authentication step (OTP or Passkey)
- On success, returns JWT token and user data
Response (direct login):
json
{
"token": "eyJ...",
"user": { ... }
}Response (requires second factor):
json
{
"requires_challenge": true,
"challenge_methods": ["otp", "passkey"],
"email": "user@example.com"
}2. OTP Authentication (BackOffice Two-Factor)
Routes:
POST /api/back-office/request-otp-- Send OTPPOST /api/back-office/verify-otp-- Verify OTP and get token
Flow:
- After initial login returns
requires_challenge: true - Client calls
request-otpwith{ email } - Server generates 6-digit OTP, stores it with TTL, sends via email
- Job dispatched:
SendBackofficeOTPMail - Client submits
{ email, otp_code }toverify-otp - Server validates OTP code and expiration
- Returns JWT token on success
3. Passkey Authentication (WebAuthn)
Routes:
POST /api/back-office/passkeys-- Get authentication challengePOST /api/back-office/authenticate-passkey-- Verify passkey response
Setup Routes (authenticated):
GET /api/back-office/passkeys/setup-- Get registration optionsPOST /api/back-office/passkeys/setup-- Register passkeyGET /api/back-office/passkeys-- List registered passkeysDELETE /api/back-office/passkeys-- Delete passkey
Flow (authentication):
- Client requests challenge:
POST /passkeyswith{ email } - Server generates WebAuthn challenge via
PasskeyService - Client uses browser WebAuthn API to sign challenge
- Client sends signed response to
authenticate-passkey - Server verifies signature against stored public key
- Returns JWT token on success
Flow (registration):
- Authenticated user requests setup options:
GET /passkeys/setup - Server returns WebAuthn registration options
- Client creates credential via browser API
- Client sends credential to
POST /passkeys/setup - Server stores public key for future authentication
4. Customer OTP Login
Routes:
POST /api/customer/send-otp-- Send OTP to phone/emailPOST /api/customer/login-- Verify OTP and login
Controller: AuthCustomerControllerService: AuthCustomerService
Flow:
- Customer provides phone or email
- OTP sent via SMS (
SendSMSjob) or email (SendVerificationCodeMailjob) - Customer submits OTP code
- Server validates and returns JWT token with
type: customer - If customer doesn't exist, account is created automatically
5. Device Activation (Kiosk/KDS)
Route: POST /api/device-auth/activateController: AuthDeviceController::activate()Service: AuthDeviceService
Flow:
- Merchant creates device in BackOffice, receives activation code
- Activation code can be sent via email:
POST /devices/{id}/send-activation-code - Physical device enters the activation code
POST /device-auth/activatewith{ activation_code }- Server validates code, marks device as active
- Returns JWT token without expiration (persistent auth)
- Token payload includes
type: kioskortype: kds
The device token persists until the device is explicitly logged out or deactivated.
Middleware Stack
Route Middleware Aliases
| Alias | Middleware Class | Description |
|---|---|---|
auth | JwtAuthenticate | JWT token validation (required) |
auth.optional | OptionalJwtAuth | JWT validation if token present |
guest | (none explicitly) | No auth required |
type:{types} | TokenType | Verify token type (user, customer, kiosk, kds) |
permission:{perm} | CheckPermission | RBAC permission check |
tenant:{context} | SetTenantDatabase | Set tenant DB connection |
capacitor.auth | CapacitorApiKey | Validate Capacitor API key header |
check-user-activity | CheckUserActivity | Track user last activity |
super-admin | SuperAdmin | Require super admin role |
Common Middleware Combinations
BackOffice routes:
auth -> type:backoffice -> tenant:backoffice -> check-user-activity -> permission:{specific}Kiosk routes:
auth -> capacitor.auth -> type:kiosk -> tenant:kioskKDS routes:
auth -> type:kds -> tenant:kdsCustomer routes:
auth -> type:customerOnline ordering (guest with optional auth):
auth.optional -> type:customer -> tenant:online-orderingPermission System
Permissions are checked by CheckPermission middleware which delegates to PermissionService.
How Permission Checks Work
- Middleware receives required permission string (e.g.,
permission:VIEW_ITEMS) - Multiple permissions can be OR'd:
permission:EDIT_USERS|EDIT_USER_LOCATION_ACCESS PermissionService::extractVendorAndLocationIds()gets context from requestPermissionService::hasPermission()checks if user's role grants the permission- Returns 403 if no matching permission found
Permission Scoping
Permissions are scoped to vendor + location:
- Global permissions: Apply across all locations for a vendor
- Location-specific permissions: Apply only to specific locations assigned to user
Key Permission Constants
Permissions are defined in App\Constants\Permissions and follow the pattern: VIEW_*, CREATE_*, EDIT_*, DELETE_*, EXPORT_*
Examples: VIEW_ITEMS, CREATE_ITEMS, EDIT_ITEMS, DELETE_ITEMS, EXPORT_ITEMS
Multi-Tenant Database Routing
The SetTenantDatabase middleware configures the MongoDB connection for the current tenant:
- Token payload contains
tenant_databasefield - Middleware sets
config('database.connections.tenant.database')to the tenant DB name - All subsequent queries on the
tenantconnection use the correct database
Context types determine how the tenant is resolved:
tenant:backoffice-- From authenticated user's vendortenant:kiosk/tenant:kds-- From device's vendortenant:online-ordering-- From URL slug parametertenant:customer-- From request context
Token Validation Endpoint
Route: GET /api/validate-token
Used by Cloudflare Workers and other services to verify token validity without full auth context.
Returns:
json
{
"valid": true,
"user_id": "...",
"user_type": "App\\RawModels\\User",
"has_global_access": false,
"issued_at": 1711500000,
"expires_at": 1711586400,
"valid_until": "2025-03-28 00:00:00",
"expires_in_seconds": 86400
}Debugging Authentication Issues
Common Problems
401 "Unauthenticated"
- Token missing from Authorization header
- Token expired (check
expclaim) - Token signature invalid (wrong JWT secret between environments)
403 "Unauthorized: Invalid user type"
- Token type doesn't match route requirement
- Example: customer token used on backoffice route
403 "Unauthorized: Insufficient permissions"
- User's role doesn't include the required permission
- Check
PermissionService::hasPermission()logic - Verify user's assigned roles and location access
Tenant database not set
- Token missing
tenant_databaseclaim - Usually happens with malformed or very old tokens
- Token missing
Quick Checks
- Decode JWT at jwt.io to inspect claims
- Verify
JWT_SECRETenvironment variable matches between services - Check
app/Services/JwtService.phpfor token generation logic - For device tokens, confirm no
expfield (they should be permanent)