FastAPI: cookie-based logout functionality

Hi,

I’m new to FastAPI and trying to create user-friendly login and logout interface based on cookie. Here is my logout code:

@app.get("/logout", name="logout")
async def logout(response: Response,):
    logging.basicConfig(level = logging.INFO)
    logging.info('debug logout - before deleting cookie')
    response.delete_cookie("token")
    logging.info('debug logout - after deleting cookie')

    set_cookie_header = response.headers.get('set-cookie')
    if set_cookie_header and 'token=;' in set_cookie_header:
        logging.info('debug logout - cookie deletion confirmed')
    else:
        logging.info('debug logout - cookie deletion not confirmed')
        response.set_cookie(key="token", value="", expires=0)
        #response.set_cookie("token", None)
        logging.info('debug logout - setting token cookie as empty')
        set_cookie_header = response.headers.get('set-cookie')
        if set_cookie_header and 'token=;' in set_cookie_header:
            logging.info('debug logout - cookie deletion confirmed')
        else:
            logging.info('debug logout - cookie deletion not confirmed')
            response.delete_cookie(key="token")
            logging.info('debug logout - after deleting cookie (2)')

    set_cookie_header = response.headers.get('set-cookie')
    logging.info(f'debug logout - Set-Cookie header: {set_cookie_header}')

    #return RedirectResponse(url="/logged_out", status_code=303)
    return "logged out; please turn off the application and start again"

It works this way (it deletes the token cookie), but the output is not user-friendly. And whenever I try to render html (using template or just rendering html in the code) it doesn’t delete the cookie then.
Please advise.

Works this way:

@app.get("/logout", response_class=HTMLResponse)
def logout(request: Request, response: Response):
    data = {"login_url": app.url_path_for('login'),}
    response = templates.TemplateResponse("logout.html", {"request": request, "data": data})
    response.delete_cookie("token")
    return response
<!DOCTYPE html>
<html>
<head>
    <title>Logout</title>
</head>
<body>
    <a href="{{ data.login_url }}">Login again</a>
</body>
</html>

It is not secure to do a logout by trying to remove a cookie.
As you found the cookie is not deleted and could be replayed by a bad actor to bypass your logout code.

You need to have the cookie contain some infomation that you track in the server, a encrypted session token for example.

To logout the user you can invalidate the session token on the server end.
That will prevent a replay of an old token working.

If you see a session token that was invalidated then redirect to the login page.

Of course it is an encrypted token.

import jwt

def create_jwt_token(data: dict):
    return jwt.encode(data, secret_key, algorithm=ALGORITHM)

@app.post("/login")
async def login(request: Request, token: str = Form(...), db: Session = Depends(get_db)):
    
    logging.basicConfig(level = logging.INFO)
    logging.info("post /login")

    users = get_users(db)

    if users:
        logging.debug(f"debug /login - users count: {len(users)}")
    else:
        logging.debug(f"debug /login - Error! users is null")

    authenticated_user = None
    for user in users:
        if pbkdf2_sha256.verify(token, user.token):
            authenticated_user = user
            break

    data = {}

    if authenticated_user:
        user_description = authenticated_user.description
        logging.info('debug 1111')
        data = {"logout_url": app.url_path_for('logout'),}
        response = templates.TemplateResponse('dashboard.html', {
            "request": request, 
            "description": user_description, 
            "current_user_id": authenticated_user.id, 
            "current_user_isadmin": authenticated_user.isadmin, 
            "current_user": authenticated_user, 
            "data": data
        })
        #return response
        logging.info('debug 1114')
        user_data = {"sub": "token", "id": authenticated_user.id, "description": user_description, "isadmin": authenticated_user.isadmin, "scopes": ["read", "write"]}
        token_jwt = create_jwt_token(user_data)
        response.set_cookie("token", token_jwt)
        logging.info('debug 1115')

        return response
    else:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid secret token. Please try again.",
            headers={"WWW-Authenticate": "Basic"},
        )

Actually I tried to use invalidation instead of deletion. Maybe I’d need to use both.

response.set_cookie(key="token", value="", expires=0)

Thank you for your input and if you have more to say regarding my code, please share.

The point i was making is that logout must not require you to set a cookie on the browser.

Doesn’t matter. Assuming the token doesn’t actually contain the user’s password (which it absolutely shouldn’t), it’s still something that should only be valid until the user chooses to log out. Otherwise, you’re vulnerable to a trivial replay attack.

I believe I understand now. So, I can generate a token and write it into the database model on the server with IsValid==1. Once the user logs out, the token’s IsValid value becomes 0. Now, even if a bad actor captures the cookie and decrypts it, the token is no longer valid in the database model anyway, and my login function doesn’t allow me to use it further.

I hope this might be considered as a bit more secure.

Sure, or just delete it :slight_smile:

Encryption can be used to prevent an attacker generate guesses at valid login tokens as the attacker does not have the private key needed to do the encryption.

Incidentally, decrypting tokens is only useful if there’s real benefit to your server in NOT going back to your database. For most apps, it’s easier to just go look it up in the database, which avoids the risk of leaking information you don’t want leaked. The cookie is nothing more than a nonce, some cryptographically secure randomness encoded into a text form, and its sole value is that it is the database key.

As a second thought, what would prevent the bad actor from using the captured one-time token while the user is still logged in?

Scenario:
The user is successfully logged in.
The one-time token is generated and stored in the database model.
The one-time token is also encrypted with a secret and stored in the cookie.

Next… I’m not sure how exactly, but a bad actor takes a cookie and just uses it from their browser to login.

Now, is there any way to make a cookie valid only within this particular machine or browser? Or any other thoughts?..

Nothing! Usually this means that the users computer has been comprised.

I am assuming that you only ever send tokens over HTTPS to prevent wiretap intercept.

That is why banks use a short time limit on user login.

Not sure how relevant the encryption here is, since you’re using a JWT. They don’t actually encrypt, they only sign. Unless there’s a layer of encryption that I’m not seeing here? But it’s probably fine to do what everyone else does and just store that nonce in the cookie.

Ultimately, you can tie that token to whatever else you like - IP address, browser fingerprint, anything you can get from the request. But it’s generally quite inconvenient, and doesn’t actually protect much anyway; if you’re running over TLS (HTTPS), you shouldn’t have to worry about a man-in-the-middle, and if the attack is actually from the user’s own end, none of that tying will help you. All you really achieve is kicking people’s sessions whenever something changes.

Right, sign. Not encrypt. I’m new to these.