added api-key/removed hashed password support
This commit is contained in:
parent
a175246a1e
commit
bcdbb6b065
|
|
@ -63,7 +63,6 @@ COPY VERSION .
|
|||
COPY plugins ./plugins
|
||||
COPY themes ./themes
|
||||
COPY locales ./locales
|
||||
COPY generate_password.py .
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p data
|
||||
|
|
|
|||
|
|
@ -267,7 +267,6 @@ NoteDiscovery is designed for **self-hosted, private use**. Please keep these se
|
|||
- ⚠️ **ENABLE AUTHENTICATION AND CHANGE THE DEFAULT PASSWORD** if exposing to a network!
|
||||
- See **[AUTHENTICATION.md](documentation/AUTHENTICATION.md)** for complete setup instructions
|
||||
- To disable auth, set `authentication.enabled: false` in `config.yaml`
|
||||
- Change password with Docker: `docker-compose exec notediscovery python generate_password.py`
|
||||
- Perfect for single-user or small team deployments
|
||||
- For multi-user setups, consider a reverse proxy with OAuth/SSO
|
||||
|
||||
|
|
|
|||
137
backend/main.py
137
backend/main.py
|
|
@ -7,6 +7,7 @@ from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Dep
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
import os
|
||||
import yaml
|
||||
|
|
@ -16,6 +17,7 @@ from typing import List, Optional
|
|||
import aiofiles
|
||||
from datetime import datetime
|
||||
import bcrypt
|
||||
import secrets
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
|
|
@ -78,39 +80,65 @@ else:
|
|||
print(f"🔐 Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)")
|
||||
|
||||
# Password configuration priority:
|
||||
# 1. AUTHENTICATION_PASSWORD env var (plain text, hashed at startup)
|
||||
# 2. AUTHENTICATION_PASSWORD_HASH env var (pre-hashed)
|
||||
# 3. authentication.password in config.yaml (plain text, hashed at startup)
|
||||
# 4. authentication.password_hash in config.yaml (pre-hashed)
|
||||
# 1. AUTHENTICATION_PASSWORD env var (hashed at startup)
|
||||
# 2. authentication.password in config.yaml (hashed at startup)
|
||||
# Default password is "admin" if nothing is configured
|
||||
if 'AUTHENTICATION_PASSWORD' in os.environ:
|
||||
# Plain text password from env var - hash it
|
||||
plain_password = os.getenv('AUTHENTICATION_PASSWORD')
|
||||
plain_password = os.getenv('AUTHENTICATION_PASSWORD', '').strip()
|
||||
if plain_password:
|
||||
config['authentication']['password_hash'] = bcrypt.hashpw(
|
||||
plain_password.encode('utf-8'),
|
||||
bcrypt.gensalt()
|
||||
).decode('utf-8')
|
||||
print("🔑 Password loaded from AUTHENTICATION_PASSWORD env var (hashed at startup)")
|
||||
elif 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
|
||||
# Pre-hashed password from env var
|
||||
config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH')
|
||||
print("🔑 Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var")
|
||||
elif config.get('authentication', {}).get('password'):
|
||||
# Plain text password from config.yaml - hash it
|
||||
plain_password = config['authentication']['password']
|
||||
print("🔑 Password loaded from AUTHENTICATION_PASSWORD env var")
|
||||
else:
|
||||
print("⚠️ WARNING: AUTHENTICATION_PASSWORD env var is empty - ignoring")
|
||||
elif config.get('authentication', {}).get('password', '').strip():
|
||||
plain_password = config['authentication']['password'].strip()
|
||||
config['authentication']['password_hash'] = bcrypt.hashpw(
|
||||
plain_password.encode('utf-8'),
|
||||
bcrypt.gensalt()
|
||||
).decode('utf-8')
|
||||
# Clear the plain text password from config for security
|
||||
del config['authentication']['password']
|
||||
print("🔑 Password loaded from config.yaml (hashed at startup)")
|
||||
print("🔑 Password loaded from config.yaml")
|
||||
|
||||
# Allow secret key to be set via environment variable (for session security)
|
||||
if 'AUTHENTICATION_SECRET_KEY' in os.environ:
|
||||
config['authentication']['secret_key'] = os.getenv('AUTHENTICATION_SECRET_KEY')
|
||||
print("🔐 Secret key loaded from AUTHENTICATION_SECRET_KEY env var")
|
||||
|
||||
# API key configuration for external integrations (MCP servers, scripts, etc.)
|
||||
# Priority: AUTHENTICATION_API_KEY env var > authentication.api_key in config.yaml
|
||||
if 'AUTHENTICATION_API_KEY' in os.environ:
|
||||
api_key_value = os.getenv('AUTHENTICATION_API_KEY', '').strip()
|
||||
if api_key_value:
|
||||
config['authentication']['api_key'] = api_key_value
|
||||
print("🔑 API key loaded from AUTHENTICATION_API_KEY env var")
|
||||
else:
|
||||
config['authentication']['api_key'] = ''
|
||||
elif config.get('authentication', {}).get('api_key', '').strip():
|
||||
print("🔑 API key loaded from config.yaml")
|
||||
else:
|
||||
config['authentication']['api_key'] = ''
|
||||
|
||||
# Warnings for missing authentication methods (only when auth is enabled)
|
||||
if config.get('authentication', {}).get('enabled', False):
|
||||
_has_password = bool(config.get('authentication', {}).get('password_hash', ''))
|
||||
_has_api_key = bool(config.get('authentication', {}).get('api_key', '').strip())
|
||||
_secret_key = config.get('authentication', {}).get('secret_key', '')
|
||||
_is_default_secret = _secret_key in ('', 'change_this_to_a_random_secret_key_in_production')
|
||||
|
||||
if not _has_password and not _has_api_key:
|
||||
print("🚨 CRITICAL: Authentication enabled but NO auth methods configured - ALL access will be denied!")
|
||||
else:
|
||||
if not _has_password:
|
||||
print("⚠️ WARNING: No password configured - web UI login will not work")
|
||||
if not _has_api_key:
|
||||
print("⚠️ WARNING: No API key configured - external integrations will require session cookies")
|
||||
|
||||
if _is_default_secret:
|
||||
print("🚨 SECURITY WARNING: Using default secret_key - sessions can be forged! Change it in config.yaml")
|
||||
|
||||
# OpenAPI tag metadata for grouping endpoints in Swagger UI
|
||||
tags_metadata = [
|
||||
{"name": "Notes", "description": "Create, read, update, delete notes"},
|
||||
|
|
@ -274,18 +302,87 @@ async def http_exception_handler(request: Request, exc: HTTPException):
|
|||
# Authentication Helpers
|
||||
# ============================================================================
|
||||
|
||||
# Security schemes for API authentication (auto_error=False for optional auth)
|
||||
# These are automatically added to OpenAPI docs (/api)
|
||||
bearer_scheme = HTTPBearer(auto_error=False, description="Bearer token authentication")
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False, description="API key header authentication")
|
||||
|
||||
|
||||
def auth_enabled() -> bool:
|
||||
"""Check if authentication is enabled in config"""
|
||||
return config.get('authentication', {}).get('enabled', False)
|
||||
|
||||
|
||||
async def require_auth(request: Request):
|
||||
"""Dependency to require authentication on protected routes"""
|
||||
def get_api_key() -> str:
|
||||
"""Get the configured API key (empty string if not set)"""
|
||||
return config.get('authentication', {}).get('api_key', '').strip()
|
||||
|
||||
|
||||
def verify_api_key(provided_key: str) -> bool:
|
||||
"""
|
||||
Verify an API key using constant-time comparison.
|
||||
|
||||
Uses secrets.compare_digest to prevent timing attacks where an attacker
|
||||
could determine the correct key by measuring response times.
|
||||
|
||||
Args:
|
||||
provided_key: The API key provided in the request
|
||||
|
||||
Returns:
|
||||
True if the key is valid, False otherwise
|
||||
"""
|
||||
configured_key = get_api_key()
|
||||
|
||||
# No API key configured = API key auth disabled
|
||||
if not configured_key:
|
||||
return False
|
||||
|
||||
# Empty provided key is always invalid
|
||||
if not provided_key:
|
||||
return False
|
||||
|
||||
# Constant-time comparison to prevent timing attacks
|
||||
try:
|
||||
return secrets.compare_digest(provided_key.encode('utf-8'), configured_key.encode('utf-8'))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def require_auth(
|
||||
request: Request,
|
||||
bearer_credentials: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
|
||||
x_api_key: Optional[str] = Depends(api_key_header)
|
||||
):
|
||||
"""
|
||||
Dependency to require authentication on protected routes.
|
||||
|
||||
Supports two authentication methods:
|
||||
1. Session-based auth (web UI login with password)
|
||||
2. API key auth (for external integrations like MCP servers)
|
||||
|
||||
API key can be provided via:
|
||||
- Authorization: Bearer YOUR_API_KEY
|
||||
- X-API-Key: YOUR_API_KEY
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if authentication fails
|
||||
"""
|
||||
if not auth_enabled():
|
||||
return # Auth disabled, allow all
|
||||
|
||||
if not request.session.get('authenticated'):
|
||||
# Always raise exception - route handlers will catch and redirect as needed
|
||||
# Method 1: Check Bearer token (parsed by FastAPI's HTTPBearer)
|
||||
if bearer_credentials and verify_api_key(bearer_credentials.credentials):
|
||||
return # Valid Bearer token - authenticated
|
||||
|
||||
# Method 2: Check X-API-Key header (parsed by FastAPI's APIKeyHeader)
|
||||
if x_api_key and verify_api_key(x_api_key):
|
||||
return # Valid API key header - authenticated
|
||||
|
||||
# Method 3: Check session-based authentication (web UI)
|
||||
if request.session.get('authenticated'):
|
||||
return # Valid session - authenticated
|
||||
|
||||
# No valid authentication method - deny access
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
|
||||
|
|
|
|||
20
config.yaml
20
config.yaml
|
|
@ -35,17 +35,21 @@ authentication:
|
|||
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
|
||||
secret_key: "change_this_to_a_random_secret_key_in_production"
|
||||
|
||||
# Password configuration - choose ONE of the following options:
|
||||
#
|
||||
# Option 1: Plain text password (recommended for ease of use)
|
||||
# The password is hashed automatically at startup
|
||||
# Password (hashed automatically at startup)
|
||||
# ⚠️ Default password is "admin" - CHANGE THIS for production!
|
||||
password: "admin"
|
||||
|
||||
# Option 2: Pre-hashed password (for advanced users)
|
||||
# Generate with: python generate_password.py
|
||||
# password_hash: "$2b$12$..."
|
||||
|
||||
# Session expiry in seconds (default: 7 days)
|
||||
session_max_age: 604800
|
||||
|
||||
# API Key Authentication (for external integrations)
|
||||
# Usage (choose one):
|
||||
# Authorization: Bearer YOUR_API_KEY
|
||||
# X-API-Key: YOUR_API_KEY
|
||||
#
|
||||
# Generate a secure key with:
|
||||
# python -c "import secrets; print(secrets.token_hex(32))"
|
||||
#
|
||||
# Leave empty to disable API key authentication (only session auth works)
|
||||
api_key: ""
|
||||
|
||||
|
|
|
|||
|
|
@ -50,11 +50,7 @@ python -c "import secrets; print(secrets.token_hex(32))"
|
|||
|
||||
### Step 2: Configure Authentication
|
||||
|
||||
Choose **one** of these options:
|
||||
|
||||
#### Option A: Plain Text Password (Recommended)
|
||||
|
||||
The easiest approach. Your password is automatically hashed at startup.
|
||||
Your password is automatically hashed at startup using bcrypt.
|
||||
|
||||
**Via Environment Variables (Docker):**
|
||||
```bash
|
||||
|
|
@ -75,29 +71,6 @@ authentication:
|
|||
|
||||
---
|
||||
|
||||
#### Option B: Pre-Hashed Password (Advanced)
|
||||
|
||||
For users who prefer to hash passwords themselves.
|
||||
|
||||
**Generate a hash:**
|
||||
```bash
|
||||
# Docker
|
||||
docker exec -it notediscovery python generate_password.py
|
||||
|
||||
# Local
|
||||
python generate_password.py
|
||||
```
|
||||
|
||||
**Then configure:**
|
||||
```yaml
|
||||
authentication:
|
||||
enabled: true
|
||||
password_hash: "$2b$12$..." # paste your hash here
|
||||
secret_key: "your_generated_secret_key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Restart & Test
|
||||
|
||||
```bash
|
||||
|
|
@ -117,16 +90,14 @@ Navigate to `http://localhost:8000` — you'll be redirected to the login page.
|
|||
|
||||
## Configuration Priority
|
||||
|
||||
If multiple sources are configured, this priority applies (first wins):
|
||||
Environment variables override config.yaml:
|
||||
|
||||
| Priority | Source | Type |
|
||||
|----------|--------|------|
|
||||
| 1st | `AUTHENTICATION_PASSWORD` env var | Plain text |
|
||||
| 2nd | `AUTHENTICATION_PASSWORD_HASH` env var | Pre-hashed |
|
||||
| 3rd | `password` in config.yaml | Plain text |
|
||||
| 4th | `password_hash` in config.yaml | Pre-hashed |
|
||||
| Priority | Source |
|
||||
|----------|--------|
|
||||
| 1st | `AUTHENTICATION_PASSWORD` env var |
|
||||
| 2nd | `password` in config.yaml |
|
||||
|
||||
**Example:** If you set `AUTHENTICATION_PASSWORD` as an env var, it overrides anything in config.yaml.
|
||||
**Example:** If you set `AUTHENTICATION_PASSWORD` as an env var, it overrides config.yaml.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -155,6 +126,42 @@ This is a **simple single-user** system. NOT suitable for:
|
|||
|
||||
---
|
||||
|
||||
## API Key Authentication
|
||||
|
||||
For external integrations (MCP servers, scripts, automation), use an API key instead of session cookies.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Generate a secure key
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
**Via Environment Variable:**
|
||||
```bash
|
||||
docker run -e AUTHENTICATION_API_KEY=your_api_key ...
|
||||
```
|
||||
|
||||
**Via config.yaml:**
|
||||
```yaml
|
||||
authentication:
|
||||
api_key: "your_64_character_hex_key"
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Option 1: Bearer token
|
||||
curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8000/api/notes
|
||||
|
||||
# Option 2: X-API-Key header
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:8000/api/notes
|
||||
```
|
||||
|
||||
Both session auth (web UI) and API key auth work simultaneously when enabled.
|
||||
|
||||
---
|
||||
|
||||
## Disabling Authentication
|
||||
|
||||
```yaml
|
||||
|
|
|
|||
|
|
@ -17,11 +17,9 @@ NoteDiscovery supports environment variables to override configuration settings,
|
|||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication |
|
||||
| `AUTHENTICATION_PASSWORD` | string | `admin` | Plain text password (hashed automatically at startup) |
|
||||
| `AUTHENTICATION_PASSWORD_HASH` | string | - | Pre-hashed bcrypt password (for advanced users) |
|
||||
| `AUTHENTICATION_PASSWORD` | string | `admin` | Password (hashed automatically at startup) |
|
||||
| `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) |
|
||||
|
||||
> **Password Priority:** `AUTHENTICATION_PASSWORD` takes precedence over `AUTHENTICATION_PASSWORD_HASH`. If both are set, the plain text password is used.
|
||||
| `AUTHENTICATION_API_KEY` | string | - | API key for external integrations (MCP, scripts) |
|
||||
|
||||
#### Example: Setting password via environment variable
|
||||
|
||||
|
|
|
|||
|
|
@ -347,6 +347,15 @@ NoteDiscovery can be installed as a standalone app on your device:
|
|||
- **Instant switch** - Change language in Settings without reload
|
||||
- **Community translations** - Contributions welcome!
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
- **Password protection** - Single-user login system
|
||||
- **Session-based auth** - Secure cookie-based sessions (7 days default)
|
||||
- **API key support** - Bearer token or `X-API-Key` header for external integrations
|
||||
- **Environment overrides** - Configure via env vars for Docker deployments
|
||||
|
||||
📄 **See [AUTHENTICATION.md](AUTHENTICATION.md)** for setup guide.
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
- **Instant loading** - No lag, no loading spinners
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper script to generate a password hash for NoteDiscovery authentication.
|
||||
Usage: python generate_password.py
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import bcrypt
|
||||
|
||||
def generate_password_hash():
|
||||
"""Generate a bcrypt password hash"""
|
||||
print("=== NoteDiscovery Password Hash Generator ===\n")
|
||||
|
||||
# Get password from user (hidden input)
|
||||
password = getpass.getpass("Enter your password: ")
|
||||
password_confirm = getpass.getpass("Confirm your password: ")
|
||||
|
||||
# Check if passwords match
|
||||
if password != password_confirm:
|
||||
print("\n❌ Error: Passwords do not match!")
|
||||
return
|
||||
|
||||
if len(password) < 4:
|
||||
print("\n⚠️ Warning: Password is too short! Use at least 8 characters for better security.")
|
||||
proceed = input("Continue anyway? (y/N): ")
|
||||
if proceed.lower() != 'y':
|
||||
return
|
||||
|
||||
# Generate hash
|
||||
print("\n🔐 Generating password hash...")
|
||||
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
print("\n✅ Password hash generated successfully!")
|
||||
print("\n" + "="*60)
|
||||
print("Copy this hash to your config.yaml:")
|
||||
print("="*60)
|
||||
print(f"\npassword_hash: \"{password_hash}\"")
|
||||
print("\n" + "="*60)
|
||||
print("\nExample config.yaml:")
|
||||
print("="*60)
|
||||
print("""
|
||||
authentication:
|
||||
enabled: true
|
||||
secret_key: "your_secret_key_here"
|
||||
password_hash: "{}"
|
||||
session_max_age: 604800
|
||||
""".format(password_hash))
|
||||
print("="*60)
|
||||
|
||||
print("\n💡 Don't forget to also set a secret key!")
|
||||
print(" Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
||||
print("\n🔒 Keep your password and secret key secure!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
generate_password_hash()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n❌ Cancelled.")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
|
||||
|
|
@ -27,9 +27,8 @@ services:
|
|||
|
||||
# ⚠️ DEMO CREDENTIALS - DO NOT USE IN PRODUCTION! ⚠️
|
||||
# These are public demo credentials for testing only.
|
||||
# Password: "admin"
|
||||
- key: AUTHENTICATION_PASSWORD_HASH
|
||||
value: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG"
|
||||
- key: AUTHENTICATION_PASSWORD
|
||||
value: "admin"
|
||||
|
||||
# ⚠️ PUBLIC SECRET KEY - DEMO ONLY! ⚠️
|
||||
# For production, generate a new key with:
|
||||
|
|
|
|||
Loading…
Reference in New Issue