Browser Cookie never expires
Asked Answered
F

1

2

I'm implementing for first time a login Auth with HTTpOnly Cookie. In my case, the cookie it's created when user calls login method in a Python service with fastapi and uvicorn.

I've read the MDN documentation to implement the expires property and so, the browser delete this cookie when the time expires.

I've implemented the Cookie in Python with http.cookies and Morsel to apply the HttpOnly property like this:

from http import cookies
from fastapi import FastAPI, Response, Cookie, Request
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

mytoken = 'blablabla'

def getUtcDate():
    sessionDate = datetime.now()
    sessionDate += timedelta(minutes=2)
    return sessionDate.strftime('%a, %d %b %Y %H:%M:%S GMT')

@app.get('cookietest')
def getCookie(response: Response):
   cookie = cookies.Morsel()
   cookie['httponly'] = True
   cookie['version'] = '1.0.0'
   cookie['domain'] = '127.0.0.1'
   cookie['path'] = '/'
   cookie['expires'] = getUtcDate()
   cookie['max-age'] = 120
   cookie['samesite'] = 'Strict'
   cookie.set("jwt", "jwt", mytoken)

   response.headers.append("Set-Cookie", cookie.output())

   return {'status':'ok'}

Doing this, the Cookies looks correctly in the browser when I call the 'cookietest' endpoint, the evidence:

Cookie with expiration date

As you can see in the picture, the cookie has an expiration datetime in Expires / Max-Age:"Wed, 12 Oct 2022 11:24:58 GMT", 2 minutes after logging in (if the user logging at 14:05:00, the cookies expires at 14:07:00)

My problem is that any browser doesn't delete the cookie when the expire time has been exceeded, so this it's confusing me. If I let several minutes pass and then make a request to another endpoint (like http://127.0.0.1:8000/info), the cookie still exists in http headers.

What is the problem? What i'm doing wrong? I'm reading a lot of documentation about cookie storing and expiration and I can't see anything about this issue.

Many thanks Regards

EDITED : PROBLEM SOLVED

As Chris says, using the set_cookie method from FastApi the problem was solved.

I still wonder why the MSD documentation indicates that the date format must be a specific one which does not cause the browser to delete the Cookie, but indicating the time in seconds works correctly.

@app.get("/cookietest")
async def cookietest(response: Response):
    response.set_cookie(
        key='jwt', 
        value=getToken(), 
        max_age=120, 
        expires=120, 
        path='/', 
        secure=False, 
        httponly=True, 
        samesite="strict", 
        domain='127.0.0.1'
    )
    return {"Result": "Ok"}
Flashbulb answered 12/10, 2022 at 11:53 Comment(3)
Just to let you know that you could create cookies using the set_cookie method of the Response object, as described in this answer. See the relevant FastAPI documentation and Starlette documentation as well.Dupree
You can set the expires flag in the set_cookie method, which takes an integer that defines the number of seconds until the cookie expires. For instance, if you want the cookie to expire in 2 minutes from the time it is created, use expires=120.Dupree
Ok, I change my code using set_cookie method from fastapi and now works but, why the MDN documentation says that Expires use a DateTime with format like this "Expires: Wed, 21 Oct 2015 07:28:00 GMT"??Flashbulb
D
5

When using the expires flag, the date must be exactly in the format you are currently using, as well as in the GMT (Greenwich Mean Time) timezone. The reason that your cookie does not get expired 2 minutes after it has been created is because you are using datetime.now(), which returns the current local date and time.

Thus, for instance, if your current local timezone is GMT+2 and time is 20:30:00 (hence, GMT time is 18:30:00), creating a cookie that expires at 20:32:00 GMT will actually tell the browser to delete this cookie in 2 hours and 2 minutes (from the time it has been created). If you look at the cookie's Expires / Max-Age column in your browser's DevTools (e.g., on Chrome, go to Network tab in the DevTools, click on the request's name and then on the Cookies tab), you will notice a Z at the end of the datetime, which means UTC (Coordinated Universal Time)—that is, an offset from UTC of zero hours-minutes-seconds. You can check the response headers as well, where you can see the cookie's expires flag set to 20:32:00 GMT. There is no noticable time difference between UTC and GMT (if you would like to learn more about their differences, have a look at this post).

Hence, you could either replace .now() with .utcnow() in your code:

from datetime import timedelta, datetime

def get_expiry():
    expiry = datetime.utcnow()
    expiry += timedelta(seconds=120)
    return expiry.strftime('%a, %d-%b-%Y %T GMT')

or use time.gmtime(), passing as secs argument the time.time() (which returns the time in seconds) plus the desired lease time (in seconds):

import time

def get_expiry():
    lease = 120  # seconds
    end = time.gmtime(time.time() + lease)
    return time.strftime('%a, %d-%b-%Y %T GMT', end)

For either the above two methods, use:

cookie['expires'] = get_expiry()

You can also use the undocumented way of passing the expiry time directly in seconds instead. For instance:

cookie['expires'] = 120

An alternative to expires is the max-age flag , which specifies the cookie's expiration in seconds from the current moment (similar to the above way). If set to zero or a negative value, the cookie is deleted immediately. Example:

cookie['max-age'] = 120

Note:

If both expires and max-age are set, max-age has precedence (see relevant documentation on MDN).

Also, as per RFC 6265:

4.1.2.1. The Expires Attribute

The Expires attribute indicates the maximum lifetime of the cookie, represented as the date and time at which the cookie expires. The user agent is not required to retain the cookie until the specified date has passed. In fact, user agents often evict cookies due to memory pressure or privacy concerns.

4.1.2.2. The Max-Age Attribute

The Max-Age attribute indicates the maximum lifetime of the cookie, represented as the number of seconds until the cookie expires. The user agent is not required to retain the cookie for the specified duration. In fact, user agents often evict cookies due to memory pressure or privacy concerns.

NOTE: Some existing user agents do not support the Max-Age 
attribute. User agents that do not support the Max-Age attribute 
ignore the attribute.

If a cookie has both the Max-Age and the Expires attribute, the Max-Age attribute has precedence and controls the expiration date of the cookie. If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie until "the current session is over" (as defined by the user agent).

Also note, as mentioned in MDN documentation regarding the expires flag:

Warning: Many web browsers have a session restore feature that will save all tabs and restore them the next time the browser is used. Session cookies will also be restored, as if the browser was never closed.

Another thing to note is that, since Sep 2022, Chrome limits the cookie's max-age to 400 days:

When cookies are set with an explicit Expires/Max-Age attribute the value will now be capped to no more than 400 days in the future. Previously, there was no limit and cookies could expire as much as multiple millennia in the future.

Using FastAPI/Starlette

It should also be noted that FastAPI/Starlette provides an easier way to set cookies on the Response object, using the set_cookie method, as described in this answer. As per Starlette documentation:

  • max_age - An integer that defines the lifetime of the cookie in seconds. A negative integer or a value of 0 will discard the cookie immediately. Optional
  • expires - An integer that defines the number of seconds until the cookie expires. Optional

Example from FastAPI documentation:

from fastapi import FastAPI, Response

app = FastAPI()

@app.post('/')
def create_cookie(response: Response):
    response.set_cookie(key='token', value='token-value', max_age=120, expires=120, httponly=True)
    return {'message': 'success'}
Dupree answered 13/10, 2022 at 19:21 Comment(1)
A lot of information and so usefull. Thanks for your explanation and time.Flashbulb

© 2022 - 2024 — McMap. All rights reserved.