Id RFC 005 session service
RFC-005 — Session Service
- *tatus:*Draft
- *ate:*2026
0408 - *uthor:*Koder Team
- *epends on:*RFC
001, RFC002
Summary
The Session service manages the hybrid session model: stateless JWT access tokens (shortlived, 15 min) combined with stateful refresh tokens (stored in KDB, 30day lifetime). It handles session creation, token refresh with rotation, session revocation, and token introspection support.
Hybrid Session Model
┌──────────────────────────────────────────────────────────┐
│ Access Token (JWT) │
│ - Self-contained (no DB lookup on every request) │
│ - Short-lived: 15 minutes │
│ - Validated by resource servers locally (verify sig) │
│ - Cannot be revoked instantly (expires naturally) │
│ - Contains: sub, scope, tenant_id, client_id, exp │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Refresh Token (opaque) │
│ - Stored in KDB (hashed) │
│ - Long-lived: 30 days │
│ - Validated by Session service (DB lookup) │
│ - Can be revoked instantly │
│ - Rotated on every use (old token invalidated) │
│ - Format: krt_{ULID} │
└──────────────────────────────────────────────────────────┘Why Hybrid?
| Approach | Latency per request | Instant revocation | Scalability |
|---|---|---|---|
| Stateful only | High (DB every request) | Yes | Limited |
| JWT only | Zero (local verify) | No | Unlimited |
| *ybrid* | *ero (local verify)* | *ithin 15 min* | *nlimited* |
The worst case is a 15minute window where a revoked access token is still valid. For critical operations (password change, privilege escalation), resource servers should call the introspection endpoint to check realtime status.
Session Lifecycle
Create Refresh Revoke
──────► ──────► ──────►
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Active │─────▶│ Refreshed │─────▶│ Revoked │
│ │ │ (new tokens) │ │ │
└──────────┘ └──────────────┘ └──────────┘
│ ▲
│ Expired (30 days) │
└───────────────────────────────────────┘API Endpoints (REST)
POST /v1/sessions
Create a new session (called by Auth service after successful authentication).
*equest:*
{
"user_id": "01HXK...",
"client_id": "my-app",
"scopes": ["openid", "profile", "email"],
"ip_address": "203.0.113.1",
"user_agent": "Mozilla/5.0..."
}*esponse:*
{
"session_id": "01HXK...",
"access_token": "eyJhbGciOi...",
"refresh_token": "krt_01HXK...",
"token_type": "Bearer",
"expires_in": 900
}POST /v1/sessions/refresh
Refresh tokens (rotation: old refresh token invalidated, new one issued).
*equest:*
{
"refresh_token": "krt_01HXK...",
"client_id": "my-app"
}*esponse:*
{
"session_id": "01HXK...",
"access_token": "eyJhbGciOi...",
"refresh_token": "krt_01HXL...",
"token_type": "Bearer",
"expires_in": 900
}DELETE /v1/sessions/{session_id}
Revoke a specific session (logout).
DELETE /v1/sessions?user_id={user_id}
Revoke all sessions for a user (force logout everywhere).
GET /v1/sessions?user_id={user_id}
List active sessions for a user (for "active sessions" management page).
*esponse:*
{
"sessions": [
{
"session_id": "01HXK...",
"client_id": "my-app",
"ip_address": "203.0.113.1",
"user_agent": "Mozilla/5.0...",
"last_active_at": "2026-04-08T10:30:00Z",
"created_at": "2026-04-07T08:00:00Z"
}
]
}gRPC Service Definition
syntax = "proto3";
package koder.id.session.v1;
service SessionService {
// Create a new session with tokens
rpc CreateSession(CreateSessionRequest) returns (SessionTokens);
// Refresh tokens (rotate refresh token)
rpc RefreshSession(RefreshSessionRequest) returns (SessionTokens);
// Revoke a specific session
rpc RevokeSession(RevokeSessionRequest) returns (RevokeSessionResponse);
// Revoke all sessions for a user
rpc RevokeUserSessions(RevokeUserSessionsRequest) returns (RevokeUserSessionsResponse);
// Get session details
rpc GetSession(GetSessionRequest) returns (Session);
// List active sessions for a user
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse);
// Validate access token (introspection support)
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
}Refresh Token Rotation
On every refresh:
- Client sends current refresh token
- Session service looks up the token hash in KDB
- If found and valid:
- Generate new refresh token
- Hash and store the new token
- Invalidate the old token
- Issue new access token (JWT)
- Update
last_active_at
- If the old (already-used) token is presented again → *oken reuse detected*
- Revoke the entire session (all tokens)
- Log security event
- This indicates the refresh token was stolen
Token Reuse Detection:
Legitimate user Attacker (stole refresh token)
│ │
Uses RT-1 ──► Gets RT-2 │
│ Uses RT-1 ──► REUSE DETECTED!
│ │ Entire session revoked
Uses RT-2 ──► Gets RT-3 │ Both user and attacker
│ │ are logged outAccess Token Generation
The Session service generates JWTs signed with the current signing key from the OAuth2/OIDC service:
type AccessTokenClaims struct {
jwt.RegisteredClaims
ClientID string `json:"client_id"`
Scope string `json:"scope"`
TenantID string `json:"tenant_id"`
}
func (s *SessionService) generateAccessToken(session *Session, signingKey *rsa.PrivateKey, kid string) (string, error) {
now := time.Now()
claims := AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: s.issuerURL,
Subject: session.UserID,
Audience: jwt.ClaimStrings{session.ClientID},
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ID: ulid.New(),
},
ClientID: session.ClientID,
Scope: strings.Join(session.Scopes, " "),
TenantID: session.TenantID,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = kid
return token.SignedString(signingKey)
}Session Configuration (per tenant)
{
"access_token_ttl": "15m",
"refresh_token_ttl": "30d",
"max_sessions_per_user": 10,
"idle_timeout": "7d",
"absolute_timeout": "30d",
"token_reuse_detection": true
}| Setting | Default | Description |
|---|---|---|
access_token_ttl |
15 min | JWT lifetime |
refresh_token_ttl |
30 days | Refresh token max age |
max_sessions_per_user |
10 | Oldest session evicted when exceeded |
idle_timeout |
7 days | Session killed if no refresh in this period |
absolute_timeout |
30 days | Session killed regardless of activity |
token_reuse_detection |
true | Revoke session on refresh token reuse |
Background Jobs
| Job | Interval | Description |
|---|---|---|
| Expired session cleanup | 1 hour | Delete sessions past absolute_timeout |
| Idle session cleanup | 1 hour | Delete sessions past idle_timeout |
| Access token metadata cleanup | 1 hour | Delete expired access_tokens records |
Security Considerations
- Refresh tokens stored as SHA-256 hashes (never plaintext)
- Token reuse detection prevents stolen refresh token exploitation
max_sessions_per_userlimits resource consumption per user- IP address and user agent tracked for anomaly detection (future)
- Idle timeout forces re-authentication for inactive users
- All session mutations produce audit log entries