Browser Cookie never expires

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("https://stackoverflow.com/")
def create_cookie(response: Response):
    response.set_cookie(key='token', value="token-value", max_age=120, expires=120, httponly=True)
    return {'message': 'success'}

Leave a Comment