Skip to content

Password Change Token Invalidation

Status: ❌ Not Implemented Priority: 🟡 MEDIUM Estimated Time: 12-16 hours Risk Level: MEDIUM Impact: Security event detection and session invalidation

← Back to Enhancements


Problem Statement

When users change their password at the Identity Provider (IdP), existing access and refresh tokens remain valid until they naturally expire. This creates a security gap where compromised credentials continue to work even after the user has taken remediation action.

Current Behavior:

  • User changes password at IdP
  • Existing access/refresh tokens stored in Seed remain valid
  • Sessions continue to work until token TTL expires (default: 1 hour access, 24 hours refresh)
  • No mechanism to detect or respond to security events

Security Impact:

  • Stolen tokens remain valid after password change
  • Account compromise window extends beyond password reset
  • Users cannot force-logout all sessions after security incident
  • No real-time response to IdP security events

Current Mitigation

Seed currently uses short-lived tokens as a partial mitigation:

  • Access tokens expire after 1 hour (configurable via TOKEN_TTL_SECONDS)
  • Automatic token refresh requires valid refresh token
  • Session TTL limits maximum session lifetime (24 hours default)

This reduces the exposure window but doesn't eliminate it.


Proposed Solutions

Approach: Periodically validate stored tokens against IdP token introspection endpoint.

Implementation:

typescript
// src/services/token-validation.ts

import { config } from "../config/index.js";
import { getTokenStore } from "./token-store.js";
import { logger } from "./logger.js";

/**
 * Validate token with IdP introspection endpoint
 */
async function introspectToken(accessToken: string): Promise<boolean> {
  const response = await fetch(config.oidc.introspectionUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      token: accessToken,
      client_id: config.oidc.clientId,
      client_secret: config.oidc.clientSecret,
    }),
  });

  const data = await response.json();
  return data.active === true;
}

/**
 * Background job to validate stored tokens
 */
export async function validateStoredTokens(): Promise<void> {
  const tokenStore = getTokenStore();
  const allTokens = await tokenStore.getAllUsers();

  for (const userId of allTokens) {
    const tokens = await tokenStore.get(userId);
    if (!tokens) continue;

    const isValid = await introspectToken(tokens.access_token);
    if (!isValid) {
      logger.warn("Token invalidated by IdP, removing from store", { userId });
      await tokenStore.delete(userId);
    }
  }
}

// Run validation every 5 minutes
setInterval(validateStoredTokens, 5 * 60 * 1000);

Pros:

  • Works with any RFC 7662 compliant IdP
  • No changes required to IdP configuration
  • Gradual detection of invalid tokens

Cons:

  • 5-minute lag between password change and detection
  • Adds load to IdP introspection endpoint
  • Requires IdP to support token introspection

Estimated Effort: 6-8 hours


Option 2: Webhook Integration (Complete Solution)

Approach: Receive real-time security events from IdP via webhooks.

Implementation:

typescript
// src/routes/idp-webhook.ts

import { Router, type Request, type Response } from "express";
import { getTokenStore } from "../services/token-store.js";
import { removeAllUserSessions } from "../mcp/mcp.js";
import { logger } from "../services/logger.js";

export const idpWebhookRouter = Router();

interface SecurityEvent {
  event_type: "password_change" | "account_locked" | "mfa_reset";
  user_id: string;
  timestamp: string;
}

/**
 * IdP Security Event Webhook
 * POST /idp/events
 */
idpWebhookRouter.post("/events", async (req: Request, res: Response) => {
  // Validate webhook signature (IdP-specific)
  const signature = req.headers["x-idp-signature"] as string;
  if (!verifyWebhookSignature(req.body, signature)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = req.body as SecurityEvent;

  logger.info("Received IdP security event", {
    eventType: event.event_type,
    userId: event.user_id,
  });

  // Invalidate all tokens and sessions for user
  const tokenStore = getTokenStore();
  await tokenStore.delete(event.user_id);
  await removeAllUserSessions(event.user_id);

  logger.warn("Invalidated all tokens and sessions due to security event", {
    eventType: event.event_type,
    userId: event.user_id,
  });

  res.status(200).json({ status: "processed" });
});

Pros:

  • Real-time invalidation (< 1 second)
  • No polling overhead
  • Comprehensive security event handling

Cons:

  • Requires IdP webhook support
  • Additional endpoint to secure
  • IdP-specific webhook format

Estimated Effort: 12-16 hours (includes IdP configuration, signature validation, testing)


Approach: Validate every token with IdP on each authenticated request.

Pros:

  • Immediate detection of invalid tokens

Cons:

  • High latency impact (adds 50-200ms per request)
  • Significant load on IdP
  • Defeats purpose of JWT validation
  • May violate IdP rate limits

Estimated Effort: 4-6 hours

Recommendation: ❌ Not recommended due to performance impact


Phase 1: Documentation (Immediate)

  • Document current behavior as "acceptable risk" with short-lived tokens
  • Add security best practices guide for token TTL configuration
  • Recommend IdP-side session management

Phase 2: Token Introspection Endpoint (6-8 hours)

  • Implement RFC 7662 token introspection endpoint
  • Add periodic validation background job (5-minute interval)
  • Monitor validation failures and invalidation rate

Phase 3: Webhook Support (12-16 hours, when IdP supports it)

  • Add IdP webhook endpoint for security events
  • Implement signature validation
  • Add comprehensive event handling (password change, account lock, MFA reset)

Configuration

bash
# Token validation settings
TOKEN_VALIDATION_ENABLED=true
TOKEN_VALIDATION_INTERVAL_MS=300000  # 5 minutes

# IdP introspection endpoint
OIDC_INTROSPECTION_URL=https://idp.example.com/oauth/introspect

# Webhook settings (Phase 3)
IDP_WEBHOOK_ENABLED=false
IDP_WEBHOOK_SECRET=your-webhook-secret

Acceptance Criteria

Phase 1 (Documentation):

  • [ ] Document current behavior in security guide
  • [ ] Add token TTL configuration best practices
  • [ ] Clarify acceptable risk window

Phase 2 (Token Introspection):

  • [ ] Implement RFC 7662 introspection endpoint
  • [ ] Add background validation job
  • [ ] Prometheus metrics for validation results
  • [ ] Test with IdP token revocation
  • [ ] Handle validation failures gracefully

Phase 3 (Webhooks):

  • [ ] Add webhook endpoint at /idp/events
  • [ ] Implement signature verification
  • [ ] Handle password change events
  • [ ] Handle account lock events
  • [ ] Comprehensive test coverage
  • [ ] Documentation for IdP configuration


References

Released under the MIT License.