]> git.ipfire.org Git - pbs.git/commitdiff
API: Set the refresh token in a cookie as well
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 20 Jun 2025 09:54:26 +0000 (09:54 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 20 Jun 2025 09:54:26 +0000 (09:54 +0000)
This will make it easier to refresh the token from a browser when it is
persistently stored as a cookie.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/api/auth.py

index 0a1982774f65487d33b2711acf833ac8070a9328..7cfce981dab07d7338ed9a497df140a5f1b9c359 100644 (file)
@@ -25,6 +25,7 @@ import kerberos
 import os
 import pydantic
 import socket
+import typing
 
 from . import apiv1
 from . import backend
@@ -136,7 +137,7 @@ def get_principal(token):
 
 @router.post("/user")
 async def auth_user(credentials: fastapi.security.OAuth2PasswordRequestForm =
-               fastapi.Depends()) -> AuthResponse:
+               fastapi.Depends()) -> fastapi.responses.JSONResponse:
        # Set keytab to use
        os.environ["KRB5_KTNAME"] = KERBEROS_KEYTAB
 
@@ -165,16 +166,39 @@ async def auth_user(credentials: fastapi.security.OAuth2PasswordRequestForm =
                type="refresh", expires_after=REFRESH_TOKEN_EXPIRY_TIME)
 
        # Send the response
-       return AuthResponse(access_token=access_token, refresh_token=refresh_token)
+       data = AuthResponse(access_token=access_token, refresh_token=refresh_token)
+
+       # Serialize the JSON response
+       response = fastapi.responses.JSONResponse(content=data.model_dump())
+
+       # Set the refresh token as a cookie too for persistent browser storage
+       response.set_cookie(
+               "refresh_token",
+               refresh_token,
+               httponly=True,
+               samesite="Strict",
+
+               # Only send the cookie when performing a refresh
+               path="/api/v1/auth/refresh",
 
+               # Forget the cookie when the token expires
+               max_age=REFRESH_TOKEN_EXPIRY_TIME,
+       )
+
+       return response
 
 class RefreshRequest(pydantic.BaseModel):
        refresh_token: str
 
 @router.post("/refresh")
-async def auth_refresh(data: RefreshRequest):
+async def auth_refresh(request: fastapi.Request, data: typing.Optional[RefreshRequest]=None):
+       # Fetch the refresh token from either the body or the cookie
+       refresh_token = data.refresh_token if data else request.cookies.get("refresh_token")
+       if not refresh_token:
+               raise fastapi.HTTPException(401, "No refresh token provided")
+
        # Fetch the principal from the given token
-       principal = get_principal(data.refresh_token)
+       principal = get_principal(refresh_token)
 
        # XXX Check if the principal actually still exists
 
@@ -183,7 +207,7 @@ async def auth_refresh(data: RefreshRequest):
                type="access", expires_after=ACCESS_TOKEN_EXPIRY_TIME)
 
        # Send the response
-       return AuthResponse(access_token=access_token, refresh_token=data.refresh_token)
+       return AuthResponse(access_token=access_token, refresh_token=refresh_token)
 
 # Add everything to the app
 apiv1.include_router(router)