Extending session login time

This commit is contained in:
Ian Roddis committed 2026-08-10 12:09:51 -03:00
1 parent 14aa2ac5d6
commit a9c4c16d4f
6 files changed
+35 -10

No files matched your search

+3
View File
@@ -25,6 +25,9 @@ AUTO_CREATE_TABLES=true
COOKIE_NAME=access_token COOKIE_NAME=access_token
# Set true when serving over HTTPS. # Set true when serving over HTTPS.
COOKIE_SECURE=false COOKIE_SECURE=false
# How long a browser login lasts before being bounced back to /login.
# Independent of ACCESS_TOKEN_EXPIRE_MINUTES, which governs JSON API tokens.
SESSION_TIMEOUT_HOURS=24
# --- LDAP (optional; direct-bind) --- # --- LDAP (optional; direct-bind) ---
# When enabled, the login page and /api/auth/ldap-login accept a directory # When enabled, the login page and /api/auth/ldap-login accept a directory
+2 -1
View File
@@ -149,8 +149,9 @@ Every setting is read from an environment variable first, then `.env`
|----------|---------|-------| |----------|---------|-------|
| `SECRET_KEY` | dev placeholder | **set a strong value in production** | | `SECRET_KEY` | dev placeholder | **set a strong value in production** |
| `DATABASE_URL` | `sqlite+aiosqlite:///./app.db` | use `postgresql+asyncpg://...` for Postgres | | `DATABASE_URL` | `sqlite+aiosqlite:///./app.db` | use `postgresql+asyncpg://...` for Postgres |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | | | `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | JSON API bearer token lifetime |
| `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | | | `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | |
| `SESSION_TIMEOUT_HOURS` | `24` | browser login (HTMX UI cookie) lifetime |
| `AUTO_CREATE_TABLES` | `true` | create tables on startup (dev); use Alembic in prod | | `AUTO_CREATE_TABLES` | `true` | create tables on startup (dev); use Alembic in prod |
| `COOKIE_SECURE` | `false` | set `true` behind HTTPS | | `COOKIE_SECURE` | `false` | set `true` behind HTTPS |
| `FIRST_SUPERUSER_EMAIL` / `_PASSWORD` | admin@example.com / changethis | used by seed | | `FIRST_SUPERUSER_EMAIL` / `_PASSWORD` | admin@example.com / changethis | used by seed |
+5 -1
View File
@@ -33,9 +33,13 @@ class Settings(BaseSettings):
database_url: str = "sqlite+aiosqlite:///./app.db" database_url: str = "sqlite+aiosqlite:///./app.db"
auto_create_tables: bool = True auto_create_tables: bool = True
# Web / cookies (the HTMX UI stores the access token in an httpOnly cookie) # Web / cookies (the HTMX UI stores the access token in an httpOnly cookie).
# session_timeout_hours governs how long a browser login lasts before the
# user is bounced back to /login; it's independent of the JSON API's
# short-lived access_token_expire_minutes.
cookie_name: str = "access_token" cookie_name: str = "access_token"
cookie_secure: bool = False cookie_secure: bool = False
session_timeout_hours: int = 24
# LDAP (direct-bind). When enabled, the login page offers an LDAP option. # LDAP (direct-bind). When enabled, the login page offers an LDAP option.
# At login the user's DN is built from ``ldap_bind_dn_template`` (with # At login the user's DN is built from ``ldap_bind_dn_template`` (with
+4 -2
View File
@@ -47,10 +47,12 @@ def _create_token(
return token, payload return token, payload
def create_access_token(subject: str | int) -> str: def create_access_token(
subject: str | int, expires_delta: timedelta | None = None
) -> str:
token, _ = _create_token( token, _ = _create_token(
subject, subject,
timedelta(minutes=settings.access_token_expire_minutes), expires_delta or timedelta(minutes=settings.access_token_expire_minutes),
ACCESS_TOKEN_TYPE, ACCESS_TOKEN_TYPE,
) )
return token return token
+12 -3
View File
@@ -1,5 +1,7 @@
"""Token issuance / rotation shared by the JSON API and the HTMX UI.""" """Token issuance / rotation shared by the JSON API and the HTMX UI."""
from datetime import timedelta
import jwt import jwt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,9 +15,16 @@ from app.crud import token as token_crud
from app.models.user import User from app.models.user import User
async def issue_token_pair(db: AsyncSession, user: User) -> tuple[str, str]: async def issue_token_pair(
"""Create an access + refresh token and persist the refresh token for revocation.""" db: AsyncSession, user: User, access_expires_delta: timedelta | None = None
access = create_access_token(user.id) ) -> tuple[str, str]:
"""Create an access + refresh token and persist the refresh token for revocation.
``access_expires_delta`` overrides the access token's default lifetime
(``settings.access_token_expire_minutes``) — used by the web login flow to
issue a longer-lived session cookie without affecting API bearer tokens.
"""
access = create_access_token(user.id, access_expires_delta)
refresh, payload = create_refresh_token(user.id) refresh, payload = create_refresh_token(user.id)
await token_crud.store( await token_crud.store(
db, db,
+9 -3
View File
@@ -1,3 +1,5 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.responses import HTMLResponse, RedirectResponse, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -36,7 +38,7 @@ def _set_auth_cookie(response: Response, access_token: str) -> None:
httponly=True, httponly=True,
secure=settings.cookie_secure, secure=settings.cookie_secure,
samesite="lax", samesite="lax",
max_age=settings.access_token_expire_minutes * 60, max_age=settings.session_timeout_hours * 3600,
path="/", path="/",
) )
@@ -79,7 +81,9 @@ async def login_submit(
}, },
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
) )
access, _ = await issue_token_pair(db, user) access, _ = await issue_token_pair(
db, user, timedelta(hours=settings.session_timeout_hours)
)
response = RedirectResponse( response = RedirectResponse(
url=_safe_next(next_url), status_code=status.HTTP_303_SEE_OTHER url=_safe_next(next_url), status_code=status.HTTP_303_SEE_OTHER
) )
@@ -110,7 +114,9 @@ async def login_ldap_submit(
}, },
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
) )
access, _ = await issue_token_pair(db, user) access, _ = await issue_token_pair(
db, user, timedelta(hours=settings.session_timeout_hours)
)
response = RedirectResponse( response = RedirectResponse(
url=_safe_next(next_url), status_code=status.HTTP_303_SEE_OTHER url=_safe_next(next_url), status_code=status.HTTP_303_SEE_OTHER
) )