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
| Token | Expiry |
|---|---|
| Access token | 4 hours |
| Refresh token | 14 days (web) / never expires (mobile app) |
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)
Every incoming request (except public routes) passes through auth_interceptor():
-
Public route skip — if
request.pathis inConstants.PUBLIC_ROUTESor starts with aConstants.PUBLIC_PREFIXESprefix, or if the method isOPTIONS, the interceptor returnsNoneand the request proceeds without auth. -
Token extraction — reads from
Authorization: Bearer <token>header. If not present, falls back to?jwt=query param. Missing token →401. -
Token decode —
decode_token(token)extractsuserIdandindustry_id(subclaim). Failure →401 "Invalid token format". -
TokenService validation (non-INTERNAL users only) — for any
industry_idother than"INTERNAL", callsTokenService.validate_token():- Checks Cosmos DB
user_tokenscontainer 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
- Checks Cosmos DB
-
verify_jwt_in_request()— Flask-JWT-Extended validates signature, expiry. Populatesg.user_permissions,g.user_id,g.industry_id.
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
After JWT validation, Flask-JWT-Extended calls the @jwt.user_lookup_loader function. This loader:
- Reads
industryIdfrom tokensubclaim - Reads
userIdfrom token additional claims - Checks
Constants.disabled_user_idsandConstants.disabled_industry_idsblocklists →420if found - Loads full user document from Cosmos DB
userscontainer - Loads full industry document from
industriescontainer - Merges them into a single
current_userdict that route handlers access viaflask_jwt_extended.current_user
The current_user dict includes:
userId,email,username,role,permissionsindustryId,industryName,unitsMappingindustryData(full industry document with meta, shifts, configs)nowUTCWithShiftDateTime— server-side UTC time adjusted for the industry shift
JWT Payload
| Claim | Description |
|---|---|
sub | Industry ID (partition key for Cosmos lookups) |
userId | User's unique ID |
email | User's email |
username | User's display name |
role | user, admin, or guest |
permissions | Array of permission strings |
loginType | DEFAULT, SSO, GOOGLE, OTP, or SWAGGER |
exp | Expiration timestamp |
Token Blocklist
Two in-memory blocklists in app/constants/constants.py:
disabled_user_ids = ['user_id_1', ...]
disabled_industry_ids = ['industry_id_1', ...]
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
The @permission_required(permission) decorator:
- Calls
verify_jwt_in_request()to ensure token is valid - Reads
claims.get("permissions", [])from the JWT - Checks if the required permission value (or all values if a list is passed) is present
- 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_hashagainst 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() tidheader is required —1404if missing or invalid- Email extracted from
payload['email']orpayload['preferred_username'] - User must already exist in Cosmos DB —
1402if not found
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
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.
401if expired or missing420if user is in the blocklist
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.
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
| Code | Meaning |
|---|---|
401 | Missing token, invalid format, or insufficient permissions |
420 | JWT token expired, or user/industry in blocklist |
440 | Session expired — token exists but marked inactive in token_logs |
1401 | Invalid credentials or user not found |
1402 | User not found (SSO/Google login) |
1403 | Missing required header (username, password, tid) |
1404 | Invalid Azure AD tenant ID |
1430 | Schema validation error |