Skip to content

Instantly share code, notes, and snippets.

@sunfmin
Created December 24, 2025 09:55
Show Gist options
  • Select an option

  • Save sunfmin/e7165d4958bf4da267a6a3c45b2fae27 to your computer and use it in GitHub Desktop.

Select an option

Save sunfmin/e7165d4958bf4da267a6a3c45b2fae27 to your computer and use it in GitHub Desktop.
iam-architecture.md

IAM Architecture for Admin Consoles

This document describes the Identity and Access Management (IAM) architecture for our suite of admin consoles (CMS, OMS, PIM, etc.). The architecture centralizes authentication and authorization through an API Gateway, allowing individual services to focus on business logic.

Table of Contents

  1. Overview
  2. Architecture
  3. Components
  4. Authentication Flows
  5. Token Management
  6. Authorization
  7. API Specifications
  8. Security Considerations

Overview

Goals

  • Single Sign-On (SSO): Users authenticate once and access all admin consoles seamlessly
  • Centralized User Management: Manage users, roles, and permissions in one place
  • Simplified Backend Services: Individual services don't implement auth logic
  • Consistent Security: Uniform authentication and authorization across all applications

Key Design Decisions

Decision Choice Rationale
Auth Location API Gateway Centralizes auth logic, simplifies backends
Token Format JWT (access) + Opaque (refresh) JWTs for stateless validation, opaque for revocability
Token Storage HttpOnly Cookies Prevents XSS token theft
Auth Protocol OAuth 2.0 + PKCE Industry standard, secure for SPAs

Architecture

High-Level Architecture

flowchart TB
    subgraph Frontends
        CMS_FE[CMS Frontend<br/>React SPA]
        OMS_FE[OMS Frontend<br/>React SPA]
        PIM_FE[PIM Frontend<br/>React SPA]
    end

    subgraph Gateway Layer
        GW[API Gateway<br/>━━━━━━━━━━━━<br/>• Authentication<br/>• Token Validation<br/>• Token Refresh<br/>• Route Management]
    end

    subgraph Backend Services
        CMS_BE[CMS Backend<br/>Go API]
        OMS_BE[OMS Backend<br/>Go API]
        PIM_BE[PIM Backend<br/>Go API]
    end

    subgraph Identity
        IAM[IAM Service<br/>━━━━━━━━━━━━<br/>• User Management<br/>• Authentication<br/>• Token Issuance<br/>• Role & Permission]
        DB[(IAM Database)]
    end

    CMS_FE --> GW
    OMS_FE --> GW
    PIM_FE --> GW

    GW --> CMS_BE
    GW --> OMS_BE
    GW --> PIM_BE
    GW <--> IAM

    IAM --> DB
Loading

Request Flow Overview

flowchart LR
    Browser[Browser] -->|1. Request + Cookies| GW[API Gateway]
    GW -->|2. Validate Token| GW
    GW -->|3. Refresh if needed| IAM[IAM]
    GW -->|4. Forward + Headers| Backend[Backend Service]
    Backend -->|5. Response| GW
    GW -->|6. Response + New Cookies| Browser
Loading

Components

API Gateway

The API Gateway is the single entry point for all frontend applications. It handles:

  • Authentication: Validates access tokens on every request
  • Token Refresh: Automatically refreshes expired access tokens
  • Request Routing: Routes requests to appropriate backend services
  • Header Injection: Adds user context headers for downstream services

IAM Service

The IAM service is responsible for:

  • User Management: CRUD operations for users
  • Authentication: Validates credentials, issues tokens
  • Session Management: Tracks active sessions for SSO
  • Role & Permission Management: Defines and assigns roles/permissions

Backend Services

Backend services (CMS, OMS, PIM) are simplified:

  • No auth logic: Trust the gateway's validation
  • Header-based context: Read user info from injected headers
  • Permission checking: Verify permissions for specific actions

Authentication Flows

Login Flow

This flow describes how a user logs into an admin console for the first time.

sequenceDiagram
    autonumber
    participant B as Browser
    participant FE as CMS Frontend
    participant GW as API Gateway
    participant IAM as IAM Service

    B->>FE: Visit /admin
    FE->>B: Load SPA

    Note over B,FE: Check for existing session
    B->>GW: GET /api/auth/me
    GW->>B: 401 Unauthorized

    Note over B,GW: Initiate login
    B->>GW: GET /api/auth/login?redirect=/admin
    GW->>GW: Generate state & PKCE
    GW->>B: Redirect to IAM /authorize

    Note over B,IAM: User authentication
    B->>IAM: GET /authorize?client_id=gateway&...
    IAM->>B: Show login page
    B->>IAM: POST /login (credentials)
    IAM->>IAM: Validate credentials
    IAM->>IAM: Create IAM session
    IAM->>B: Set IAM session cookie<br/>Redirect to /callback?code=xxx

    Note over B,GW: Token exchange
    B->>GW: GET /api/auth/callback?code=xxx&state=yyy
    GW->>GW: Validate state
    GW->>IAM: POST /token (code + PKCE verifier)
    IAM->>IAM: Validate code & PKCE
    IAM->>GW: access_token + refresh_token
    GW->>B: Set httpOnly cookies<br/>Redirect to /admin

    Note over B,FE: Authenticated session
    B->>FE: Load /admin
    FE->>GW: GET /api/cms/dashboard
    GW->>GW: Validate token ✓
    GW->>B: Dashboard data
Loading

SSO Flow (Already Logged In)

When a user is already logged into one console and accesses another.

sequenceDiagram
    autonumber
    participant B as Browser
    participant OMS as OMS Frontend
    participant GW as API Gateway
    participant IAM as IAM Service

    Note over B: User already logged into CMS<br/>Has IAM session cookie

    B->>OMS: Visit OMS /admin
    OMS->>B: Load SPA

    B->>GW: GET /api/auth/me
    Note over GW: No OMS cookies yet
    GW->>B: 401 Unauthorized

    B->>GW: GET /api/auth/login?redirect=/admin
    GW->>B: Redirect to IAM /authorize

    B->>IAM: GET /authorize?client_id=gateway&...
    Note over IAM: IAM session cookie present!
    IAM->>IAM: Session valid, skip login
    IAM->>B: Redirect to /callback?code=xxx

    B->>GW: GET /api/auth/callback?code=xxx
    GW->>IAM: POST /token (exchange code)
    IAM->>GW: access_token + refresh_token
    GW->>B: Set cookies, redirect to /admin

    Note over B,OMS: User is now logged into OMS<br/>without entering credentials
Loading

API Request Flow

Standard flow for authenticated API requests.

sequenceDiagram
    autonumber
    participant B as Browser
    participant FE as CMS Frontend
    participant GW as API Gateway
    participant BE as CMS Backend

    B->>FE: User clicks "Save"
    FE->>GW: POST /api/cms/content<br/>Cookie: access_token=xxx

    GW->>GW: Extract token from cookie
    GW->>GW: Validate JWT signature
    GW->>GW: Check token expiry ✓

    Note over GW: Token valid, inject headers
    GW->>BE: POST /content<br/>X-User-ID: user123<br/>X-User-Email: user@example.com<br/>X-User-Roles: editor<br/>X-User-Permissions: cms:content:*

    BE->>BE: Check permission<br/>cms:content:write ✓
    BE->>BE: Process request
    BE->>GW: 200 OK + response body

    GW->>B: 200 OK + response body
    FE->>B: Show success message
Loading

Token Refresh Flow

Automatic token refresh when access token expires.

sequenceDiagram
    autonumber
    participant B as Browser
    participant GW as API Gateway
    participant IAM as IAM Service
    participant BE as CMS Backend

    B->>GW: GET /api/cms/content<br/>Cookie: access_token=xxx, refresh_token=yyy

    GW->>GW: Validate access_token
    Note over GW: Token expired!

    GW->>IAM: POST /token<br/>grant_type=refresh_token<br/>refresh_token=yyy

    IAM->>IAM: Validate refresh token
    IAM->>IAM: Check not revoked
    IAM->>IAM: Generate new tokens
    IAM->>IAM: Rotate refresh token

    IAM->>GW: new access_token<br/>new refresh_token

    Note over GW: Continue with new token
    GW->>BE: GET /content<br/>X-User-ID: user123<br/>...

    BE->>GW: 200 OK + data

    GW->>B: 200 OK + data<br/>Set-Cookie: access_token=new_xxx<br/>Set-Cookie: refresh_token=new_yyy
Loading

Logout Flow

sequenceDiagram
    autonumber
    participant B as Browser
    participant FE as Frontend
    participant GW as API Gateway
    participant IAM as IAM Service

    B->>FE: Click "Logout"
    FE->>GW: POST /api/auth/logout<br/>Cookie: access_token, refresh_token

    GW->>IAM: POST /revoke<br/>refresh_token=yyy
    IAM->>IAM: Revoke refresh token
    IAM->>IAM: Revoke token family
    IAM->>GW: 200 OK

    GW->>B: 200 OK<br/>Set-Cookie: access_token=#59; Max-Age=0<br/>Set-Cookie: refresh_token=#59; Max-Age=0

    FE->>B: Redirect to login page

    Note over B,IAM: Optional: Full SSO logout
    B->>IAM: GET /logout?post_logout_redirect=...
    IAM->>IAM: Clear IAM session
    IAM->>B: Clear IAM cookie<br/>Redirect to app
Loading

Session Expiry Flow

When refresh token is expired or revoked.

sequenceDiagram
    autonumber
    participant B as Browser
    participant FE as Frontend
    participant GW as API Gateway
    participant IAM as IAM Service

    B->>GW: GET /api/cms/content<br/>Cookie: access_token=xxx, refresh_token=yyy

    GW->>GW: Validate access_token
    Note over GW: Token expired!

    GW->>IAM: POST /token<br/>grant_type=refresh_token<br/>refresh_token=yyy

    IAM->>IAM: Validate refresh token
    Note over IAM: Refresh token expired<br/>or revoked!

    IAM->>GW: 401 Invalid refresh token

    GW->>B: 401 Unauthorized<br/>Set-Cookie: access_token=#59; Max-Age=0<br/>Set-Cookie: refresh_token=#59; Max-Age=0

    FE->>FE: Detect 401 response
    FE->>B: Redirect to /api/auth/login

    Note over B,IAM: User must re-authenticate
Loading

Token Management

Token Types

flowchart LR
    subgraph Access Token
        AT[JWT<br/>━━━━━━━━━━<br/>Short-lived: 15-60 min<br/>Contains user claims<br/>Validated locally]
    end

    subgraph Refresh Token
        RT[Opaque<br/>━━━━━━━━━━<br/>Long-lived: 7-30 days<br/>Stored in database<br/>Supports revocation]
    end

    subgraph IAM Session
        IS[Cookie<br/>━━━━━━━━━━<br/>Enables SSO<br/>Stored in IAM<br/>Browser session or persistent]
    end
Loading

Access Token Structure

{
  "header": {
    "alg": "RS256",
    "typ": "JWT",
    "kid": "key-2024-01"
  },
  "payload": {
    "iss": "https://iam.example.com",
    "sub": "user_abc123",
    "aud": "admin-gateway",
    "exp": 1703001600,
    "iat": 1703000700,
    "jti": "token_xyz789",
    
    "email": "user@example.com",
    "name": "John Doe",
    "roles": ["cms_editor", "oms_viewer"],
    "permissions": [
      "cms:content:read",
      "cms:content:write",
      "oms:orders:read"
    ]
  }
}

Refresh Token Rotation

flowchart TD
    subgraph Token Family
        RT1[Refresh Token 1<br/>Created at login]
        RT2[Refresh Token 2<br/>After 1st refresh]
        RT3[Refresh Token 3<br/>After 2nd refresh]
    end

    RT1 -->|Used| RT2
    RT2 -->|Used| RT3

    RT1 -.->|Reuse attempt| REVOKE[Revoke entire family<br/>Possible token theft!]

    style REVOKE fill:#ff6b6b
Loading

Cookie Configuration

Cookie Scope Flags Max-Age
access_token /api HttpOnly, Secure, SameSite=Strict 15-60 min
refresh_token /api/auth HttpOnly, Secure, SameSite=Strict 7-30 days
iam_session IAM domain HttpOnly, Secure, SameSite=Lax Session or 30 days

Authorization

Permission Model

flowchart TB
    subgraph Users
        U1[Alice]
        U2[Bob]
        U3[Charlie]
    end

    subgraph Roles
        R1[CMS Admin]
        R2[CMS Editor]
        R3[OMS Viewer]
    end

    subgraph Permissions
        P1[cms:*]
        P2[cms:content:read]
        P3[cms:content:write]
        P4[cms:settings:read]
        P5[oms:orders:read]
    end

    U1 --> R1
    U2 --> R2
    U2 --> R3
    U3 --> R3

    R1 --> P1
    R2 --> P2
    R2 --> P3
    R3 --> P5
Loading

Permission Format

Permissions follow the pattern: {service}:{resource}:{action}

Component Description Examples
Service Target application cms, oms, pim
Resource Entity type content, orders, products, settings
Action Operation read, write, delete, * (all)

Examples:

  • cms:content:read - Read CMS content
  • cms:content:* - All operations on CMS content
  • cms:* - All CMS permissions
  • *:*:read - Read access to everything

Authorization Check Flow

flowchart TD
    REQ[Incoming Request] --> GW{API Gateway}

    GW -->|Validate Token| CHECK1{Token Valid?}
    CHECK1 -->|No| DENY1[401 Unauthorized]
    CHECK1 -->|Yes| INJECT[Inject User Headers]

    INJECT --> BE[Backend Service]
    BE --> CHECK2{Has Permission?}
    CHECK2 -->|No| DENY2[403 Forbidden]
    CHECK2 -->|Yes| PROCESS[Process Request]
    PROCESS --> RESP[200 Response]
Loading

Permission Checking in Backend

flowchart LR
    subgraph Request Headers
        H1[X-User-ID: user123]
        H2[X-User-Permissions:<br/>cms:content:read,<br/>cms:content:write]
    end

    subgraph Backend Logic
        CHECK{Permission<br/>Check}
        BL[Business Logic]
    end

    H2 --> CHECK
    CHECK -->|Has cms:content:write| BL
    CHECK -->|Missing permission| DENY[403 Forbidden]
Loading

API Specifications

Gateway Auth Endpoints

GET /api/auth/login

Initiates the login flow.

Query Parameters:

Parameter Type Description
redirect string URL to redirect after login

Response: Redirects to IAM authorize endpoint


GET /api/auth/callback

Handles OAuth callback from IAM.

Query Parameters:

Parameter Type Description
code string Authorization code from IAM
state string State parameter for CSRF protection

Response:

  • Success: Redirects to original destination, sets cookies
  • Error: Redirects to login with error

GET /api/auth/me

Returns current user information.

Response:

{
  "id": "user_abc123",
  "email": "user@example.com",
  "name": "John Doe",
  "roles": ["cms_editor", "oms_viewer"],
  "permissions": [
    "cms:content:read",
    "cms:content:write",
    "oms:orders:read"
  ]
}

POST /api/auth/logout

Logs out the current user.

Response:

  • Clears auth cookies
  • Returns 200 OK

IAM Endpoints

POST /token

Token endpoint for code exchange and refresh.

Request (Authorization Code):

{
  "grant_type": "authorization_code",
  "code": "auth_code_xxx",
  "redirect_uri": "https://gateway/api/auth/callback",
  "client_id": "admin-gateway",
  "code_verifier": "pkce_verifier_xxx"
}

Request (Refresh Token):

{
  "grant_type": "refresh_token",
  "refresh_token": "refresh_token_xxx",
  "client_id": "admin-gateway"
}

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "new_refresh_token_xxx"
}

POST /revoke

Revokes a refresh token.

Request:

{
  "token": "refresh_token_xxx",
  "token_type_hint": "refresh_token"
}

Response: 200 OK


Injected Headers

Headers added by the gateway for downstream services:

Header Description Example
X-User-ID Unique user identifier user_abc123
X-User-Email User's email address user@example.com
X-User-Name User's display name John Doe
X-User-Roles Comma-separated roles cms_editor,oms_viewer
X-User-Permissions Comma-separated permissions cms:content:read,cms:content:write
X-Request-ID Unique request identifier req_xyz789

Security Considerations

Token Security

flowchart TB
    subgraph Threats
        T1[XSS Attack]
        T2[CSRF Attack]
        T3[Token Theft]
        T4[Replay Attack]
    end

    subgraph Mitigations
        M1[HttpOnly Cookies<br/>Tokens not accessible via JS]
        M2[SameSite=Strict<br/>CSRF protection]
        M3[Short-lived access tokens<br/>Refresh token rotation]
        M4[Token binding<br/>JTI tracking]
    end

    T1 -.-> M1
    T2 -.-> M2
    T3 -.-> M3
    T4 -.-> M4
Loading

Security Checklist

  • HTTPS Only: All communication over TLS
  • HttpOnly Cookies: Tokens not accessible to JavaScript
  • SameSite Cookies: Protection against CSRF
  • PKCE: Prevents authorization code interception
  • State Parameter: CSRF protection in OAuth flow
  • Token Rotation: New refresh token on each use
  • Token Family Tracking: Detect and revoke on reuse
  • Short Access Token Lifetime: Limit exposure window
  • Secure Token Storage: Encrypted at rest in database
  • Rate Limiting: Prevent brute force attacks
  • Audit Logging: Track authentication events

Network Security

flowchart TB
    subgraph Public Internet
        Browser[Browser]
    end

    subgraph DMZ
        GW[API Gateway]
    end

    subgraph Private Network
        BE1[CMS Backend]
        BE2[OMS Backend]
        IAM[IAM Service]
        DB[(Database)]
    end

    Browser -->|HTTPS| GW
    GW -->|Internal| BE1
    GW -->|Internal| BE2
    GW -->|Internal| IAM
    IAM -->|Internal| DB

    style Browser fill:#f9f
    style GW fill:#ff9
    style BE1 fill:#9f9
    style BE2 fill:#9f9
    style IAM fill:#9f9
    style DB fill:#99f
Loading

Header Spoofing Prevention

Backend services must only accept user context headers from the trusted gateway:

flowchart LR
    subgraph External Request
        EXT[Browser] -->|X-User-ID: hacker| GW[Gateway]
    end

    subgraph Gateway Processing
        GW -->|Strip external headers| STRIP[Remove X-User-*]
        STRIP -->|Add verified headers| ADD[X-User-ID: real_user]
    end

    subgraph Backend
        ADD --> BE[Backend Service]
        BE -->|Trust only gateway| PROCESS[Process]
    end
Loading

Appendix

Environment Configuration

# Gateway Configuration
gateway:
  iam:
    issuer: https://iam.example.com
    client_id: admin-gateway
    client_secret: ${IAM_CLIENT_SECRET}
    
  cookies:
    domain: .example.com
    secure: true
    same_site: strict
    
  tokens:
    access_token_ttl: 15m
    refresh_token_ttl: 7d

# IAM Configuration  
iam:
  signing:
    algorithm: RS256
    key_rotation_days: 90
    
  session:
    ttl: 24h
    
  security:
    max_failed_attempts: 5
    lockout_duration: 15m

Error Codes

Code HTTP Status Description
invalid_token 401 Token is malformed or signature invalid
token_expired 401 Access token has expired
refresh_expired 401 Refresh token has expired
token_revoked 401 Token has been revoked
insufficient_scope 403 Token lacks required permissions
invalid_grant 400 Authorization code is invalid
invalid_request 400 Missing or invalid parameters

FAQ / Open Questions

Security Concerns

1. Permissions embedded in JWT

Permissions are included directly in the access token payload. This raises concerns:

  • Token bloat: Users with many permissions will have large JWTs
  • Stale permissions: If permissions are revoked, the user retains old permissions until the access token expires (up to 60 min)

Options to consider:

  • Only include roles in JWT, fetch permissions at runtime from cache/DB
  • Reduce access token TTL to minimize staleness window

2. No immediate access token revocation

The architecture relies on short-lived access tokens, but there's no mechanism to immediately invalidate a compromised access token.

Options to consider:

  • Implement a token denylist at the gateway (checked on every request)
  • Use shorter access token TTL (15 min max for admin consoles)

3. Backend trust verification

The Header Spoofing Prevention section describes stripping external headers, but how do backends verify that requests actually came from the gateway vs. a compromised internal service?

Options to consider:

  • mTLS between gateway and backends
  • Shared secret header (e.g., X-Gateway-Secret)
  • Network-level isolation (only gateway can reach backends)

4. SameSite=Lax for IAM session cookie

The IAM session cookie uses SameSite=Lax to enable SSO redirects. This allows the cookie to be sent on top-level navigations from external sites.

Action needed:

  • Ensure CSRF protection on IAM login/logout endpoints (state parameter may not be sufficient)

Architectural Questions

5. Multi-tenant support

Will different admin consoles serve different organizations/tenants? If so:

  • How is tenant isolation handled in the permission model?
  • Should permissions include tenant context (e.g., tenant_123:cms:content:read)?

Answer: No multi-tenant requirements. All admin consoles serve a single organization.

6. Token refresh race condition

When multiple concurrent requests hit the gateway with an expired access token:

  • Will they all attempt to refresh simultaneously?
  • After refresh token rotation, subsequent requests with the "old" refresh token could trigger theft detection and revoke the entire token family

Options to consider:

  • Gateway-level locking/deduplication for refresh requests
  • Grace period for old refresh tokens after rotation

7. SSO logout behavior

The "Full SSO logout" is marked as optional. If a user logs out of CMS but not IAM:

  • They remain logged into OMS, PIM, etc.
  • For admin consoles with sensitive data, should SSO logout be mandatory?

8. JWKS endpoint

The gateway needs to validate JWT signatures and handle key rotation (mentioned in config). Is there a /.well-known/jwks.json endpoint on IAM?

9. Cookie domain for multi-subdomain setup

Proposed domain structure:

demo.dev.qortex.com           → Customer Demo (public)
console.dev.qortex.com        → Admin landing page (lists all admin apps)

PIM Admin:
  pim.console.dev.qortex.com      → Admin SPA
  pim.api.console.dev.qortex.com  → Admin API (gateway)
  Cookie domain: .console.dev.qortex.com

PIM Customer:
  pim.api.demo.dev.qortex.com     → Customer-facing API

Analysis:

Domain Cookie .console.dev.qortex.com Notes
pim.console.dev.qortex.com ✓ Receives cookie Admin SPA
pim.api.console.dev.qortex.com ✓ Receives cookie Admin API
oms.console.dev.qortex.com ✓ Receives cookie Other admin apps too
pim.api.demo.dev.qortex.com ✗ No cookie Customer API isolated

This works, but consider these points:

  1. Naming convention: pim.api.console.dev.qortex.com is a sibling of pim.console.dev.qortex.com, not a child. Alternative structure:

    • Option A: pim.console.dev.qortex.com/api/* (same domain, path-based routing)
    • Option B: api.pim.console.dev.qortex.com (API as subdomain of app)
  2. Gateway placement: Is pim.api.console.dev.qortex.com a per-app API, or is there a shared gateway at api.console.dev.qortex.com?

    • Per-app APIs: More isolation, but duplicates gateway logic per service
    • Shared gateway: api.console.dev.qortex.com routes to all backends
  3. Cookie path: With separate API domain, cookie path can be / since the domain already isolates admin vs customer.

Recommendation: If using a shared gateway pattern (per this architecture doc), consider:

console.dev.qortex.com            → Admin landing page
api.console.dev.qortex.com        → Shared API Gateway (all admin APIs)
pim.console.dev.qortex.com        → PIM Admin SPA
oms.console.dev.qortex.com        → OMS Admin SPA

Cookie domain: .console.dev.qortex.com
Cookie path: /

This keeps the gateway centralized and simplifies cookie management.

10. Rate limiting details

Rate limiting is mentioned but not specified:

  • Per-user or per-IP?
  • Applied at gateway or IAM?
  • Which endpoints? (login, token refresh, API calls)

Minor Items

  • Wildcard permissions: *:*:read is powerful — ensure wildcard permissions are carefully audited and restricted
  • Error codes: Consider adding session_expired for IAM session expiry vs token expiry
  • Audit logging: Specify which events to log (login, logout, permission changes, failed attempts, token refresh, revocation)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment