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:
2025-10-05 02:02:46 +02:00
parent b65b7bcd0a
commit a768d95ed3
10 changed files with 1550 additions and 0 deletions
+323
View 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
View 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
View 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
View 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
View 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
View 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
View 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)