Search code examples
axioscorsfastapisession-cookiesfastapi-middleware

Http-only cookie in response header from backend, but not being stored in client browser


I have a fullstack app made up of a React frontend and a FastAPI (python) backend, both of which are currently being served locally on 127.0.0.1 (localhost).

BACKEND:

Here is the code for my backend server, with CORSMiddleware and SessionMiddleware configured:

from fastapi import FastAPI, HTTPException, status, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
import spotipy
from spotipy.oauth2 import SpotifyOAuth

app = FastAPI()

# generated via cmd: 'openssl rand -hex 32'
SECRET_KEY = "XXX"

origins = [
    "http://localhost",
    "https://localhost",
    "http://localhost:3000",
    "https://localhost:3000",
    "http://127.0.0.1",
    "https://127.0.0.1",
    "http://127.0.0.1:3000",
    "https://127.0.0.1:3000",
]

# https://www.starlette.io/middleware/#corsmiddleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# https://www.starlette.io/middleware/#sessionmiddleware
app.add_middleware(
    SessionMiddleware,
    secret_key=SECRET_KEY,
    https_only=True,
    max_age=3600,  # 3600s = 1hr, the life of spotify access token
    same_site="none",
)

CLIENT_ID = "YYYY"
CLIENT_SECRET = "ZZZ"
OAUTH_REDIRECT_URI = "http://127.0.0.1:3000/"
SCOPE = "playlist-read-private playlist-read-collaborative user-read-private user-read-email"

# Docs: https://spotipy.readthedocs.io/en/2.24.0/#module-spotipy.oauth2
sp_oauth = SpotifyOAuth(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri=OAUTH_REDIRECT_URI, scope=SCOPE)

@app.post("/spotify-auth", tags=["auth"])
def exchange_token(code: str, request: Request):
    # Exchanges token, with check_cache=False to ensure new users can be registered
    token_info = sp_oauth.get_access_token(code=code, check_cache=False)
    print(token_info)
    # https://www.starlette.io/middleware/#sessionmiddleware
    request.session["access_token"] = token_info["access_token"]
    request.session["refresh_token"] = token_info["refresh_token"]
    return {"tokenSaved": True}

In other endpoints, i.e. /user, when I try to access request.session.get("access_token"), I get absolutely nothing back.

FRONTEND:

here is the axios POST request from my frontend (react/typescript) that will hit the /spotify-auth endpoint on my backend:

const exchangeSpotifyAuthToken = async (
    code: string,
): Promise<AccessTokenReponse> => {
    const searchParams = new URLSearchParams();
    searchParams.append("code", code);
    const response = await axios.post(
        `${API_URL_BASE}/spotify-auth?` + searchParams.toString(),
        { withCredentials: true },
    );
    return response.data;
};

But as I said, my attempts to use the access_token in other routes, which should be accessible via request.session.get("access_token") all fail. Each of my axios requests also include this { withCredentials: true } config, so that shouldn't be the problem.

THINGS I'VE TRIED:

I've seen a lot of CORS related posts regarding this issue, and I think I have implemented the requirements as follows:

  • CORSMiddleware allowed origins on backend configured to receive requests from my frontend (127.0.0.1:3000)
  • allow_credentials=True on backend
  • Frontend and backend located at same domain: 127.0.0.1
  • requests from frontend configured with withCredentials: true

BROWSER TOOLS:

I also checked the browser tools Network tab and saw that my session was being correctly recorded following my POST request to login/authenticate:

enter image description here

But any followup requests to my backend do not contain this session cookie, so they promptly fail my authentication check:

enter image description here

Shouldn't my session cookie show up in those request headers so that my backend can retrieve them? What am I doing wrong?

Thanks!


Solution

  • I must not have been keeping good track of which possible solution I was trying... I think I may have mix and mashed them, which resulted in the cookies not being stored.

    Ultimately I had domain=".localhost" in my SessionMiddleware, like so:

    app.add_middleware(
        SessionMiddleware,
        secret_key=SECRET_KEY,
        https_only=True,
        max_age=3600,  # 3600s = 1hr, the life of spotify access token
        same_site="none",
        domain=".localhost"   # THIS WAS WHAT WAS KILLING ME
    )
    

    This was a suggestion I had seen online, but that must have been when I was serving my frontend on localhost and not 127.0.0.1.

    Now that both my backend and frontend are being served on 127.0.0.1, I recognized this faulty domain specification and removed it. The backend session middleware now correctly sends cookies to the default domain (that of the backend, 127.0.0.1) and my cookies are being correctly stored!!

    Still have to confirm that I can actually use the session cookies as intended in my other API endpoints, but I'll just enjoy the moment and worry about that tomorrow :)

    Thanks for the comments/suggestions!