Skip to content

Comments

feat: Add session-based admin authentication#23

Merged
mverteuil merged 17 commits intomainfrom
feature/admin-user
Feb 18, 2026
Merged

feat: Add session-based admin authentication#23
mverteuil merged 17 commits intomainfrom
feature/admin-user

Conversation

@mverteuil
Copy link
Owner

Summary

Adds a complete session-based authentication system for admin access:

  • Setup wizard: First-time setup flow at /admin/setup to create admin user
  • Login/logout: Session-based authentication with secure cookie handling
  • Route protection: @require_admin decorator for protecting API endpoints and views
  • SQLAdmin integration: Unified authentication across main app and database admin
  • Middleware stack: Proper ordering of Session → Auth → SetupRedirect middleware

Features

  • Argon2id password hashing for secure credential storage
  • Redis-backed session storage with configurable expiry
  • Automatic redirect to setup when no admin exists
  • Protected API endpoints return 303 redirect to login
  • /api/auth/status endpoint for frontend auth state checks
  • Session regeneration on login to prevent fixation attacks

Test plan

  • Unit tests for auth routes (4 tests)
  • Integration tests updated with authentication fixtures
  • UAT: Setup wizard flow
  • UAT: Login/logout flow
  • UAT: Protected route access
  • UAT: SQLAdmin integration
  • All 558 web/integration tests passing

Implements complete authentication system using Starlette's built-in
authentication framework with Redis-backed sessions.

Features:
- File-based admin user storage with Argon2 password hashing
- Session management via starsessions with Redis backend
- Setup wizard for first-time admin account creation
- Login/logout with session regeneration for security
- Authentication middleware protecting all admin routes
- SQLAdmin integration with custom auth backend

Architecture:
- AuthService: Manages admin user file (JSON) and password hashing
- SessionAuthBackend: Starlette authentication backend
- SetupRedirectMiddleware: Redirects to setup if no admin exists
- require_admin: Syntactic sugar decorator for @requires("authenticated")

Protected routes:
- /admin/settings (settings management)
- /admin/services (service status)
- /admin/logs (log viewer)
- /admin/update (system updates)
- /admin/database/* (SQLAdmin interface)

Security:
- Passwords hashed with Argon2 (memory-hard, GPU-resistant)
- Session regeneration on login/setup (prevents fixation attacks)
- HttpOnly cookies with SameSite protection
- Admin user file permissions: 0600 (owner read/write only)
- Redis TTL: 24 hours with rolling expiration

Dependencies added:
- starsessions[redis]>=2.2.1 (session management)
- passlib[argon2]>=1.7.4 (password hashing)
Protected routes requiring admin authentication:
- Livestream view (/livestream)
- All admin API routes (settings, system, update, logs)

Admin API routes protected:
- Settings API: config validation and saving
- System API: hardware status, service management, reboot
- Update API: system updates, git configuration, region packs
- Logs API: log viewing and streaming

Reports routes remain public (detections, analysis, best recordings, species).

This ensures administrative functions require authentication while keeping
public detection views accessible to users.
Add missing Request parameter to all routes decorated with @require_admin
as required by Starlette's authentication system. The @requires decorator
needs a request or websocket parameter to access authentication state.

Changes:
- Add Request parameter to logs_api_routes.py (3 routes)
- Add Request parameter to update_api_routes.py (6 routes)
- Update all docstrings to document the request parameter
- All non-expensive tests pass (1964 passed)
Remove path restriction from SessionAutoloadMiddleware since the
AuthenticationMiddleware runs on all requests and needs access to
the session handler. Previously sessions were only loaded for /admin
and /api paths, causing KeyError: 'session_handler' on root endpoint.
SessionMiddleware already provides lazy-loaded request.session, so
we don't need SessionAutoloadMiddleware or explicit load_session()
calls. This fixes the KeyError: 'session_handler' error that occurred
when accessing the root endpoint.

Changes:
- Remove SessionAutoloadMiddleware from middleware stack
- Remove load_session() call from SessionAuthBackend.authenticate()
- Remove unused starsessions imports
- Session is now automatically available via SessionMiddleware
- Add AuthService mocking to conftest.py app_with_temp_data fixture
- Create authenticated_client fixture for easy authenticated testing
- Update test fixtures in test_settings_routes.py, test_update_view_routes.py,
  and test_ebird_detection_filtering_integration.py to include authentication
- All fixtures now mock AdminUser with username "admin" and password "testpassword"
- Fixtures log in via POST to /admin/login to get session cookies

This fixes 16 failing tests that were getting 303 redirects instead of
accessing protected routes. 118 tests still failing in update integration tests.
- Create tests/auth_helpers.py with authenticate_sync_client() and authenticate_async_client()
- Update all test files to import from tests.auth_helpers instead of conftest
- Fixes pyright type checking errors that occurred with conftest imports
- Follows DRY principle for test client authentication
- Makes authentication helpers properly importable and type-safe

Files updated:
- tests/auth_helpers.py (new)
- tests/integration/test_update_integration.py
- tests/integration/test_update_integration_unhappy.py
- tests/birdnetpi/web/routers/test_update_view_routes.py
- tests/birdnetpi/web/routers/test_settings_routes.py
- tests/integration/test_ebird_detection_filtering_integration.py
- Convert authenticate_sync_client and authenticate_async_client to fixtures that return callables
- Add fixture parameters to all test fixtures and methods that use authentication
- Remove incorrect tests.auth_helpers module (tests/ is not a package)
- Add authentication to 21 AsyncClient instances across eBird filtering tests

This follows proper pytest patterns where fixtures are requested via parameters
rather than imported as modules. The authentication fixtures return callable
functions that can be used to authenticate any TestClient or AsyncClient.

Files modified:
- tests/conftest.py: Made authentication helpers proper fixtures
- tests/birdnetpi/web/routers/test_settings_routes.py
- tests/birdnetpi/web/routers/test_update_view_routes.py
- tests/integration/test_ebird_detection_filtering_integration.py (16 instances)
- tests/integration/test_ebird_detection_filtering_simple.py (5 instances)
- tests/integration/test_update_integration.py
- tests/integration/test_update_integration_unhappy.py
Mock the redis_client in the app_with_temp_data fixture to prevent
"Event loop is closed" errors during test teardown. The FastAPI app's
shutdown handler tries to close redis_client asynchronously, but
TestClient closes the event loop synchronously first.

Changes:
- Add redis.asyncio import to conftest.py
- Mock redis_client with AsyncMock spec'd to redis.asyncio.Redis
- Mock async methods: set, get, delete, close
- Add redis_client reset_override in cleanup

Fixes 15 tests that were failing with "Event loop is closed" errors.
The previous redis mock always returned None for get(), causing session
data to be lost after login. This made API endpoints fail authentication
even though the client had successfully logged in.

Changes:
- Create redis_storage dict for in-memory session data
- Implement mock_set/mock_get/mock_delete with side_effect
- Sessions now persist across requests in the same test
- API endpoints can now properly authenticate

Fixes 34 additional tests (update integration tests).
Total progress: 118 failures → 69 failures (49 tests fixed).
Add authenticate_sync_client fixture to test fixtures and methods
in API route tests to properly authenticate test clients.

Changes:
- test_update_api_routes.py: Add auth to client fixture
- test_update_git_api.py: Add auth parameter to test methods
- test_system_services_api_routes.py: Mock require_admin decorator
- test_logs_api_routes.py: Add full auth setup to fixture

These tests create custom FastAPI apps or use app fixtures, so they
need proper authentication setup to access admin-protected routes.
- Convert client fixture to async to initialize database properly
- Add full auth setup (cache, redis, auth_service mocks)
- Override Container class-level providers before create_app()
- Use in-memory session storage for authentication persistence
- Fixes 15 tests that were failing due to AuthenticationMiddleware errors
- Add redis and auth_service mocks to app_with_ebird_filtering fixture
- Use in-memory session storage for authentication persistence
- Add imports for redis.asyncio, AdminUser, AuthService, pwd_context
- Reset redis_client and auth_service overrides in cleanup
- Fixes 4 tests that were failing with 303 redirect responses
- Inject auth_service into SetupRedirectMiddleware via constructor
- Fix middleware ordering: Session → Auth → SetupRedirect
- Add Request parameter to @require_admin protected routes
- Move auth test helpers from module to conftest.py fixtures
- Add comprehensive test coverage for auth routes
- Polish login page styling and navigation
- Add setup_admin_user helper to create admin during e2e setup
- Add get_authenticated_client helper for authenticated requests
- Update e2e tests to use authenticated clients for protected routes
- Apply to both standard and profiling container tests
- Replace module imports with pytest fixtures for authenticated clients
- Add authenticated_e2e_client and authenticated_profiling_client fixtures
- Inline auth setup in SQLAdmin test (container restart case)
@mverteuil mverteuil merged commit 8546ded into main Feb 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant