Skip to content

feat(auth): dashboard authentication system (#65)#160

Merged
thebtf merged 7 commits into
mainfrom
feat/dashboard-auth
Apr 14, 2026
Merged

feat(auth): dashboard authentication system (#65)#160
thebtf merged 7 commits into
mainfrom
feat/dashboard-auth

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 14, 2026

Summary

Adds user/password authentication with invitation-based registration and optional Authentik SSO to the engram dashboard. Closes #65.

Backend (6 commits)

Data Model (migration 080)

  • users table: email, bcrypt password_hash, role (admin/operator), disabled
  • invitations table: single-use registration codes with created_by/used_by tracking
  • sessions table: DB-backed sessions with expiry

Auth Endpoints

  • POST /api/auth/setup — first-time admin account creation
  • POST /api/auth/user-login — email/password login with rate limiting (5/min/IP)
  • POST /api/auth/user-logout — session invalidation
  • GET /api/auth/me — current user info
  • GET /api/auth/setup-needed — check if setup required
  • POST /api/auth/register — invitation-based registration
  • POST /api/admin/invitations — generate invitation codes (admin only)
  • GET /api/admin/invitations — list invitations (admin only)
  • GET /api/admin/users — list users without password hashes (admin only)
  • PUT /api/admin/users/{id} — disable/enable/promote/demote (admin only)

Middleware

  • Session cookie validation (engram_auth) alongside existing token auth
  • Authentik forward-auth header detection (X-Authentik-Email) with trusted proxy whitelist
  • Auto-provisioning for SSO users (configurable)

Frontend (2 commits)

  • SetupView.vue — first-time admin creation
  • LoginView.vue — email/password + token tabs
  • RegisterView.vue — invitation-based registration
  • AdminView.vue — user management + invitation generation + copy-to-clipboard
  • useAuth.ts — extended with user session management, fetchMe, checkSetupNeeded
  • Router — setup redirect, auth guards, admin-only routes
  • Sidebar — admin nav link (visible to admins only)

Security

  • bcrypt cost 12 for password hashing
  • Rate limiting on login (5 attempts/min/IP)
  • HttpOnly, SameSite=Lax session cookies
  • Admin-only endpoints server-side enforced
  • Last-admin protection (cannot disable/demote last admin)
  • Authentik headers trusted only from configured proxy IPs
  • API token auth unchanged (MCP clients unaffected)

Summary by CodeRabbit

  • Новые возможности
    • Добавлена аутентификация по электронной почте и паролю с управлением сессиями (создание, удаление, очистка истёкших).
    • Реализован мастер настройки для создания первой учётной записи администратора.
    • Добавлена регистрация через одноразовые коды приглашения и админский интерфейс для их управления.
    • Запущена панель администратора для управления пользователями (роли, блокировка) и приглашениями.
    • Добавлена поддержка Authentik SSO (forward-auth) и соответствующие настройки.
    • Обновлён интерфейс входа: вкладки для e-mail/пароля и токена; новые маршруты регистрации/настройки.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73caa796-81cf-4d91-a223-0bd4564f4a70

📥 Commits

Reviewing files that changed from the base of the PR and between d8c7922 and e50669f.

📒 Files selected for processing (2)
  • internal/worker/auth_handlers.go
  • ui/src/views/AdminView.vue

Walkthrough

Добавлена полноценная email/password аутентификация с GORM-моделями, хранилищами и миграциями, серверными обработчиками/маршрутами и middleware; реализована интеграция forward-auth с Authentik и админская панель + фронтенд-интерфейсы для регистрации, настроек и управления приглашениями/пользователями.

Changes

Cohort / File(s) Summary
Configuration
internal/config/config.go
Добавлены опции Authentik: AuthentikEnabled, AuthentikAutoProvision, AuthentikTrustedProxies и загрузка из окружения.
GORM Models
internal/db/gorm/auth_models.go
Новые модели User, Invitation, AuthSession с методами TableName() для таблиц users, invitations, sessions.
DB Stores
internal/db/gorm/user_store.go, internal/db/gorm/invitation_store.go, internal/db/gorm/auth_session_store.go
Реализованы хранилища: UserStore, InvitationStore, AuthSessionStore с операциями CRUD, генерацией кодов/ID, управлением сессиями и очисткой просроченных записей.
Migrations
internal/db/gorm/migrations.go
Добавлена миграция 080_create_auth_tables для создания таблиц users, invitations, sessions и соответствующий откат.
Auth Handlers
internal/worker/auth_handlers.go
Новые HTTP-обработчики для setup, регистрации по приглашению, вход/выход, /api/auth/me, управление приглашениями и пользователями (админ).
Middleware & Service Wiring
internal/worker/middleware.go, internal/worker/service.go
TokenAuth расширён: проверка DB-сессий (engram_auth), Authentik forward-auth (trusted proxies + auto-provision), новые сеттеры конфигурации; сервис создаёт и подключает stores и AuthHandlers, регистрирует маршруты.
Frontend: Auth Logic
ui/src/composables/useAuth.ts
Добавлено состояние user, isAdmin, функции fetchMe, checkSetupNeeded, loginWithCredentials, userLogout; обновлены потоки входа/выхода.
Frontend: Routes & Views
ui/src/router/index.ts, ui/src/views/SetupView.vue, ui/src/views/RegisterView.vue, ui/src/views/LoginView.vue, ui/src/views/AdminView.vue
Добавлены маршруты /setup, /register, /admin с охраной; новые view для setup, регистрации, админ-панели; логин разделён на вкладки (email/password и token).
Frontend: UI Updates
ui/src/components/layout/AppSidebar.vue, ui/package.json, ui/tsconfig.tsbuildinfo
Добавлена видимость ссылки на /admin для админов; обновлён vue-tsc devDependency и tsbuildinfo обновлён для новых view/composables.

Sequence Diagram

sequenceDiagram
    participant Client as Клиент
    participant Router as Route Guard
    participant Auth as Auth Service
    participant DB as База данных
    participant Authentik as Authentik Proxy

    Note over Client,Auth: Email/password login
    Client->>Router: POST /api/auth/user-login
    Router->>Auth: validateCredentials(email,password)
    Auth->>DB: GetUserByEmail(email)
    DB-->>Auth: User record
    Auth->>Auth: verify bcrypt(password)
    Auth->>DB: CreateSession(userID, expires)
    DB-->>Auth: Session (id)
    Auth->>Client: Set `engram_auth` cookie + 200

    Note over Client,Authentik: Authentik forward-auth
    Client->>Authentik: Request (via proxy) with X-Authentik-Email
    Authentik->>Auth: Forward request
    Auth->>Auth: check request IP ∈ trustedProxies
    alt trusted and header present
      Auth->>DB: GetUserByEmail(headerEmail)
      DB-->>Auth: User or nil
      alt user not found and auto-provision
        Auth->>DB: CreateUser(email, role=operator)
        DB-->>Auth: New User
      end
      Auth->>Auth: set role in context
      Auth->>Client: forward allowed
    else not trusted or no header
      Auth->>Client: forward denied or pass through
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Я — кролик, код копаю в норе,
Сессии в земле, приглашенья в лапе,
Authentik шлёт мне свежую зорю,
Админ взмахнул — и панель сияет, ага!
Пусть EnGram бежит — безопасно, в паре.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR objectives describe dashboard authentication features, but the linked issue #65 requires session reuse in bulk import—no such changes appear in the raw_summary. The PR does not implement the stated requirements from issue #65. Add implementation of session_id parameter in handlers_import_export.go and openclaw-engram plugin files to address bulk import session reuse as specified in issue #65.
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(auth): dashboard authentication system' accurately summarizes the main changes: adding a complete user/password authentication system with invitations and SSO for the dashboard.
Out of Scope Changes check ✅ Passed All changes are focused on dashboard authentication (config, auth models, session/user stores, handlers, middleware, UI views, routing). No unrelated modifications to bulk import or other features appear outside the stated scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-auth

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Apr 14, 2026

@coderabbitai review focus:security

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all auth-related backend files
fd -e py -e ts -e vue . --exec echo {} | sort

# Find auth endpoint files
rg -rn "auth" --type py -l
rg -rn "auth" --type ts -l

# Find migration file 080
fd "080" .

# Look for session, rate limiting, authentik, cookie config
rg -rn "rate.limit\|RateLimit\|slowapi\|limiter" --type py -C2

Length of output: 5483


🏁 Script executed:

#!/bin/bash
# Find Python backend auth files
fd -e py . | xargs grep -l "auth\|session\|login\|invitation" 2>/dev/null | head -30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find backend directory structure
tree -L 3 --dirsfirst -I 'node_modules|__pycache__|.git|dist|build' 2>/dev/null || find . -maxdepth 3 -type d | grep -v node_modules | grep -v __pycache__ | grep -v .git | sort

echo "---FILES IN ROOT---"
ls -la

Length of output: 11407


🏁 Script executed:

#!/bin/bash
# Look at auth composable and router for frontend security
cat ui/src/composables/useAuth.ts
echo "===ROUTER==="
cat ui/src/router/index.ts

Length of output: 7173


🏁 Script executed:

#!/bin/bash
cat internal/worker/handlers_auth.go
echo "=== AUTH_HANDLERS ==="
cat internal/worker/auth_handlers.go
echo "=== MIDDLEWARE ==="
cat internal/worker/middleware.go
echo "=== RATELIMIT ==="
cat internal/worker/ratelimit.go

Length of output: 57620


@thebtf Here is a security-focused review of the PR.


Security Review — feat(auth): dashboard authentication system

🔴 Critical

1. Rate-limit key includes TCP port — trivially bypassable

File: internal/worker/auth_handlers.go, handleLogin + checkRateLimit

func (h *AuthHandlers) handleLogin(w http.ResponseWriter, r *http.Request) {
    ip := r.RemoteAddr   // e.g. "203.0.113.5:54321" — port included!
    if !h.checkRateLimit(ip) { ...

r.RemoteAddr is host:port. Because every new TCP connection gets a fresh ephemeral port, an attacker rotates source ports to reset the 5-attempt window with zero effort. The fix is to strip the port:

import "net"

func (h *AuthHandlers) handleLogin(w http.ResponseWriter, r *http.Request) {
    ip, _, err := net.SplitHostPort(r.RemoteAddr)
    if err != nil {
        ip = r.RemoteAddr // fallback
    }
    if !h.checkRateLimit(ip) { ...

🟠 High

2. engram_auth session cookie missing Secure flag

File: internal/worker/auth_handlers.go, handleLogin

The older engram_session HMAC cookie correctly sets Secure: r.TLS != nil. The new engram_auth DB-backed cookie does not:

http.SetCookie(w, &http.Cookie{
    Name:     authSessionCookieName,
    Value:    sess.ID,
    HttpOnly: true,
    SameSite: http.SameSiteLaxMode,
    // ⚠️ No Secure flag — cookie transmitted over plain HTTP
})

Add Secure: r.TLS != nil (or Secure: true if TLS termination happens upstream). Same fix needed in handleLogout when clearing the cookie.

3. Authentik trusted-proxy check breaks on IPv6

File: internal/worker/middleware.go, isTrustedProxy

remoteIP := strings.Split(r.RemoteAddr, ":")[0]

For an IPv6 address like [::1]:8080, this returns [ — matching nothing, so the entire Authentik SSO path silently fails. For a hand-crafted address like ::1 (no brackets), it returns the empty string before the first colon. Use net.SplitHostPort here too.


🟡 Medium

4. loginAttempts sync.Map grows unboundedly

File: internal/worker/auth_handlers.go, checkRateLimit

IP keys (and their per-IP mutexes) are stored in loginAttempts but never deleted. An attacker sending single requests from a rotating set of source IPs (or with rotating ports, see issue 1) causes unbounded memory growth. Add periodic eviction or switch to a TTL-based structure:

// After pruning attempts, remove the key entirely if the slice is empty
if len(*attempts) == 0 {
    h.loginAttempts.Delete(attemptsKey)
    h.loginAttempts.Delete(muKey)
}

5. TOCTOU race in first-time setup

File: internal/worker/auth_handlers.go, handleSetup

count, _ := h.users.CountUsers()   // check
if count > 0 { /* 409 */ }
// ... time passes ...
h.users.CreateUser(...)             // act

Two simultaneous POST requests can both pass the count == 0 check. The unique constraint in the DB prevents a duplicate admin row, but the second request gets a 500 instead of a 409, leaking that setup raced. A sync.Mutex on AuthHandlers (or a DB-level advisory lock) makes this clean.

6. Client-supplied X-Request-ID passed through unsanitised

File: internal/worker/middleware.go, RequestID

requestID := r.Header.Get("X-Request-ID")
if requestID == "" { /* generate */ }
w.Header().Set("X-Request-ID", requestID)

A client can inject arbitrary bytes into response headers and potentially into log lines. Add a length cap and character allowlist (e.g. [a-zA-Z0-9\-]{1,64}).


🔵 Low / Informational

7. isAdmin frontend guard broken for master-token users

File: ui/src/composables/useAuth.ts, ui/src/router/index.ts

GET /api/auth/me returns {authenticated, role} (no user object) for HMAC/master-token sessions. The frontend computes:

const isAdmin = computed(() => user.value?.role === 'admin')

Since user.value is null for master-token logins, isAdmin is always false, and the /admin route is never accessible for master-token sessions. The backend /api/admin/* endpoints enforce the role correctly — so this is a UX gap, not a privilege-escalation — but it should be consistent.

8. No email format validation in setup / register

File: internal/worker/auth_handlers.go

Both handleSetup and handleRegister accept any non-empty string as an email. A basic net/mail.ParseAddress check would prevent obviously invalid addresses from being stored.

9. SameSite inconsistency between the two session cookies

engram_sessionSameSiteStrictMode
engram_authSameSiteLaxMode — consider aligning to Strict unless cross-site navigation to the dashboard is intentionally supported.

10. handleCreateInvitation re-reads the session cookie after admin check

File: internal/worker/auth_handlers.go, handleCreateInvitation

role, _ := r.Context().Value(authRoleKey{}).(string)
if role != "admin" { /* 403 */ }
// ... then fetches cookie again to get user ID
cookie, err := r.Cookie(authSessionCookieName)

The user ID should be stored in the request context by the auth middleware (alongside the role) rather than doing a second cookie + DB round-trip here. This also means master-token admins can never create invitations, which is silently inconsistent.


Summary table

# Severity File Issue
1 🔴 Critical auth_handlers.go Rate-limit key includes TCP port
2 🟠 High auth_handlers.go engram_auth cookie missing Secure
3 🟠 High middleware.go Authentik IPv6 proxy check broken
4 🟡 Medium auth_handlers.go loginAttempts map never GC'd
5 🟡 Medium auth_handlers.go TOCTOU in setup
6 🟡 Medium middleware.go Unsanitised X-Request-ID
7 🔵 Low useAuth.ts / router isAdmin false for master-token sessions
8 🔵 Low auth_handlers.go No email format validation
9 🔵 Low auth_handlers.go SameSite=Lax vs Strict inconsistency
10 🔵 Low auth_handlers.go Invitation handler re-reads cookie

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive authentication and user management system, including email/password login, invitation-based registration, and Authentik SSO integration. It introduces new database models, stores, and migrations for users, invitations, and sessions, as well as backend handlers and frontend views for setup and administration. Feedback identifies critical error handling gaps in the user update logic that could bypass security checks and suggests improving the UI by replacing disruptive alerts with inline error messages.

Comment thread internal/worker/auth_handlers.go Outdated
Comment thread ui/src/views/AdminView.vue
…h reactive error state

- auth_handlers.go: handle errors from CountAdmins() and GetUserByID() in
  UpdateUser handler; previously ignored errors could bypass last-admin guard
- AdminView.vue: replace alert() calls with usersError/invitationsError reactive
  refs for consistent declarative error display
@thebtf thebtf merged commit 5fe3e89 into main Apr 14, 2026
1 check passed
@thebtf thebtf deleted the feat/dashboard-auth branch April 14, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant