Auth & Credentials — login, OAuth2, MCP, refresh tokens

Six flows showing how identity moves through the system: browser login (cookie), refresh rotation, OAuth2 Authorization Code + PKCE for agents, MCP tool calls with Bearer JWT, internal gRPC trust, and Stripe webhook signatures. Source: 05-auth-flows.mermaid

100%
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FF9900','primaryBorderColor':'#232F3E','primaryTextColor':'#232F3E','lineColor':'#232F3E','actorBkg':'#FF9900','actorBorder':'#232F3E','actorTextColor':'#232F3E','noteBkgColor':'#FDF6E3','noteTextColor':'#232F3E','noteBorderColor':'#232F3E','fontFamily':'Helvetica, Arial, sans-serif'}}}%%
sequenceDiagram
    autonumber
    actor U as Customer (browser)
    actor DEV as Developer (terminal)
    participant CLI as Claude / MCP client
    participant MCP as ticketing-mcp-server (local)
    participant KC as OS Keychain
    participant FE as client (Next.js SSR)
    participant K as kong-gateway
    participant AS as auth-service (NestJS)
    participant RD as Redis (auth-service)
    participant DS as downstream services
(order/ticket/venue/payment) participant ST as Stripe %% ----------- FLOW A: Browser login (cookie + refresh) ---------- rect rgb(250,243,219) Note over U,AS: Flow A — Browser login · email+password → JWT cookie + refresh token U->>FE: GET /signin (SSR page) U->>FE: POST credentials (Server Action) FE->>K: POST /auth/login (no JWT required) K->>AS: forward (rate-limited) AS->>AS: bcrypt verify · sign RS256 JWT (15m, sub, email, jti, iss=auth-service) AS->>RD: SET refresh:<jti> {userId, family} TTL 7d AS-->>K: Set-Cookie: access_token (HttpOnly, Secure, SameSite=Lax, 15m)
Set-Cookie: refresh_token (HttpOnly, /auth/refresh, 7d) K-->>FE: cookies forwarded FE-->>U: 302 / (logged in) end %% ----------- FLOW B: Refresh token rotation ------------------- rect rgb(234,242,251) Note over U,AS: Flow B — Refresh rotation · access token expired, refresh still valid U->>K: any request → 401 (JWT expired) FE->>K: POST /auth/refresh (refresh_token cookie) K->>AS: forward AS->>RD: GET refresh:<jti> · validate family · DEL old jti AS->>AS: issue new JWT + new refresh (rotated jti, same family) AS->>RD: SET refresh:<new_jti> TTL 7d AS-->>FE: new cookies Note right of AS: Reuse of revoked refresh token → revoke entire family
(token theft detection) end %% ----------- FLOW C: OAuth2 Auth Code + PKCE (MCP) ------------ rect rgb(245,238,245) Note over DEV,AS: Flow C — Agent onboarding · OAuth2 Authorization Code + PKCE (RFC 7636) DEV->>CLI: claude /mcp authorize ticketing CLI->>MCP: spawn local MCP server (stdio) MCP->>MCP: generate code_verifier · S256(code_verifier) → code_challenge
start loopback listener http://127.0.0.1:19836/callback MCP->>DEV: print authorize URL (browser opens) DEV->>U: paste / auto-open in browser U->>K: GET /oauth/authorize?client_id=ticketing-mcp&code_challenge=...&scope=orders:create+... K->>AS: forward (JWT cookie validated — user must be logged in) alt first-party (ticketing-mcp) AS->>AS: auto-approve else dynamic / third-party client AS->>RD: store PendingConsent (TTL 10m) → request_id AS-->>U: 302 /oauth/consent?request_id=... U->>FE: render scope card · click Allow FE->>K: POST /oauth/consent/<id> (JWT cookie) K->>AS: forward · resolve consent end AS->>RD: SET oauth:code:<code> {clientId, userId, scope, code_challenge} TTL 600s AS-->>U: 302 http://127.0.0.1:19836/callback?code=<code> U-->>MCP: browser hits loopback listener MCP->>K: POST /oauth/token (code, code_verifier, client_id) K->>AS: forward (no JWT — public endpoint) AS->>RD: GET + DEL oauth:code:<code> · verify S256(code_verifier)==stored challenge AS->>RD: SET refresh:<jti> + oauth:session-scope:<sid> {scope, clientId} TTL 24h AS-->>MCP: { access_token (15m, +scope+client_id claims), refresh_token, expires_in, scope } MCP->>KC: store tokens in OS keychain (never on disk) end %% ----------- FLOW D: Authenticated MCP tool call -------------- rect rgb(231,242,222) Note over CLI,DS: Flow D — MCP tool call · Bearer JWT, scope enforced at gateway DEV->>CLI: "create order for ticket X" CLI->>MCP: invoke tool createOrder MCP->>KC: read access_token alt access_token expired MCP->>K: POST /oauth/token (grant_type=refresh_token) K->>AS: forward · rotate · return new tokens AS-->>MCP: new { access_token, refresh_token } MCP->>KC: update keychain end MCP->>K: POST /orders
Authorization: Bearer <jwt> K->>K: jwt plugin · verify RS256 via JWKS (kid match) · check exp/iss K->>K: jwt-scope.lua · require "orders:create" in token.scope K->>K: jwt-sub.lua · inject X-User-Id, X-User-Email, X-Client-Id K->>DS: forward (mesh-internal, plain HTTP) DS-->>K: 201 Created K-->>MCP: 201 MCP-->>CLI: tool result end %% ----------- FLOW E: Internal service-to-service ------------- rect rgb(253,238,238) Note over DS,DS: Flow E — Internal gRPC · no JWT, mesh-internal trust boundary DS->>DS: order-service → ticket-service gRPC ReserveQuota
(metadata: x-correlation-id, x-user-id propagated) Note right of DS: Inside VPC + EKS NetworkPolicy ·
SecurityGroups restrict pod-to-pod ·
no Internet ingress to gRPC ports end %% ----------- FLOW F: Stripe webhook (no JWT) ------------------ rect rgb(240,240,253) Note over ST,DS: Flow F — Stripe webhook · signed payload, signature header replaces JWT ST->>K: POST /payments/webhook
Stripe-Signature: t=...,v1=HMAC_SHA256(body, secret) K->>K: route bypasses jwt plugin (allowlist) · forward K->>DS: payment-service receives raw body DS->>DS: verify HMAC · idempotency on stripe_event_id · UPSERT payment_webhooks DS-->>K: 200 OK (must respond <5s) end Note over K: Token security · access JWT 15m · refresh 7d (browser) / 24h (MCP)
RS256 keypair rotated quarterly · JWKS at /.well-known/jwks.json
All 4xx/5xx auth failures audited to CloudWatch · brute-force throttled per IP+user