Skip to main content

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:

{
"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

ClaimDescriptionExample
issIssuer (who created token)q01-auth-service
subSubject (user identifier)user@example.com
audAudience (intended recipient)q01-core-api
expExpiration timestamp1735574400
iatIssued at timestamp1735488000
nbfNot before timestamp1735488000
jtiJWT ID (unique identifier)uuid-...

Custom Claims

ClaimDescriptionExample
user_idUser email/usernameuser@example.com
tenant_idTenant identifiertenant_123
sessionIdSession reference (TB_ANAG_SESSIONS00)uuid-session-123
sourceApplication/tenant sourceproductManagement
centro_dettOrganizational unitadmin
pesoUser level (1=admin, 2=manager, 3=user)1
ambienteEnvironmentproduction
linguaUser languageen
grantsPermission 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:

  1. HttpOnly Cookies (Most Secure)

    • ✅ Not accessible via JavaScript (XSS protection)
    • ✅ Automatically sent with requests
    • ⚠️ Requires server-side cookie handling
    • ⚠️ CSRF protection needed
  2. SessionStorage (Moderate Security)

    • ✅ Cleared when tab closes
    • ✅ Not shared across tabs
    • ⚠️ Vulnerable to XSS attacks
    • ✅ Simple implementation
  3. 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.

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:

  1. Every API request requires valid JWT in Authorization header
  2. Tokens are cryptographically signed and self-contained
  3. JWT includes sessionId referencing TB_ANAG_SESSIONS00 for detailed session data
  4. Access tokens expire quickly (15 min active + 60 min renewable)
  5. Handle 401 errors by refreshing token and retrying
  6. Store tokens securely (httpOnly cookies or sessionStorage)
  7. Always use HTTPS to protect tokens in transit
  8. Clear tokens on logout
  9. 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