Authentication
Overview
Q01 Core APIs use JSON Web Tokens (JWT) for stateless authentication. Every API request must include a valid JWT in the Authorization header. Tokens carry user identity, tenant context, and permissions.
Benefits:
- ✅ Stateless (no server-side session storage)
- ✅ Self-contained (includes user claims)
- ✅ Cryptographically signed (tamper-proof)
- ✅ Expiring (security via short TTL)
- ✅ Portable (works across services)
Authentication Flow:
1. Client → POST /auth/login (username/password)
2. Server validates credentials
3. Server generates access token (15 min active + 60 min renewable)
4. Server generates refresh token (30 days TTL)
5. Client stores tokens
6. Client → API request with access token
7. Server validates token
8. Server executes request
9. Access token expires → 401 Unauthorized
10. Client → POST /auth/refresh (refresh token)
11. Server generates new access token
12. Repeat from step 6
Three-Phase Authentication Process
The authentication system consists of three distinct phases that occur during login:
Phase 1: Authentication
Credential Verification:
- User submits username/password OR access_token
- Credentials validated against msauth microservice
- Invalid credentials → 401 Unauthorized
- Session establishment begins
Purpose: Verify user identity
Phase 2: Profiling
User Context Retrieval:
- User information loaded (email, name, tenant_id)
- User grants fetched from TB_MENU (nested set hierarchy)
- User level (peso) determined (1=admin, 2=manager, 3=user)
- Context established: source, centro_dett, ambiente, modulo, lingua
- Default preferences loaded
Purpose: Build user authorization context
Phase 3: Interface/Database Accessibility
Session Data Storage:
- BOX and MENU items user can access retrieved
- Interface rendering data prepared (visible menu items, buttons, forms)
- Database connection information configured (per tenant)
- All profiling data stored in TB_ANAG_SESSIONS00
- sessionId generated and inserted into JWT payload
Purpose: Prepare application session environment
Complete Flow:
Login Request
↓
Phase 1: Verify credentials (msauth)
↓
Phase 2: Load user profile + grants + context (TB_MENU)
↓
Phase 3: Prepare interface data + store in TB_ANAG_SESSIONS00
↓
Generate JWT with sessionId reference
↓
Return access_token + refresh_token
JWT Token Structure
Token Components
JWT has three parts: Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidXNlckBleGFtcGxlLmNvbSIsInRlbmFudF9pZCI6InRlbmFudF8xMjMiLCJzb3VyY2UiOiJwcm9kdWN0TWFuYWdlbWVudCIsImNlbnRyb19kZXR0IjoiYWRtaW4iLCJwZXNvIjoiMSIsImFtYmllbnRlIjoicHJvZHVjdGlvbiIsImdyYW50cyI6WyJwcm9kdWN0cy5yZWFkIiwicHJvZHVjdHMud3JpdGUiXSwiZXhwIjoxNzM1NTc0NDAwLCJpYXQiOjE3MzU0ODgwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Base64-decoded structure:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
{
"user_id": "user@example.com",
"tenant_id": "tenant_123",
"sessionId": "uuid-session-123",
"source": "productManagement",
"centro_dett": "admin",
"peso": "1",
"ambiente": "production",
"lingua": "en",
"grants": ["products.read", "products.write", "orders.read"],
"exp": 1735574400,
"iat": 1735488000,
"iss": "q01-auth-service",
"sub": "user@example.com"
}
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Standard Claims
| Claim | Description | Example |
|---|---|---|
iss | Issuer (who created token) | q01-auth-service |
sub | Subject (user identifier) | user@example.com |
aud | Audience (intended recipient) | q01-core-api |
exp | Expiration timestamp | 1735574400 |
iat | Issued at timestamp | 1735488000 |
nbf | Not before timestamp | 1735488000 |
jti | JWT ID (unique identifier) | uuid-... |
Custom Claims
| Claim | Description | Example |
|---|---|---|
user_id | User email/username | user@example.com |
tenant_id | Tenant identifier | tenant_123 |
sessionId | Session reference (TB_ANAG_SESSIONS00) | uuid-session-123 |
source | Application/tenant source | productManagement |
centro_dett | Organizational unit | admin |
peso | User level (1=admin, 2=manager, 3=user) | 1 |
ambiente | Environment | production |
lingua | User language | en |
grants | Permission array | ["products.read", "products.write"] |
Session Management
sessionId Concept
The JWT payload includes a sessionId field that references a server-side session record in TB_ANAG_SESSIONS00. This hybrid approach combines stateless JWT benefits with server-side session data.
Why sessionId?
- JWT remains stateless and portable
- Server can look up detailed session data without bloating token
- Enables server-side session management (revocation, updates)
- Stores database connections, grants, interface permissions
TB_ANAG_SESSIONS00 Structure:
CREATE TABLE TB_ANAG_SESSIONS00 (
SESSION_ID VARCHAR(36) PRIMARY KEY, -- UUID referenced in JWT
USER_ID VARCHAR(100), -- User identifier
TENANT_ID VARCHAR(50), -- Tenant
GRANTS JSON, -- User permissions from TB_MENU
DB_CONNECTIONS JSON, -- Source-specific DB configs
INTERFACE_DATA JSON, -- BOX/MENU items user can access
CONTEXT JSON, -- source, peso, ambiente, centro_dett
CREATED_AT TIMESTAMP,
EXPIRES_AT TIMESTAMP,
LAST_ACTIVITY TIMESTAMP
);
Example Session Data:
{
"SESSION_ID": "uuid-session-123",
"USER_ID": "user@example.com",
"TENANT_ID": "tenant_123",
"GRANTS": [
{"COD_MENU": "settings-2", "NLEFT": 2, "NRIGHT": 9, "OPERATIONS": ["read", "write"]},
{"COD_MENU": "sales-1", "NLEFT": 20, "NRIGHT": 35, "OPERATIONS": ["read"]}
],
"DB_CONNECTIONS": {
"productManagement": {
"host": "db.example.com",
"database": "tenant_123_products"
}
},
"INTERFACE_DATA": {
"menus": ["Products", "Orders", "Reports"],
"boxes": ["Dashboard", "Analytics"]
},
"CONTEXT": {
"source": "productManagement",
"peso": 1,
"ambiente": "production",
"centro_dett": "admin",
"lingua": "en"
},
"CREATED_AT": "2025-12-19T14:00:00Z",
"EXPIRES_AT": "2025-12-19T15:00:00Z",
"LAST_ACTIVITY": "2025-12-19T14:30:00Z"
}
How sessionId Works:
API Request with JWT
↓
Server decodes JWT payload
↓
Extract sessionId from payload
↓
SELECT * FROM TB_ANAG_SESSIONS00 WHERE SESSION_ID = ?
↓
Load user grants, context, DB connections
↓
Execute request with session context
Benefits:
- ✅ Reduce JWT size (don't embed all grants in token)
- ✅ Server-side session revocation (delete session record)
- ✅ Update permissions without re-issuing token
- ✅ Audit trail (track session creation, expiration, activity)
- ✅ Multi-database routing (source-specific DB configs)
Token Validation
Server-Side Validation
CoreQuery/CoreWrite validate every request:
// RequestValidatorSubscriber.php
public function validateJWT(string $token): array {
// 1. Decode header and payload
$parts = explode('.', $token);
if (count($parts) !== 3) {
throw new AuthenticationException('Invalid token format');
}
[$headerB64, $payloadB64, $signatureB64] = $parts;
// 2. Verify signature
$secret = getenv('JWT_SECRET');
$expectedSignature = hash_hmac(
'sha256',
"$headerB64.$payloadB64",
$secret,
true
);
if (!hash_equals(base64_decode($signatureB64), $expectedSignature)) {
throw new AuthenticationException('Invalid token signature');
}
// 3. Decode payload
$payload = json_decode(base64_decode($payloadB64), true);
// 4. Check expiration
if ($payload['exp'] < time()) {
throw new AuthenticationException('Token expired');
}
// 5. Check not-before
if (isset($payload['nbf']) && $payload['nbf'] > time()) {
throw new AuthenticationException('Token not yet valid');
}
// 6. Check issuer
if ($payload['iss'] !== 'q01-auth-service') {
throw new AuthenticationException('Invalid token issuer');
}
return $payload;
}
Validation Failures
TOKEN_INVALID:
{
"error": "AuthenticationError",
"message": "Invalid or malformed token",
"code": "TOKEN_INVALID",
"status": 401
}
TOKEN_EXPIRED:
{
"error": "AuthenticationError",
"message": "Token has expired",
"code": "TOKEN_EXPIRED",
"status": 401,
"expired_at": "2025-12-19T15:00:00Z"
}
TOKEN_MISSING:
{
"error": "AuthenticationError",
"message": "Authorization header missing",
"code": "TOKEN_MISSING",
"status": 401
}
Token Validator Endpoint
The platform provides a dedicated endpoint for validating JWT tokens without executing any business logic.
Endpoint:
GET /api/v4/auth/tokenvalidator
Headers:
Authorization: Bearer {access_token}
microservice: msauth
Success Response (200):
{
"code": 200,
"message": "Token is Valid"
}
Error Response (401):
{
"code": 401,
"msg": "Invalid Auth Token provided. Kindly login again to get valid token"
}
Use Cases:
- Health check for token validity
- Pre-flight validation before expensive operations
- Client-side token validation before API calls
- Token debugging and troubleshooting
Example JavaScript Usage:
async function isTokenValid(token) {
try {
const response = await fetch('/api/v4/auth/tokenvalidator', {
headers: {
'Authorization': `Bearer ${token}`,
'microservice': 'msauth'
}
});
if (response.status === 200) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
}
// Usage
if (!await isTokenValid(currentToken)) {
await refreshToken();
}
Login Flow
Obtaining Tokens
POST /auth/login:
POST /auth/login
Content-Type: application/json
{
"username": "user@example.com",
"password": "secure_password",
"source": "productManagement",
"centro_dett": "admin"
}
Response:
{
"status": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"user_id": "user@example.com",
"tenant_id": "tenant_123",
"source": "productManagement",
"centro_dett": "admin",
"peso": "1",
"grants": ["products.read", "products.write"]
}
}
}
Access Token Authentication
As an alternative to username/password, the login endpoint accepts an existing access_token for authentication. This is useful for:
- OAuth/SSO integrations
- Service-to-service authentication
- Token exchange scenarios
- Migration from other authentication systems
POST /auth/login with access_token:
POST /auth/login
Content-Type: application/json
{
"access_token": "existing_valid_token",
"resource": "productManagement",
"tenantId": "tenant_123"
}
Response:
Same as username/password login - returns new access_token and refresh_token for Q01 platform.
{
"status": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"user_id": "user@example.com",
"tenant_id": "tenant_123",
"source": "productManagement",
"peso": "1"
}
}
}
Use Case Example - OAuth Integration:
// User logs in via OAuth provider (Google, Microsoft, etc.)
const oauthToken = await oauthProvider.authenticate();
// Exchange OAuth token for Q01 platform token
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
access_token: oauthToken,
resource: 'productManagement',
tenantId: 'tenant_123'
})
});
const { access_token, refresh_token } = await response.json();
// Now use Q01 access_token for Core API requests
const products = await fetch('/api/v4/core/PRD', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
JavaScript Implementation
class AuthService {
constructor() {
this.authBase = 'https://api.example.com';
}
async login(username, password, source, centro_dett) {
const response = await fetch(`${this.authBase}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password,
source,
centro_dett
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const data = await response.json();
// Store tokens securely
this.storeTokens(data.data.access_token, data.data.refresh_token);
return data.data.user;
}
storeTokens(accessToken, refreshToken) {
// ✅ Good - httpOnly cookie (server-side)
// Set by server via Set-Cookie header
// ⚠️ Alternative - sessionStorage (less secure, but simpler)
sessionStorage.setItem('access_token', accessToken);
sessionStorage.setItem('refresh_token', refreshToken);
}
getAccessToken() {
return sessionStorage.getItem('access_token');
}
getRefreshToken() {
return sessionStorage.getItem('refresh_token');
}
}
Making Authenticated Requests
Request Header
Every API request includes JWT:
GET /api/v4/core/PRD
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JavaScript API Client
class CoreAPIClient {
constructor(authService) {
this.authService = authService;
this.apiBase = 'https://api.example.com';
}
async request(path, options = {}) {
const token = this.authService.getAccessToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${this.apiBase}${path}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
// Handle token expiration
if (response.status === 401) {
const refreshed = await this.handleTokenExpiration();
if (refreshed) {
// Retry request with new token
return this.request(path, options);
} else {
// Redirect to login
window.location.href = '/login';
throw new Error('Authentication required');
}
}
return response;
}
async handleTokenExpiration() {
try {
await this.authService.refreshAccessToken();
return true;
} catch (error) {
return false;
}
}
async getProducts() {
const response = await this.request('/api/v4/core/PRD');
return response.json();
}
async createProduct(data) {
const response = await this.request('/api/v4/core/PRD', {
method: 'POST',
body: JSON.stringify({ data })
});
return response.json();
}
}
Token Refresh
Refresh Flow
Access token expires (15-75 minutes) → Use refresh token to get new access token
Client: Access token expired (401)
↓
Client: POST /auth/refresh (refresh token)
↓
Server: Validate refresh token
↓
Server: Generate new access token
↓
Client: Store new access token
↓
Client: Retry original request
Refresh Endpoint
POST /auth/refresh:
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response:
{
"status": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
}
JavaScript Refresh Implementation
class AuthService {
async refreshAccessToken() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.authBase}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
});
if (!response.ok) {
// Refresh token invalid or expired
this.clearTokens();
throw new Error('Refresh token expired');
}
const data = await response.json();
// Store new access token
sessionStorage.setItem('access_token', data.data.access_token);
return data.data.access_token;
}
clearTokens() {
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('refresh_token');
}
}
Automatic Refresh
Proactively refresh before expiration:
class AuthService {
startTokenRefreshTimer() {
// Decode token to get expiration
const token = this.getAccessToken();
const payload = this.decodeToken(token);
const expiresAt = payload.exp * 1000; // Convert to milliseconds
const now = Date.now();
// Refresh 5 minutes before expiration
const refreshIn = expiresAt - now - (5 * 60 * 1000);
if (refreshIn > 0) {
setTimeout(async () => {
try {
await this.refreshAccessToken();
this.startTokenRefreshTimer(); // Schedule next refresh
} catch (error) {
console.error('Token refresh failed:', error);
window.location.href = '/login';
}
}, refreshIn);
}
}
decodeToken(token) {
const parts = token.split('.');
const payload = JSON.parse(atob(parts[1]));
return payload;
}
}
// Usage
const authService = new AuthService();
await authService.login(username, password, source, centro_dett);
authService.startTokenRefreshTimer(); // Auto-refresh
Token Storage
Security Considerations
Options:
-
HttpOnly Cookies (Most Secure)
- ✅ Not accessible via JavaScript (XSS protection)
- ✅ Automatically sent with requests
- ⚠️ Requires server-side cookie handling
- ⚠️ CSRF protection needed
-
SessionStorage (Moderate Security)
- ✅ Cleared when tab closes
- ✅ Not shared across tabs
- ⚠️ Vulnerable to XSS attacks
- ✅ Simple implementation
-
LocalStorage (Least Secure)
- ❌ Persistent across sessions
- ❌ Shared across tabs
- ❌ Vulnerable to XSS attacks
- ✅ Survives page reloads
Recommendation: Use httpOnly cookies for production, sessionStorage for development.
HttpOnly Cookie Implementation
Server sets cookie:
Set-Cookie: access_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
Set-Cookie: refresh_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000; Path=/auth/refresh
Client automatically sends cookie:
// No need to manually add Authorization header
const response = await fetch('/api/v4/core/PRD', {
credentials: 'include' // Send cookies
});
Logout Flow
Logout Endpoint
POST /auth/logout:
POST /auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"status": "success",
"message": "Logged out successfully"
}
JavaScript Logout
class AuthService {
async logout() {
const token = this.getAccessToken();
if (token) {
try {
await fetch(`${this.authBase}/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
} catch (error) {
console.error('Logout failed:', error);
}
}
// Clear local tokens
this.clearTokens();
// Redirect to login
window.location.href = '/login';
}
}
Best Practices
✅ DO:
Use HTTPS in production:
// ✅ Good - encrypted transport
const apiBase = 'https://api.example.com';
Refresh tokens proactively:
// ✅ Good - refresh 5 minutes before expiration
if (tokenExpiresIn() < 5 * 60) {
await refreshAccessToken();
}
Handle 401 errors gracefully:
// ✅ Good - auto-retry with refresh
if (response.status === 401) {
await refreshAccessToken();
return retryRequest();
}
Clear tokens on logout:
// ✅ Good - complete cleanup
sessionStorage.clear();
❌ DON'T:
Don't store tokens in localStorage:
// ❌ Bad - XSS vulnerable
localStorage.setItem('token', jwt);
Don't log tokens:
// ❌ Bad - exposes tokens in logs
console.log('Token:', token);
Don't send tokens in URL:
// ❌ Bad - visible in browser history
fetch(`/api/products?token=${token}`);
Don't skip token validation:
// ❌ Bad - trusting client-side token
const payload = JSON.parse(atob(token.split('.')[1]));
// Always validate server-side!
Summary
- ✅ JWT tokens provide stateless authentication
- ✅ Tokens include user identity, context, and permissions (including sessionId)
- ✅ Access tokens have two states: active (15 min) + renewable (60 min)
- ✅ Refresh tokens extend session (30 days)
- ✅ Session data stored in TB_ANAG_SESSIONS00 (referenced by sessionId)
- ✅ Three-phase authentication: Authentication → Profiling → Interface Accessibility
- ✅ Server validates signature, expiration, issuer
- ✅ 401 errors trigger token refresh
- ✅ HttpOnly cookies recommended for storage
- ✅ HTTPS required in production
Key Takeaways:
- Every API request requires valid JWT in Authorization header
- Tokens are cryptographically signed and self-contained
- JWT includes sessionId referencing TB_ANAG_SESSIONS00 for detailed session data
- Access tokens expire quickly (15 min active + 60 min renewable)
- Handle 401 errors by refreshing token and retrying
- Store tokens securely (httpOnly cookies or sessionStorage)
- Always use HTTPS to protect tokens in transit
- Clear tokens on logout
- Can authenticate with username/password OR access_token
Token Lifecycle:
Login → Access Token (15min active + 60min renewable) + Refresh Token (30d)
↓
API Requests (Authorization: Bearer {access_token})
↓
Access Token Expires → 401
↓
Refresh Token → New Access Token
↓
Continue API Requests
↓
Refresh Token Expires → Redirect to Login
Related Concepts
- Authorization - TB_MENU grants
- Security Overview - Multi-level security
- Tenant Isolation - Multi-tenancy
- Context Security - source/peso/ambiente
- Error Handling - 401/403 errors