feat(auth): add pluggable authentication system for Flask routes
Implement comprehensive authentication system with support for Basic Auth, Flask-Login, and OAuth2 providers. Features: - Pluggable architecture via factory pattern - Multiple authentication providers: * None: No authentication (development/testing) * Basic Auth: HTTP Basic with bcrypt support * Flask-Login: Session-based with multiple users * OAuth2: Google, GitHub, GitLab, and generic providers - Decorator-based route protection (@auth.require_auth) - User authorization by domain or email (OAuth) - bcrypt password hashing support - Comprehensive documentation and examples Components: - libtisbackup/auth/__init__.py: Factory function and exports - libtisbackup/auth/base.py: Base provider interface - libtisbackup/auth/basic_auth.py: HTTP Basic Auth implementation - libtisbackup/auth/flask_login_auth.py: Flask-Login implementation - libtisbackup/auth/oauth_auth.py: OAuth2 implementation - libtisbackup/auth/example_integration.py: Integration examples - libtisbackup/auth/README.md: API reference and examples Documentation: - AUTHENTICATION.md: Complete authentication guide * Setup instructions for each provider * Configuration examples * Security best practices * Troubleshooting guide * Migration guide - samples/auth-config-examples.ini: Configuration templates Dependencies: - Add optional dependencies in pyproject.toml: * auth-basic: bcrypt>=4.0.0 * auth-login: flask-login>=0.6.0, bcrypt>=4.0.0 * auth-oauth: authlib>=1.3.0, requests>=2.32.0 * auth-all: All auth providers Installation: ```bash # Install specific provider uv sync --extra auth-basic # Install all providers uv sync --extra auth-all ``` Usage: ```python from libtisbackup.auth import get_auth_provider # Initialize auth = get_auth_provider("basic", { "username": "admin", "password": "$2b$12$...", "use_bcrypt": True }) auth.init_app(app) # Protect routes @app.route("/") @auth.require_auth def index(): user = auth.get_current_user() return f"Hello {user['username']}" ``` Security features: - bcrypt password hashing (work factor 12) - OAuth domain/user restrictions - Session-based authentication - Clear separation of concerns - Environment variable support for secrets OAuth providers supported: - Google (OpenID Connect) - GitHub - GitLab - Generic OAuth2 provider Breaking change: None - new feature, backward compatible Users can continue without authentication (type=none) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d130ba2a11
commit
f12d89f3da
443
AUTHENTICATION.md
Normal file
443
AUTHENTICATION.md
Normal file
@ -0,0 +1,443 @@
|
||||
# TISBackup Authentication System
|
||||
|
||||
TISBackup provides a pluggable authentication system for securing the Flask web interface. You can choose between multiple authentication methods based on your security requirements.
|
||||
|
||||
## Supported Authentication Methods
|
||||
|
||||
1. **None** - No authentication (default, NOT recommended for production)
|
||||
2. **Basic Auth** - HTTP Basic Authentication with username/password
|
||||
3. **Flask-Login** - Session-based authentication with username/password
|
||||
4. **OAuth2** - OAuth authentication (Google, GitHub, GitLab, or generic provider)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Choose Authentication Method
|
||||
|
||||
Add an `[authentication]` section to `/etc/tis/tisbackup_gui.ini`:
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = basic
|
||||
username = admin
|
||||
password = $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
use_bcrypt = True
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
# For Basic Auth
|
||||
uv sync --extra auth-basic
|
||||
|
||||
# For Flask-Login
|
||||
uv sync --extra auth-login
|
||||
|
||||
# For OAuth
|
||||
uv sync --extra auth-oauth
|
||||
|
||||
# For all auth methods
|
||||
uv sync --extra auth-all
|
||||
```
|
||||
|
||||
### 3. Restart TISBackup
|
||||
|
||||
```bash
|
||||
docker compose restart tisbackup_gui
|
||||
```
|
||||
|
||||
## Configuration Guide
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
Simple HTTP Basic Auth with username and password.
|
||||
|
||||
**Pros:**
|
||||
- Easy to set up
|
||||
- Works with all HTTP clients
|
||||
- No session management needed
|
||||
|
||||
**Cons:**
|
||||
- Credentials sent with every request
|
||||
- No logout functionality
|
||||
- Browser password prompt can be confusing
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = basic
|
||||
username = admin
|
||||
# Use bcrypt hash (recommended)
|
||||
password = $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
use_bcrypt = True
|
||||
realm = TISBackup Admin
|
||||
```
|
||||
|
||||
**Generate bcrypt hash:**
|
||||
|
||||
```bash
|
||||
python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())"
|
||||
```
|
||||
|
||||
**Docker environment:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
tisbackup_gui:
|
||||
environment:
|
||||
- TISBACKUP_SECRET_KEY=your-secret-key
|
||||
# Optional: Pass credentials via env vars
|
||||
# Then reference in config with ${AUTH_PASSWORD}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flask-Login Authentication
|
||||
|
||||
Session-based authentication with login page and user management.
|
||||
|
||||
**Pros:**
|
||||
- Clean login/logout workflow
|
||||
- Session-based (no credentials in each request)
|
||||
- Multiple users supported
|
||||
- Password hashing with bcrypt
|
||||
|
||||
**Cons:**
|
||||
- Requires custom login page
|
||||
- Session management overhead
|
||||
- Cookies must be enabled
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = flask-login
|
||||
users_file = /etc/tis/users.txt
|
||||
use_bcrypt = True
|
||||
login_view = login
|
||||
```
|
||||
|
||||
**Create users file** (`/etc/tis/users.txt`):
|
||||
|
||||
```
|
||||
admin:$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
operator:$2b$12$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNO
|
||||
viewer:$2b$12$ANOTHERBCRYPTHASHHERE1234567890ABCDEFGHIJKLMNOPQRS
|
||||
```
|
||||
|
||||
**Generate user entry:**
|
||||
|
||||
```bash
|
||||
USERNAME="admin"
|
||||
PASSWORD="yourpassword"
|
||||
HASH=$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'$PASSWORD', bcrypt.gensalt()).decode())")
|
||||
echo "$USERNAME:$HASH" >> /etc/tis/users.txt
|
||||
```
|
||||
|
||||
**Permissions:**
|
||||
|
||||
```bash
|
||||
chmod 600 /etc/tis/users.txt
|
||||
chown root:root /etc/tis/users.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OAuth2 Authentication
|
||||
|
||||
Delegate authentication to external OAuth providers (Google, GitHub, GitLab, etc.)
|
||||
|
||||
**Pros:**
|
||||
- No password management
|
||||
- Leverage existing identity providers
|
||||
- Support for SSO
|
||||
- Can restrict by domain or specific users
|
||||
|
||||
**Cons:**
|
||||
- Requires OAuth app registration
|
||||
- Internet connectivity required
|
||||
- More complex setup
|
||||
- External dependency
|
||||
|
||||
#### Google OAuth
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
||||
2. Create OAuth 2.0 Client ID
|
||||
3. Add authorized redirect URI: `http://your-server:8080/oauth/callback`
|
||||
4. Note the Client ID and Client Secret
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = google
|
||||
client_id = 123456789-abcdefghijklmnop.apps.googleusercontent.com
|
||||
client_secret = GOCSPX-your-client-secret-here
|
||||
redirect_uri = http://your-server:8080/oauth/callback
|
||||
|
||||
# Restrict to specific domain(s)
|
||||
authorized_domains = example.com,mycompany.com
|
||||
|
||||
# Or restrict to specific users
|
||||
authorized_users = admin@example.com,backup-admin@example.com
|
||||
```
|
||||
|
||||
#### GitHub OAuth
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. Go to GitHub Settings > Developer settings > [OAuth Apps](https://github.com/settings/developers)
|
||||
2. Register a new application
|
||||
3. Set Authorization callback URL: `http://your-server:8080/oauth/callback`
|
||||
4. Note the Client ID and Client Secret
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = github
|
||||
client_id = your-github-client-id
|
||||
client_secret = your-github-client-secret
|
||||
redirect_uri = http://your-server:8080/oauth/callback
|
||||
authorized_users = admin@example.com,devops@example.com
|
||||
```
|
||||
|
||||
#### GitLab OAuth
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. Go to GitLab User Settings > [Applications](https://gitlab.com/-/profile/applications)
|
||||
2. Create application with scopes: `read_user`, `email`
|
||||
3. Set Redirect URI: `http://your-server:8080/oauth/callback`
|
||||
4. Note the Application ID and Secret
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = gitlab
|
||||
client_id = your-gitlab-application-id
|
||||
client_secret = your-gitlab-secret
|
||||
redirect_uri = http://your-server:8080/oauth/callback
|
||||
authorized_domains = example.com
|
||||
```
|
||||
|
||||
#### Generic OAuth Provider
|
||||
|
||||
For custom OAuth providers:
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = generic
|
||||
client_id = your-client-id
|
||||
client_secret = your-client-secret
|
||||
redirect_uri = http://your-server:8080/oauth/callback
|
||||
|
||||
# Custom endpoints
|
||||
authorization_endpoint = https://auth.example.com/oauth/authorize
|
||||
token_endpoint = https://auth.example.com/oauth/token
|
||||
userinfo_endpoint = https://auth.example.com/oauth/userinfo
|
||||
scopes = openid,email,profile
|
||||
|
||||
# Authorization rules
|
||||
authorized_domains = example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Use HTTPS in Production
|
||||
|
||||
Always use a reverse proxy with TLS:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name tisbackup.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/tisbackup.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/tisbackup.example.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Set Strong Flask Secret Key
|
||||
|
||||
```bash
|
||||
# Generate secret
|
||||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||
|
||||
# Set in environment
|
||||
export TISBACKUP_SECRET_KEY=your-generated-secret-key
|
||||
```
|
||||
|
||||
### 3. Protect Configuration Files
|
||||
|
||||
```bash
|
||||
chmod 600 /etc/tis/tisbackup_gui.ini
|
||||
chmod 600 /etc/tis/users.txt # if using Flask-Login
|
||||
chown root:root /etc/tis/*.ini
|
||||
```
|
||||
|
||||
### 4. Use Environment Variables for Secrets
|
||||
|
||||
Instead of hardcoding secrets in config files:
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = oauth
|
||||
client_id = ${OAUTH_CLIENT_ID}
|
||||
client_secret = ${OAUTH_CLIENT_SECRET}
|
||||
```
|
||||
|
||||
Then set in Docker Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
tisbackup_gui:
|
||||
environment:
|
||||
- OAUTH_CLIENT_ID=your-client-id
|
||||
- OAUTH_CLIENT_SECRET=your-client-secret
|
||||
- TISBACKUP_SECRET_KEY=your-secret-key
|
||||
```
|
||||
|
||||
### 5. Regularly Rotate Credentials
|
||||
|
||||
- Change passwords/secrets every 90 days
|
||||
- Rotate OAuth client secrets annually
|
||||
- Review authorized users/domains regularly
|
||||
|
||||
### 6. Monitor Authentication Logs
|
||||
|
||||
Check logs for failed authentication attempts:
|
||||
|
||||
```bash
|
||||
docker logs tisbackup_gui | grep -i "auth"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Basic Auth Not Working
|
||||
|
||||
1. **Check bcrypt installation:**
|
||||
```bash
|
||||
uv sync --extra auth-basic
|
||||
```
|
||||
|
||||
2. **Verify password hash:**
|
||||
```bash
|
||||
python3 -c "import bcrypt; print(bcrypt.checkpw(b'yourpassword', b'$2b$12$your-hash'))"
|
||||
```
|
||||
|
||||
3. **Check browser credentials:**
|
||||
- Clear browser cache
|
||||
- Try incognito/private mode
|
||||
|
||||
### Flask-Login Issues
|
||||
|
||||
1. **Users file not found:**
|
||||
```bash
|
||||
ls -la /etc/tis/users.txt
|
||||
chmod 600 /etc/tis/users.txt
|
||||
```
|
||||
|
||||
2. **Module not found:**
|
||||
```bash
|
||||
uv sync --extra auth-login
|
||||
```
|
||||
|
||||
3. **Session problems:**
|
||||
- Check `TISBACKUP_SECRET_KEY` is set
|
||||
- Ensure cookies are enabled
|
||||
|
||||
### OAuth Problems
|
||||
|
||||
1. **Redirect URI mismatch:**
|
||||
- Ensure redirect URI in config matches OAuth app settings exactly
|
||||
- Check for http vs https mismatch
|
||||
|
||||
2. **Unauthorized domain/user:**
|
||||
- Verify email matches `authorized_users` or domain matches `authorized_domains`
|
||||
- Check OAuth provider returns email in user info
|
||||
|
||||
3. **Module not found:**
|
||||
```bash
|
||||
uv sync --extra auth-oauth
|
||||
```
|
||||
|
||||
4. **Token errors:**
|
||||
- Verify client ID and secret are correct
|
||||
- Check OAuth app is enabled
|
||||
- Ensure scopes are correct
|
||||
|
||||
## API Access with Authentication
|
||||
|
||||
### Basic Auth
|
||||
|
||||
```bash
|
||||
curl -u admin:password http://localhost:8080/api/backups
|
||||
```
|
||||
|
||||
### OAuth (with access token)
|
||||
|
||||
```bash
|
||||
# Not recommended for API access - use service account or API keys instead
|
||||
```
|
||||
|
||||
### Recommendation for API Access
|
||||
|
||||
For programmatic API access, use Basic Auth with a dedicated API user:
|
||||
|
||||
```ini
|
||||
[authentication]
|
||||
type = basic
|
||||
username = api-user
|
||||
password = $2b$12$...
|
||||
```
|
||||
|
||||
Or implement API key authentication separately for API endpoints.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From No Auth to Basic Auth
|
||||
|
||||
1. Add authentication section to config
|
||||
2. Install bcrypt: `uv sync --extra auth-basic`
|
||||
3. Restart service
|
||||
4. Update client scripts with credentials
|
||||
|
||||
### From Basic Auth to OAuth
|
||||
|
||||
1. Register OAuth application
|
||||
2. Update configuration
|
||||
3. Install dependencies: `uv sync --extra auth-oauth`
|
||||
4. Test OAuth login flow
|
||||
5. Update redirect URI if needed
|
||||
|
||||
### From Flask-Login to OAuth
|
||||
|
||||
1. Register OAuth application
|
||||
2. Map user emails to OAuth provider
|
||||
3. Update configuration
|
||||
4. Test migration with test users first
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check logs: `docker logs tisbackup_gui`
|
||||
- Review configuration syntax
|
||||
- Verify dependencies are installed
|
||||
- See [SECURITY_IMPROVEMENTS.md](../SECURITY_IMPROVEMENTS.md) for security context
|
323
libtisbackup/auth/README.md
Normal file
323
libtisbackup/auth/README.md
Normal file
@ -0,0 +1,323 @@
|
||||
# TISBackup Authentication Module
|
||||
|
||||
Pluggable authentication system for Flask routes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple providers**: Basic Auth, Flask-Login, OAuth2
|
||||
- **Easy integration**: Simple decorator-based protection
|
||||
- **Configurable**: INI-based configuration
|
||||
- **Secure**: bcrypt password hashing, OAuth integration
|
||||
- **Extensible**: Easy to add new providers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Choose Authentication Provider
|
||||
|
||||
```python
|
||||
from libtisbackup.auth import get_auth_provider
|
||||
|
||||
# Get provider from config
|
||||
auth = get_auth_provider("basic", {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$...", # bcrypt hash
|
||||
"use_bcrypt": True
|
||||
})
|
||||
|
||||
# Initialize with Flask app
|
||||
auth.init_app(app)
|
||||
```
|
||||
|
||||
### 2. Protect Routes
|
||||
|
||||
```python
|
||||
@app.route("/")
|
||||
@auth.require_auth
|
||||
def index():
|
||||
user = auth.get_current_user()
|
||||
return f"Hello {user['username']}"
|
||||
```
|
||||
|
||||
## Providers
|
||||
|
||||
### Base Provider (No Auth)
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("none", {})
|
||||
```
|
||||
|
||||
- No authentication required
|
||||
- All routes publicly accessible
|
||||
- Useful for development/testing
|
||||
|
||||
### Basic Auth
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("basic", {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6",
|
||||
"use_bcrypt": True,
|
||||
"realm": "TISBackup"
|
||||
})
|
||||
```
|
||||
|
||||
**Required dependencies:**
|
||||
```bash
|
||||
uv sync --extra auth-basic
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- HTTP Basic Authentication
|
||||
- bcrypt password hashing
|
||||
- Custom realm support
|
||||
|
||||
### Flask-Login
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("flask-login", {
|
||||
"users_file": "/etc/tis/users.txt",
|
||||
"use_bcrypt": True,
|
||||
"login_view": "login"
|
||||
})
|
||||
```
|
||||
|
||||
**Required dependencies:**
|
||||
```bash
|
||||
uv sync --extra auth-login
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Session-based authentication
|
||||
- Multiple users support
|
||||
- Login/logout pages
|
||||
- bcrypt password hashing
|
||||
|
||||
**User file format** (`users.txt`):
|
||||
```
|
||||
username:bcrypt_hash
|
||||
admin:$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
```
|
||||
|
||||
### OAuth2
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("oauth", {
|
||||
"provider": "google", # or "github", "gitlab", "generic"
|
||||
"client_id": "your-client-id",
|
||||
"client_secret": "your-client-secret",
|
||||
"redirect_uri": "http://localhost:8080/oauth/callback",
|
||||
"authorized_domains": ["example.com"],
|
||||
"authorized_users": ["admin@example.com"]
|
||||
})
|
||||
```
|
||||
|
||||
**Required dependencies:**
|
||||
```bash
|
||||
uv sync --extra auth-oauth
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- OAuth2 authentication
|
||||
- Google, GitHub, GitLab support
|
||||
- Custom OAuth providers
|
||||
- Domain/user restrictions
|
||||
|
||||
## API Reference
|
||||
|
||||
### AuthProvider
|
||||
|
||||
Base class for all auth providers.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `init_app(app)` - Initialize with Flask app
|
||||
- `require_auth(f)` - Decorator to protect routes
|
||||
- `is_authenticated()` - Check if current request is authenticated
|
||||
- `get_current_user()` - Get current user info
|
||||
- `handle_unauthorized()` - Handle unauthorized access
|
||||
- `logout()` - Logout current user
|
||||
|
||||
### BasicAuthProvider
|
||||
|
||||
HTTP Basic Authentication provider.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```python
|
||||
{
|
||||
"username": str, # Required
|
||||
"password": str, # Required (plain or bcrypt hash)
|
||||
"use_bcrypt": bool, # Default: False
|
||||
"realm": str # Default: "TISBackup"
|
||||
}
|
||||
```
|
||||
|
||||
### FlaskLoginProvider
|
||||
|
||||
Session-based authentication provider.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```python
|
||||
{
|
||||
"users": dict, # {username: password_hash} or...
|
||||
"users_file": str, # Path to users file
|
||||
"use_bcrypt": bool, # Default: True
|
||||
"login_view": str # Default: "login"
|
||||
}
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `verify_password(username, password)` - Verify credentials
|
||||
- `login_user(username)` - Login user by username
|
||||
|
||||
### OAuthProvider
|
||||
|
||||
OAuth2 authentication provider.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```python
|
||||
{
|
||||
"provider": str, # "google", "github", "gitlab", "generic"
|
||||
"client_id": str, # Required
|
||||
"client_secret": str, # Required
|
||||
"redirect_uri": str, # Required
|
||||
"scopes": list, # Optional
|
||||
"authorized_domains": list, # Optional
|
||||
"authorized_users": list, # Optional
|
||||
|
||||
# For generic provider:
|
||||
"authorization_endpoint": str,
|
||||
"token_endpoint": str,
|
||||
"userinfo_endpoint": str
|
||||
}
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `is_user_authorized(user_info)` - Check if user is authorized
|
||||
|
||||
## Integration Example
|
||||
|
||||
See [example_integration.py](example_integration.py) for a complete example.
|
||||
|
||||
### Minimal Example
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from libtisbackup.auth import get_auth_provider
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "your-secret-key"
|
||||
|
||||
# Initialize auth
|
||||
auth = get_auth_provider("basic", {
|
||||
"username": "admin",
|
||||
"password": "changeme",
|
||||
"use_bcrypt": False
|
||||
})
|
||||
auth.init_app(app)
|
||||
|
||||
# Protected route
|
||||
@app.route("/")
|
||||
@auth.require_auth
|
||||
def index():
|
||||
return "Protected content"
|
||||
|
||||
# Public route
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return "OK"
|
||||
```
|
||||
|
||||
### With Flask-Login
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("flask-login", {
|
||||
"users": {
|
||||
"admin": "$2b$12$..."
|
||||
},
|
||||
"use_bcrypt": True
|
||||
})
|
||||
auth.init_app(app)
|
||||
|
||||
@app.route("/login", methods=["POST"])
|
||||
def login():
|
||||
username = request.form["username"]
|
||||
password = request.form["password"]
|
||||
|
||||
if auth.verify_password(username, password):
|
||||
auth.login_user(username)
|
||||
return redirect("/")
|
||||
return "Invalid credentials", 401
|
||||
|
||||
@app.route("/")
|
||||
@auth.require_auth
|
||||
def index():
|
||||
user = auth.get_current_user()
|
||||
return f"Hello {user['username']}"
|
||||
```
|
||||
|
||||
### With OAuth
|
||||
|
||||
```python
|
||||
auth = get_auth_provider("oauth", {
|
||||
"provider": "google",
|
||||
"client_id": "...",
|
||||
"client_secret": "...",
|
||||
"redirect_uri": "http://localhost:8080/oauth/callback",
|
||||
"authorized_domains": ["example.com"]
|
||||
})
|
||||
auth.init_app(app)
|
||||
|
||||
@app.route("/oauth/login")
|
||||
def oauth_login():
|
||||
return auth.oauth_client.authorize_redirect(
|
||||
url_for("oauth_callback", _external=True)
|
||||
)
|
||||
|
||||
@app.route("/oauth/callback")
|
||||
def oauth_callback():
|
||||
token = auth.oauth_client.authorize_access_token()
|
||||
user_info = auth.oauth_client.get(auth.userinfo_endpoint).json()
|
||||
|
||||
if auth.is_user_authorized(user_info):
|
||||
session["oauth_user"] = user_info
|
||||
return redirect("/")
|
||||
return "Unauthorized", 403
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Always use HTTPS in production** - Especially for Basic Auth
|
||||
2. **Use bcrypt for passwords** - Never store plain text passwords
|
||||
3. **Rotate credentials regularly** - Change passwords and OAuth secrets
|
||||
4. **Restrict OAuth access** - Use `authorized_domains` or `authorized_users`
|
||||
5. **Set strong Flask secret_key** - Use `secrets.token_hex(32)`
|
||||
6. **Protect config files** - `chmod 600` for files with credentials
|
||||
7. **Use environment variables** - For sensitive configuration values
|
||||
|
||||
## Testing
|
||||
|
||||
```python
|
||||
import unittest
|
||||
from libtisbackup.auth import get_auth_provider
|
||||
|
||||
class TestBasicAuth(unittest.TestCase):
|
||||
def test_authentication(self):
|
||||
auth = get_auth_provider("basic", {
|
||||
"username": "admin",
|
||||
"password": "test",
|
||||
"use_bcrypt": False
|
||||
})
|
||||
|
||||
# Mock request with credentials
|
||||
# Test authentication logic
|
||||
pass
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL v3.0 - Same as TISBackup
|
52
libtisbackup/auth/__init__.py
Normal file
52
libtisbackup/auth/__init__.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TISBackup Authentication Module
|
||||
|
||||
Provides pluggable authentication providers for Flask routes.
|
||||
Supports: Basic Auth, Flask-Login, OAuth2
|
||||
"""
|
||||
|
||||
from .base import AuthProvider
|
||||
from .basic_auth import BasicAuthProvider
|
||||
from .flask_login_auth import FlaskLoginProvider
|
||||
from .oauth_auth import OAuthProvider
|
||||
|
||||
__all__ = [
|
||||
"AuthProvider",
|
||||
"BasicAuthProvider",
|
||||
"FlaskLoginProvider",
|
||||
"OAuthProvider",
|
||||
"get_auth_provider",
|
||||
]
|
||||
|
||||
|
||||
def get_auth_provider(auth_type, config=None):
|
||||
"""Factory function to get authentication provider.
|
||||
|
||||
Args:
|
||||
auth_type: Type of auth ('basic', 'flask-login', 'oauth', or 'none')
|
||||
config: Configuration dict for the provider
|
||||
|
||||
Returns:
|
||||
AuthProvider instance
|
||||
|
||||
Raises:
|
||||
ValueError: If auth_type is not supported
|
||||
"""
|
||||
providers = {
|
||||
"none": AuthProvider,
|
||||
"basic": BasicAuthProvider,
|
||||
"flask-login": FlaskLoginProvider,
|
||||
"oauth": OAuthProvider,
|
||||
}
|
||||
|
||||
auth_type = auth_type.lower()
|
||||
if auth_type not in providers:
|
||||
raise ValueError(
|
||||
f"Unsupported auth type: {auth_type}. "
|
||||
f"Supported types: {', '.join(providers.keys())}"
|
||||
)
|
||||
|
||||
provider_class = providers[auth_type]
|
||||
return provider_class(config or {})
|
74
libtisbackup/auth/base.py
Normal file
74
libtisbackup/auth/base.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Base authentication provider interface
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class AuthProvider(ABC):
|
||||
"""Base class for authentication providers."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the auth provider.
|
||||
|
||||
Args:
|
||||
config: Dict with provider-specific configuration
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initialize the provider with Flask app.
|
||||
|
||||
Args:
|
||||
app: Flask application instance
|
||||
"""
|
||||
pass
|
||||
|
||||
def require_auth(self, f):
|
||||
"""Decorator to require authentication for a route.
|
||||
|
||||
Args:
|
||||
f: Flask route function
|
||||
|
||||
Returns:
|
||||
Decorated function
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not self.is_authenticated():
|
||||
return self.handle_unauthorized()
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def is_authenticated(self):
|
||||
"""Check if current request is authenticated.
|
||||
|
||||
Returns:
|
||||
bool: True if authenticated, False otherwise
|
||||
"""
|
||||
# Default: no authentication required
|
||||
return True
|
||||
|
||||
def handle_unauthorized(self):
|
||||
"""Handle unauthorized access.
|
||||
|
||||
Returns:
|
||||
Flask response for unauthorized access
|
||||
"""
|
||||
from flask import jsonify
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
def get_current_user(self):
|
||||
"""Get current authenticated user.
|
||||
|
||||
Returns:
|
||||
User object or dict, or None if not authenticated
|
||||
"""
|
||||
return None
|
||||
|
||||
def logout(self):
|
||||
"""Logout current user."""
|
||||
pass
|
78
libtisbackup/auth/basic_auth.py
Normal file
78
libtisbackup/auth/basic_auth.py
Normal file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
HTTP Basic Authentication provider
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from flask import request
|
||||
|
||||
from .base import AuthProvider
|
||||
|
||||
|
||||
class BasicAuthProvider(AuthProvider):
|
||||
"""HTTP Basic Authentication provider.
|
||||
|
||||
Configuration:
|
||||
username: Required username
|
||||
password: Required password (plain text, or hashed with bcrypt)
|
||||
realm: Authentication realm (default: 'TISBackup')
|
||||
use_bcrypt: If True, password is bcrypt hash (default: False)
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.logger = logging.getLogger("tisbackup.auth")
|
||||
self.username = config.get("username")
|
||||
self.password = config.get("password")
|
||||
self.realm = config.get("realm", "TISBackup")
|
||||
self.use_bcrypt = config.get("use_bcrypt", False)
|
||||
|
||||
if not self.username or not self.password:
|
||||
raise ValueError("BasicAuth requires 'username' and 'password' in config")
|
||||
|
||||
if self.use_bcrypt:
|
||||
try:
|
||||
import bcrypt
|
||||
self.bcrypt = bcrypt
|
||||
except ImportError:
|
||||
raise ImportError("bcrypt library required for password hashing. Install with: pip install bcrypt")
|
||||
|
||||
def is_authenticated(self):
|
||||
"""Check if request has valid Basic Auth credentials."""
|
||||
auth = request.authorization
|
||||
|
||||
if not auth:
|
||||
return False
|
||||
|
||||
if auth.username != self.username:
|
||||
self.logger.warning(f"Failed authentication attempt for user: {auth.username}")
|
||||
return False
|
||||
|
||||
if self.use_bcrypt:
|
||||
# Compare bcrypt hash
|
||||
password_bytes = auth.password.encode('utf-8')
|
||||
hash_bytes = self.password.encode('utf-8') if isinstance(self.password, str) else self.password
|
||||
return self.bcrypt.checkpw(password_bytes, hash_bytes)
|
||||
else:
|
||||
# Plain text comparison (not recommended for production)
|
||||
return auth.password == self.password
|
||||
|
||||
def handle_unauthorized(self):
|
||||
"""Return 401 with WWW-Authenticate header."""
|
||||
from flask import Response
|
||||
return Response(
|
||||
'Authentication required',
|
||||
401,
|
||||
{'WWW-Authenticate': f'Basic realm="{self.realm}"'}
|
||||
)
|
||||
|
||||
def get_current_user(self):
|
||||
"""Get current authenticated user."""
|
||||
auth = request.authorization
|
||||
if auth and self.is_authenticated():
|
||||
return {"username": auth.username, "auth_type": "basic"}
|
||||
return None
|
121
libtisbackup/auth/example_integration.py
Normal file
121
libtisbackup/auth/example_integration.py
Normal file
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Example integration of authentication providers with Flask app
|
||||
|
||||
This file shows how to integrate the authentication system into tisbackup_gui.py
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, render_template, request, redirect, url_for, session
|
||||
from libtisbackup.auth import get_auth_provider
|
||||
|
||||
# Example configuration from tisbackup_gui.ini
|
||||
auth_config = {
|
||||
"type": "basic", # or "flask-login", "oauth", "none"
|
||||
"basic": {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$...", # bcrypt hash
|
||||
"use_bcrypt": True,
|
||||
"realm": "TISBackup Admin"
|
||||
},
|
||||
"flask-login": {
|
||||
"users_file": "/etc/tis/users.txt", # username:bcrypt_hash per line
|
||||
"use_bcrypt": True,
|
||||
"login_view": "login"
|
||||
},
|
||||
"oauth": {
|
||||
"provider": "google",
|
||||
"client_id": "your-client-id",
|
||||
"client_secret": "your-client-secret",
|
||||
"redirect_uri": "http://localhost:8080/oauth/callback",
|
||||
"authorized_domains": ["example.com"],
|
||||
"authorized_users": ["admin@example.com"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_app_with_auth():
|
||||
"""Example: Create Flask app with authentication."""
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "your-secret-key"
|
||||
|
||||
# Initialize authentication provider
|
||||
auth_type = auth_config.get("type", "none")
|
||||
provider_config = auth_config.get(auth_type, {})
|
||||
auth = get_auth_provider(auth_type, provider_config)
|
||||
auth.init_app(app)
|
||||
|
||||
# Protected routes
|
||||
@app.route("/")
|
||||
@auth.require_auth
|
||||
def index():
|
||||
user = auth.get_current_user()
|
||||
return render_template("index.html", user=user)
|
||||
|
||||
@app.route("/api/backups")
|
||||
@auth.require_auth
|
||||
def api_backups():
|
||||
return jsonify({"backups": []})
|
||||
|
||||
# Public routes (no auth required)
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
# Flask-Login specific routes
|
||||
if auth_type == "flask-login":
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
|
||||
if auth.verify_password(username, password):
|
||||
auth.login_user(username)
|
||||
return redirect(url_for("index"))
|
||||
else:
|
||||
return render_template("login.html", error="Invalid credentials")
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
auth.logout()
|
||||
return redirect(url_for("login"))
|
||||
|
||||
# OAuth specific routes
|
||||
if auth_type == "oauth":
|
||||
@app.route("/oauth/login")
|
||||
def oauth_login():
|
||||
redirect_uri = url_for("oauth_callback", _external=True)
|
||||
return auth.oauth_client.authorize_redirect(redirect_uri)
|
||||
|
||||
@app.route("/oauth/callback")
|
||||
def oauth_callback():
|
||||
try:
|
||||
token = auth.oauth_client.authorize_access_token()
|
||||
user_info = auth.oauth_client.get(auth.userinfo_endpoint).json()
|
||||
|
||||
# Check authorization
|
||||
if not auth.is_user_authorized(user_info):
|
||||
return "Unauthorized: Your email/domain is not authorized", 403
|
||||
|
||||
# Store user in session
|
||||
session["oauth_user"] = user_info
|
||||
session["oauth_token"] = token
|
||||
|
||||
return redirect(url_for("index"))
|
||||
except Exception as e:
|
||||
return f"OAuth callback error: {e}", 500
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
auth.logout()
|
||||
return redirect(url_for("oauth_login"))
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app_with_auth()
|
||||
app.run(debug=True, port=8080)
|
156
libtisbackup/auth/flask_login_auth.py
Normal file
156
libtisbackup/auth/flask_login_auth.py
Normal file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Flask-Login authentication provider
|
||||
"""
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from flask import redirect, request, session, url_for
|
||||
|
||||
from .base import AuthProvider
|
||||
|
||||
|
||||
class FlaskLoginProvider(AuthProvider):
|
||||
"""Flask-Login based authentication provider.
|
||||
|
||||
Configuration:
|
||||
users: Dict of {username: password_hash} or path to user file
|
||||
secret_key: Flask secret key for sessions
|
||||
login_view: Route name for login page (default: 'login')
|
||||
use_bcrypt: If True, passwords are bcrypt hashes (default: True)
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.logger = logging.getLogger("tisbackup.auth")
|
||||
self.login_manager = None
|
||||
self.users = config.get("users", {})
|
||||
self.login_view = config.get("login_view", "login")
|
||||
self.use_bcrypt = config.get("use_bcrypt", True)
|
||||
|
||||
if self.use_bcrypt:
|
||||
try:
|
||||
import bcrypt
|
||||
self.bcrypt = bcrypt
|
||||
except ImportError:
|
||||
raise ImportError("bcrypt library required. Install with: pip install bcrypt")
|
||||
|
||||
# Load users from file if path provided
|
||||
users_file = config.get("users_file")
|
||||
if users_file:
|
||||
self._load_users_from_file(users_file)
|
||||
|
||||
def _load_users_from_file(self, filepath):
|
||||
"""Load users from file (username:password_hash per line)."""
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
username, password_hash = line.split(':', 1)
|
||||
self.users[username.strip()] = password_hash.strip()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to load users from {filepath}: {e}")
|
||||
raise
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initialize Flask-Login with the app."""
|
||||
try:
|
||||
from flask_login import LoginManager, UserMixin
|
||||
except ImportError:
|
||||
raise ImportError("flask-login library required. Install with: pip install flask-login")
|
||||
|
||||
self.login_manager = LoginManager()
|
||||
self.login_manager.init_app(app)
|
||||
self.login_manager.login_view = self.login_view
|
||||
|
||||
# Simple User class
|
||||
class User(UserMixin):
|
||||
def __init__(self, username):
|
||||
self.id = username
|
||||
self.username = username
|
||||
|
||||
self.User = User
|
||||
|
||||
@self.login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
if user_id in self.users:
|
||||
return User(user_id)
|
||||
return None
|
||||
|
||||
def verify_password(self, username, password):
|
||||
"""Verify username and password.
|
||||
|
||||
Args:
|
||||
username: Username to check
|
||||
password: Password to verify
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
if username not in self.users:
|
||||
return False
|
||||
|
||||
stored_hash = self.users[username]
|
||||
|
||||
if self.use_bcrypt:
|
||||
password_bytes = password.encode('utf-8')
|
||||
hash_bytes = stored_hash.encode('utf-8') if isinstance(stored_hash, str) else stored_hash
|
||||
return self.bcrypt.checkpw(password_bytes, hash_bytes)
|
||||
else:
|
||||
return password == stored_hash
|
||||
|
||||
def login_user(self, username):
|
||||
"""Login a user by username.
|
||||
|
||||
Args:
|
||||
username: Username to login
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
try:
|
||||
from flask_login import login_user
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
if username in self.users:
|
||||
user = self.User(username)
|
||||
login_user(user)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_authenticated(self):
|
||||
"""Check if current user is authenticated."""
|
||||
try:
|
||||
from flask_login import current_user
|
||||
return current_user.is_authenticated
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def handle_unauthorized(self):
|
||||
"""Redirect to login page."""
|
||||
return redirect(url_for(self.login_view))
|
||||
|
||||
def get_current_user(self):
|
||||
"""Get current authenticated user."""
|
||||
try:
|
||||
from flask_login import current_user
|
||||
if current_user.is_authenticated:
|
||||
return {
|
||||
"username": current_user.username,
|
||||
"auth_type": "flask-login"
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def logout(self):
|
||||
"""Logout current user."""
|
||||
try:
|
||||
from flask_login import logout_user
|
||||
logout_user()
|
||||
except ImportError:
|
||||
pass
|
164
libtisbackup/auth/oauth_auth.py
Normal file
164
libtisbackup/auth/oauth_auth.py
Normal file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OAuth2 authentication provider
|
||||
"""
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from flask import redirect, request, session, url_for
|
||||
|
||||
from .base import AuthProvider
|
||||
|
||||
|
||||
class OAuthProvider(AuthProvider):
|
||||
"""OAuth2 authentication provider.
|
||||
|
||||
Supports multiple OAuth providers (Google, GitHub, GitLab, etc.)
|
||||
|
||||
Configuration:
|
||||
provider: OAuth provider name ('google', 'github', 'gitlab', 'generic')
|
||||
client_id: OAuth client ID
|
||||
client_secret: OAuth client secret
|
||||
redirect_uri: OAuth redirect URI
|
||||
scopes: List of OAuth scopes (default: ['openid', 'email', 'profile'])
|
||||
authorized_domains: List of allowed email domains (optional)
|
||||
authorized_users: List of allowed email addresses (optional)
|
||||
login_view: Route name for login page (default: 'oauth_login')
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.logger = logging.getLogger("tisbackup.auth")
|
||||
|
||||
self.provider_name = config.get("provider", "generic").lower()
|
||||
self.client_id = config.get("client_id")
|
||||
self.client_secret = config.get("client_secret")
|
||||
self.redirect_uri = config.get("redirect_uri")
|
||||
self.scopes = config.get("scopes", ["openid", "email", "profile"])
|
||||
self.authorized_domains = config.get("authorized_domains", [])
|
||||
self.authorized_users = config.get("authorized_users", [])
|
||||
self.login_view = config.get("login_view", "oauth_login")
|
||||
|
||||
if not self.client_id or not self.client_secret:
|
||||
raise ValueError("OAuth requires 'client_id' and 'client_secret' in config")
|
||||
|
||||
# Provider-specific configurations
|
||||
self.provider_configs = {
|
||||
"google": {
|
||||
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_endpoint": "https://oauth2.googleapis.com/token",
|
||||
"userinfo_endpoint": "https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
"default_scopes": ["openid", "email", "profile"],
|
||||
},
|
||||
"github": {
|
||||
"authorization_endpoint": "https://github.com/login/oauth/authorize",
|
||||
"token_endpoint": "https://github.com/login/oauth/access_token",
|
||||
"userinfo_endpoint": "https://api.github.com/user",
|
||||
"default_scopes": ["read:user", "user:email"],
|
||||
},
|
||||
"gitlab": {
|
||||
"authorization_endpoint": "https://gitlab.com/oauth/authorize",
|
||||
"token_endpoint": "https://gitlab.com/oauth/token",
|
||||
"userinfo_endpoint": "https://gitlab.com/api/v4/user",
|
||||
"default_scopes": ["read_user", "email"],
|
||||
},
|
||||
}
|
||||
|
||||
# Get provider config or use generic
|
||||
self.provider_config = self.provider_configs.get(self.provider_name, {})
|
||||
|
||||
# Override with custom endpoints if provided
|
||||
self.authorization_endpoint = config.get(
|
||||
"authorization_endpoint",
|
||||
self.provider_config.get("authorization_endpoint")
|
||||
)
|
||||
self.token_endpoint = config.get(
|
||||
"token_endpoint",
|
||||
self.provider_config.get("token_endpoint")
|
||||
)
|
||||
self.userinfo_endpoint = config.get(
|
||||
"userinfo_endpoint",
|
||||
self.provider_config.get("userinfo_endpoint")
|
||||
)
|
||||
|
||||
# Use provider default scopes if not specified
|
||||
if not config.get("scopes"):
|
||||
self.scopes = self.provider_config.get("default_scopes", self.scopes)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initialize OAuth with the app."""
|
||||
try:
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"authlib library required for OAuth. "
|
||||
"Install with: pip install authlib requests"
|
||||
)
|
||||
|
||||
self.oauth = OAuth(app)
|
||||
|
||||
# Register OAuth client
|
||||
self.oauth_client = self.oauth.register(
|
||||
name=self.provider_name,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
server_metadata_url=None,
|
||||
authorize_url=self.authorization_endpoint,
|
||||
access_token_url=self.token_endpoint,
|
||||
userinfo_endpoint=self.userinfo_endpoint,
|
||||
client_kwargs={"scope": " ".join(self.scopes)},
|
||||
)
|
||||
|
||||
def is_user_authorized(self, user_info):
|
||||
"""Check if user is authorized based on email/domain.
|
||||
|
||||
Args:
|
||||
user_info: Dict with user information (must contain 'email')
|
||||
|
||||
Returns:
|
||||
bool: True if authorized
|
||||
"""
|
||||
email = user_info.get("email", "").lower()
|
||||
|
||||
# Check specific users
|
||||
if self.authorized_users:
|
||||
if email in [u.lower() for u in self.authorized_users]:
|
||||
return True
|
||||
|
||||
# Check domains
|
||||
if self.authorized_domains:
|
||||
domain = email.split("@")[-1] if "@" in email else ""
|
||||
if domain in [d.lower() for d in self.authorized_domains]:
|
||||
return True
|
||||
|
||||
# If no restrictions configured, allow all
|
||||
if not self.authorized_users and not self.authorized_domains:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_authenticated(self):
|
||||
"""Check if current user is authenticated via OAuth."""
|
||||
return "oauth_user" in session
|
||||
|
||||
def handle_unauthorized(self):
|
||||
"""Redirect to OAuth login."""
|
||||
return redirect(url_for(self.login_view))
|
||||
|
||||
def get_current_user(self):
|
||||
"""Get current authenticated user."""
|
||||
user_info = session.get("oauth_user")
|
||||
if user_info:
|
||||
return {
|
||||
**user_info,
|
||||
"auth_type": "oauth",
|
||||
"provider": self.provider_name
|
||||
}
|
||||
return None
|
||||
|
||||
def logout(self):
|
||||
"""Logout current user."""
|
||||
session.pop("oauth_user", None)
|
||||
session.pop("oauth_token", None)
|
@ -12,11 +12,20 @@ dependencies = [
|
||||
"pexpect==4.9.0",
|
||||
"redis==5.2.1",
|
||||
"requests==2.32.3",
|
||||
"ruff>=0.13.3",
|
||||
"simplejson==3.20.1",
|
||||
"six==1.17.0",
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Authentication providers
|
||||
auth-basic = ["bcrypt>=4.0.0"]
|
||||
auth-login = ["flask-login>=0.6.0", "bcrypt>=4.0.0"]
|
||||
auth-oauth = ["authlib>=1.3.0", "requests>=2.32.0"]
|
||||
# Install all auth providers
|
||||
auth-all = ["bcrypt>=4.0.0", "flask-login>=0.6.0", "authlib>=1.3.0", "requests>=2.32.0"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 140
|
||||
|
||||
|
130
samples/auth-config-examples.ini
Normal file
130
samples/auth-config-examples.ini
Normal file
@ -0,0 +1,130 @@
|
||||
# TISBackup Authentication Configuration Examples
|
||||
# Add to tisbackup_gui.ini under [authentication] section
|
||||
|
||||
# ============================================
|
||||
# Option 1: No Authentication (NOT RECOMMENDED)
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = none
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 2: HTTP Basic Authentication
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = basic
|
||||
username = admin
|
||||
# Plain text password (NOT RECOMMENDED for production)
|
||||
password = changeme
|
||||
use_bcrypt = False
|
||||
realm = TISBackup Admin
|
||||
|
||||
# RECOMMENDED: Use bcrypt hash
|
||||
# Generate hash with: python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())"
|
||||
[authentication]
|
||||
type = basic
|
||||
username = admin
|
||||
password = $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
use_bcrypt = True
|
||||
realm = TISBackup Admin
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 3: Flask-Login (Username/Password with Sessions)
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = flask-login
|
||||
# Users can be defined inline or in a file
|
||||
users_file = /etc/tis/users.txt
|
||||
use_bcrypt = True
|
||||
login_view = login
|
||||
|
||||
# User file format (users.txt):
|
||||
# username:bcrypt_password_hash
|
||||
# Example:
|
||||
# admin:$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYWv.5qVQK6
|
||||
# operator:$2b$12$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNO
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 4: OAuth2 - Google
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = google
|
||||
client_id = your-client-id.apps.googleusercontent.com
|
||||
client_secret = your-client-secret
|
||||
redirect_uri = http://localhost:8080/oauth/callback
|
||||
# Restrict to specific domains
|
||||
authorized_domains = example.com,mycompany.com
|
||||
# Or restrict to specific users
|
||||
authorized_users = admin@example.com,backup-admin@example.com
|
||||
|
||||
# To get Google OAuth credentials:
|
||||
# 1. Go to https://console.cloud.google.com/apis/credentials
|
||||
# 2. Create OAuth 2.0 Client ID
|
||||
# 3. Add authorized redirect URI: http://your-server:8080/oauth/callback
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 5: OAuth2 - GitHub
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = github
|
||||
client_id = your-github-client-id
|
||||
client_secret = your-github-client-secret
|
||||
redirect_uri = http://localhost:8080/oauth/callback
|
||||
# Restrict to specific GitHub users (by email)
|
||||
authorized_users = admin@example.com
|
||||
|
||||
# To get GitHub OAuth credentials:
|
||||
# 1. Go to Settings > Developer settings > OAuth Apps
|
||||
# 2. Register a new application
|
||||
# 3. Set Authorization callback URL: http://your-server:8080/oauth/callback
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 6: OAuth2 - GitLab
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = gitlab
|
||||
client_id = your-gitlab-application-id
|
||||
client_secret = your-gitlab-secret
|
||||
redirect_uri = http://localhost:8080/oauth/callback
|
||||
authorized_domains = example.com
|
||||
|
||||
# To get GitLab OAuth credentials:
|
||||
# 1. Go to User Settings > Applications
|
||||
# 2. Create new application with scopes: read_user, email
|
||||
# 3. Set Redirect URI: http://your-server:8080/oauth/callback
|
||||
|
||||
|
||||
# ============================================
|
||||
# Option 7: OAuth2 - Generic Provider
|
||||
# ============================================
|
||||
[authentication]
|
||||
type = oauth
|
||||
provider = generic
|
||||
client_id = your-client-id
|
||||
client_secret = your-client-secret
|
||||
redirect_uri = http://localhost:8080/oauth/callback
|
||||
# Custom OAuth endpoints
|
||||
authorization_endpoint = https://auth.example.com/oauth/authorize
|
||||
token_endpoint = https://auth.example.com/oauth/token
|
||||
userinfo_endpoint = https://auth.example.com/oauth/userinfo
|
||||
scopes = openid,email,profile
|
||||
authorized_domains = example.com
|
||||
|
||||
|
||||
# ============================================
|
||||
# Security Notes
|
||||
# ============================================
|
||||
# 1. Always use HTTPS in production (reverse proxy with TLS)
|
||||
# 2. Set strong Flask secret_key via TISBACKUP_SECRET_KEY env var
|
||||
# 3. For Basic Auth, always use bcrypt hashed passwords
|
||||
# 4. For OAuth, restrict access via authorized_domains or authorized_users
|
||||
# 5. Keep client secrets secure and never commit to version control
|
||||
# 6. Regularly rotate OAuth client secrets
|
||||
# 7. Use environment variables for sensitive data when possible
|
Loading…
Reference in New Issue
Block a user