Token Lifecycle
JWT tokens are the primary authentication mechanism throughout the MATIH platform. The IAM service generates, validates, and manages the lifecycle of multiple token types. Every authenticated request -- whether from a browser, CLI, or service-to-service call -- carries a JWT that the receiving service validates to extract identity and authorization context.
Token Types
The JwtTokenProvider generates four distinct token types, each with a specific purpose and claims structure:
| Token Type | Purpose | Default TTL | Subject |
|---|---|---|---|
| Access Token | User authentication | 15 minutes | User email |
| Refresh Token | Access token renewal | 7 days | User email |
| OAuth2 Access Token | OAuth2 client flows | Client-configured | User email or client ID |
| Impersonation Token | Admin troubleshooting | 1-4 hours | Target user email |
Access Token Claims
The standard access token is issued during login and registration. Its claims carry the user's identity and authorization context:
{
"sub": "jane.doe@acme.com",
"iss": "matih-platform",
"aud": "matih-api",
"iat": 1707735000,
"exp": 1707735900,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"user_id": 42,
"tenant_id": 7,
"roles": ["ADMIN", "ANALYST"]
}Claims Reference
| Claim | Type | Description |
|---|---|---|
sub | String | User email address (the subject) |
iss | String | Token issuer, always "matih-platform" |
aud | String | Token audience, always "matih-api" |
iat | Number | Issued-at timestamp (Unix epoch seconds) |
exp | Number | Expiration timestamp (Unix epoch seconds) |
jti | String | Unique token identifier (UUID v4) |
user_id | Number | MATIH user ID |
tenant_id | Number | Tenant ID for multi-tenancy scoping |
roles | Array | List of role authority strings |
Tenant Isolation via Claims
The tenant_id claim is the cornerstone of multi-tenant data isolation. Every service that receives a request extracts this claim and uses it to scope database queries, API calls, and resource access:
// Example: extracting tenant context in a data plane service
Long tenantId = jwtTokenProvider.getTenantIdFromToken(token);
TenantContext.setCurrentTenant(tenantId);This pattern is enforced by the shared commons-java library. Services that fail to extract and apply the tenant context will reject the request.
Refresh Token Claims
Refresh tokens have a minimal claims set to reduce their attack surface:
{
"sub": "jane.doe@acme.com",
"iss": "matih-platform",
"aud": "matih-api",
"iat": 1707735000,
"exp": 1708339800,
"jti": "660e8400-e29b-41d4-a716-446655440001",
"type": "refresh"
}The type: "refresh" claim distinguishes refresh tokens from access tokens. The isRefreshToken() method on JwtTokenProvider checks for this claim.
Refresh Token Rotation
When a refresh token is used to obtain a new access token, the IAM service implements token rotation:
- The old refresh token is looked up in the
refresh_tokenstable - Its
usedflag is set totrue - A new refresh token is generated and persisted
- Both new access token and new refresh token are returned
If a previously-used refresh token is presented (replay attack), all refresh tokens for that user are revoked and the user must re-authenticate.
Client IAM Service Database
| | |
| POST /auth/refresh | |
| {refreshToken: "old"} | |
|------------------------->| |
| | Lookup refresh token |
| |------------------------------>|
| | Mark old token as used |
| | Generate new access token |
| | Generate new refresh token |
| | Persist new refresh token |
| |------------------------------>|
| {accessToken, refreshToken} |
|<-------------------------| |OAuth2 Token Claims
OAuth2 access tokens include client and scope information:
Authorization Code Grant Token
{
"sub": "jane.doe@acme.com",
"iss": "matih-platform",
"aud": "my-app-client-id",
"iat": 1707735000,
"exp": 1707738600,
"jti": "770e8400-e29b-41d4-a716-446655440002",
"client_id": "my-app-client-id",
"scope": "read write",
"user_id": 42,
"tenant_id": 7,
"token_type": "access_token"
}Client Credentials Grant Token
{
"sub": "my-service-client-id",
"iss": "matih-platform",
"aud": "my-service-client-id",
"iat": 1707735000,
"exp": 1707738600,
"jti": "880e8400-e29b-41d4-a716-446655440003",
"client_id": "my-service-client-id",
"scope": "api:read api:write",
"tenant_id": 7,
"token_type": "access_token",
"grant_type": "client_credentials"
}Note that client credentials tokens do not contain a user_id claim since they operate in a service context.
Impersonation Token Claims
Impersonation tokens allow administrators to act as another user for troubleshooting. They include full audit trail information:
{
"sub": "target.user@acme.com",
"iss": "matih-platform",
"aud": "matih-api",
"iat": 1707735000,
"exp": 1707742200,
"jti": "990e8400-e29b-41d4-a716-446655440004",
"user_id": 99,
"tenant_id": 7,
"roles": ["ANALYST"],
"impersonation": true,
"impersonation_session_id": "imp_a1b2c3d4e5f6",
"impersonator_id": 1,
"impersonator_email": "admin@matih.ai"
}| Claim | Description |
|---|---|
impersonation | Boolean flag, always true for impersonation tokens |
impersonation_session_id | Links to the ImpersonationSession record |
impersonator_id | The admin user performing the impersonation |
impersonator_email | The admin's email for audit trail |
Services can detect impersonation by checking the impersonation claim and log both the acting user and the impersonator for audit compliance.
Token Validation
The JwtTokenProvider.validateToken() method performs the following checks:
| Check | Exception on Failure |
|---|---|
| Signature verification (HS256) | SignatureException |
| Token structure (well-formed JWT) | MalformedJwtException |
| Expiration check | ExpiredJwtException |
| Supported algorithm | UnsupportedJwtException |
| Non-empty claims | IllegalArgumentException |
Validation in Other Services
Data plane services validate tokens using the shared commons-java JwtTokenProvider class. The validation key is distributed via Kubernetes secrets:
# Helm values for a data plane service
env:
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: matih-jwt-secret
key: secret-keyThis ensures all services use the same signing key and can validate tokens independently without calling back to the IAM service.
Token Extraction Methods
The JwtTokenProvider provides typed extraction methods for each claim:
// Extract username (subject)
String username = jwtTokenProvider.getUsernameFromToken(token);
// Extract user ID
Long userId = jwtTokenProvider.getUserIdFromToken(token);
// Extract tenant ID
Long tenantId = jwtTokenProvider.getTenantIdFromToken(token);
// Extract scopes (OAuth2 tokens)
Set<String> scopes = jwtTokenProvider.getScopesFromToken(token);
// Extract JTI (token ID)
String jti = jwtTokenProvider.getJtiFromToken(token);
// Extract client ID (OAuth2 tokens)
String clientId = jwtTokenProvider.getClientIdFromToken(token);
// Check token type
boolean isRefresh = jwtTokenProvider.isRefreshToken(token);
boolean isOAuth2 = jwtTokenProvider.isOAuth2Token(token);
boolean isImpersonation = jwtTokenProvider.isImpersonationToken(token);
// Extract impersonation details
Long impersonatorId = jwtTokenProvider.getImpersonatorId(token);
String impersonatorEmail = jwtTokenProvider.getImpersonatorEmail(token);
String sessionId = jwtTokenProvider.getImpersonationSessionId(token);
// Extract from HTTP request
Long userId = jwtTokenProvider.getUserIdFromRequest(httpServletRequest);Token Revocation
Tokens can be revoked through several mechanisms:
| Mechanism | Scope | Latency |
|---|---|---|
| Refresh token revocation | Single refresh token | Immediate |
| Session revocation | All tokens in a session | Immediate |
| OAuth2 token revocation | Single access or refresh token | Immediate |
| User lockout | All active sessions | Immediate |
| Permission cache flush | Refreshes on next request | Eventual (cache TTL) |
Since access tokens are stateless JWTs, revocation of access tokens is not immediate. The token remains valid until expiration. For immediate revocation, services can check a token blocklist stored in Redis:
Redis Key: "token:revoked:{jti}"
TTL: Same as the token's remaining lifetimeThe API Gateway checks this blocklist on every request, providing platform-wide immediate revocation at a small performance cost.
Token Configuration
Token lifetimes are configurable through Spring Boot properties:
| Property | Default | Description |
|---|---|---|
matih.security.jwt.access-token-expiration | 900000 (15 min) | Access token lifetime in milliseconds |
matih.security.jwt.refresh-token-expiration | 604800000 (7 days) | Refresh token lifetime in milliseconds |
matih.security.jwt.secret-key | (from secret) | HS256 signing key (minimum 256 bits) |
matih.security.jwt.issuer | matih-platform | Token issuer claim |
matih.security.jwt.audience | matih-api | Token audience claim |
OAuth2 clients have their own configurable token lifetimes set per client:
| Client Property | Description |
|---|---|
accessTokenValidity | Access token lifetime in seconds |
refreshTokenValidity | Refresh token lifetime in seconds |
Security Considerations
Key management. The JWT signing key is stored as a Kubernetes secret and injected via environment variable. It is never committed to source code or configuration files.
Algorithm choice. HS256 (HMAC-SHA256) is used because all services share the same signing key via Kubernetes secrets. If the platform evolves to support third-party token issuers, migration to RS256 (RSA-SHA256) with public/private key pairs would be required.
Minimum key length. The JwtTokenProvider constructor enforces a minimum 32-byte (256-bit) key length. If the configured key is shorter, it is padded to meet the HS256 minimum requirement.
Token size. Access tokens with standard claims are approximately 400-600 bytes. OAuth2 tokens with scope lists may be larger. The API Gateway is configured to handle tokens up to 8 KB.
Next Steps
- Session Management -- how tokens bind to sessions
- API Reference -- all token-related endpoints
- API Gateway -- how tokens are validated at the edge