diff --git a/mealie/routes/auth/auth.py b/mealie/routes/auth/auth.py index 565088341..91bf8ddc7 100644 --- a/mealie/routes/auth/auth.py +++ b/mealie/routes/auth/auth.py @@ -1,3 +1,5 @@ +from typing import Literal + from authlib.integrations.starlette_client import OAuth from fastapi import APIRouter, Depends, Request, Response, status from fastapi.exceptions import HTTPException @@ -52,7 +54,9 @@ class MealieAuthToken(BaseModel): token_type: str = "bearer" @classmethod - def set_cookie(cls, response: Response, token: str, expires_in: int | float | None = None): + def set_cookie( + cls, response: Response, token: str, *, expires_in: int | float | None = None, samesite: str | None = None + ): expires_in = int(expires_in) if expires_in else None # httponly=False to allow JS access for frontend @@ -62,6 +66,7 @@ class MealieAuthToken(BaseModel): httponly=False, max_age=expires_in, secure=settings.PRODUCTION, + samesite=samesite, ) @classmethod @@ -69,6 +74,22 @@ class MealieAuthToken(BaseModel): return cls(access_token=token, token_type=token_type).model_dump() +def get_samesite(request: Request) -> Literal["lax", "none"]: + """ + Determine the appropriate samesite attribute for cookies. + + `samesite="none"` is required for iframe support (i.e. embedding Mealie in another site) + but only works over HTTPS. If `samesite="none"` is set over HTTP, most browsers will reject the cookie. + + `samesite="lax"` is the default, which works regardless of HTTP or HTTPS, + but does not support hosting in iframes. + """ + if request.url.scheme == "https" and settings.PRODUCTION: + return "none" + else: + return "lax" + + @public_router.post("/token") def get_token( request: Request, @@ -100,7 +121,12 @@ def get_token( access_token, duration = auth expires_in = duration.total_seconds() if duration else None - MealieAuthToken.set_cookie(response, access_token, expires_in) + MealieAuthToken.set_cookie( + response, + access_token, + expires_in=expires_in, + samesite=get_samesite(request), + ) return MealieAuthToken.respond(access_token) @@ -153,7 +179,12 @@ async def oauth_callback(request: Request, response: Response, session: Session access_token, duration = auth expires_in = duration.total_seconds() if duration else None - MealieAuthToken.set_cookie(response, access_token, expires_in) + MealieAuthToken.set_cookie( + response, + access_token, + expires_in=expires_in, + samesite=get_samesite(request), + ) return MealieAuthToken.respond(access_token)