Metadata-Version: 2.4
Name: fastpluggy-auth-user
Version: 0.5.1
Summary: User Authentication plugin for Fastpluggy
Author: FastPluggy Team
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: FastPluggy>=0.4.37
Requires-Dist: authlib
Requires-Dist: bcrypt
Requires-Dist: httpx
Requires-Dist: click
Requires-Dist: passlib
Requires-Dist: pyotp
Requires-Dist: python-multipart
Requires-Dist: qrcode[svg]
Provides-Extra: tests
Requires-Dist: pytest>=7.0; extra == "tests"
Requires-Dist: pytest-cov>=4.0; extra == "tests"
Requires-Dist: pytest-asyncio>=0.21; extra == "tests"
Provides-Extra: e2e
Requires-Dist: fastpluggy-cli; extra == "e2e"
Requires-Dist: uvicorn[standard]; extra == "e2e"

# Auth User Plugin

![Auth User](https://img.shields.io/badge/FastPluggy-Auth%20User-blue)
[![Release](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/-/badges/release.svg)](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/-/releases)
[![Pipeline Status](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/badges/main/pipeline.svg?key_text=CI)](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/-/pipelines?ignore_skipped=true)
[![Coverage](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/badges/main/coverage.svg)](https://gitlab.ggcorp.fr/open/fastpluggy/plugins/auth_user/-/pipelines)

The Auth User plugin provides comprehensive user authentication and management functionality for FastPluggy applications.

## 📊 Current Status

**✅ PRODUCTION READY FEATURES:**
- Complete admin interface for user management (CRUD operations)
- User authentication with secure password hashing
- Session management with configurable expiration
- User profile pages with authentication protection
- Automatic admin user creation on first install
- Menu integration for user functions

**⚠️ PARTIALLY IMPLEMENTED:**
- Login routes (exist but commented out - ready for activation)

**📋 ROADMAP:** See [Implementation Roadmap](docs/fastpluggy-auth-roadmap.md) for detailed development plan.

## Features

- **User Authentication**: Secure username/password authentication with BCrypt hashing
- **Admin Interface**: Complete user management with list, add, edit functionality
- **Session Management**: Configurable session handling with JSON data storage
- **User Profiles**: Protected profile pages for authenticated users
- **Menu Integration**: Automatic menu items for profile and logout
- **Template System**: Jinja2 templates for login and profile pages
- **Database Integration**: SQLAlchemy models with FastPluggy core

## Models

### FastPluggyBaseUser
The main user model with the following fields:
- `username`: Unique username (String, 255 chars)
- `hashed_password`: Securely hashed password (String, 255 chars)
- `is_admin`: Boolean flag indicating admin privileges (default: False)

### Session
Session management model for user authentication:
- `session_id`: Unique session identifier (String, 255 chars)
- `session_data`: JSON data stored in the session
- `session_lifetime`: Session expiration timestamp
- `session_time`: Session creation timestamp
- `ip`: Client IP captured at login (nullable; only set when `collect_login_ip` is enabled)
- `user_agent`: Raw User-Agent captured at login (nullable)
- `last_seen_at`: Last activity timestamp, refreshed (throttled) on authenticated requests (nullable)

## Session management

Both authenticated users and admins can review and revoke sessions:

- **Self-service** — `GET /profile/sessions/` lists the current user's active
  sessions with device (heuristic User-Agent parse), IP, relative last-seen,
  created/expires. Revoke a single session or "revoke all other sessions".
  The IP column is **hidden entirely** when `collect_login_ip` is disabled.
- **Admin-wide** — `GET /admin/users/sessions/` (requires the `fp_admin` role)
  lists every active session across all users with a username filter, and can
  revoke any individual session or all of a given user's sessions.

`last_seen_at` is refreshed inside the authentication path but **throttled** to
at most one write per `session_last_seen_throttle` seconds, and is wrapped so a
tracking failure can never break authentication. Set `session_track_metadata`
to `false` to disable User-Agent capture and last-seen tracking entirely.

## Configuration

The plugin uses `AuthUserConfig` class with the following settings:

```python
from fastpluggy.core.config import BaseDatabaseSettings
from typing import Optional, Literal

class AuthUserConfig(BaseDatabaseSettings):
    # Set to None to enable UI-based setup on first install
    # If None, admin user creation will be handled via setup UI
    default_admin_username: Optional[str] = None
    default_admin_password: Optional[str] = None

    # Auth backend: "session" (cookies + tokens + login redirect) or "basic" (HTTP Basic popup)
    auth_backend: Literal["session", "basic"] = "session"

    login_template: str = 'auth_user/login.html'
    collect_login_ip: bool = True
    allow_api_tokens: bool = True  # set to False to disable personal access tokens globally

    # Per-session metadata (device / IP / last-seen on the sessions pages)
    session_track_metadata: bool = True   # capture user-agent + refresh last_seen_at
    session_last_seen_throttle: int = 60  # min seconds between last_seen_at writes
```

| Setting | Default | Effect |
|---------|---------|--------|
| `collect_login_ip` | `True` | Capture client IP at login; gates the IP column on the sessions pages |
| `session_track_metadata` | `True` | Capture User-Agent at login and refresh `last_seen_at` on activity |
| `session_last_seen_throttle` | `60` | Minimum seconds between `last_seen_at` writes (keeps the auth path cheap) |

### Auth backends

| Backend | `auth_backend` | How it works |
|---------|---------------|-------------|
| **Session** (default) | `"session"` | Cookie-based sessions + Bearer tokens + HTTP Basic with `__token__` (pip/twine). Redirects to login page on auth failure. |
| **Basic** | `"basic"` | HTTP Basic authentication only. Triggers browser popup on auth failure. |
| **SSO** | `"proxy"` | Trusted forward-auth headers from a reverse proxy (Caddy `forward_auth` + Authelia, authentik, oauth2-proxy…). See below. |

The auth backend is configurable via environment variable or database settings:

```bash
AUTH_USER_AUTH_BACKEND=basic  # or "session" (default), or "proxy"
```

### SSO backend (trusted forward-auth headers)

The proxy authenticates the user and injects `Remote-User` / `Remote-Email` /
`Remote-Groups` into every proxied request; the `sso` backend trusts those
headers — **only** when the request provably came from the proxy:

- `proxy_trusted_ips` (**required**, fail closed): client IP must match one of
  these IPs/CIDRs before headers are even read.
- `proxy_secret` (**strongly recommended on Docker**): a shared secret the
  proxy injects as `X-FP-SSO-Secret` (e.g. Caddy `header_up`). On Docker bridge
  networking, hairpin SNAT makes the proxy and any co-located container arrive
  from the same bridge-gateway IP, so IP trust alone has a same-host hole.

Behavior:

- Auth order per request: **API tokens → trusted headers → local session cookie**
  (tokens first so direct-port app→app clients keep working; cookie fallback so
  break-glass local logins still authenticate).
- Unknown identities are JIT-provisioned (`proxy_auto_create_users`, default on)
  with an **unusable password** — the local login form can never match them.
  Usernames are lowercased (Authelia lowercases identities).
- `is_admin` syncs from `Remote-Groups` ⊇ `proxy_admin_group` (default
  `fp-admins`) **per request** — an SSO group revoke demotes immediately. Set
  `proxy_manage_admin_locally=True` to keep `is_admin` under local control.
- Identity key is `username` (default) or `email` via `proxy_identity_attribute`.
- Logout redirects to `proxy_logout_url` (the proxy owns the real session); the
  local `/login` form is disabled unless `proxy_local_login_fallback=True`;
  local TOTP UI is hidden (the SSO provider owns MFA).

Minimal setup behind Authelia:

```bash
AUTH_BACKEND=proxy
AUTH_USER_PROXY_TRUSTED_PROXIES='["172.17.0.1/32"]'   # your proxy's IP as seen by the app
AUTH_USER_PROXY_PROXY_SECRET=<random-shared-secret>    # + Caddy: header_up X-FP-SSO-Secret <same>
AUTH_USER_PROXY_LOGOUT_URL=https://auth.example.lab/logout
```

## Admin User Setup

The plugin provides **intelligent admin user setup** with two approaches:

### 🎯 **Recommended: UI-Based Setup (Default)**
By default, `default_admin_username` and `default_admin_password` are set to `None`, which enables the setup UI:

1. **First Install**: When no users exist, visit `/setup/` to create the first admin user
2. **Secure**: No hardcoded credentials - you choose the username and password
3. **User-Friendly**: Clean web interface with validation and error handling
4. **Automatic Detection**: The system automatically detects when setup is needed

### 🔧 **Alternative: Automatic Creation with Configured Credentials**
If you prefer automatic admin creation, configure default credentials:

```python
# In your environment or config
default_admin_username = "your_admin_username"
default_admin_password = "your_secure_password"
```

When configured, the plugin will automatically create the admin user on first startup.

### 📋 **Setup Process**
1. **Plugin Load**: System checks if any users exist
2. **If No Users**:
   - **With configured credentials**: Automatically creates admin user
   - **Without credentials (default)**: Logs message to visit `/setup/`
3. **Setup UI**: Visit `/setup/` to create admin user with custom credentials
4. **Redirect**: After successful setup, redirects to login page

### 🔍 **Setup Status Check**
You can check if setup is needed programmatically:

```python
from fastpluggy_plugin.auth_user.src.repository import needs_initial_setup
from fastpluggy.core.database import get_db

if needs_initial_setup(get_db()):
    print("Setup required - visit /setup/")
else:
    print("Admin user exists")
```

## Routes

### Setup Routes
- `GET /setup/` - Initial admin user setup page (only accessible when no users exist)
- `POST /setup/` - Process admin user creation form
- `GET /setup/check` - API endpoint to check if setup is needed

### Authentication Routes
- `GET /logout` - Logout user and clear session

### Profile Routes
- `GET /profile/` - User profile page (requires authentication)

## Menu Integration

The plugin automatically adds the following menu items:
- **Profile** (`/profile/`) - User profile page
- **Logout** (`/logout`) - Logout functionality

## Templates

The plugin expects the following Jinja2 templates:
- `auth_user/profile.html.j2` - User profile page template
- `auth_user/login.html.j2` - Login page template (referenced in config)

## Usage

1. **Install** the plugin — it is auto-discovered via the `fastpluggy.plugins` entry point
2. **Auth is auto-wired** — the plugin calls `fast_pluggy.set_auth_manager()` during `on_load_complete`, so you do **not** need to pass `auth_manager=` to `FastPluggy()`. Just `FastPluggy(app)` is enough.
3. **First run** — visit `/setup/` to create the first admin user (or set `default_admin_username` / `default_admin_password` in config for automatic creation)
4. **Profile** — authenticated users can manage their profile at `/profile/`

> If you pass `auth_manager=` explicitly to `FastPluggy()`, the plugin respects it and does not override.

## Personal Access Tokens

Users can create long-lived API tokens for programmatic access (CI/CD, pip, twine, scripts).

### Managing tokens

Tokens are managed via the UI at `/tokens/`:
- Create a token with a name and optional expiry (in days)
- The raw token value is shown **once** — copy it immediately
- Revoke any token at any time

### Using tokens — API clients (Bearer)

```http
Authorization: Bearer <your-token>
```

Any endpoint protected by `require_authentication` accepts this header.

### Using tokens — pip / twine (HTTP Basic)

pip and twine use HTTP Basic auth. Use `__token__` as the username and your token as the password:

```bash
# pip install
pip install --index-url https://__token__:<your-token>@registry.fastpluggy.xyz/pypi/simple/ my-package

# twine upload
twine upload --repository-url https://registry.fastpluggy.xyz/pypi/ \
  -u __token__ -p <your-token> dist/*
```

Or configure `~/.pypirc`:

```ini
[fastpluggy]
repository = https://registry.fastpluggy.xyz/pypi/
username = __token__
password = <your-token>
```

`SessionAuthManager` detects the `__token__` username in Basic auth headers and validates the password as a personal access token (SHA256 hash lookup), so no separate auth scheme is needed for pip/twine.

### Token model

| Field | Description |
|-------|-------------|
| `name` | User-friendly label |
| `token_hash` | SHA256 of the raw token (never stored in plaintext) |
| `expires_at` | Optional expiry timestamp |
| `last_used_at` | Updated on every successful use |

## Security Notes

- Passwords are securely hashed using BCrypt (`BasicAuthManager.hash_password()`)
- Sessions are managed with configurable lifetime (default: 1 hour)
- Admin privileges are controlled via the `is_admin` boolean field
- API tokens are stored as SHA256 hashes (never in plaintext)
- **Important**: Change the default admin password after first login for security


### OIDC backend (OpenID Connect client)

The app itself runs the authorization-code flow against the provider — no
reverse proxy needed, so it works for any host that can reach the provider
over HTTPS (and needs no shared cookie domain).

- `/login` → `/oidc/login` → provider's authorization endpoint → back to
  `/oidc/callback`, which validates the ID token (authlib: issuer, JWKS
  signature, nonce, state) and creates a **normal local session**.
- Claims map to the local user through the same identity settings the proxy
  backend uses (`proxy_identity_attribute`, `proxy_auto_create_users`,
  `proxy_admin_group`, `proxy_manage_admin_locally`); username claim and
  groups claim are configurable (`oidc_username_claim`, `oidc_groups_claim`).
- The provider must have the app registered as a client (for Authelia: a
  static config block with the hashed client secret and
  `https://<app>/oidc/callback` as redirect URI).

```bash
AUTH_BACKEND=oidc
OIDC_ISSUER=https://auth.example.lab
OIDC_CLIENT_ID=my-app
OIDC_CLIENT_SECRET=<plaintext — provider stores only the hash>
OIDC_CA_BUNDLE=/etc/ssl/certs/internal-root.crt   # if the provider uses an internal CA
OIDC_LOGOUT_URL=https://auth.example.lab/logout   # optional
```
