Extending session login time
This commit is contained in:
1 parent
14aa2ac5d6
commit
a9c4c16d4f
6 files changed
+35
-10
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user