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
%%{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