Token Lifecycle
This page documents the complete lifecycle of JWT tokens in the MATIH Platform, from initial issuance through refresh, revocation, and expiration. Understanding this lifecycle is essential for implementing correct authentication flows in clients and for debugging token-related issues.
Lifecycle Overview
Every token in the MATIH Platform passes through a defined set of states:
| State | Description | Duration |
|---|---|---|
| Issued | Token is created and returned to the client | Instantaneous |
| Active | Token is valid and can be used for authentication | Until expiration or revocation |
| Expired | Token has passed its exp claim timestamp | Permanent |
| Revoked | Token has been explicitly invalidated before expiration | Permanent |
The transitions between states are one-directional. Once a token expires or is revoked, it cannot be reactivated.
Issuance Flow
User Authentication
The standard user authentication flow produces a token pair (access + refresh):
Step 1: User submits credentials
The client sends a POST request to the IAM service's login endpoint:
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@acme-corp.com",
"password": "********",
"tenant_slug": "acme-corp"
}Step 2: IAM service validates credentials
The IAM service verifies the password against the stored bcrypt hash, checks account status, and loads role assignments for the specified tenant.
Step 3: MFA challenge (if enabled)
If the user has MFA enabled, the IAM service returns a challenge token instead of the full token pair:
{
"mfa_required": true,
"challenge_token": "mfa-challenge-uuid",
"mfa_methods": ["totp", "email"]
}The client must then submit the MFA code:
POST /api/v1/auth/mfa/verify
Content-Type: application/json
{
"challenge_token": "mfa-challenge-uuid",
"code": "123456",
"method": "totp"
}Step 4: Token pair generation
Upon successful authentication (and MFA if required), the IAM service generates a token pair:
// From JwtTokenProvider.java
public TokenPair generateTokenPair(String userId, String tenantId, Set<String> roles) {
return new TokenPair(
generateAccessToken(userId, tenantId, roles),
generateRefreshToken(userId, tenantId)
);
}Step 5: Response to client
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 900,
"tenant_id": "acme-corp",
"roles": ["analyst", "operator"]
}Step 6: Audit logging
Every successful and failed authentication attempt is logged:
// From AuditLogger.java
public void logLogin(String userId, String sourceIp) {
log(AuditEvent.authentication()
.action(AuditEvent.Action.LOGIN)
.userId(userId)
.sourceIp(sourceIp)
.tenantId(LogContext.getTenantId())
.correlationId(LogContext.getCorrelationId())
.success()
.build());
}
public void logLoginFailure(String attemptedUser, String sourceIp, String reason) {
log(AuditEvent.authentication()
.action(AuditEvent.Action.LOGIN)
.detail("attemptedUser", attemptedUser)
.sourceIp(sourceIp)
.failure(reason)
.build());
}Token Refresh Flow
When an access token expires (or is about to expire), the client uses the refresh token to obtain a new access token without requiring the user to re-authenticate.
Step 1: Client detects expiration
The client checks the exp claim or the expires_in value from the original response. Best practice is to refresh proactively when the token has less than 60 seconds remaining.
Step 2: Refresh request
POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiJ9..."
}Step 3: IAM service validates refresh token
The IAM service performs the following checks:
// From JwtTokenValidator.java
public ValidationResult validateRefreshToken(String token) {
ValidationResult result = validate(token);
if (!result.isValid()) {
return result;
}
String type = result.getClaims().get("type", String.class);
if (!"refresh".equals(type)) {
return ValidationResult.failure("Token is not a refresh token");
}
return result;
}- Signature verification (is the token signed by our key?)
- Expiration check (has the refresh token expired?)
- Type validation (is this actually a refresh token, not an access token?)
- Revocation check (has this refresh token been revoked in Redis?)
Step 4: Fresh role lookup
The IAM service loads the user's current roles from the database. This ensures that any role changes made since the last login take effect immediately at the next refresh.
Step 5: New token pair generation
A completely new token pair is generated. The old refresh token is consumed (single-use refresh tokens).
Step 6: Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...(new)",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...(new)",
"token_type": "Bearer",
"expires_in": 900
}Token Revocation
Tokens can be explicitly revoked before their natural expiration. The platform supports three revocation scopes:
Single Token Revocation
Revoke a specific token by its jti claim:
POST /api/v1/auth/revoke
Authorization: Bearer <access_token>
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}The token's jti is added to a Redis blacklist with a TTL matching the token's remaining lifetime. This ensures the blacklist entry is automatically cleaned up after the token would have expired naturally.
Session Revocation
Revoke all tokens associated with a user session:
POST /api/v1/auth/logout
Authorization: Bearer <access_token>This revokes both the access token and the associated refresh token, effectively logging the user out.
User-wide Revocation
Revoke all tokens for a specific user (used by administrators):
POST /api/v1/admin/users/{userId}/revoke-tokens
Authorization: Bearer <admin_access_token>This increments a per-user token version counter in Redis. All tokens issued before this version are considered revoked. This is more efficient than blacklisting individual tokens when a user has many active sessions.
Expiration Handling
Access Token Expiration
Access tokens expire after 15 minutes by default. The expiration time is configurable at JwtTokenProvider construction:
// Default: 15 minutes access, 7 days refresh
public JwtTokenProvider(String secret, String issuer) {
this(secret, issuer, Duration.ofMinutes(15), Duration.ofDays(7));
}
// Custom durations
public JwtTokenProvider(String secret, String issuer,
Duration accessTokenValidity, Duration refreshTokenValidity) {
// ...
}When an expired access token is presented, the validator returns a clear error:
// From JwtTokenValidator.java
try {
Jws<Claims> jws = parser.parseSignedClaims(token);
Claims claims = jws.getPayload();
return ValidationResult.success(claims);
} catch (ExpiredJwtException e) {
return ValidationResult.failure("Token has expired");
}Refresh Token Expiration
Refresh tokens expire after 7 days by default. When a refresh token expires, the user must re-authenticate with their credentials. This provides a hard limit on how long a user can remain authenticated without proving their identity.
Service Token Expiration
Service tokens have a very short lifetime of 5 minutes. Services generate a new token for each inter-service call or batch of calls. This minimizes the window of exposure if a service token is intercepted.
API Key Expiration
API key tokens have configurable expiration set at creation time. The maximum allowed expiration is governed by tenant-level policy (default: 365 days).
Token Lifetime Configuration
| Token Type | Default Lifetime | Configurable | Configuration Method |
|---|---|---|---|
| Access token | 15 minutes | Yes | JwtTokenProvider constructor |
| Refresh token | 7 days | Yes | JwtTokenProvider constructor |
| Service token | 5 minutes | No | Hardcoded for security |
| API key token | Variable | Yes | Set at key creation time |
Environment-based Configuration
In production deployments, token lifetimes are configured via Helm values:
# infrastructure/helm/control-plane/iam-service/values.yaml
env:
JWT_ACCESS_TOKEN_VALIDITY_MINUTES: "15"
JWT_REFRESH_TOKEN_VALIDITY_DAYS: "7"Clock Skew Handling
In distributed systems, clock differences between servers can cause tokens to appear expired on one server while still valid on another. The JJWT library's parser can be configured with a clock skew tolerance:
this.parser = Jwts.parser()
.verifyWith(signingKey)
.requireIssuer(expectedIssuer)
.build();The platform relies on NTP synchronization across all Kubernetes nodes to keep clocks within acceptable bounds. Kubernetes nodes are expected to have clocks synchronized to within 1 second.
Token Storage Best Practices
Client-side Storage
| Storage Method | Security Level | Recommended For |
|---|---|---|
| HttpOnly cookie | High | Web applications |
| Secure memory | High | Mobile applications |
| localStorage | Medium | Single-page apps (with XSS protection) |
| sessionStorage | Medium | Tab-scoped sessions |
The platform's frontend applications store access tokens in memory and refresh tokens in HttpOnly cookies. This prevents XSS attacks from extracting the refresh token.
Server-side Storage
Refresh tokens are tracked server-side in Redis for revocation support:
Key: matih:auth:refresh:{jti}
Value: {userId, tenantId, issuedAt}
TTL: 7 days (matches token expiration)Related Pages
- Token Structure -- Header, payload, and signature details
- Token Validation -- Signature verification and claim validation
- Authentication -- Full authentication architecture