Powered by Entire:
  This conversation will be linked to your next commit.


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/model</command-name>
            <command-message>model</command-message>
            <command-args></command-args>


<local-command-stdout>Set model to Opus 4.7 (1M context) for this session</local-command-stdout>


Do a security audit of the authentication in this application.


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Fast read-only search agent for locating code. Use it to find files by pattern (eg. "src/components/**/*.tsx"), grep for symbols or keywords (eg. "API endpoints"), or answer "where is X defined / which files reference Y." Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: "quick" for a single targeted lookup, "medium" for moderate exploration, or "very thorough" to search across multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- frontend-design:frontend-design: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the current diff and apply the fixes — equivalent to /code-review --fix.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

frontend-design:frontend-design update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Agent

Explore auth architecture

Prompt:
Explore the authentication architecture in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit and need a thorough map.

Focus on `internal/auth/` and identify:

1. **Authenticator implementations** — files in internal/auth/ for each authenticator (local, LDAP, OIDC, JWT). For each one:
   - How CanLogin and Login methods work
   - How credentials are validated
   - How session/token is created
   - Any cryptographic operations (hashing, signing, verification)

2. **Session management** — how sessions are stored and validated (gorilla/sessions? cookies?). Find:
   - Session secret loading and rotation
   - Cookie flags (HttpOnly, Secure, SameSite)
   - Session timeout/expiry
   - Where session validation happens for incoming requests

3. **JWT specifics** — how JWTs are signed and verified:
   - Algorithm used (look for ed25519, HS256, RS256, etc.)
   - Key loading from env/.env
   - Token validation (exp, iss, aud checks)
   - Whether `none` algorithm is rejected
   - Token revocation mechanism (if any)

4. **Password hashing** — for local accounts:
   - Algorithm (bcrypt, argon2, scrypt, pbkdf2)
   - Cost/iteration parameters
   - Salt handling
   - Constant-time comparison

5. **Rate limiting** — login attempt throttling:
   - Where it's implemented
   - What it tracks (IP, username)
   - Lockout thresholds

For each file you read, give exact line numbers for the security-relevant code. Quote short snippets where useful but don't dump whole files. Report under 800 words.


Explore auth wiring and middleware

Prompt:
Explore how authentication is wired into the HTTP/GraphQL/REST request paths in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit.

Find and report on:

1. **Auth middleware** — where requests are authenticated. Look in:
   - `internal/auth/` (middleware files)
   - `internal/api/rest.go` (route registration)
   - `internal/routerConfig/` (if it exists)
   - `cmd/cc-backend/` entry point for HTTP server setup

   For each middleware: how it extracts credentials (header, cookie, query param), how it rejects requests, how it passes user context downstream.

2. **CSRF protection** — search for `csrf`, gorilla/csrf, samesite, etc. Is CSRF mitigated for state-changing endpoints?

3. **CORS configuration** — search for `cors`, `Access-Control`, allowed origins. Is it permissive (`*`)?

4. **TLS/HTTPS** — where TLS is set up, whether HSTS is sent, whether cookies require Secure.

5. **Logout** — how logout invalidates sessions/tokens.

6. **API key / token endpoints** — endpoints that issue or accept long-lived tokens. Where are they? Who can call them?

7. **Authorization (not just authN)** — role checks (`admin`, `support`, `api`, `manager`). Where are they enforced? Are there endpoints missing role checks?

8. **NATS API auth** — the project has a NATS-based API per CLAUDE.md (`internal/api/nats.go`). What auth does it require? Can anyone with NATS access call privileged ops?

For each item, cite exact files and line numbers. Quote snippets where useful. Report under 800 words.


Explore auth-adjacent risk surfaces

Prompt:
Explore auth-adjacent attack surfaces in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit. I already have agents looking at the auth code itself and how it's wired into requests — your job is the surrounding surfaces.

Investigate and report:

1. **Secret loading** — how secrets are loaded from .env / env vars:
   - JWT signing keys (private/public)
   - Session secret
   - LDAP bind password
   - OIDC client secret
   - Are any defaulted to a hardcoded value if env is missing?
   - Are secrets logged anywhere?

2. **User input handling in auth code**:
   - SQL queries in user/auth repositories (`internal/repository/user.go` and similar) — are they parameterized?
   - LDAP search filters — escaping of user input (LDAP injection)?
   - OIDC state/nonce validation — replay protection?
   - Redirect URLs after login — open redirect possible?

3. **Error responses** — do auth failure paths leak information (e.g. "user not found" vs "bad password")? Look in login handlers.

4. **Logging** — search for `log.*password`, `log.*token`, `log.*secret`, etc. Are credentials accidentally logged?

5. **Default credentials / seeded admin** — search `internal/repository/` and the init flow for default users or seed credentials. The README mentions `-add-user demo:admin:demo` — is that documented as production-unsafe?

6. **Config file handling** — does anything in `config.json` get reflected back to users? Could a malicious operator inject XSS via cluster names etc.?

7. **Frontend auth** — `web/frontend/src/` — how does the frontend handle tokens/sessions? Are JWTs stored in localStorage (XSS-exposed) vs httpOnly cookies?

8. **Dependencies** — check go.mod for known-stale auth-related libs (golang-jwt v3 vs v4/v5, deprecated crypto packages).

For each finding, cite file paths and line numbers. Quote short snippets only when needed. Report under 800 words.


Perfect. Now let me document my findings in a comprehensive security audit report:

## SECURITY AUDIT: ClusterCockpit Authentication Architecture

Based on thorough analysis of `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/`, here's the complete security map:

### 1. AUTHENTICATOR IMPLEMENTATIONS

**Local Authentication** (`local.go`, lines 27-48)
- **CanLogin**: Checks `user.AuthSource == schema.AuthViaLocalPassword` (line 33)
- **Login**: Uses `bcrypt.CompareHashAndPassword()` for credential validation (line 41)
- **Session Creation**: Delegated to auth.SaveSession() after successful login

**LDAP Authentication** (`ldap.go`, lines 67-142)
- **CanLogin**: Checks `user.AuthSource == schema.AuthViaLDAP` or syncs new users from LDAP (lines 75-79)
- **Login**: Binds to LDAP with username/password via `l.Bind(userDn, password)` (line 135)
- **Credential Validation**: LDAP server validates; client must have proper DN escaping via `ldap.EscapeDN()` (line 134)

**JWT Authentication** (`jwt.go`, lines 71-142)
- **CanLogin**: Checks for X-Auth-Token or Authorization header (lines 75-78)
- **Login**: Parses token with Ed25519 signature verification only (line 87-88)
- **Token Creation**: `ProvideJWT()` uses Ed25519 private key to sign (line 141)

**JWT Session (HS256/HS512)** (`jwtSession.go`, lines 41-90)
- **CanLogin**: Checks Authorization header or `login-token` query param (line 47-48)
- **Login**: Accepts HS256 or HS512 algorithms; extracts user from claims (line 62)

**JWT Cookie Session** (`jwtCookieSession.go`, lines 85-172)
- **CanLogin**: Checks JWT cookie existence (lines 99-102)
- **Login**: EdDSA-only algorithm enforcement (line 123), issuer validation against `TrustedIssuer` config (lines 127-131), deletes JWT cookie post-login (line 162-169)

**OIDC Authentication** (`oidc.go`, lines 95-313)
- **OAuth2 Flow**: Authorization code + PKCE + nonce protection
- **Token Validation**: ID token verification via `verifier.Verify()` (line 153), nonce check (line 160)
- **Role Extraction**: Realm roles + resource roles from ID token claims (lines 227-234)

### 2. SESSION MANAGEMENT

**Storage**: Gorilla `sessions.CookieStore` (line 120, auth.go)

**Secret Loading** (`auth.go:188-202`):
- Loads `SESSION_KEY` env var (base64-decoded 32 bytes)
- Falls back to cryptographically random 32-byte key if unset (line 192)
- **Issue**: Non-persistent random keys on restart lose all sessions

**Cookie Flags** (`auth.go:315-327`):
- `MaxAge`: Configurable via `config.Keys.SessionMaxAge` (line 316)
- `Secure`: Only set if TLS or `X-Forwarded-Proto: https` detected (line 318-325)
- `SameSite`: Lax mode enforced (line 327)
- `HttpOnly`: Yes (Gorilla default)

**Validation** (`auth.go:128-179`):
- Type-checked extraction of username, projects, roles (lines 143-165)
- Invalidates corrupted sessions (lines 147, 162)
- Validates `authSource` field (lines 167-170)

### 3. JWT SPECIFICS

**Algorithm Enforcement**:
- **Standard JWT Auth**: EdDSA only (jwt.go:87-88)
- **JWT Session**: HS256 or HS512 allowed (jwtSession.go:62)
- **JWT Cookie Session**: EdDSA only (jwtCookieSession.go:123)
- **`none` algorithm**: REJECTED (no SigningMethodNone handling found)

**Key Loading**:
- `JWT_PUBLIC_KEY` + `JWT_PRIVATE_KEY`: Base64-decoded Ed25519 keys (jwt.go:50-66)
- `CROSS_LOGIN_JWT_PUBLIC_KEY`: Optional external issuer public key (jwtCookieSession.go:49-61)
- `CROSS_LOGIN_JWT_HS512_KEY`: HS512 key for session tokens (jwtSession.go:28-35)

**Token Claims Validation** (`jwtHelpers.go:100-138`):
- **sub (subject)**: Required; must exist (line 102-103)
- **exp (expiration)**: Auto-set in ProvideJWT() (jwt.go:138)
- **iat (issued-at)**: Set in ProvideJWT() (jwt.go:131)
- **iss (issuer)**: Validated against `TrustedIssuer` config in cookie session (jwtCookieSession.go:128)
- **aud (audience)**: NOT validated (no audience claim checks found)

**Token Revocation**: NONE IMPLEMENTED. No blacklist/revocation mechanism; tokens valid until expiration.

### 4. PASSWORD HASHING

**Algorithm**: bcrypt (golang.org/x/crypto/bcrypt)

**Cost Parameter**: `bcrypt.DefaultCost` (hardcoded, ~12 iterations) (repository/user.go line found in grep)

**Salt Handling**: Automatic, included in bcrypt hash

**Comparison**: `bcrypt.CompareHashAndPassword()` (local.go:41) — uses constant-time comparison internally

### 5. RATE LIMITING

**Implementation** (`auth.go:55-106`):
- Per IP:username combination tracking
- **Threshold**: 5 attempts per 15 minutes (line 76)
- **Limiter Type**: `rate.Limiter` from `golang.org/x/time/rate`
- **Storage**: In-memory sync.Map with cleanup background routine
- **Cleanup**: Entries unused for 24+ hours purged hourly (lines 99-105)

**Application** (`auth.go:345-357`):
- Checked on every login attempt (line 352-357)
- If exhausted, returns "too many login attempts" error

### CRITICAL FINDINGS

1. **Session Key Rotation Missing**: Non-persistent random session keys restart-unsafe
2. **JWT Revocation Gap**: No token blacklist; compromised tokens remain valid until expiration
3. **Audience Validation Missing**: JWTs lack audience claim validation
4. **Issuer Pre-validation Risk**: Cookie session checks issuer BEFORE signature validation (jwtCookieSession.go:127-131) — theoretically safe due to subsequent token.Valid check but slightly unusual order
5. **No HttpOnly Default**: Session cookies lack HttpOnly enforcement if TLS unavailable (degrades gracefully with warning)

---

**Absolute paths to critical files**:
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (core orchestration)
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/jwt.go` (EdDSA tokens)
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/local.go` (bcrypt)
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/jwtHelpers.go` (claim extraction)

Prompt:
Explore the authentication architecture in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit and need a thorough map.

Focus on `internal/auth/` and identify:

1. **Authenticator implementations** — files in internal/auth/ for each authenticator (local, LDAP, OIDC, JWT). For each one:
   - How CanLogin and Login methods work
   - How credentials are validated
   - How session/token is created
   - Any cryptographic operations (hashing, signing, verification)

2. **Session management** — how sessions are stored and validated (gorilla/sessions? cookies?). Find:
   - Session secret loading and rotation
   - Cookie flags (HttpOnly, Secure, SameSite)
   - Session timeout/expiry
   - Where session validation happens for incoming requests

3. **JWT specifics** — how JWTs are signed and verified:
   - Algorithm used (look for ed25519, HS256, RS256, etc.)
   - Key loading from env/.env
   - Token validation (exp, iss, aud checks)
   - Whether `none` algorithm is rejected
   - Token revocation mechanism (if any)

4. **Password hashing** — for local accounts:
   - Algorithm (bcrypt, argon2, scrypt, pbkdf2)
   - Cost/iteration parameters
   - Salt handling
   - Constant-time comparison

5. **Rate limiting** — login attempt throttling:
   - Where it's implemented
   - What it tracks (IP, username)
   - Lockout thresholds

For each file you read, give exact line numbers for the security-relevant code. Quote short snippets where useful but don't dump whole files. Report under 800 words.


Perfect! Now let me create a comprehensive final report by compiling all my findings.

## SECURITY AUDIT REPORT: cc-backend Authentication & Authorization

Based on my systematic examination of the codebase, here are the key findings:

### 1. AUTH MIDDLEWARE

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (lines 435-589)**

The system uses **five separate middleware chains**:

- `AuthAPI()` (lines 435-476): JWT-only, requires `RoleAPI` OR (`RoleAdmin` + `RoleAPI`)
- `AuthUserAPI()` (lines 478-512): JWT-only, requires `RoleAPI` + (RoleUser|Manager|Support|Admin)
- `AuthMetricStoreAPI()` (lines 514-548): JWT-only, requires `RoleAPI` + (RoleUser|Manager|Admin)
- `AuthConfigAPI()` (lines 550-569): **Session-only**, requires `RoleAdmin` only
- `AuthFrontendAPI()` (lines 571-587): Session-only, any authenticated user

Credential extraction:
- **JWT**: Headers `X-Auth-Token` or `Authorization: Bearer ...` (line 75-78)
- **Sessions**: Gorilla cookie-based with keys from `SESSION_KEY` env var (lines 188-201)

Rejection: HTTP 401 with JSON error response via `onFailureResponse()` (server.go:61-68)

### 2. CSRF PROTECTION

**Status: PARTIAL**

- **SameSite=Lax** set on session cookies (auth.go:327, oidc.go)
- **PKCE protection** on OIDC flow (oidc.go comment line 104)
- **NO traditional CSRF token** found in codebase (grep yielded only SameSite mentions)

Risk: OIDC OAuth callback handling exists but no visible double-submit CSRF tokens on state-changing POST endpoints.

### 3. CORS CONFIGURATION

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/cmd/cc-backend/server.go` (lines 131-136)**

```go
cors.Handler(cors.Options{
    AllowCredentials: true,
    AllowedOrigins:   []string{"*"},  // CRITICAL
    AllowedMethods:   []string{"GET", "POST", "HEAD", "OPTIONS"},
})
```

**CRITICAL RISK**: `AllowedOrigins: []string{"*"}` + `AllowCredentials: true` = **permissive CORS**. Any origin can make credentialed requests.

### 4. TLS/HTTPS

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/cmd/cc-backend/server.go` (lines 376-394)**

- TLS configured: loads certs from `https-cert-file` and `https-key-file` (lines 376-390)
- Cipher suites hardened: `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`, `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`
- MinVersion: TLS 1.2 (line 388)

**Missing: NO HSTS header** (Strict-Transport-Security not found anywhere)

**Session Secure flag**: Conditional (auth.go:318-326) — only set if `r.TLS != nil` OR `X-Forwarded-Proto: https`, otherwise warns (logs: "Session cookies will not have Secure flag set").

### 5. LOGOUT

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (lines 592-610)**

Invalidates sessions by setting `session.Options.MaxAge = -1` and saving the expired session. Correctly handled via gorilla/sessions.

**No explicit token revocation for JWTs**—tokens remain valid until expiration (max-age from config).

### 6. API KEY / TOKEN ENDPOINTS

**Token generation** (`/api/config/jwt/` via `getJWT()`):
- **File**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/api/rest.go` (lines 315-351)
- **Authorization**: Admins can sign for any user; regular users only for themselves (line 329)
- **Returns**: Raw JWT string (line 284: `generateJWT`)
- **Endpoint**: `/frontend/jwt/` (session-auth required) — restricted to authenticated users

**CLI token generation** (`--gen-jwt` flag):
- **File**: `/Users/jan/prg/ClusterCockpit/cc-backend/cmd/cc-backend/main.go` (lines 268-286)
- Requires user to have `RoleAPI` (warning at line 276)
- Invoked offline via CLI

### 7. AUTHORIZATION (ROLE CHECKS)

**Critical Gaps Found:**

1. **`startJob()` REST endpoint** (job.go:717-787): **NO role check**. Only JWT auth middleware validates RoleAPI, but endpoint itself has no access control. NATS handler `handleStartJob()` (nats.go:227-294) also has **zero authorization checks**.

2. **`stopJob()` REST endpoint** (job.go:805+): **NO explicit role check** in endpoint.

3. **GraphQL mutations** (graph/schema.resolvers.go):
   - Lines 143-146, 195-196, etc. show role checks for tags
   - Lines 825, 892, 955 restrict some queries to Admin/Support
   - **Inconsistent**: not all mutations check roles

4. **Config API endpoints** (rest.go:232, 277, 289): Properly check `RoleAdmin` before allowing notice updates, tagger operations.

### 8. NATS API AUTH

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/api/nats.go` (lines 129-157)**

**CRITICAL: ZERO authentication on NATS messages.**

```go
func (api *NatsAPI) StartSubscriptions() error {
    client.Subscribe(s.SubjectJobEvent, func(subject string, data []byte) {
        api.jobCh <- natsMessage{...}
    })
}
```

- Subscribes to configurable NATS subjects (`APISubjects.SubjectJobEvent`, `SubjectNodeState`)
- **No auth check** on message handlers
- `handleStartJob()` and `handleStopJob()` (lines 227-350) process messages **without validating sender identity or roles**
- **Risk**: Anyone with NATS broker access can start/stop jobs, update node state

---

### SUMMARY OF HIGH-RISK FINDINGS

| Issue | Severity | Location |
|-------|----------|----------|
| CORS allows `*` with credentials | **CRITICAL** | server.go:135 |
| NATS API has zero auth checks | **CRITICAL** | nats.go:142-151 |
| startJob/stopJob have no role validation | **HIGH** | job.go:717, nats.go:227 |
| No HSTS header | **MEDIUM** | missing entirely |
| No JWT token revocation | **MEDIUM** | jwt.go (design) |
| Session Secure flag conditional | **MEDIUM** | auth.go:318-326 |

**Total lines audited**: ~600 across auth/, api/, and cmd/ packages.

Prompt:
Explore how authentication is wired into the HTTP/GraphQL/REST request paths in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit.

Find and report on:

1. **Auth middleware** — where requests are authenticated. Look in:
   - `internal/auth/` (middleware files)
   - `internal/api/rest.go` (route registration)
   - `internal/routerConfig/` (if it exists)
   - `cmd/cc-backend/` entry point for HTTP server setup

   For each middleware: how it extracts credentials (header, cookie, query param), how it rejects requests, how it passes user context downstream.

2. **CSRF protection** — search for `csrf`, gorilla/csrf, samesite, etc. Is CSRF mitigated for state-changing endpoints?

3. **CORS configuration** — search for `cors`, `Access-Control`, allowed origins. Is it permissive (`*`)?

4. **TLS/HTTPS** — where TLS is set up, whether HSTS is sent, whether cookies require Secure.

5. **Logout** — how logout invalidates sessions/tokens.

6. **API key / token endpoints** — endpoints that issue or accept long-lived tokens. Where are they? Who can call them?

7. **Authorization (not just authN)** — role checks (`admin`, `support`, `api`, `manager`). Where are they enforced? Are there endpoints missing role checks?

8. **NATS API auth** — the project has a NATS-based API per CLAUDE.md (`internal/api/nats.go`). What auth does it require? Can anyone with NATS access call privileged ops?

For each item, cite exact files and line numbers. Quote snippets where useful. Report under 800 words.


Excellent! I have all the information I need. Let me write my report:

## Security Audit Report: Auth-Adjacent Attack Surfaces

### 1. **CRITICAL: Open Redirect Vulnerability**
**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (lines 391-394)

The login handler directly uses user-supplied `redirect` form parameter without validation:
```go
if r.FormValue("redirect") != "" {
    http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
```
An attacker can craft URLs like `login?redirect=https://evil.com` to redirect authenticated users post-login. This is a classic open redirect enabling phishing attacks. The redirect should be validated to ensure it's relative or to a whitelisted domain.

---

### 2. **Secret Loading Issues**

**JWT Keys:**
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/jwt.go` (lines 50-66): Keys loaded from `JWT_PUBLIC_KEY` and `JWT_PRIVATE_KEY` env vars. No hardcoded defaults.
- `/Users/jan/prg/ClusterCockpit/cc-backend/cmd/cc-backend/init.go` (lines 24-25): Default keys provided in sample `.env` file with clear warnings ("DO NOT USE THESE TWO IN PRODUCTION!").

**Session Secret:**
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (lines 188-202): `SESSION_KEY` env var, or random bytes generated on startup if missing. The generated random key is non-persistent (session won't survive restarts).
- Init file shows hardcoded sample: `SESSION_KEY="REDACTED"` — documented as test-only.

**LDAP Bind Password:**
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/ldap.go` (line 47): Loaded from `LDAP_ADMIN_PASSWORD`. Warning issued if unset, no hardcoded default.

**OIDC Client Secret:**
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/oidc.go` (lines 73-76): Loaded from `OID_CLIENT_SECRET`. Warning issued if unset, no hardcoded default.

**Finding:** Secrets are not logged anywhere in auth code. No information leakage detected in that respect.

---

### 3. **SQL Injection: User Repository**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/user.go`

SQL queries are properly parameterized:
- Line 78-79: Uses `sq.Select().Where(..., username)` with parameterized query builder
- Line 163: `sq.Insert("hpc_user").Columns(cols...).Values(vals...)` — safe builder pattern
- Line 224, 234, 241, 247: All `sq.Update()` calls use parameterized `Where()` clauses
- Line 302: `DELETE` uses parameterized `WHERE ... = ?`

**No SQL injection found.** The codebase uses Masterminds/squirrel query builder consistently.

---

### 4. **LDAP Injection Protection**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/ldap.go`

- Line 91: `ldap.EscapeFilter(username)` — properly escapes LDAP filter metacharacters
- Line 134: `ldap.EscapeDN(user.Username)` — properly escapes DN metacharacters

**No LDAP injection vulnerability found.**

---

### 5. **OIDC: State/Nonce Validation**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/oidc.go`

- Lines 96-114: State validation via cookie comparison (CSRF protection)
- Lines 147-163: Nonce validation (replay attack protection) and ID token verification
- Lines 289-299: State and nonce properly generated with random values

**Replay protection is implemented correctly.**

---

### 6. **Error Messages: Information Disclosure**

**Files:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/local.go` (line 44), `ldap.go` (line 138)

Both local and LDAP authenticators return generic error: `fmt.Errorf("Authentication failed")`

**Good:** No user enumeration via "user not found" vs "bad password" differences. All failures return the same generic message.

---

### 7. **Session Cookie Security**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/internal/auth/auth.go` (lines 315-327)

- `SameSite=Lax` set (line 327) — good CSRF protection
- **Warning issued** if not HTTPS (line 320)
- `Secure` flag correctly set to `false` only for non-TLS, with explicit warning
- `HttpOnly` NOT explicitly set in code! This is a potential XSS exposure risk if session is stored in cookies.

**Issue:** No explicit `HttpOnly: true` found in `session.Options`. Gorilla sessions may default to HttpOnly=true, but this should be explicitly set.

---

### 8. **Frontend Token Storage**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/generic/utils.js` (line 31)

JWT is passed via `Authorization: Bearer` header (not localStorage). Good.

**localStorage usage** found in `/Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/config/admin/Options.svelte` (lines 25-31) but only for non-sensitive UI state (`cc-scramble-names` flag), not for tokens.

**No XSS-exposed JWT storage found.**

---

### 9. **Default Credentials**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/README.md`

The README example shows: `./cc-backend -init-db -add-user demo:admin:demo`

**Documentation clearly states:** "Please do not use it for production!" before showing the demo command. Users must explicitly use `-add-user` flag; no auto-seeded admin accounts.

---

### 10. **Dependencies**

**File:** `/Users/jan/prg/ClusterCockpit/cc-backend/go.mod` (line 25)

- `github.com/golang-jwt/jwt/v5` v5.3.1 — current version, Ed25519 support, no stale v3 usage
- `golang.org/x/crypto` v0.52.0 — current, non-deprecated

**No stale crypto libraries found.**

---

## Summary

**Critical Issue:** Open redirect in login handler (lines 391-394 of auth.go) — user-supplied redirect parameter not validated.

**Minor Issues:**
- Session `HttpOnly` flag should be explicitly confirmed in cookie options
- Session key is randomly regenerated on restart (non-persistent) — document this or use persistent default if needed

**Strengths:** Proper parameterized SQL, LDAP/OIDC escaping, nonce/state validation, generic error messages, current dependencies, frontend JWT not in localStorage.

Prompt:
Explore auth-adjacent attack surfaces in this Go codebase at /Users/jan/prg/ClusterCockpit/cc-backend. I'm doing a security audit. I already have agents looking at the auth code itself and how it's wired into requests — your job is the surrounding surfaces.

Investigate and report:

1. **Secret loading** — how secrets are loaded from .env / env vars:
   - JWT signing keys (private/public)
   - Session secret
   - LDAP bind password
   - OIDC client secret
   - Are any defaulted to a hardcoded value if env is missing?
   - Are secrets logged anywhere?

2. **User input handling in auth code**:
   - SQL queries in user/auth repositories (`internal/repository/user.go` and similar) — are they parameterized?
   - LDAP search filters — escaping of user input (LDAP injection)?
   - OIDC state/nonce validation — replay protection?
   - Redirect URLs after login — open redirect possible?

3. **Error responses** — do auth failure paths leak information (e.g. "user not found" vs "bad password")? Look in login handlers.

4. **Logging** — search for `log.*password`, `log.*token`, `log.*secret`, etc. Are credentials accidentally logged?

5. **Default credentials / seeded admin** — search `internal/repository/` and the init flow for default users or seed credentials. The README mentions `-add-user demo:admin:demo` — is that documented as production-unsafe?

6. **Config file handling** — does anything in `config.json` get reflected back to users? Could a malicious operator inject XSS via cluster names etc.?

7. **Frontend auth** — `web/frontend/src/` — how does the frontend handle tokens/sessions? Are JWTs stored in localStorage (XSS-exposed) vs httpOnly cookies?

8. **Dependencies** — check go.mod for known-stale auth-related libs (golang-jwt v3 vs v4/v5, deprecated crypto packages).

For each finding, cite file paths and line numbers. Quote short snippets only when needed. Report under 800 words.


I have a comprehensive picture from the agents. Now let me verify the most critical findings by reading the relevant code directly — the agents' summaries describe what they intended to find, and a security audit needs precision on the actual code.


1	// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
2	// All rights reserved. This file is part of cc-backend.
3	// Use of this source code is governed by a MIT-style
4	// license that can be found in the LICENSE file.
5
6	// Package auth implements various authentication methods
7	package auth
8
9	import (
10		"bytes"
11		"context"
12		"crypto/rand"
13		"database/sql"
14		"encoding/base64"
15		"encoding/json"
16		"errors"
17		"fmt"
18		"net"
19		"net/http"
20		"os"
21		"sync"
22		"time"
23
24		"golang.org/x/time/rate"
25
26		"github.com/ClusterCockpit/cc-backend/internal/config"
27		"github.com/ClusterCockpit/cc-backend/internal/repository"
28		cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
29		"github.com/ClusterCockpit/cc-lib/v2/schema"
30		"github.com/ClusterCockpit/cc-lib/v2/util"
31		"github.com/gorilla/sessions"
32	)
33
34	// Authenticator is the interface for all authentication methods.
35	// Each authenticator determines if it can handle a login request (CanLogin)
36	// and performs the actual authentication (Login).
37	type Authenticator interface {
38		// CanLogin determines if this authenticator can handle the login request.
39		// It returns the user object if available and a boolean indicating if this
40		// authenticator should attempt the login. This method should not perform
41		// expensive operations or actual authentication.
42		CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)
43
44		// Login performs the actually authentication for the user.
45		// It returns the authenticated user or an error if authentication fails.
46		// The user parameter may be nil if the user doesn't exist in the database yet.
47		Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
48	}
49
50	var (
51		initOnce     sync.Once
52		authInstance *Authentication
53	)
54
55	// rateLimiterEntry tracks a rate limiter and its last use time for cleanup
56	type rateLimiterEntry struct {
57		limiter  *rate.Limiter
58		lastUsed time.Time
59	}
60
61	var ipUserLimiters sync.Map
62
63	// getIPUserLimiter returns a rate limiter for the given IP and username combination.
64	// Rate limiters are created on demand and track 5 attempts per 15 minutes.
65	func getIPUserLimiter(ip, username string) *rate.Limiter {
66		key := ip + ":" + username
67		now := time.Now()
68
69		if entry, ok := ipUserLimiters.Load(key); ok {
70			rle := entry.(*rateLimiterEntry)
71			rle.lastUsed = now
72			return rle.limiter
73		}
74
75		// More aggressive rate limiting: 5 attempts per 15 minutes
76		newLimiter := rate.NewLimiter(rate.Every(15*time.Minute/5), 5)
77		ipUserLimiters.Store(key, &rateLimiterEntry{
78			limiter:  newLimiter,
79			lastUsed: now,
80		})
81		return newLimiter
82	}
83
84	// cleanupOldRateLimiters removes rate limiters that haven't been used recently
85	func cleanupOldRateLimiters(olderThan time.Time) {
86		ipUserLimiters.Range(func(key, value any) bool {
87			entry := value.(*rateLimiterEntry)
88			if entry.lastUsed.Before(olderThan) {
89				ipUserLimiters.Delete(key)
90				cclog.Debugf("Cleaned up rate limiter for %v", key)
91			}
92			return true
93		})
94	}
95
96	// startRateLimiterCleanup starts a background goroutine to clean up old rate limiters
97	func startRateLimiterCleanup() {
98		go func() {
99			ticker := time.NewTicker(1 * time.Hour)
100			defer ticker.Stop()
101			for range ticker.C {
102				// Clean up limiters not used in the last 24 hours
103				cleanupOldRateLimiters(time.Now().Add(-24 * time.Hour))
104			}
105		}()
106	}
107
108	// AuthConfig contains configuration for all authentication methods
109	type AuthConfig struct {
110		LdapConfig   *LdapConfig    `json:"ldap"`
111		JwtConfig    *JWTAuthConfig `json:"jwts"`
112		OpenIDConfig *OpenIDConfig  `json:"oidc"`
113	}
114
115	// Keys holds the global authentication configuration
116	var Keys AuthConfig
117
118	// Authentication manages all authentication methods and session handling
119	type Authentication struct {
120		sessionStore   *sessions.CookieStore
121		LdapAuth       *LdapAuthenticator
122		JwtAuth        *JWTAuthenticator
123		LocalAuth      *LocalAuthenticator
124		authenticators []Authenticator
125		SessionMaxAge  time.Duration
126	}
127
128	func (auth *Authentication) AuthViaSession(
129		rw http.ResponseWriter,
130		r *http.Request,
131	) (*schema.User, error) {
132		session, err := auth.sessionStore.Get(r, "session")
133		if err != nil {
134			cclog.Error("Error while getting session store")
135			return nil, err
136		}
137
138		if session.IsNew {
139			return nil, nil
140		}
141
142		// Validate session data with proper type checking
143		username, ok := session.Values["username"].(string)
144		if !ok || username == "" {
145			cclog.Warn("Invalid session: missing or invalid username")
146			// Invalidate the corrupted session
147			session.Options.MaxAge = -1
148			_ = auth.sessionStore.Save(r, rw, session)
149			return nil, errors.New("invalid session data")
150		}
151
152		projects, ok := session.Values["projects"].([]string)
153		if !ok {
154			cclog.Warn("Invalid session: projects not found or invalid type, using empty list")
155			projects = []string{}
156		}
157
158		roles, ok := session.Values["roles"].([]string)
159		if !ok || len(roles) == 0 {
160			cclog.Warn("Invalid session: missing or invalid roles")
161			// Invalidate the corrupted session
162			session.Options.MaxAge = -1
163			_ = auth.sessionStore.Save(r, rw, session)
164			return nil, errors.New("invalid session data")
165		}
166
167		authSourceInt, ok := session.Values["authSource"].(int)
168		if !ok {
169			authSourceInt = int(schema.AuthViaLocalPassword)
170		}
171
172		return &schema.User{
173			Username:   username,
174			Projects:   projects,
175			Roles:      roles,
176			AuthType:   schema.AuthSession,
177			AuthSource: schema.AuthSource(authSourceInt),
178		}, nil
179	}
180
181	func Init(authCfg *json.RawMessage) {
182		initOnce.Do(func() {
183			authInstance = &Authentication{}
184
185			// Start background cleanup of rate limiters
186			startRateLimiterCleanup()
187
188			sessKey := os.Getenv("SESSION_KEY")
189			if sessKey == "" {
190				cclog.Warn("environment variable 'SESSION_KEY' not set (will use non-persistent random key)")
191				bytes := make([]byte, 32)
192				if _, err := rand.Read(bytes); err != nil {
193					cclog.Fatal("Error while initializing authentication -> failed to generate random bytes for session key")
194				}
195				authInstance.sessionStore = sessions.NewCookieStore(bytes)
196			} else {
197				bytes, err := base64.StdEncoding.DecodeString(sessKey)
198				if err != nil {
199					cclog.Fatal("Error while initializing authentication -> decoding session key failed")
200				}
201				authInstance.sessionStore = sessions.NewCookieStore(bytes)
202			}
203
204			if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil {
205				authInstance.SessionMaxAge = d
206			}
207
208			if authCfg == nil {
209				return
210			}
211
212			config.Validate(configSchema, *authCfg)
213			dec := json.NewDecoder(bytes.NewReader(*authCfg))
214			dec.DisallowUnknownFields()
215			if err := dec.Decode(&Keys); err != nil {
216				cclog.Errorf("error while decoding ldap config: %v", err)
217			}
218
219			if Keys.LdapConfig != nil {
220				ldapAuth := &LdapAuthenticator{}
221				if err := ldapAuth.Init(); err != nil {
222					cclog.Warn("Error while initializing authentication -> ldapAuth init failed")
223				} else {
224					authInstance.LdapAuth = ldapAuth
225					authInstance.authenticators = append(authInstance.authenticators, authInstance.LdapAuth)
226				}
227			} else {
228				cclog.Info("Missing LDAP configuration: No LDAP support!")
229			}
230
231			if Keys.JwtConfig != nil {
232				authInstance.JwtAuth = &JWTAuthenticator{}
233				if err := authInstance.JwtAuth.Init(); err != nil {
234					cclog.Fatal("Error while initializing authentication -> jwtAuth init failed")
235				}
236
237				jwtSessionAuth := &JWTSessionAuthenticator{}
238				if err := jwtSessionAuth.Init(); err != nil {
239					cclog.Info("jwtSessionAuth init failed: No JWT login support!")
240				} else {
241					authInstance.authenticators = append(authInstance.authenticators, jwtSessionAuth)
242				}
243
244				jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
245				if err := jwtCookieSessionAuth.Init(); err != nil {
246					cclog.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
247				} else {
248					authInstance.authenticators = append(authInstance.authenticators, jwtCookieSessionAuth)
249				}
250			} else {
251				cclog.Info("Missing JWT configuration: No JWT token support!")
252			}
253
254			authInstance.LocalAuth = &LocalAuthenticator{}
255			if err := authInstance.LocalAuth.Init(); err != nil {
256				cclog.Fatal("Error while initializing authentication -> localAuth init failed")
257			}
258			authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
259		})
260	}
261
262	func GetAuthInstance() *Authentication {
263		if authInstance == nil {
264			cclog.Fatal("Authentication module not initialized!")
265		}
266
267		return authInstance
268	}
269
270	// handleUserSync syncs or updates a user in the database based on configuration.
271	// This is used for LDAP, JWT and OIDC authentications when syncUserOnLogin or updateUserOnLogin is enabled.
272	func handleUserSync(user *schema.User, syncUserOnLogin, updateUserOnLogin bool) {
273		r := repository.GetUserRepository()
274		dbUser, err := r.GetUser(user.Username)
275
276		if err != nil && err != sql.ErrNoRows {
277			cclog.Errorf("Error while loading user '%s': %v", user.Username, err)
278			return
279		}
280
281		if err == sql.ErrNoRows && syncUserOnLogin { // Add new user
282			if err := r.AddUser(user); err != nil {
283				cclog.Errorf("Error while adding user '%s' to DB: %v", user.Username, err)
284			}
285		} else if err == nil && updateUserOnLogin { // Update existing user
286			if err := r.UpdateUser(dbUser, user); err != nil {
287				cclog.Errorf("Error while updating user '%s' in DB: %v", dbUser.Username, err)
288			}
289		}
290	}
291
292	// handleTokenUser syncs JWT token user with database
293	func handleTokenUser(tokenUser *schema.User) {
294		handleUserSync(tokenUser, Keys.JwtConfig.SyncUserOnLogin, Keys.JwtConfig.UpdateUserOnLogin)
295	}
296
297	// handleOIDCUser syncs OIDC user with database
298	func handleOIDCUser(OIDCUser *schema.User) {
299		handleUserSync(OIDCUser, Keys.OpenIDConfig.SyncUserOnLogin, Keys.OpenIDConfig.UpdateUserOnLogin)
300	}
301
302	// handleLdapUser syncs LDAP user with database
303	func handleLdapUser(ldapUser *schema.User) {
304		handleUserSync(ldapUser, Keys.LdapConfig.SyncUserOnLogin, Keys.LdapConfig.UpdateUserOnLogin)
305	}
306
307	func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error {
308		session, err := auth.sessionStore.New(r, "session")
309		if err != nil {
310			cclog.Errorf("session creation failed: %s", err.Error())
311			http.Error(rw, err.Error(), http.StatusInternalServerError)
312			return err
313		}
314
315		if auth.SessionMaxAge != 0 {
316			session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
317		}
318		if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
319			// If neither TLS or an encrypted reverse proxy are used, do not mark cookies as secure.
320			cclog.Warn("Authenticating with unencrypted request. Session cookies will not have Secure flag set (insecure for production)")
321			if r.Header.Get("X-Forwarded-Proto") == "" {
322				// This warning will not be printed if e.g. X-Forwarded-Proto == http
323				cclog.Warn("If you are using a reverse proxy, make sure X-Forwarded-Proto is set")
324			}
325			session.Options.Secure = false
326		}
327		session.Options.SameSite = http.SameSiteLaxMode
328		session.Values["username"] = user.Username
329		session.Values["projects"] = user.Projects
330		session.Values["roles"] = user.Roles
331		session.Values["authSource"] = int(user.AuthSource)
332		if err := auth.sessionStore.Save(r, rw, session); err != nil {
333			cclog.Warnf("session save failed: %s", err.Error())
334			http.Error(rw, err.Error(), http.StatusInternalServerError)
335			return err
336		}
337
338		return nil
339	}
340
341	func (auth *Authentication) Login(
342		onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
343	) http.Handler {
344		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
345			ip, _, err := net.SplitHostPort(r.RemoteAddr)
346			if err != nil {
347				ip = r.RemoteAddr
348			}
349
350			username := r.FormValue("username")
351
352			limiter := getIPUserLimiter(ip, username)
353			if !limiter.Allow() {
354				cclog.Warnf("AUTH/RATE > Too many login attempts for combination IP: %s, Username: %s", ip, username)
355				onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes"))
356				return
357			}
358
359			var dbUser *schema.User
360			if username != "" {
361				var err error
362				dbUser, err = repository.GetUserRepository().GetUser(username)
363				if err != nil && err != sql.ErrNoRows {
364					cclog.Errorf("Error while loading user '%v'", username)
365				}
366			}
367
368			for _, authenticator := range auth.authenticators {
369				var ok bool
370				var user *schema.User
371				if user, ok = authenticator.CanLogin(dbUser, username, rw, r); !ok {
372					continue
373				} else {
374					cclog.Debugf("Can login with user %v", user)
375				}
376
377				user, err := authenticator.Login(user, rw, r)
378				if err != nil {
379					cclog.Warnf("user login failed: %s", err.Error())
380					onfailure(rw, r, err)
381					return
382				}
383
384				if err := auth.SaveSession(rw, r, user); err != nil {
385					return
386				}
387
388				cclog.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
389				ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
390
391				if r.FormValue("redirect") != "" {
392					http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
393					return
394				}
395
396				http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
397				return
398			}
399
400			cclog.Debugf("login failed: no authenticator applied")
401			onfailure(rw, r, errors.New("no authenticator applied"))
402		})
403	}
404
405	func (auth *Authentication) Auth(
406		onsuccess http.Handler,
407		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
408	) http.Handler {
409		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
410			user, err := auth.JwtAuth.AuthViaJWT(rw, r)
411			if err != nil {
412				cclog.Infof("auth -> authentication failed: %s", err.Error())
413				http.Error(rw, err.Error(), http.StatusUnauthorized)
414				return
415			}
416			if user == nil {
417				user, err = auth.AuthViaSession(rw, r)
418				if err != nil {
419					cclog.Infof("auth -> authentication failed: %s", err.Error())
420					http.Error(rw, err.Error(), http.StatusUnauthorized)
421					return
422				}
423			}
424			if user != nil {
425				ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
426				onsuccess.ServeHTTP(rw, r.WithContext(ctx))
427				return
428			}
429
430			cclog.Infof("auth -> authentication failed: no valid session or JWT for %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
431			onfailure(rw, r, errors.New("unauthorized (please login first)"))
432		})
433	}
434
435	func (auth *Authentication) AuthAPI(
436		onsuccess http.Handler,
437		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
438	) http.Handler {
439		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
440			user, err := auth.JwtAuth.AuthViaJWT(rw, r)
441			if err != nil {
442				cclog.Infof("auth api -> authentication failed: %s", err.Error())
443				onfailure(rw, r, err)
444				return
445			}
446
447			ipErr := securedCheck(user, r)
448			if ipErr != nil {
449				cclog.Infof("auth api -> secured check failed: %s", ipErr.Error())
450				onfailure(rw, r, ipErr)
451				return
452			}
453
454			if user != nil {
455				switch {
456				case len(user.Roles) == 1:
457					if user.HasRole(schema.RoleAPI) {
458						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
459						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
460						return
461					}
462				case len(user.Roles) >= 2:
463					if user.HasAllRoles([]schema.Role{schema.RoleAdmin, schema.RoleAPI}) {
464						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
465						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
466						return
467					}
468				default:
469					cclog.Info("auth api -> authentication failed: missing role")
470					onfailure(rw, r, errors.New("unauthorized"))
471				}
472			}
473			cclog.Info("auth api -> authentication failed: no auth")
474			onfailure(rw, r, errors.New("unauthorized"))
475		})
476	}
477
478	func (auth *Authentication) AuthUserAPI(
479		onsuccess http.Handler,
480		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
481	) http.Handler {
482		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
483			user, err := auth.JwtAuth.AuthViaJWT(rw, r)
484			if err != nil {
485				cclog.Infof("auth user api -> authentication failed: %s", err.Error())
486				onfailure(rw, r, err)
487				return
488			}
489
490			if user != nil {
491				switch {
492				case len(user.Roles) == 1:
493					if user.HasRole(schema.RoleAPI) {
494						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
495						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
496						return
497					}
498				case len(user.Roles) >= 2:
499					if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleSupport, schema.RoleAdmin}) {
500						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
501						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
502						return
503					}
504				default:
505					cclog.Info("auth user api -> authentication failed: missing role")
506					onfailure(rw, r, errors.New("unauthorized"))
507				}
508			}
509			cclog.Info("auth user api -> authentication failed: no auth")
510			onfailure(rw, r, errors.New("unauthorized"))
511		})
512	}
513
514	func (auth *Authentication) AuthMetricStoreAPI(
515		onsuccess http.Handler,
516		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
517	) http.Handler {
518		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
519			user, err := auth.JwtAuth.AuthViaJWT(rw, r)
520			if err != nil {
521				cclog.Infof("auth metricstore api -> authentication failed: %s", err.Error())
522				onfailure(rw, r, err)
523				return
524			}
525
526			if user != nil {
527				switch {
528				case len(user.Roles) == 1:
529					if user.HasRole(schema.RoleAPI) {
530						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
531						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
532						return
533					}
534				case len(user.Roles) >= 2:
535					if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
536						ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
537						onsuccess.ServeHTTP(rw, r.WithContext(ctx))
538						return
539					}
540				default:
541					cclog.Info("auth metricstore api -> authentication failed: missing role")
542					onfailure(rw, r, errors.New("unauthorized"))
543				}
544			}
545			cclog.Info("auth metricstore api -> authentication failed: no auth")
546			onfailure(rw, r, errors.New("unauthorized"))
547		})
548	}
549
550	func (auth *Authentication) AuthConfigAPI(
551		onsuccess http.Handler,
552		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
553	) http.Handler {
554		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
555			user, err := auth.AuthViaSession(rw, r)
556			if err != nil {
557				cclog.Infof("auth config api -> authentication failed: %s", err.Error())
558				onfailure(rw, r, err)
559				return
560			}
561			if user != nil && user.HasRole(schema.RoleAdmin) {
562				ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
563				onsuccess.ServeHTTP(rw, r.WithContext(ctx))
564				return
565			}
566			cclog.Info("auth config api -> authentication failed: no auth")
567			onfailure(rw, r, errors.New("unauthorized"))
568		})
569	}
570
571	func (auth *Authentication) AuthFrontendAPI(
572		onsuccess http.Handler,
573		onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
574	) http.Handler {
575		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
576			user, err := auth.AuthViaSession(rw, r)
577			if err != nil {
578				cclog.Infof("auth frontend api -> authentication failed: %s", err.Error())
579				onfailure(rw, r, err)
580				return
581			}
582			if user != nil {
583				ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
584				onsuccess.ServeHTTP(rw, r.WithContext(ctx))
585				return
586			}
587			cclog.Info("auth frontend api -> authentication failed: no auth")
588			onfailure(rw, r, errors.New("unauthorized"))
589		})
590	}
591
592	func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
593		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
594			session, err := auth.sessionStore.Get(r, "session")
595			if err != nil {
596				http.Error(rw, err.Error(), http.StatusInternalServerError)
597				return
598			}
599
600			if !session.IsNew {
601				session.Options.MaxAge = -1
602				if err := auth.sessionStore.Save(r, rw, session); err != nil {
603					http.Error(rw, err.Error(), http.StatusInternalServerError)
604					return
605				}
606			}
607
608			onsuccess.ServeHTTP(rw, r)
609		})
610	}
611
612	// Helper Moved To MiddleWare Auth Handlers
613	func securedCheck(user *schema.User, r *http.Request) error {
614		if user == nil {
615			return fmt.Errorf("no user for secured check")
616		}
617
618		// extract IP address for checking
619		IPAddress := r.Header.Get("X-Real-Ip")
620		if IPAddress == "" {
621			IPAddress = r.Header.Get("X-Forwarded-For")
622		}
623		if IPAddress == "" {
624			IPAddress = r.RemoteAddr
625		}
626
627		// Handle both IPv4 and IPv6 addresses properly
628		// For IPv6, this will strip the port and brackets
629		// For IPv4, this will strip the port
630		if host, _, err := net.SplitHostPort(IPAddress); err == nil {
631			IPAddress = host
632		}
633		// If SplitHostPort fails, IPAddress is already just a host (no port)
634
635		// If nothing declared in config: Continue
636		if len(config.Keys.APIAllowedIPs) == 0 {
637			return nil
638		}
639		// If wildcard declared in config: Continue
640		if config.Keys.APIAllowedIPs[0] == "*" {
641			return nil
642		}
643		// check if IP is allowed
644		if !util.Contains(config.Keys.APIAllowedIPs, IPAddress) {
645			return fmt.Errorf("unknown ip: %v", IPAddress)
646		}
647
648		return nil
649	}
650

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-backend.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Package auth implements various authentication methods
package auth

import (
	"bytes"
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"sync"
	"time"

	"golang.org/x/time/rate"

	"github.com/ClusterCockpit/cc-backend/internal/config"
	"github.com/ClusterCockpit/cc-backend/internal/repository"
	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	"github.com/ClusterCockpit/cc-lib/v2/schema"
	"github.com/ClusterCockpit/cc-lib/v2/util"
	"github.com/gorilla/sessions"
)

// Authenticator is the interface for all authentication methods.
// Each authenticator determines if it can handle a login request (CanLogin)
// and performs the actual authentication (Login).
type Authenticator interface {
	// CanLogin determines if this authenticator can handle the login request.
	// It returns the user object if available and a boolean indicating if this
	// authenticator should attempt the login. This method should not perform
	// expensive operations or actual authentication.
	CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)

	// Login performs the actually authentication for the user.
	// It returns the authenticated user or an error if authentication fails.
	// The user parameter may be nil if the user doesn't exist in the database yet.
	Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
}

var (
	initOnce     sync.Once
	authInstance *Authentication
)

// rateLimiterEntry tracks a rate limiter and its last use time for cleanup
type rateLimiterEntry struct {
	limiter  *rate.Limiter
	lastUsed time.Time
}

var ipUserLimiters sync.Map

// getIPUserLimiter returns a rate limiter for the given IP and username combination.
// Rate limiters are created on demand and track 5 attempts per 15 minutes.
func getIPUserLimiter(ip, username string) *rate.Limiter {
	key := ip + ":" + username
	now := time.Now()

	if entry, ok := ipUserLimiters.Load(key); ok {
		rle := entry.(*rateLimiterEntry)
		rle.lastUsed = now
		return rle.limiter
	}

	// More aggressive rate limiting: 5 attempts per 15 minutes
	newLimiter := rate.NewLimiter(rate.Every(15*time.Minute/5), 5)
	ipUserLimiters.Store(key, &rateLimiterEntry{
		limiter:  newLimiter,
		lastUsed: now,
	})
	return newLimiter
}

// cleanupOldRateLimiters removes rate limiters that haven't been used recently
func cleanupOldRateLimiters(olderThan time.Time) {
	ipUserLimiters.Range(func(key, value any) bool {
		entry := value.(*rateLimiterEntry)
		if entry.lastUsed.Before(olderThan) {
			ipUserLimiters.Delete(key)
			cclog.Debugf("Cleaned up rate limiter for %v", key)
		}
		return true
	})
}

// startRateLimiterCleanup starts a background goroutine to clean up old rate limiters
func startRateLimiterCleanup() {
	go func() {
		ticker := time.NewTicker(1 * time.Hour)
		defer ticker.Stop()
		for range ticker.C {
			// Clean up limiters not used in the last 24 hours
			cleanupOldRateLimiters(time.Now().Add(-24 * time.Hour))
		}
	}()
}

// AuthConfig contains configuration for all authentication methods
type AuthConfig struct {
	LdapConfig   *LdapConfig    `json:"ldap"`
	JwtConfig    *JWTAuthConfig `json:"jwts"`
	OpenIDConfig *OpenIDConfig  `json:"oidc"`
}

// Keys holds the global authentication configuration
var Keys AuthConfig

// Authentication manages all authentication methods and session handling
type Authentication struct {
	sessionStore   *sessions.CookieStore
	LdapAuth       *LdapAuthenticator
	JwtAuth        *JWTAuthenticator
	LocalAuth      *LocalAuthenticator
	authenticators []Authenticator
	SessionMaxAge  time.Duration
}

func (auth *Authentication) AuthViaSession(
	rw http.ResponseWriter,
	r *http.Request,
) (*schema.User, error) {
	session, err := auth.sessionStore.Get(r, "session")
	if err != nil {
		cclog.Error("Error while getting session store")
		return nil, err
	}

	if session.IsNew {
		return nil, nil
	}

	// Validate session data with proper type checking
	username, ok := session.Values["username"].(string)
	if !ok || username == "" {
		cclog.Warn("Invalid session: missing or invalid username")
		// Invalidate the corrupted session
		session.Options.MaxAge = -1
		_ = auth.sessionStore.Save(r, rw, session)
		return nil, errors.New("invalid session data")
	}

	projects, ok := session.Values["projects"].([]string)
	if !ok {
		cclog.Warn("Invalid session: projects not found or invalid type, using empty list")
		projects = []string{}
	}

	roles, ok := session.Values["roles"].([]string)
	if !ok || len(roles) == 0 {
		cclog.Warn("Invalid session: missing or invalid roles")
		// Invalidate the corrupted session
		session.Options.MaxAge = -1
		_ = auth.sessionStore.Save(r, rw, session)
		return nil, errors.New("invalid session data")
	}

	authSourceInt, ok := session.Values["authSource"].(int)
	if !ok {
		authSourceInt = int(schema.AuthViaLocalPassword)
	}

	return &schema.User{
		Username:   username,
		Projects:   projects,
		Roles:      roles,
		AuthType:   schema.AuthSession,
		AuthSource: schema.AuthSource(authSourceInt),
	}, nil
}

func Init(authCfg *json.RawMessage) {
	initOnce.Do(func() {
		authInstance = &Authentication{}

		// Start background cleanup of rate limiters
		startRateLimiterCleanup()

		sessKey := os.Getenv("SESSION_KEY")
		if sessKey == "" {
			cclog.Warn("environment variable 'SESSION_KEY' not set (will use non-persistent random key)")
			bytes := make([]byte, 32)
			if _, err := rand.Read(bytes); err != nil {
				cclog.Fatal("Error while initializing authentication -> failed to generate random bytes for session key")
			}
			authInstance.sessionStore = sessions.NewCookieStore(bytes)
		} else {
			bytes, err := base64.StdEncoding.DecodeString(sessKey)
			if err != nil {
				cclog.Fatal("Error while initializing authentication -> decoding session key failed")
			}
			authInstance.sessionStore = sessions.NewCookieStore(bytes)
		}

		if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil {
			authInstance.SessionMaxAge = d
		}

		if authCfg == nil {
			return
		}

		config.Validate(configSchema, *authCfg)
		dec := json.NewDecoder(bytes.NewReader(*authCfg))
		dec.DisallowUnknownFields()
		if err := dec.Decode(&Keys); err != nil {
			cclog.Errorf("error while decoding ldap config: %v", err)
		}

		if Keys.LdapConfig != nil {
			ldapAuth := &LdapAuthenticator{}
			if err := ldapAuth.Init(); err != nil {
				cclog.Warn("Error while initializing authentication -> ldapAuth init failed")
			} else {
				authInstance.LdapAuth = ldapAuth
				authInstance.authenticators = append(authInstance.authenticators, authInstance.LdapAuth)
			}
		} else {
			cclog.Info("Missing LDAP configuration: No LDAP support!")
		}

		if Keys.JwtConfig != nil {
			authInstance.JwtAuth = &JWTAuthenticator{}
			if err := authInstance.JwtAuth.Init(); err != nil {
				cclog.Fatal("Error while initializing authentication -> jwtAuth init failed")
			}

			jwtSessionAuth := &JWTSessionAuthenticator{}
			if err := jwtSessionAuth.Init(); err != nil {
				cclog.Info("jwtSessionAuth init failed: No JWT login support!")
			} else {
				authInstance.authenticators = append(authInstance.authenticators, jwtSessionAuth)
			}

			jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
			if err := jwtCookieSessionAuth.Init(); err != nil {
				cclog.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
			} else {
				authInstance.authenticators = append(authInstance.authenticators, jwtCookieSessionAuth)
			}
		} else {
			cclog.Info("Missing JWT configuration: No JWT token support!")
		}

		authInstance.LocalAuth = &LocalAuthenticator{}
		if err := authInstance.LocalAuth.Init(); err != nil {
			cclog.Fatal("Error while initializing authentication -> localAuth init failed")
		}
		authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
	})
}

func GetAuthInstance() *Authentication {
	if authInstance == nil {
		cclog.Fatal("Authentication module not initialized!")
	}

	return authInstance
}

// handleUserSync syncs or updates a user in the database based on configuration.
// This is used for LDAP, JWT and OIDC authentications when syncUserOnLogin or updateUserOnLogin is enabled.
func handleUserSync(user *schema.User, syncUserOnLogin, updateUserOnLogin bool) {
	r := repository.GetUserRepository()
	dbUser, err := r.GetUser(user.Username)

	if err != nil && err != sql.ErrNoRows {
		cclog.Errorf("Error while loading user '%s': %v", user.Username, err)
		return
	}

	if err == sql.ErrNoRows && syncUserOnLogin { // Add new user
		if err := r.AddUser(user); err != nil {
			cclog.Errorf("Error while adding user '%s' to DB: %v", user.Username, err)
		}
	} else if err == nil && updateUserOnLogin { // Update existing user
		if err := r.UpdateUser(dbUser, user); err != nil {
			cclog.Errorf("Error while updating user '%s' in DB: %v", dbUser.Username, err)
		}
	}
}

// handleTokenUser syncs JWT token user with database
func handleTokenUser(tokenUser *schema.User) {
	handleUserSync(tokenUser, Keys.JwtConfig.SyncUserOnLogin, Keys.JwtConfig.UpdateUserOnLogin)
}

// handleOIDCUser syncs OIDC user with database
func handleOIDCUser(OIDCUser *schema.User) {
	handleUserSync(OIDCUser, Keys.OpenIDConfig.SyncUserOnLogin, Keys.OpenIDConfig.UpdateUserOnLogin)
}

// handleLdapUser syncs LDAP user with database
func handleLdapUser(ldapUser *schema.User) {
	handleUserSync(ldapUser, Keys.LdapConfig.SyncUserOnLogin, Keys.LdapConfig.UpdateUserOnLogin)
}

func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error {
	session, err := auth.sessionStore.New(r, "session")
	if err != nil {
		cclog.Errorf("session creation failed: %s", err.Error())
		http.Error(rw, err.Error(), http.StatusInternalServerError)
		return err
	}

	if auth.SessionMaxAge != 0 {
		session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
	}
	if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
		// If neither TLS or an encrypted reverse proxy are used, do not mark cookies as secure.
		cclog.Warn("Authenticating with unencrypted request. Session cookies will not have Secure flag set (insecure for production)")
		if r.Header.Get("X-Forwarded-Proto") == "" {
			// This warning will not be printed if e.g. X-Forwarded-Proto == http
			cclog.Warn("If you are using a reverse proxy, make sure X-Forwarded-Proto is set")
		}
		session.Options.Secure = false
	}
	session.Options.SameSite = http.SameSiteLaxMode
	session.Values["username"] = user.Username
	session.Values["projects"] = user.Projects
	session.Values["roles"] = user.Roles
	session.Values["authSource"] = int(user.AuthSource)
	if err := auth.sessionStore.Save(r, rw, session); err != nil {
		cclog.Warnf("session save failed: %s", err.Error())
		http.Error(rw, err.Error(), http.StatusInternalServerError)
		return err
	}

	return nil
}

func (auth *Authentication) Login(
	onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			ip = r.RemoteAddr
		}

		username := r.FormValue("username")

		limiter := getIPUserLimiter(ip, username)
		if !limiter.Allow() {
			cclog.Warnf("AUTH/RATE > Too many login attempts for combination IP: %s, Username: %s", ip, username)
			onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes"))
			return
		}

		var dbUser *schema.User
		if username != "" {
			var err error
			dbUser, err = repository.GetUserRepository().GetUser(username)
			if err != nil && err != sql.ErrNoRows {
				cclog.Errorf("Error while loading user '%v'", username)
			}
		}

		for _, authenticator := range auth.authenticators {
			var ok bool
			var user *schema.User
			if user, ok = authenticator.CanLogin(dbUser, username, rw, r); !ok {
				continue
			} else {
				cclog.Debugf("Can login with user %v", user)
			}

			user, err := authenticator.Login(user, rw, r)
			if err != nil {
				cclog.Warnf("user login failed: %s", err.Error())
				onfailure(rw, r, err)
				return
			}

			if err := auth.SaveSession(rw, r, user); err != nil {
				return
			}

			cclog.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)

			if r.FormValue("redirect") != "" {
				http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
				return
			}

			http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
			return
		}

		cclog.Debugf("login failed: no authenticator applied")
		onfailure(rw, r, errors.New("no authenticator applied"))
	})
}

func (auth *Authentication) Auth(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth -> authentication failed: %s", err.Error())
			http.Error(rw, err.Error(), http.StatusUnauthorized)
			return
		}
		if user == nil {
			user, err = auth.AuthViaSession(rw, r)
			if err != nil {
				cclog.Infof("auth -> authentication failed: %s", err.Error())
				http.Error(rw, err.Error(), http.StatusUnauthorized)
				return
			}
		}
		if user != nil {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}

		cclog.Infof("auth -> authentication failed: no valid session or JWT for %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
		onfailure(rw, r, errors.New("unauthorized (please login first)"))
	})
}

func (auth *Authentication) AuthAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		ipErr := securedCheck(user, r)
		if ipErr != nil {
			cclog.Infof("auth api -> secured check failed: %s", ipErr.Error())
			onfailure(rw, r, ipErr)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasAllRoles([]schema.Role{schema.RoleAdmin, schema.RoleAPI}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthUserAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth user api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleSupport, schema.RoleAdmin}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth user api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth user api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthMetricStoreAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
		if err != nil {
			cclog.Infof("auth metricstore api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}

		if user != nil {
			switch {
			case len(user.Roles) == 1:
				if user.HasRole(schema.RoleAPI) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			case len(user.Roles) >= 2:
				if user.HasRole(schema.RoleAPI) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
					ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
					onsuccess.ServeHTTP(rw, r.WithContext(ctx))
					return
				}
			default:
				cclog.Info("auth metricstore api -> authentication failed: missing role")
				onfailure(rw, r, errors.New("unauthorized"))
			}
		}
		cclog.Info("auth metricstore api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthConfigAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.AuthViaSession(rw, r)
		if err != nil {
			cclog.Infof("auth config api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}
		if user != nil && user.HasRole(schema.RoleAdmin) {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}
		cclog.Info("auth config api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) AuthFrontendAPI(
	onsuccess http.Handler,
	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		user, err := auth.AuthViaSession(rw, r)
		if err != nil {
			cclog.Infof("auth frontend api -> authentication failed: %s", err.Error())
			onfailure(rw, r, err)
			return
		}
		if user != nil {
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
			return
		}
		cclog.Info("auth frontend api -> authentication failed: no auth")
		onfailure(rw, r, errors.New("unauthorized"))
	})
}

func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		session, err := auth.sessionStore.Get(r, "session")
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
			return
		}

		if !session.IsNew {
			session.Options.MaxAge = -1
			if err := auth.sessionStore.Save(r, rw, session); err != nil {
				http.Error(rw, err.Error(), http.StatusInternalServerError)
				return
			}
		}

		onsuccess.ServeHTTP(rw, r)
	})
}

// Helper Moved To MiddleWare Auth Handlers
func securedCheck(user *schema.User, r *http.Request) error {
	if user == nil {
		return fmt.Errorf("no user for secured check")
	}

	// extract IP address for checking
	IPAddress := r.Header.Get("X-Real-Ip")
	if IPAddress == "" {
		IPAddress = r.Header.Get("X-Forwarded-For")
	}
	if IPAddress == "" {
		IPAddress = r.RemoteAddr
	}

	// Handle both IPv4 and IPv6 addresses properly
	// For IPv6, this will strip the port and brackets
	// For IPv4, this will strip the port
	if host, _, err := net.SplitHostPort(IPAddress); err == nil {
		IPAddress = host
	}
	// If SplitHostPort fails, IPAddress is already just a host (no port)

	// If nothing declared in config: Continue
	if len(config.Keys.APIAllowedIPs) == 0 {
		return nil
	}
	// If wildcard declared in config: Continue
	if config.Keys.APIAllowedIPs[0] == "*" {
		return nil
	}
	// check if IP is allowed
	if !util.Contains(config.Keys.APIAllowedIPs, IPAddress) {
		return fmt.Errorf("unknown ip: %v", IPAddress)
	}

	return nil
}