Id RFC 005 session service

RFC-005 — Session Service

  • *tatus:*Draft
  • *ate:*20260408
  • *uthor:*Koder Team
  • *epends on:*RFC001, 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:

  1. Client sends current refresh token
  2. Session service looks up the token hash in KDB
  3. 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
  4. 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 out

Access 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_user limits 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

Source: ../home/koder/dev/koder/meta/docs/stack/rfcs/id-RFC-005-session-service.md