mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
fix(fastapi): rework cookies - do not re-emit generically
This change removes cookie re-emission of AURLANG and AURTZ, adds the AURREMEMBER cookie (the state of the "Remember Me" checkbox on login), and re-emits AURSID based on the AURREMEMBER cookie. Previously, re-emission of AURSID was forcefully modifying the expiration of the AURSID cookie. The introduction of AURREMEMBER allows us to deduct the correct cookie expiration timing based on configuration variables. With this addition, we now re-emit the AURSID cookie with an updated expiration based on the "Remember Me" checkbox on login. Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
7418c33a30
commit
034288711b
6 changed files with 100 additions and 50 deletions
68
aurweb/cookies.py
Normal file
68
aurweb/cookies.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import Response
|
||||||
|
|
||||||
|
from aurweb import config
|
||||||
|
|
||||||
|
|
||||||
|
def samesite() -> str:
|
||||||
|
""" Produce cookie SameSite value based on options.disable_http_login.
|
||||||
|
|
||||||
|
When options.disable_http_login is True, "strict" is returned. Otherwise,
|
||||||
|
"lax" is returned.
|
||||||
|
|
||||||
|
:returns "strict" if options.disable_http_login else "lax"
|
||||||
|
"""
|
||||||
|
secure = config.getboolean("options", "disable_http_login")
|
||||||
|
return "strict" if secure else "lax"
|
||||||
|
|
||||||
|
|
||||||
|
def timeout(extended: bool) -> int:
|
||||||
|
""" Produce a session timeout based on `remember_me`.
|
||||||
|
|
||||||
|
This method returns one of AUR_CONFIG's options.persistent_cookie_timeout
|
||||||
|
and options.login_timeout based on the `extended` argument.
|
||||||
|
|
||||||
|
The `extended` argument is typically the value of the AURREMEMBER
|
||||||
|
cookie, defaulted to False.
|
||||||
|
|
||||||
|
If `extended` is False, options.login_timeout is returned. Otherwise,
|
||||||
|
if `extended` is True, options.persistent_cookie_timeout is returned.
|
||||||
|
|
||||||
|
:param extended: Flag which generates an extended timeout when True
|
||||||
|
:returns: Cookie timeout based on configuration options
|
||||||
|
"""
|
||||||
|
timeout = config.getint("options", "login_timeout")
|
||||||
|
if bool(extended):
|
||||||
|
timeout = config.getint("options", "persistent_cookie_timeout")
|
||||||
|
return timeout
|
||||||
|
|
||||||
|
|
||||||
|
def update_response_cookies(request: Request, response: Response,
|
||||||
|
aurtz: str = None, aurlang: str = None,
|
||||||
|
aursid: str = None) -> Response:
|
||||||
|
""" Update session cookies. This method is particularly useful
|
||||||
|
when updating a cookie which was already set.
|
||||||
|
|
||||||
|
The AURSID cookie's expiration is based on the AURREMEMBER cookie,
|
||||||
|
which is retrieved from `request`.
|
||||||
|
|
||||||
|
:param request: FastAPI request
|
||||||
|
:param response: FastAPI response
|
||||||
|
:param aurtz: Optional AURTZ cookie value
|
||||||
|
:param aurlang: Optional AURLANG cookie value
|
||||||
|
:param aursid: Optional AURSID cookie value
|
||||||
|
:returns: Updated response
|
||||||
|
"""
|
||||||
|
secure = config.getboolean("options", "disable_http_login")
|
||||||
|
if aurtz:
|
||||||
|
response.set_cookie("AURTZ", aurtz, secure=secure, httponly=secure,
|
||||||
|
samesite=samesite())
|
||||||
|
if aurlang:
|
||||||
|
response.set_cookie("AURLANG", aurlang, secure=secure, httponly=secure,
|
||||||
|
samesite=samesite())
|
||||||
|
if aursid:
|
||||||
|
remember_me = bool(request.cookies.get("AURREMEMBER", False))
|
||||||
|
response.set_cookie("AURSID", aursid, secure=secure, httponly=secure,
|
||||||
|
max_age=timeout(remember_me),
|
||||||
|
samesite=samesite())
|
||||||
|
return response
|
|
@ -10,7 +10,7 @@ from sqlalchemy import and_, func, or_
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
|
||||||
from aurweb import db, l10n, logging, models, time, util
|
from aurweb import cookies, db, l10n, logging, models, time, util
|
||||||
from aurweb.auth import account_type_required, auth_required
|
from aurweb.auth import account_type_required, auth_required
|
||||||
from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token
|
from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token
|
||||||
from aurweb.l10n import get_translator_for_request
|
from aurweb.l10n import get_translator_for_request
|
||||||
|
@ -585,16 +585,19 @@ async def account_edit_post(request: Request,
|
||||||
user.update_password(P)
|
user.update_password(P)
|
||||||
|
|
||||||
if user == request.user:
|
if user == request.user:
|
||||||
|
remember_me = request.cookies.get("AURREMEMBER", False)
|
||||||
|
|
||||||
# If the target user is the request user, login with
|
# If the target user is the request user, login with
|
||||||
# the updated password and update AURSID.
|
# the updated password to update the Session record.
|
||||||
request.cookies["AURSID"] = user.login(request, P)
|
user.login(request, P, cookies.timeout(remember_me))
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
context["complete"] = True
|
context["complete"] = True
|
||||||
|
|
||||||
# Update cookies with requests, in case they were changed.
|
# Update cookies with requests, in case they were changed.
|
||||||
response = render_template(request, "account/edit.html", context)
|
response = render_template(request, "account/edit.html", context)
|
||||||
return util.migrate_cookies(request, response)
|
return cookies.update_response_cookies(request, response,
|
||||||
|
aurtz=TZ, aurlang=L)
|
||||||
|
|
||||||
|
|
||||||
account_template = (
|
account_template = (
|
||||||
|
|
|
@ -6,7 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
|
||||||
from aurweb import util
|
from aurweb import cookies
|
||||||
from aurweb.auth import auth_required
|
from aurweb.auth import auth_required
|
||||||
from aurweb.models import User
|
from aurweb.models import User
|
||||||
from aurweb.templates import make_variable_context, render_template
|
from aurweb.templates import make_variable_context, render_template
|
||||||
|
@ -42,12 +42,7 @@ async def login_post(request: Request,
|
||||||
return await login_template(request, next,
|
return await login_template(request, next,
|
||||||
errors=["Bad username or password."])
|
errors=["Bad username or password."])
|
||||||
|
|
||||||
cookie_timeout = 0
|
cookie_timeout = cookies.timeout(remember_me)
|
||||||
|
|
||||||
if remember_me:
|
|
||||||
cookie_timeout = aurweb.config.getint(
|
|
||||||
"options", "persistent_cookie_timeout")
|
|
||||||
|
|
||||||
sid = user.login(request, passwd, cookie_timeout)
|
sid = user.login(request, passwd, cookie_timeout)
|
||||||
if not sid:
|
if not sid:
|
||||||
return await login_template(request, next,
|
return await login_template(request, next,
|
||||||
|
@ -61,14 +56,17 @@ async def login_post(request: Request,
|
||||||
response = RedirectResponse(url=next,
|
response = RedirectResponse(url=next,
|
||||||
status_code=HTTPStatus.SEE_OTHER)
|
status_code=HTTPStatus.SEE_OTHER)
|
||||||
|
|
||||||
secure_cookies = aurweb.config.getboolean("options", "disable_http_login")
|
secure = aurweb.config.getboolean("options", "disable_http_login")
|
||||||
response.set_cookie("AURSID", sid, expires=expires_at,
|
response.set_cookie("AURSID", sid, expires=expires_at,
|
||||||
secure=secure_cookies, httponly=True)
|
secure=secure, httponly=secure,
|
||||||
|
samesite=cookies.samesite())
|
||||||
response.set_cookie("AURTZ", user.Timezone,
|
response.set_cookie("AURTZ", user.Timezone,
|
||||||
secure=secure_cookies, httponly=True)
|
secure=secure, httponly=secure,
|
||||||
|
samesite=cookies.samesite())
|
||||||
response.set_cookie("AURLANG", user.LangPreference,
|
response.set_cookie("AURLANG", user.LangPreference,
|
||||||
secure=secure_cookies, httponly=True)
|
secure=secure, httponly=secure,
|
||||||
return util.add_samesite_fields(response, "strict")
|
samesite=cookies.samesite())
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
|
|
|
@ -11,7 +11,7 @@ from sqlalchemy import and_, case, or_
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
import aurweb.models.package_request
|
import aurweb.models.package_request
|
||||||
|
|
||||||
from aurweb import db, models, util
|
from aurweb import cookies, db, models, util
|
||||||
from aurweb.cache import db_count_cache
|
from aurweb.cache import db_count_cache
|
||||||
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
|
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
|
||||||
from aurweb.models.package_request import PENDING_ID
|
from aurweb.models.package_request import PENDING_ID
|
||||||
|
@ -53,10 +53,11 @@ async def language(request: Request,
|
||||||
# In any case, set the response's AURLANG cookie that never expires.
|
# In any case, set the response's AURLANG cookie that never expires.
|
||||||
response = RedirectResponse(url=f"{next}{query_string}",
|
response = RedirectResponse(url=f"{next}{query_string}",
|
||||||
status_code=HTTPStatus.SEE_OTHER)
|
status_code=HTTPStatus.SEE_OTHER)
|
||||||
secure_cookies = aurweb.config.getboolean("options", "disable_http_login")
|
secure = aurweb.config.getboolean("options", "disable_http_login")
|
||||||
response.set_cookie("AURLANG", set_lang,
|
response.set_cookie("AURLANG", set_lang,
|
||||||
secure=secure_cookies, httponly=True)
|
secure=secure, httponly=secure,
|
||||||
return util.add_samesite_fields(response, "strict")
|
samesite=cookies.samesite())
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
|
|
@ -16,7 +16,7 @@ from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
|
||||||
from aurweb import captcha, l10n, time, util
|
from aurweb import captcha, cookies, l10n, time, util
|
||||||
|
|
||||||
# Prepare jinja2 objects.
|
# Prepare jinja2 objects.
|
||||||
_loader = jinja2.FileSystemLoader(os.path.join(
|
_loader = jinja2.FileSystemLoader(os.path.join(
|
||||||
|
@ -148,9 +148,12 @@ def render_template(request: Request,
|
||||||
""" Render a template as an HTMLResponse. """
|
""" Render a template as an HTMLResponse. """
|
||||||
rendered = render_raw_template(request, path, context)
|
rendered = render_raw_template(request, path, context)
|
||||||
response = HTMLResponse(rendered, status_code=int(status_code))
|
response = HTMLResponse(rendered, status_code=int(status_code))
|
||||||
secure_cookies = aurweb.config.getboolean("options", "disable_http_login")
|
|
||||||
response.set_cookie("AURLANG", context.get("language"),
|
sid = None
|
||||||
secure=secure_cookies, httponly=True)
|
if request.user.is_authenticated():
|
||||||
response.set_cookie("AURTZ", context.get("timezone"),
|
sid = request.cookies.get("AURSID")
|
||||||
secure=secure_cookies, httponly=True)
|
|
||||||
return util.add_samesite_fields(response, "strict")
|
# Re-emit SID via update_response_cookies with an updated expiration.
|
||||||
|
# This extends the life of a user session based on the AURREMEMBER
|
||||||
|
# cookie, which is always set to the "Remember Me" state on login.
|
||||||
|
return cookies.update_response_cookies(request, response, aursid=sid)
|
||||||
|
|
|
@ -14,7 +14,6 @@ from zoneinfo import ZoneInfo
|
||||||
import fastapi
|
import fastapi
|
||||||
|
|
||||||
from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email
|
from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email
|
||||||
from fastapi.responses import Response
|
|
||||||
from jinja2 import pass_context
|
from jinja2 import pass_context
|
||||||
|
|
||||||
import aurweb.config
|
import aurweb.config
|
||||||
|
@ -103,16 +102,6 @@ def valid_ssh_pubkey(pk):
|
||||||
return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1]
|
return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1]
|
||||||
|
|
||||||
|
|
||||||
def migrate_cookies(request, response):
|
|
||||||
whitelist = {"AURSID", "AURTZ", "AURLANG"}
|
|
||||||
|
|
||||||
secure_cookies = aurweb.config.getboolean("options", "disable_http_login")
|
|
||||||
for k, v in request.cookies.items():
|
|
||||||
if k in whitelist:
|
|
||||||
response.set_cookie(k, v, secure=secure_cookies, httponly=True)
|
|
||||||
return add_samesite_fields(response, "strict")
|
|
||||||
|
|
||||||
|
|
||||||
@pass_context
|
@pass_context
|
||||||
def account_url(context, user):
|
def account_url(context, user):
|
||||||
request = context.get("request")
|
request = context.get("request")
|
||||||
|
@ -159,18 +148,6 @@ def jsonify(obj):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def add_samesite_fields(response: Response, value: str):
|
|
||||||
""" Set the SameSite field on all cookie headers found.
|
|
||||||
Taken from https://github.com/tiangolo/fastapi/issues/1099. """
|
|
||||||
for idx, header in enumerate(response.raw_headers):
|
|
||||||
if header[0].decode() == "set-cookie":
|
|
||||||
cookie = header[1].decode()
|
|
||||||
if f"SameSite={value}" not in cookie:
|
|
||||||
cookie += f"; SameSite={value}"
|
|
||||||
response.raw_headers[idx] = (header[0], cookie.encode())
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_fingerprints():
|
def get_ssh_fingerprints():
|
||||||
return aurweb.config.get_section("fingerprints") or {}
|
return aurweb.config.get_section("fingerprints") or {}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue