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.
- Overview
- Architecture
- Components
- Authentication Flows
- Token Management
- Authorization
- API Specifications
- Security Considerations
- 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
| 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 |
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
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
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
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 (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
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
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
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
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
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
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
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
{
"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"
]
}
}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
| 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 |
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
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 contentcms:content:*- All operations on CMS contentcms:*- All CMS permissions*:*:read- Read access to everything
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]
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]
Initiates the login flow.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| redirect | string | URL to redirect after login |
Response: Redirects to IAM authorize endpoint
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
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"
]
}Logs out the current user.
Response:
- Clears auth cookies
- Returns 200 OK
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"
}Revokes a refresh token.
Request:
{
"token": "refresh_token_xxx",
"token_type_hint": "refresh_token"
}Response: 200 OK
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 |
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
- 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
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
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
# 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| 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 |
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
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)
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)
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)
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.
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
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?
The gateway needs to validate JWT signatures and handle key rotation (mentioned in config). Is there a /.well-known/jwks.json endpoint on IAM?
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:
-
Naming convention:
pim.api.console.dev.qortex.comis a sibling ofpim.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)
- Option A:
-
Gateway placement: Is
pim.api.console.dev.qortex.coma per-app API, or is there a shared gateway atapi.console.dev.qortex.com?- Per-app APIs: More isolation, but duplicates gateway logic per service
- Shared gateway:
api.console.dev.qortex.comroutes to all backends
-
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.
Rate limiting is mentioned but not specified:
- Per-user or per-IP?
- Applied at gateway or IAM?
- Which endpoints? (login, token refresh, API calls)
- Wildcard permissions:
*:*:readis powerful — ensure wildcard permissions are carefully audited and restricted - Error codes: Consider adding
session_expiredfor IAM session expiry vs token expiry - Audit logging: Specify which events to log (login, logout, permission changes, failed attempts, token refresh, revocation)