New UniAuth.auth to be used by other services
This commit is contained in:
parent
33b7d7c5c1
commit
87db2ee746
0
UniAuth/__init__.py
Normal file
0
UniAuth/__init__.py
Normal file
53
UniAuth/auth.py
Normal file
53
UniAuth/auth.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"""Authentication via UniAuth."""
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import werkzeug
|
||||||
|
from itsdangerous.url_safe import URLSafeTimedSerializer
|
||||||
|
|
||||||
|
max_age = 60 * 60 * 24 * 90
|
||||||
|
|
||||||
|
|
||||||
|
def is_authentication() -> bool:
|
||||||
|
"""User is authenticated."""
|
||||||
|
return not flask.current_app.config.get("REQUIRE_AUTH") or bool(
|
||||||
|
(token := flask.request.cookies.get("auth_token")) and verify_auth_token(token)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_auth_token(token: str) -> str | None:
|
||||||
|
"""Verify the authentication token."""
|
||||||
|
serializer = URLSafeTimedSerializer(flask.current_app.config["SECRET_KEY"])
|
||||||
|
try:
|
||||||
|
username = serializer.loads(token, salt="auth", max_age=max_age)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert isinstance(username, str)
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def generate_auth_token(username: str) -> str:
|
||||||
|
"""Generate a secure authentication token."""
|
||||||
|
serializer = URLSafeTimedSerializer(flask.current_app.config["SECRET_KEY"])
|
||||||
|
token = typing.cast(str, serializer.dumps(username, salt="auth"))
|
||||||
|
assert isinstance(token, str)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def require_authentication() -> werkzeug.Response | None:
|
||||||
|
"""Require authentication and redirect with return URL."""
|
||||||
|
if not flask.current_app.config.get("REQUIRE_AUTH"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
token = flask.request.cookies.get("auth_token")
|
||||||
|
if token and verify_auth_token(token):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Construct the redirect URL with the original URL as a parameter
|
||||||
|
return flask.redirect(
|
||||||
|
flask.current_app.config["UNIAUTH_URL"]
|
||||||
|
+ "/login?next="
|
||||||
|
+ werkzeug.urls.url_quote(flask.request.url)
|
||||||
|
)
|
0
UniAuth/py.typed
Normal file
0
UniAuth/py.typed
Normal file
30
main.py
30
main.py
|
@ -8,33 +8,13 @@ from typing import Any, Callable
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import werkzeug
|
import werkzeug
|
||||||
from itsdangerous.url_safe import URLSafeTimedSerializer
|
|
||||||
|
import UniAuth.auth
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.debug = True
|
app.debug = True
|
||||||
app.config.from_object("config.default")
|
app.config.from_object("config.default")
|
||||||
|
|
||||||
serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"])
|
|
||||||
max_age = 60 * 60 * 24 * 90
|
|
||||||
|
|
||||||
|
|
||||||
def generate_auth_token(username: str) -> str:
|
|
||||||
"""Generate a secure authentication token."""
|
|
||||||
token = typing.cast(str, serializer.dumps(username, salt="auth"))
|
|
||||||
assert isinstance(token, str)
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def verify_auth_token(token: str) -> str | None:
|
|
||||||
"""Verify the authentication token."""
|
|
||||||
try:
|
|
||||||
username = serializer.loads(token, salt="auth", max_age=max_age)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
assert isinstance(username, str)
|
|
||||||
return username
|
|
||||||
|
|
||||||
|
|
||||||
def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
|
def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Route requires login decorator."""
|
"""Route requires login decorator."""
|
||||||
|
@ -42,7 +22,7 @@ def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def decorated_function(*args, **kwargs) -> werkzeug.Response: # type: ignore
|
def decorated_function(*args, **kwargs) -> werkzeug.Response: # type: ignore
|
||||||
token = flask.request.cookies.get("auth_token")
|
token = flask.request.cookies.get("auth_token")
|
||||||
if not token or verify_auth_token(token) is None:
|
if not token or UniAuth.auth.verify_auth_token(token) is None:
|
||||||
# Save the original URL in the session and redirect to login
|
# Save the original URL in the session and redirect to login
|
||||||
flask.session["next"] = flask.request.url
|
flask.session["next"] = flask.request.url
|
||||||
return flask.redirect(flask.url_for("login_page"))
|
return flask.redirect(flask.url_for("login_page"))
|
||||||
|
@ -93,10 +73,12 @@ def login_page() -> str | werkzeug.Response:
|
||||||
expire_date = datetime.now() + timedelta(days=180)
|
expire_date = datetime.now() + timedelta(days=180)
|
||||||
flask.flash("Welcome back! You have successfully logged in.")
|
flask.flash("Welcome back! You have successfully logged in.")
|
||||||
|
|
||||||
|
token = UniAuth.auth.generate_auth_token(user["username"])
|
||||||
|
|
||||||
response = flask.redirect(redirect_to)
|
response = flask.redirect(redirect_to)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
"auth_token",
|
"auth_token",
|
||||||
generate_auth_token(user["username"]),
|
token,
|
||||||
expires=expire_date,
|
expires=expire_date,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=True,
|
secure=True,
|
||||||
|
|
Loading…
Reference in a new issue