fix: Actually Fix Token Time (#6215)

This commit is contained in:
Michael Genson
2025-09-21 19:51:19 -05:00
committed by GitHub
parent b27977fbdf
commit cec6d2c5ec
5 changed files with 48 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import { defineNuxtConfig } from "nuxt/config"; import { defineNuxtConfig } from "nuxt/config";
const AUTH_TOKEN = "mealie.auth.token"; const AUTH_TOKEN = "mealie.access_token";
export default defineNuxtConfig({ export default defineNuxtConfig({
// Global page headers: https://go.nuxtjs.dev/config-head // Global page headers: https://go.nuxtjs.dev/config-head
@@ -142,7 +142,6 @@ export default defineNuxtConfig({
signInResponseTokenPointer: "/access_token", signInResponseTokenPointer: "/access_token",
type: "Bearer", type: "Bearer",
cookieName: AUTH_TOKEN, cookieName: AUTH_TOKEN,
maxAgeInSeconds: parseInt(process.env.TOKEN_TIME || "48") * 3600, // TOKEN_TIME is in hours
}, },
pages: { pages: {
login: "/login", login: "/login",

View File

@@ -30,7 +30,23 @@ export default defineNuxtPlugin(() => {
return response; return response;
}, },
(error) => { (error) => {
if (error?.response?.data?.detail?.message) alert.error(error.response.data.detail.message as string); if (error?.response?.data?.detail?.message) {
alert.error(error.response.data.detail.message as string);
};
// If we receive a 401 Unauthorized response, clear the token cookie and redirect to login
if (error?.response?.status === 401) {
// If tokenCookie is not set, we may just be an unauthenticated user using the wrong API, so don't redirect
const tokenCookie = useCookie(tokenName);
if (tokenCookie.value) {
tokenCookie.value = null;
// Disable beforeunload warnings to prevent "Are you sure you want to leave?" popups
window.onbeforeunload = null;
window.location.href = "/login";
}
}
return Promise.reject(error); return Promise.reject(error);
}, },
); );

View File

@@ -30,8 +30,8 @@ class AuthProvider[T](metaclass=abc.ABCMeta):
settings = get_app_settings() settings = get_app_settings()
duration = timedelta(hours=settings.TOKEN_TIME) duration = timedelta(hours=settings.TOKEN_TIME)
if remember_me and remember_me_duration > duration: if remember_me:
duration = remember_me_duration duration = max(remember_me_duration, duration)
return AuthProvider.create_access_token({"sub": str(user.id)}, duration) return AuthProvider.create_access_token({"sub": str(user.id)}, duration)

View File

@@ -123,6 +123,17 @@ class AppSettings(AppLoggingSettings):
TOKEN_TIME: int = 48 TOKEN_TIME: int = 48
"""time in hours""" """time in hours"""
@field_validator("TOKEN_TIME")
@classmethod
def validate_token_time(cls, v: int) -> int:
if v < 1:
raise ValueError("TOKEN_TIME must be at least 1 hour")
# If TOKEN_TIME is unreasonably high (e.g. hundreds of years), JWT encoding
# can overflow, so we set the max to 10 years (87600 hours).
if v > 87600:
raise ValueError("TOKEN_TIME is too high; maximum is 87600 hours (10 years)")
return v
SECRET: str SECRET: str
SESSION_SECRET: str SESSION_SECRET: str

View File

@@ -1,5 +1,3 @@
from datetime import timedelta
from authlib.integrations.starlette_client import OAuth from authlib.integrations.starlette_client import OAuth
from fastapi import APIRouter, Depends, Request, Response, status from fastapi import APIRouter, Depends, Request, Response, status
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
@@ -25,7 +23,6 @@ public_router = APIRouter(tags=["Users: Authentication"])
user_router = UserAPIRouter(tags=["Users: Authentication"]) user_router = UserAPIRouter(tags=["Users: Authentication"])
logger = root_logger.get_logger("auth") logger = root_logger.get_logger("auth")
remember_me_duration = timedelta(days=14)
settings = get_app_settings() settings = get_app_settings()
if settings.OIDC_READY: if settings.OIDC_READY:
@@ -54,6 +51,19 @@ class MealieAuthToken(BaseModel):
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer"
@classmethod
def set_cookie(cls, response: Response, token: str, expires_in: int | float | None = None):
expires_in = int(expires_in) if expires_in else None
# httponly=False to allow JS access for frontend
response.set_cookie(
key="mealie.access_token",
value=token,
httponly=False,
max_age=expires_in,
secure=settings.PRODUCTION,
)
@classmethod @classmethod
def respond(cls, token: str, token_type: str = "bearer") -> dict: def respond(cls, token: str, token_type: str = "bearer") -> dict:
return cls(access_token=token, token_type=token_type).model_dump() return cls(access_token=token, token_type=token_type).model_dump()
@@ -86,17 +96,11 @@ def get_token(
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
) )
access_token, duration = auth access_token, duration = auth
expires_in = duration.total_seconds() if duration else None expires_in = duration.total_seconds() if duration else None
response.set_cookie(
key="mealie.access_token",
value=access_token,
httponly=True,
max_age=expires_in,
secure=settings.PRODUCTION,
)
MealieAuthToken.set_cookie(response, access_token, expires_in)
return MealieAuthToken.respond(access_token) return MealieAuthToken.respond(access_token)
@@ -145,18 +149,11 @@ async def oauth_callback(request: Request, response: Response, session: Session
if not auth: if not auth:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
access_token, duration = auth
access_token, duration = auth
expires_in = duration.total_seconds() if duration else None expires_in = duration.total_seconds() if duration else None
response.set_cookie( MealieAuthToken.set_cookie(response, access_token, expires_in)
key="mealie.access_token",
value=access_token,
httponly=True,
max_age=expires_in,
secure=settings.PRODUCTION,
)
return MealieAuthToken.respond(access_token) return MealieAuthToken.respond(access_token)