Skip to main content

Authentication

Overview

AquaGen API uses Flask-JWT-Extended for JWT-based authentication. Every protected endpoint goes through a two-layer check: the auth_interceptor (before_request hook) and then @jwt_required() on the route itself.


Token Configuration

TokenExpiry
Access token4 hours
Refresh token14 days (web) / never expires (mobile app)
note

Mobile app refresh tokens are configured to never expire so that users stay logged in indefinitely. Web clients use a 14-day rolling window.

Tokens are accepted via:

  • Authorization: Bearer <token> header
  • ?jwt=<token> query string parameter

How auth_interceptor Works (Global Before-Request Hook)

info

Every incoming request (except public routes) passes through auth_interceptor():

  1. Public route skip — if request.path is in Constants.PUBLIC_ROUTES or starts with a Constants.PUBLIC_PREFIXES prefix, or if the method is OPTIONS, the interceptor returns None and the request proceeds without auth.

  2. Token extraction — reads from Authorization: Bearer <token> header. If not present, falls back to ?jwt= query param. Missing token → 401.

  3. Token decodedecode_token(token) extracts userId and industry_id (sub claim). Failure → 401 "Invalid token format".

  4. TokenService validation (non-INTERNAL users only) — for any industry_id other than "INTERNAL", calls TokenService.validate_token():

    • Checks Cosmos DB user_tokens container for an active token record matching {industry_id}_{user_id}_{device_id}_{base_url_hash}
    • If the token is not found, inactive, or the stored token doesn't match → 440 (session expired / token revoked)
    • INTERNAL industry users skip this check entirely
  5. verify_jwt_in_request() — Flask-JWT-Extended validates signature, expiry. Populates g.user_permissions, g.user_id, g.industry_id.

note

INTERNAL industry users bypass TokenService validation (step 4) entirely. All other industries must have an active record in the user_tokens Cosmos DB container.


How current_user Is Loaded

info

After JWT validation, Flask-JWT-Extended calls the @jwt.user_lookup_loader function. This loader:

  1. Reads industryId from token sub claim
  2. Reads userId from token additional claims
  3. Checks Constants.disabled_user_ids and Constants.disabled_industry_ids blocklists → 420 if found
  4. Loads full user document from Cosmos DB users container
  5. Loads full industry document from industries container
  6. Merges them into a single current_user dict that route handlers access via flask_jwt_extended.current_user

The current_user dict includes:

  • userId, email, username, role, permissions
  • industryId, industryName, unitsMapping
  • industryData (full industry document with meta, shifts, configs)
  • nowUTCWithShiftDateTime — server-side UTC time adjusted for the industry shift

JWT Payload

ClaimDescription
subIndustry ID (partition key for Cosmos lookups)
userIdUser's unique ID
emailUser's email
usernameUser's display name
roleuser, admin, or guest
permissionsArray of permission strings
loginTypeDEFAULT, SSO, GOOGLE, OTP, or SWAGGER
expExpiration timestamp

Token Blocklist

Two in-memory blocklists in app/constants/constants.py:

disabled_user_ids = ['user_id_1', ...]
disabled_industry_ids = ['industry_id_1', ...]
warning

These are loaded at startup. Matching either → 420 "Invalid token" on every request for that user or industry. Logout adds the token's jti to the JWT blocklist via Flask-JWT-Extended's revocation mechanism.


Permission Checking

info

The @permission_required(permission) decorator:

  1. Calls verify_jwt_in_request() to ensure token is valid
  2. Reads claims.get("permissions", []) from the JWT
  3. Checks if the required permission value (or all values if a list is passed) is present
  4. Missing → 401 "Access denied. Missing required permissions: <permission>"

Login Types

DEFAULT (username + password)

All login fields are sent as HTTP request headers, not as a JSON body.

POST /api/user/login
username: john.doe
password: yourpassword
LoginType: DEFAULT
  • Password checked with werkzeug.security.check_password_hash against stored bcrypt hash

SSO (Azure AD)

POST /api/user/login
Authorization: Bearer <azure_ad_token>
tid: <tenant_id>
LoginType: SSO
  • Token verified against tenant's JWKS endpoint via azure_ad_verify_token.verify_jwt()
  • tid header is required — 1404 if missing or invalid
  • Email extracted from payload['email'] or payload['preferred_username']
  • User must already exist in Cosmos DB — 1402 if not found
warning

The tid (tenant ID) header is mandatory for SSO login. Omitting it returns 1404. The user account must also already exist in Cosmos DB before SSO login will succeed.

GOOGLE

POST /api/user/login
Authorization: Bearer <google_id_token>
LoginType: GOOGLE
  • Google ID token verified via Google's token verification endpoint
  • If the user's industry is COMPANY_INDUSTRY_ID, impersonates that company's industry

OTP (phone)

POST /api/user/login
username: +919999999999
LoginType: OTP
  • OTP generated as a TOTP code: key = base32(SECRET_KEY + phNumber)
  • 30-second interval, 5-minute validity window
  • OTP is sent via SMS gateway
  • Verify step: same TOTP algorithm re-runs with the submitted OTP
note

Rate limits: generate 3/min, 10/hour, 20/day — verify 5/min, 15/hour.


Refresh Token

POST /api/user/refreshToken
Authorization: Bearer <refresh_token>

Issues a new access token with the same claims. Refresh tokens are not rotated.

  • 401 if expired or missing
  • 420 if user is in the blocklist
warning

Refresh tokens are not rotated on use. Store them securely — a compromised refresh token remains valid until explicitly revoked via logout (web: 14-day window; mobile: indefinitely).


Logout

POST /api/user/logout
Authorization: Bearer <access_token>

Adds the token jti to the JWT blocklist. All subsequent requests with this exact token return 420.


Swagger Login

POST /api/external/auth/swaggerLogin
Content-Type: application/x-www-form-urlencoded
username=john.doe&password=yourpassword

Used only for Swagger UI access. On success, sets access_token_cookie (httponly, 1-hour expiry) and redirects to /api/external/docs. The token issued has role=guest and loginType=SWAGGER.

note

Swagger login tokens are issued with role=guest and loginType=SWAGGER. They are intended solely for Swagger UI exploration and carry restricted permissions compared to standard user tokens.


Error Codes

CodeMeaning
401Missing token, invalid format, or insufficient permissions
420JWT token expired, or user/industry in blocklist
440Session expired — token exists but marked inactive in token_logs
1401Invalid credentials or user not found
1402User not found (SSO/Google login)
1403Missing required header (username, password, tid)
1404Invalid Azure AD tenant ID
1430Schema validation error