Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b4077f8
docs: SSO/OIDC integration design for Sprint 20
BadgerOps Feb 18, 2026
6b9d9c5
docs: SSO/OIDC implementation plan — 17 tasks for Sprint 20
BadgerOps Feb 18, 2026
e395843
deps: add coreos/go-oidc and golang.org/x/oauth2 for OIDC support
BadgerOps Feb 18, 2026
4588ce0
feat: OIDC domain types, migration 0017, OIDCProviderStore with memor…
BadgerOps Feb 18, 2026
2e802d7
feat: AES-256-GCM encryption for OIDC client secrets
BadgerOps Feb 18, 2026
45cf5d8
feat: SQLite OIDCProviderStore implementation
BadgerOps Feb 18, 2026
a57e276
feat: OIDC provider package — discovery, code exchange, claim mapping
BadgerOps Feb 18, 2026
3004e70
security: cached user active-status check in DualAuthMiddleware
BadgerOps Feb 18, 2026
8c6109f
feat: enforce local auth toggle — reject password login when disabled
BadgerOps Feb 18, 2026
dcbb0d5
feat: OIDC login, callback, and refresh handlers with JIT provisioning
BadgerOps Feb 18, 2026
315b95a
feat: OIDC provider admin CRUD with secret encryption and test endpoint
BadgerOps Feb 18, 2026
cd7da95
feat: SSO login buttons on login page
BadgerOps Feb 18, 2026
29b89b1
feat: wire OIDC subsystem into main.go
BadgerOps Feb 18, 2026
40206b1
feat: OIDC provider management UI in Security settings
BadgerOps Feb 18, 2026
46653a4
feat: silent session re-auth for OIDC users via hidden iframe
BadgerOps Feb 18, 2026
13be73a
docs: update CHANGELOG and CLAUDE.md for v0.8.0 OIDC integration
BadgerOps Feb 18, 2026
6c69e7e
build: rebuild frontend with OIDC components
BadgerOps Feb 18, 2026
ec52d52
fix: sanitize OIDC callback error param and add missing PostgreSQL ta…
BadgerOps Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ CloudPAM is an intelligent IP Address Management (IPAM) platform designed to man

## Implementation Status

The project is in **Phase 4** of a 5-phase, 20-week roadmap. See `IMPLEMENTATION_ROADMAP.md` for the complete plan.
The project is in **Phase 5** of a 5-phase, 20-week roadmap. See `IMPLEMENTATION_ROADMAP.md` for the complete plan.

**Current State** (Sprint 19 complete):
**Current State** (Sprint 20 complete):
- SSO/OIDC: generic OIDC provider integration, JIT user provisioning, role mapping, silent session re-auth, client secret encryption, local auth toggle, provider management UI (Sprint 20)
- Auth hardening: auth-always default, CSRF protection, password policy (NIST 800-63B), session limits, login rate limiting, trusted proxies, security settings UI, API key scope elevation prevention (Sprint 19)
- AI Planning: LLM-powered conversational planning with SSE streaming, plan generation and apply (Sprints 17-18)
- AWS Organizations discovery: org-mode agent, cross-account AssumeRole, bulk ingest, Terraform/CF modules, wizard org mode (Sprint 16b)
Expand Down Expand Up @@ -211,7 +212,7 @@ The storage layer uses build tags to switch between implementations:
- Migrations apply automatically on startup
- Forward-only; no rollback support
- Use `./cloudpam -migrate status` to check schema version
- Current migrations: `0001_init.sql` through `0012_account_key_unique.sql`
- Current migrations: `0001_init.sql` through `0017_oidc_providers.sql`

- **PostgreSQL Support** (`-tags postgres`)
- Production-grade database with native CIDR operations
Expand All @@ -231,6 +232,7 @@ The storage layer uses build tags to switch between implementations:
| `internal/cidr` | CIDR math utilities | Implemented |
| `internal/planning` | Smart planning engine (analysis, gaps, fragmentation, compliance, recommendations) | Implemented (Phase 3 analysis + recommendations) |
| `internal/planning/llm` | LLM provider abstraction (OpenAI-compatible) | Implemented (Phase 4) |
| `internal/auth/oidc` | OIDC provider integration (discovery, exchange, claims, crypto) | Implemented (Phase 5) |

### HTTP Layer

Expand Down Expand Up @@ -260,6 +262,13 @@ The storage layer uses build tags to switch between implementations:
- `/api/v1/auth/users/{id}/revoke-sessions` - revoke all sessions for a user (POST)
- `/api/v1/auth/setup` - first-boot admin account creation (POST)
- `/api/v1/settings/security` - security settings (GET/PATCH)
- `/api/v1/auth/oidc/providers` - list enabled OIDC providers (GET, public)
- `/api/v1/auth/oidc/login` - initiate OIDC login flow (GET)
- `/api/v1/auth/oidc/callback` - OIDC callback handler (GET)
- `/api/v1/auth/oidc/refresh` - get OIDC refresh URL (POST)
- `/api/v1/settings/oidc/providers` - OIDC provider admin CRUD (GET/POST)
- `/api/v1/settings/oidc/providers/{id}` - OIDC provider admin (GET/PATCH/DELETE)
- `/api/v1/settings/oidc/providers/{id}/test` - test OIDC provider connection (POST)
- `/api/v1/search` - unified search with CIDR containment queries
- `/api/v1/discovery/resources` - list discovered cloud resources (filterable)
- `/api/v1/discovery/resources/{id}` - get single discovered resource
Expand Down Expand Up @@ -377,7 +386,7 @@ When adding endpoints:
- Tailwind CSS for styling, `lucide-react` for icons
- Static assets built to `web/dist/` and embedded at build time via `web/embed.go`
- UI is served at `/` by `handleSPA()` with SPA fallback for client-side routes
- API hooks in `ui/src/hooks/` (usePools, useAccounts, useBlocks, useAudit, useDiscovery, useAuth, useToast, useRecommendations)
- API hooks in `ui/src/hooks/` (usePools, useAccounts, useBlocks, useAudit, useDiscovery, useAuth, useToast, useRecommendations, useOIDCProviders, useOIDCAdmin, useSessionRefresh)
- Shared types in `ui/src/api/types.ts`, API client in `ui/src/api/client.ts`
- Schema Planner wizard lives in `ui/src/wizard/` (existing from Sprint 8)
- Run `cd ui && npm run dev` for hot-reload development (proxied to Go backend)
Expand All @@ -398,6 +407,10 @@ When adding endpoints:
- `CLOUDPAM_ADMIN_EMAIL`: Bootstrap admin email (default: `{username}@localhost`)
- `CLOUDPAM_TRUSTED_PROXIES`: Comma-separated CIDRs for trusted reverse proxies (e.g., `10.0.0.0/8,172.16.0.0/12`)

### OIDC/SSO
- `CLOUDPAM_OIDC_ENCRYPTION_KEY`: 32-byte hex-encoded AES-256 key for OIDC client secret encryption (auto-generated with warning if not set)
- `CLOUDPAM_OIDC_CALLBACK_URL`: OIDC callback URL (default: `http://localhost:8080/api/v1/auth/oidc/callback`)

### Observability
- `CLOUDPAM_LOG_LEVEL`: Log level - debug, info, warn, error (default: `info`)
- `CLOUDPAM_LOG_FORMAT`: Log format - json, text (default: `json`)
Expand Down Expand Up @@ -459,6 +472,16 @@ Common workflows:
- Revoke user sessions: `POST /api/v1/auth/users/{id}/revoke-sessions`
- First-boot setup: `POST /api/v1/auth/setup` with `{"username":"...","password":"...","email":"..."}`
- Security settings: `GET /api/v1/settings/security`, `PATCH /api/v1/settings/security` with JSON body
- OIDC providers (public): `GET /api/v1/auth/oidc/providers`
- OIDC login: `GET /api/v1/auth/oidc/login?provider_id={id}` (redirects to IdP)
- OIDC callback: `GET /api/v1/auth/oidc/callback` (handles IdP redirect)
- OIDC refresh: `POST /api/v1/auth/oidc/refresh` (returns redirect URL for silent re-auth)
- OIDC admin list: `GET /api/v1/settings/oidc/providers`
- OIDC admin create: `POST /api/v1/settings/oidc/providers` with `{"name":"...","issuer_url":"...","client_id":"...","client_secret":"..."}`
- OIDC admin get: `GET /api/v1/settings/oidc/providers/{id}`
- OIDC admin update: `PATCH /api/v1/settings/oidc/providers/{id}` with partial fields
- OIDC admin delete: `DELETE /api/v1/settings/oidc/providers/{id}`
- OIDC admin test: `POST /api/v1/settings/oidc/providers/{id}/test`
- Search: `GET /api/v1/search?q=prod&cidr_contains=10.1.2.5&type=pool,account`
- Discovery resources: `GET /api/v1/discovery/resources?account_id=1&status=active&resource_type=vpc`
- Discovery resource detail: `GET /api/v1/discovery/resources/{id}`
Expand Down Expand Up @@ -594,6 +617,7 @@ cloudpam/
│ │ ├── discovery.go # Discovery domain types
│ │ ├── recommendations.go # Recommendation types
│ │ ├── settings.go # SecuritySettings type
│ │ ├── oidc.go # OIDCProvider type
│ │ └── models.go # Extended models (planned)
│ ├── api/ # HTTP server, routes, handlers
│ │ ├── server.go # Server struct, route registration, helpers
Expand All @@ -609,6 +633,7 @@ cloudpam/
│ │ ├── csrf.go # CSRF double-submit cookie middleware
│ │ ├── recommendation_handlers.go # Recommendation API (generate, apply, dismiss)
│ │ ├── ai_handlers.go # AI Planning API (chat, sessions, plan apply)
│ │ ├── oidc_handlers.go # OIDC login/callback/refresh + admin CRUD
│ │ ├── middleware.go # Middleware (logging, auth, rate limit, trusted proxies)
│ │ ├── context.go # Request context helpers
│ │ ├── cidr.go # IPv4 CIDR validation utilities
Expand All @@ -621,12 +646,15 @@ cloudpam/
│ │ ├── recommendations_memory.go # In-memory RecommendationStore
│ │ ├── settings.go # SettingsStore interface
│ │ ├── settings_memory.go # In-memory SettingsStore
│ │ ├── oidc.go # OIDCProviderStore interface
│ │ ├── oidc_memory.go # In-memory OIDCProviderStore
│ │ ├── errors.go # Sentinel errors (ErrNotFound, etc.)
│ │ ├── sqlite/ # SQLite implementation
│ │ │ ├── sqlite.go
│ │ │ ├── discovery.go # SQLite DiscoveryStore
│ │ │ ├── recommendations.go # SQLite RecommendationStore
│ │ │ ├── settings.go # SQLite SettingsStore
│ │ │ ├── oidc.go # SQLite OIDCProviderStore
│ │ │ └── migrator.go
│ │ └── postgres/ # PostgreSQL implementation
│ │ ├── postgres.go
Expand All @@ -643,20 +671,25 @@ cloudpam/
│ │ ├── users.go # User types and store interfaces
│ │ ├── password.go # Password hashing and validation (NIST 800-63B)
│ │ ├── sessions.go # Session management
│ │ └── sqlite.go # SQLite implementations
│ │ ├── sqlite.go # SQLite implementations
│ │ └── oidc/ # OIDC provider integration
│ │ ├── provider.go # OIDC discovery, auth URL, code exchange
│ │ ├── claims.go # ID token claims and role mapping
│ │ └── crypto.go # AES-256-GCM client secret encryption
│ ├── audit/ # Audit logging
│ ├── cidr/ # Reusable CIDR math utilities
│ ├── validation/ # Input validation
│ ├── planning/ # Smart planning engine (analysis, gaps, fragmentation, compliance, recommendations)
│ │ └── llm/ # LLM provider abstraction (OpenAI-compatible)
│ ├── observability/ # Logging, metrics, tracing
│ └── docs/ # Internal documentation handlers
├── migrations/ # SQL migrations (0001-0016)
├── migrations/ # SQL migrations (0001-0017)
│ ├── embed.go
│ ├── 0001_init.sql
│ ├── 0002_accounts_meta.sql
│ ├── ...
│ ├── 0008_discovered_resources.sql # Discovery tables
│ ├── 0017_oidc_providers.sql # OIDC providers + user OIDC columns
│ └── postgres/ # PostgreSQL migrations
├── deploy/ # Deployment configurations
│ ├── terraform/aws-org-discovery/ # AWS Organizations discovery IAM
Expand Down Expand Up @@ -727,7 +760,7 @@ See `IMPLEMENTATION_ROADMAP.md` for the detailed 20-week plan. Summary:

**Phase 5 (Enterprise):**
- Multi-tenancy enforcement (schema exists, not enforced)
- SSO/OIDC integration
- ~~SSO/OIDC integration~~ ✅ (Sprint 20)
- Log shipping / SIEM integration
- Per-org rate limiting and quotas

Expand Down
39 changes: 39 additions & 0 deletions cmd/cloudpam/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"context"
"crypto/rand"
"encoding/hex"
"flag"
"net/http"
"os"
Expand Down Expand Up @@ -188,6 +190,13 @@ func main() {
settingsSrv := api.NewSettingsServer(srv, settingsStore)
logger.Info("settings subsystem initialized")

// OIDC subsystem
oidcStore := storage.NewMemoryOIDCProviderStore()
oidcEncKey := parseOIDCEncryptionKey(logger)
oidcCallbackURL := os.Getenv("CLOUDPAM_OIDC_CALLBACK_URL")
oidcSrv := api.NewOIDCServer(srv, oidcStore, sessionStore, userStore, settingsStore, oidcEncKey, oidcCallbackURL)
logger.Info("oidc subsystem initialized")

// Auth is always enabled — register protected routes with RBAC.
srv.RegisterProtectedRoutes(keyStore, sessionStore, userStore, logger.Slog())
authSrv := api.NewAuthServerWithStores(srv, keyStore, sessionStore, userStore, auditLogger)
Expand All @@ -204,6 +213,9 @@ func main() {
recSrv.RegisterProtectedRecommendationRoutes(dualMW, logger.Slog())
aiSrv.RegisterProtectedAIPlanningRoutes(dualMW, logger.Slog())
settingsSrv.RegisterProtectedSettingsRoutes(dualMW, logger.Slog())
oidcSrv.RegisterOIDCRoutes(logger.Slog())
oidcSrv.RegisterOIDCAdminRoutes(dualMW, logger.Slog())
userSrv.SetSettingsStore(settingsStore)

if len(existingUsers) == 0 {
logger.Info("first-boot setup required", "hint", "visit the UI to create an admin account")
Expand Down Expand Up @@ -338,6 +350,33 @@ func bootstrapAdmin(logger observability.Logger, userStore auth.UserStore, usern
logger.Info("bootstrap admin user created", "username", username)
}

// parseOIDCEncryptionKey reads a hex-encoded AES-256 key from CLOUDPAM_OIDC_ENCRYPTION_KEY
// environment variable. If not set, generates a random key (warning: won't survive restarts).
func parseOIDCEncryptionKey(logger observability.Logger) []byte {
hexKey := os.Getenv("CLOUDPAM_OIDC_ENCRYPTION_KEY")
if hexKey != "" {
key, err := hex.DecodeString(hexKey)
if err != nil {
logger.Error("invalid CLOUDPAM_OIDC_ENCRYPTION_KEY (must be hex-encoded)", "error", err)
os.Exit(1)
}
if len(key) != 32 {
logger.Error("CLOUDPAM_OIDC_ENCRYPTION_KEY must be 32 bytes (64 hex chars)", "length", len(key))
os.Exit(1)
}
return key
}

// Auto-generate key — logs a warning
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
logger.Error("failed to generate OIDC encryption key", "error", err)
os.Exit(1)
}
logger.Warn("CLOUDPAM_OIDC_ENCRYPTION_KEY not set — using auto-generated key (OIDC provider secrets won't survive restarts)")
return key
}

// runMigrationsCLI executes migration commands.
func runMigrationsCLI(logger observability.Logger, cmd string) {
switch cmd {
Expand Down
68 changes: 68 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,73 @@ All notable changes to CloudPAM will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.0] - SSO/OIDC Integration Sprint 20

### Added

#### SSO/OIDC Authentication
- Generic OIDC provider integration using `coreos/go-oidc/v3` and `golang.org/x/oauth2`
- OIDC Authorization Code Flow with PKCE-ready architecture
- OIDC Discovery (`.well-known/openid-configuration`) for automatic endpoint configuration
- ID Token verification via JWKS with RSA-signed JWTs
- JIT (Just-In-Time) user provisioning from IdP claims with configurable role mapping
- Claim-to-role mapping: IdP groups mapped to CloudPAM roles using `auth.RoleLevel` for priority
- Silent session re-authentication via `prompt=none` hidden iframe with `Sec-Fetch-Dest: iframe` detection
- Client secret encryption at rest using AES-256-GCM (`internal/auth/oidc/crypto.go`)
- Local auth toggle: disable password login when OIDC is configured (break-glass admin remains)
- User active-status cache in `DualAuthMiddleware` with 30-second TTL (`sync.Map`)

#### OIDC API Endpoints
- `GET /api/v1/auth/oidc/providers` — list enabled providers (public, no auth required)
- `GET /api/v1/auth/oidc/login` — initiate OIDC login flow with state cookie
- `GET /api/v1/auth/oidc/callback` — handle IdP callback, exchange code, JIT provision user
- `POST /api/v1/auth/oidc/refresh` — get refresh redirect URL for silent re-auth
- `GET /api/v1/settings/oidc/providers` — list all providers (admin)
- `POST /api/v1/settings/oidc/providers` — create provider (admin)
- `GET /api/v1/settings/oidc/providers/{id}` — get provider with masked secret (admin)
- `PATCH /api/v1/settings/oidc/providers/{id}` — update provider (admin)
- `DELETE /api/v1/settings/oidc/providers/{id}` — delete provider (admin)
- `POST /api/v1/settings/oidc/providers/{id}/test` — test provider discovery (admin)

#### OIDC Storage & Domain
- `OIDCProvider` domain type with encrypted client secret storage
- `OIDCProviderStore` interface (7 methods: Create/Get/GetByIssuer/List/ListEnabled/Update/Delete)
- In-memory and SQLite `OIDCProviderStore` implementations
- `GetByOIDCIdentity` method added to `UserStore` interface (memory, SQLite, PostgreSQL)
- Migration `0017_oidc_providers.sql` (oidc_providers table + user OIDC columns)

#### OIDC Frontend
- SSO login buttons on login page (one per enabled provider)
- OIDC provider management UI in Config > Security settings
- Provider list table with name, issuer URL, enabled status
- Add/Edit/Delete provider modals with full configuration
- Test Connection button for provider discovery validation
- Role mapping editor (IdP group → CloudPAM role)
- Local auth toggle with confirmation modal
- Silent session re-auth hook (`useSessionRefresh`) for OIDC users
- Polls `/auth/me` every 60s, triggers re-auth in last 20% of session lifetime
- Hidden iframe with `prompt=none` for seamless token refresh

#### OIDC Package (`internal/auth/oidc/`)
- `provider.go` — Provider struct wrapping go-oidc + oauth2, NewProvider/AuthCodeURL/Exchange methods
- `claims.go` — Claims struct and MapRole function for group-to-role mapping
- `crypto.go` — AES-256-GCM Encrypt/Decrypt for client secrets

#### New Environment Variables
- `CLOUDPAM_OIDC_ENCRYPTION_KEY` — 32-byte hex-encoded AES key for client secret encryption (auto-generated if not set)
- `CLOUDPAM_OIDC_CALLBACK_URL` — OIDC callback URL (default: `http://localhost:8080/api/v1/auth/oidc/callback`)

#### Dependencies
- `github.com/coreos/go-oidc/v3` v3.17.0
- `golang.org/x/oauth2` v0.35.0
- `github.com/go-jose/go-jose/v4` v4.1.3

### Changed
- `DualAuthMiddleware` now includes user active-status cache (30s TTL) to avoid per-request DB lookup
- `handleMe` response extended with `auth_provider` and `session_expires_at` fields
- CSRF middleware exempts `/api/v1/auth/oidc/*` paths (OIDC uses state parameter for security)
- User domain type extended with `AuthProvider`, `OIDCSubject`, `OIDCIssuer` fields

## [0.7.0] - Auth Hardening Sprint 19

### BREAKING
Expand Down Expand Up @@ -668,6 +735,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- IPv4 only (IPv6 planned)
- Block detection marks exact CIDR matches as used

[0.8.0]: https://github.com/BadgerOps/cloudpam/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/BadgerOps/cloudpam/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/BadgerOps/cloudpam/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/BadgerOps/cloudpam/compare/v0.5.0...v0.6.0
Expand Down
Loading
Loading