Skip to content

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

FilePurpose
app/Http/Middleware/JwtAuthenticate.phpCore JWT validation middleware
app/Http/Middleware/Authenticate.phpLaravel auth middleware override
app/Http/Middleware/TokenType.phpValidates user type matches route
app/Http/Middleware/OptionalJwtAuth.phpOptional auth (for guest + auth routes)
app/Http/Middleware/CheckPermission.phpRBAC permission checking
app/Http/Middleware/SetTenantDatabase.phpMulti-tenant database routing
app/Http/Middleware/CapacitorApiKey.phpAPI key auth for Capacitor apps
app/Services/JwtService.phpJWT token creation and validation
app/Services/AuthService.phpBackOffice auth business logic
app/Services/AuthCustomerService.phpCustomer auth business logic
app/Services/AuthDeviceService.phpDevice auth business logic
app/Services/PasskeyService.phpWebAuthn 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_TTL env variable)
  • Device tokens: No expiration (exp field 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 requirements

Authentication Flows

1. Email/Password Login (BackOffice)

Route: POST /api/loginController: AuthController::login()Service: AuthService::loginWithDeviceCheck()

Flow:

  1. Client sends { email, password }
  2. LoginRequest validates input
  3. AuthService::loginWithDeviceCheck() verifies credentials
  4. If the user has a passkey registered, the response includes requires_challenge: true
    • Client must complete a second authentication step (OTP or Passkey)
  5. 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 OTP
  • POST /api/back-office/verify-otp -- Verify OTP and get token

Flow:

  1. After initial login returns requires_challenge: true
  2. Client calls request-otp with { email }
  3. Server generates 6-digit OTP, stores it with TTL, sends via email
  4. Job dispatched: SendBackofficeOTPMail
  5. Client submits { email, otp_code } to verify-otp
  6. Server validates OTP code and expiration
  7. Returns JWT token on success

3. Passkey Authentication (WebAuthn)

Routes:

  • POST /api/back-office/passkeys -- Get authentication challenge
  • POST /api/back-office/authenticate-passkey -- Verify passkey response

Setup Routes (authenticated):

  • GET /api/back-office/passkeys/setup -- Get registration options
  • POST /api/back-office/passkeys/setup -- Register passkey
  • GET /api/back-office/passkeys -- List registered passkeys
  • DELETE /api/back-office/passkeys -- Delete passkey

Flow (authentication):

  1. Client requests challenge: POST /passkeys with { email }
  2. Server generates WebAuthn challenge via PasskeyService
  3. Client uses browser WebAuthn API to sign challenge
  4. Client sends signed response to authenticate-passkey
  5. Server verifies signature against stored public key
  6. Returns JWT token on success

Flow (registration):

  1. Authenticated user requests setup options: GET /passkeys/setup
  2. Server returns WebAuthn registration options
  3. Client creates credential via browser API
  4. Client sends credential to POST /passkeys/setup
  5. Server stores public key for future authentication

4. Customer OTP Login

Routes:

  • POST /api/customer/send-otp -- Send OTP to phone/email
  • POST /api/customer/login -- Verify OTP and login

Controller: AuthCustomerControllerService: AuthCustomerService

Flow:

  1. Customer provides phone or email
  2. OTP sent via SMS (SendSMS job) or email (SendVerificationCodeMail job)
  3. Customer submits OTP code
  4. Server validates and returns JWT token with type: customer
  5. 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:

  1. Merchant creates device in BackOffice, receives activation code
  2. Activation code can be sent via email: POST /devices/{id}/send-activation-code
  3. Physical device enters the activation code
  4. POST /device-auth/activate with { activation_code }
  5. Server validates code, marks device as active
  6. Returns JWT token without expiration (persistent auth)
  7. Token payload includes type: kiosk or type: kds

The device token persists until the device is explicitly logged out or deactivated.


Middleware Stack

Route Middleware Aliases

AliasMiddleware ClassDescription
authJwtAuthenticateJWT token validation (required)
auth.optionalOptionalJwtAuthJWT validation if token present
guest(none explicitly)No auth required
type:{types}TokenTypeVerify token type (user, customer, kiosk, kds)
permission:{perm}CheckPermissionRBAC permission check
tenant:{context}SetTenantDatabaseSet tenant DB connection
capacitor.authCapacitorApiKeyValidate Capacitor API key header
check-user-activityCheckUserActivityTrack user last activity
super-adminSuperAdminRequire 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:kiosk

KDS routes:

auth -> type:kds -> tenant:kds

Customer routes:

auth -> type:customer

Online ordering (guest with optional auth):

auth.optional -> type:customer -> tenant:online-ordering

Permission System

Permissions are checked by CheckPermission middleware which delegates to PermissionService.

How Permission Checks Work

  1. Middleware receives required permission string (e.g., permission:VIEW_ITEMS)
  2. Multiple permissions can be OR'd: permission:EDIT_USERS|EDIT_USER_LOCATION_ACCESS
  3. PermissionService::extractVendorAndLocationIds() gets context from request
  4. PermissionService::hasPermission() checks if user's role grants the permission
  5. 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:

  1. Token payload contains tenant_database field
  2. Middleware sets config('database.connections.tenant.database') to the tenant DB name
  3. All subsequent queries on the tenant connection use the correct database

Context types determine how the tenant is resolved:

  • tenant:backoffice -- From authenticated user's vendor
  • tenant:kiosk / tenant:kds -- From device's vendor
  • tenant:online-ordering -- From URL slug parameter
  • tenant: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

  1. 401 "Unauthenticated"

    • Token missing from Authorization header
    • Token expired (check exp claim)
    • Token signature invalid (wrong JWT secret between environments)
  2. 403 "Unauthorized: Invalid user type"

    • Token type doesn't match route requirement
    • Example: customer token used on backoffice route
  3. 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
  4. Tenant database not set

    • Token missing tenant_database claim
    • Usually happens with malformed or very old tokens

Quick Checks

  • Decode JWT at jwt.io to inspect claims
  • Verify JWT_SECRET environment variable matches between services
  • Check app/Services/JwtService.php for token generation logic
  • For device tokens, confirm no exp field (they should be permanent)