tiny_auth_support #6
11
.env.example
11
.env.example
@@ -13,11 +13,6 @@ PYTORRENT_SCGI_RETRIES=8
|
|||||||
# css/js libs
|
# css/js libs
|
||||||
PYTORRENT_USE_OFFLINE_LIBS=true
|
PYTORRENT_USE_OFFLINE_LIBS=true
|
||||||
|
|
||||||
# Reverse proxy / HTTPS
|
|
||||||
PYTORRENT_PROXY_FIX_ENABLE=false
|
|
||||||
PYTORRENT_SESSION_COOKIE_SECURE=false
|
|
||||||
# PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS=https://your-domain.com
|
|
||||||
|
|
||||||
# Retention / Smart Queue
|
# Retention / Smart Queue
|
||||||
PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
|
PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
|
||||||
PYTORRENT_JOBS_RETENTION_DAYS=30
|
PYTORRENT_JOBS_RETENTION_DAYS=30
|
||||||
@@ -64,3 +59,9 @@ PYTORRENT_AUTH_PROXY_AUTO_CREATE=true
|
|||||||
# Defaults for auto-created users
|
# Defaults for auto-created users
|
||||||
PYTORRENT_AUTH_PROXY_AUTO_CREATE_ROLE=admin
|
PYTORRENT_AUTH_PROXY_AUTO_CREATE_ROLE=admin
|
||||||
PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION=rw
|
PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION=rw
|
||||||
|
|
||||||
|
# Reverse proxy / HTTPS
|
||||||
|
PYTORRENT_PROXY_FIX_ENABLE=true
|
||||||
|
PYTORRENT_SESSION_COOKIE_SECURE=false
|
||||||
|
#PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS=https://pytorrent.domain.com
|
||||||
|
#PYTORRENT_API_ALLOWED_ORIGINS=https://pytorrent.domain.com
|
||||||
26
auth.md
26
auth.md
@@ -33,6 +33,28 @@ PYTORRENT_AUTH_PROXY_AUTO_CREATE_ROLE=admin
|
|||||||
PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION=rw
|
PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION=rw
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Reverse proxy origin checks
|
||||||
|
|
||||||
|
pyTorrent blocks unsafe API requests when the browser `Origin`/`Referer` does not match the application origin. Behind HTTPS reverse proxy this requires either correct forwarded headers or an explicit API origin allowlist.
|
||||||
|
|
||||||
|
Recommended variables for reverse proxy mode:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PYTORRENT_PROXY_FIX_ENABLE=true
|
||||||
|
PYTORRENT_SESSION_COOKIE_SECURE=true
|
||||||
|
PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS=https://pytorrent.example.com
|
||||||
|
PYTORRENT_API_ALLOWED_ORIGINS=https://pytorrent.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
`PYTORRENT_API_ALLOWED_ORIGINS` accepts a comma-separated list, for example:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PYTORRENT_API_ALLOWED_ORIGINS=https://pytorrent.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If `PYTORRENT_API_ALLOWED_ORIGINS` is not set, pyTorrent reuses `PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS` for API origin checks.
|
||||||
|
|
||||||
## Local authentication
|
## Local authentication
|
||||||
|
|
||||||
Use this when pyTorrent should manage its own login screen and passwords.
|
Use this when pyTorrent should manage its own login screen and passwords.
|
||||||
@@ -80,14 +102,14 @@ location / {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /tinyauth {
|
location /tinyauth {
|
||||||
proxy_pass http://10.87.7.99:3000/api/auth/nginx;
|
proxy_pass http://10.10.11.11:3000/api/auth/nginx;
|
||||||
proxy_set_header x-forwarded-proto $scheme;
|
proxy_set_header x-forwarded-proto $scheme;
|
||||||
proxy_set_header x-forwarded-host $http_host;
|
proxy_set_header x-forwarded-host $http_host;
|
||||||
proxy_set_header x-forwarded-uri $request_uri;
|
proxy_set_header x-forwarded-uri $request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
location @tinyauth_login {
|
location @tinyauth_login {
|
||||||
return 302 http://auth.linuxiarz.pl/login?redirect_uri=$scheme://$http_host$request_uri;
|
return 302 http://auth.domian/login?redirect_uri=$scheme://$http_host$request_uri;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,14 @@ PROXY_FIX_X_HOST = _env_int("PYTORRENT_PROXY_FIX_X_HOST", 1, 0)
|
|||||||
PROXY_FIX_X_PORT = _env_int("PYTORRENT_PROXY_FIX_X_PORT", 1, 0)
|
PROXY_FIX_X_PORT = _env_int("PYTORRENT_PROXY_FIX_X_PORT", 1, 0)
|
||||||
PROXY_FIX_X_PREFIX = _env_int("PYTORRENT_PROXY_FIX_X_PREFIX", 1, 0)
|
PROXY_FIX_X_PREFIX = _env_int("PYTORRENT_PROXY_FIX_X_PREFIX", 1, 0)
|
||||||
|
|
||||||
|
def _env_csv(name: str) -> list[str]:
|
||||||
|
return [item.strip().rstrip("/") for item in os.getenv(name, "").split(",") if item.strip()]
|
||||||
|
|
||||||
_SOCKETIO_CORS = os.getenv("PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS", "").strip()
|
_SOCKETIO_CORS = os.getenv("PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS", "").strip()
|
||||||
SOCKETIO_CORS_ALLOWED_ORIGINS = None if not _SOCKETIO_CORS else [item.strip() for item in _SOCKETIO_CORS.split(",") if item.strip()]
|
SOCKETIO_CORS_ALLOWED_ORIGINS = None if not _SOCKETIO_CORS else [item.strip() for item in _SOCKETIO_CORS.split(",") if item.strip()]
|
||||||
|
# Note: API origin checks are separate from Socket.IO CORS. When unset, reuse the Socket.IO allowlist for operator-friendly reverse proxy setups.
|
||||||
|
_API_ALLOWED_ORIGINS = _env_csv("PYTORRENT_API_ALLOWED_ORIGINS")
|
||||||
|
API_ALLOWED_ORIGINS = _API_ALLOWED_ORIGINS or _env_csv("PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS")
|
||||||
|
|
||||||
TRAFFIC_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS", 90, 1)
|
TRAFFIC_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS", 90, 1)
|
||||||
JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1)
|
JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ..config import (
|
|||||||
AUTH_PROXY_AUTO_CREATE_PERMISSION,
|
AUTH_PROXY_AUTO_CREATE_PERMISSION,
|
||||||
AUTH_PROXY_AUTO_CREATE_ROLE,
|
AUTH_PROXY_AUTO_CREATE_ROLE,
|
||||||
AUTH_PROXY_USER_HEADER,
|
AUTH_PROXY_USER_HEADER,
|
||||||
|
API_ALLOWED_ORIGINS,
|
||||||
)
|
)
|
||||||
from ..db import connect, default_user_id, utcnow
|
from ..db import connect, default_user_id, utcnow
|
||||||
|
|
||||||
@@ -171,14 +172,29 @@ def visible_profile_ids(user_id: int | None = None) -> set[int] | None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _origin_key(value: str) -> str:
|
||||||
|
parsed = urlparse(str(value or "").strip())
|
||||||
|
if not parsed.scheme or not parsed.netloc:
|
||||||
|
return ""
|
||||||
|
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _request_origin() -> str:
|
||||||
|
return _origin_key(f"{request.scheme}://{request.host}")
|
||||||
|
|
||||||
|
|
||||||
def same_origin_request() -> bool:
|
def same_origin_request() -> bool:
|
||||||
"""Return False only when an unsafe request clearly comes from another origin."""
|
"""Return False only when an unsafe API request clearly comes from an untrusted origin."""
|
||||||
origin = request.headers.get("Origin") or request.headers.get("Referer")
|
origin = request.headers.get("Origin") or request.headers.get("Referer")
|
||||||
if not origin:
|
if not origin:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
parsed = urlparse(origin)
|
source_origin = _origin_key(origin)
|
||||||
return parsed.scheme == request.scheme and parsed.netloc == request.host
|
if not source_origin:
|
||||||
|
return False
|
||||||
|
if source_origin == _request_origin():
|
||||||
|
return True
|
||||||
|
return source_origin in set(API_ALLOWED_ORIGINS)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user