Django, how to get the time until a cookie expires - django

Before marked as dup (I believe many answers are incorrect becase):
According to my research:
request.session.get_expiry_age returns the TOTAL lifespan of the cookie, not the time from now until it expires. It always returns the same value, irrespecitive of how much time is left.
Similarly get_expiry_date adds the total lifespan to the now time. It always increases when called.
Hence niether of them help.
settings.SESSION_SAVE_EVERY_REQUEST refreshes the cookie on every request, this is NOT what I am trying to achieve.
get_session_cookie_age() ONLY returns the value of settings.SESSION_COOKIE_AGE, as seen in the sourcecode:
def get_session_cookie_age(self):
return settings.SESSION_COOKIE_AGE
I use the cached_db session back end. The cached part is made none in void because it has to save the cookie everytime, hence SESSION_SAVE_EVERY_REQUEST is not applicable. The reason I ask this question is because I wish to refresh a cookie ONLY if the time left has gone below a threshold, and hence have some hysterious (have the benefits of caching and auto refreshing).
Here is my code that does not work because of the behaviour of get_expiry_age/get_expiry_date as explained above:
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
class SessionExpiry(MiddlewareMixin):
def process_request(self, request):
"""Automatically refresh a session expirary at a certain limit.
If request.session age goes below settings.SESSION_REFRESH, then
set the sessions expirary age to settings.SESSION_COOKIE_AGE. The
session is not refreshed every time to have the caching benefits of
the cached_db backend.
"""
try:
empty = request.session.is_empty()
except AttributeError:
return None
if (empty or not getattr(settings, 'SESSION_REFRESH', None)):
return None
if request.session.get_expiry_age() < settings.SESSION_REFRESH:
request.session.set_expiry(settings.SESSION_COOKIE_AGE)
return None

This is an area of Django that's confusing, in both design and documentation. The easiest way to figure out what's going on is to look at the source code.
To get what you want you need to set a custom expiration as a datetime. If you use seconds (including the default SESSION_COOKIE_AGE) that is interpreted as the number of seconds from now; that is, from whenever you ask.
So to set the expiration, use:
# Using timedelta will cause the session to save the expiration time as a datetime.
request.session.set_expiry(timedelta(seconds=settings.SESSION_COOKIE_AGE))
If you do that, calls to get_expiry_age() will return the actual difference between the current datetime and the datetime set as the expiry.
Be sure to note the warning in the set_expiry() documentation: "Note that datetime and timedelta values are only serializable if you are using the PickleSerializer."

Came here from the Django ticket. Here is what I came up with avoiding the need for pickle serializer and SESSION_SAVE_EVERY_REQUEST. So instead of using the datetime we can use plain timestamp, and since set_expire does not support that, we will just add another session key to store that.
class RefreshSessionMiddleware(middleware.SessionMiddleware):
def process_response(self, request, response):
session = request.session
if not (session.is_empty() or session.get_expire_at_browser_close()):
expiry = session.get('_session_expire_at_ts')
now_ts = int(time.time())
cookie_lifetime = session.get_expiry_age()
if expiry is None or now_ts + cookie_lifetime / 2 > expiry:
# This will set modified flag and update the cookie expiration time
session['_session_expire_at_ts'] = now_ts + cookie_lifetime
return super().process_response(request, response)

Related

Expiring a Django session at a specific time for a specific user (eg when their shift ends) at login

I have a system that has overlapping shift workers on it 24/7. Currently it is not uncommon for one to forget to log out and the next worker pick up their session and run with it. This causes some accountability issues.
I do realise there are options for session length ie settings.SESSION_COOKIE_AGE but these are a bit blunt for our purposes. We have different workers with different shift lengths, managers who have 2FA on-action and it's basically just not the path we want to pursue. Simply put...
I want to programmatically set the session death time on login.
We already have a custom Login view but this bubbles up through the built-in django.contrib.auth.forms.AuthenticationForm. And even there I can't see how to set an expiry on a particular session.
Any suggestions?
Edit: request.session's .get_expiry_age() and set_expiry(value) seem relevant but they do appear to update because they cycle around based on when the session was last modified, not when the session started. I need something that sets a maximum age on the session.
Edit 2: I guess I could write into the session on login and run something externally (a cronned management whatsit) that checked the expiries (if existed) and nuked each session that lapsed.
Came up with an answer thanks to the comments. On login I insert the timestamp into session:
request.session['login_timestamp'] = timezone.now().timestamp()
If you're wondering why timestamp, and not datetime.datetime.now() or timezone.now(), Django's default session encoder uses JSON and Django's JSON encoder does not handle datetimes. This is circumventable by writing an encoder that can handle datetimes... But just using an integer seconds-since-epoch value is enough for me.
And then have a little middleware to check that session against the current time.
from django.contrib.auth import logout
class MyMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# other checks to make sure this middleware should run.
# eg, this only happens on authenticated URLs
login_timestamp_ago = timezone.now().timestamp() - request.session.get('login_timestamp', timezone.now().timestamp())
if settings.RECEPTION_LOGIN_DURATION and <insert user checks here> and login_timestamp_ago >= settings.RECEPTION_LOGIN_DURATION:
logout(request) # nukes session
messages.warning(request, "Your session has expired. We need you to log in again to confirm your identity.")
return redirect(request.get_full_path())
The order of events here is quite important. logout(request) destroys the whole session. If you write a message (stored in session) beforehand, it'll be missing after the logout(request).

How to manually clear/update a cached view in django

My goal is to cache a view until an event occurs where the view's cache would need to expire, otherwise cache for 1 hour. This is what I have in urls.py
url(r'^get_some_stuff/$', cache_page(60 * 60, key_prefix='get_some_stuff')(views.StuffView.as_view())),
And this works fine. Now I'm trying to fetch the cached view to verify that there's something there and I tried this:
from django.core.cache import cache
cache.get('get_some_stuff')
But this returns None. I was hoping to do something like this:
from django.core.cache import cache
#relevant event happened
cache.delete('get_some_stuff')
What's the right way to handle cache?
I've tried passing the uri path:
cache.get('/api/get_some_stuff/')
And I still get None returned.
>>> cache.has_key('/api/get_some_stuff/')
False
>>> cache.has_key('/api/get_some_stuff')
False
>>> cache.has_key('get_some_stuff')
False
I've reviewed the suggested answer and it does not solve the underlying issue at all. It does not appear to be as trivial as passing the uri routing path as the key since keys are somewhat abstracted within django.
Here is a code snippet from Relekang about expire #cache_page
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.http import HttpRequest
from django.utils.cache import get_cache_key
def expire_page_cache(view, args=None):
"""
Removes cache created by cache_page functionality.
Parameters are used as they are in reverse()
"""
if args is None:
path = reverse(view)
else:
path = reverse(view, args=args)
request = HttpRequest()
request.path = path
key = get_cache_key(request)
if cache.has_key(key):
cache.delete(key)
Django's Cache framework only allows to cache data for predefined time and to clear expired cache data you may need to use django-signals to notify some receiver function which clears cache.
And cache.get, cache.has_key, cache.delete requires complete cache_key to be passed not the url or key-prefix. As django takes care of the keys we don't have much control to get or delete data.
If you are using database caching then use raw sql query to delete cache record from the database table when it's stale. write a query which says delete from cache_table with cache_key like ('%1:views.decorators.cache.cache_page%')
I faced same issues with per-view caching and I went with low-level cache api. I cached final result querysets using cache.set() and good part is you can set your own key and play with it.

Django: Limit user's session by time

I'm working on the app where I need to limit the ability to log in and be authenticated for a specified time of the day. Let's say from 8am to 5pm. To limit the ability to log in I created a custom auth backend where authenticate() method returns user object only if current time is within allowed period of time.
Now I want to terminate user's auth session after specified time.
Setting session expiry_date date and cookie's Expiry seems to be the best way to achieve this, but after reading Django docs and digging in the source code for some time I did not found a good solution to it. How do I do this? Any help is appreciated.
Changing the auth backend is probably not the solution you are looking for (at least I wouldn't recommend it), since you are changing security-critical parts of your application.
I would suggest a custom middleware: If registered users trying to access your site between 8am and 5pm, they'll see a warning that this site cannot be used.
from django.utils import timezone
from django.core.exceptions import PermissionDenied
class AccessRestrictionMiddleware:
def process_request(self, request):
current_hour = timezone.now().hour
is_time_restricted = current_hour >= 8 and current_hour < 17
if request.user.is_authenticated() and is_time_restricted:
raise PermissionDenied

Django, creating cache key from request object

I have been assigned a new project. In this django project the previous developer uses django.core.cache module a lot.
I decided to keep it like this.
My question is this. Can I make a unique string out of the request object that would let me know if the request object is the same as before?
Request comes with a set of 15 parameters (even more) and it is hard to choose one or some of them to create a key. It has to be all, because different combinations lead to different results.
This is the code I want to change (some code left out for brevity):
#login_required
def compare(request, username):
cache_key = 'key__%d' % (request.GET.to_unique_id_or_similar())
cache_value = cache.get(cache_key)
if cache_value is not None:
return cache_value
Django provides a super easy way to do this with the #cache_page decorator.

Django Cookies, how can I set them?

I have a web site which shows different content based on a location
the visitor chooses. e.g: User enters in 55812 as the zip. I know what
city and area lat/long. that is and give them their content pertinent
to that area. My question is how can I store this in a cookie so that
when they return they are not required to always enter their zip code?
I see it as follows:
Set persistent cookie based on their area.
When they return read cookie, grab zipcode.
Return content based on the zip code in their cookie.
I can't seem to find any solid information on setting a cookie. Any
help is greatly appreciated.
Using Django's session framework should cover most scenarios, but Django also now provide direct cookie manipulation methods on the request and response objects (so you don't need a helper function).
Setting a cookie:
def view(request):
response = HttpResponse('blah')
response.set_cookie('cookie_name', 'cookie_value')
Retrieving a cookie:
def view(request):
value = request.COOKIES.get('cookie_name')
if value is None:
# Cookie is not set
# OR
try:
value = request.COOKIES['cookie_name']
except KeyError:
# Cookie is not set
UPDATE : check Peter's answer below for a builtin solution :
This is a helper to set a persistent cookie:
import datetime
def set_cookie(response, key, value, days_expire=7):
if days_expire is None:
max_age = 365 * 24 * 60 * 60 # one year
else:
max_age = days_expire * 24 * 60 * 60
expires = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age),
"%a, %d-%b-%Y %H:%M:%S GMT",
)
response.set_cookie(
key,
value,
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,
secure=settings.SESSION_COOKIE_SECURE or None,
)
Use the following code before sending a response.
def view(request):
response = HttpResponse("hello")
set_cookie(response, 'name', 'jujule')
return response
UPDATE : check Peter's answer below for a builtin solution :
You could manually set the cookie, but depending on your use case (and if you might want to add more types of persistent/session data in future) it might make more sense to use Django's sessions feature. This will let you get and set variables tied internally to the user's session cookie. Cool thing about this is that if you want to store a lot of data tied to a user's session, storing it all in cookies will add a lot of weight to HTTP requests and responses. With sessions the session cookie is all that is sent back and forth (though there is the overhead on Django's end of storing the session data to keep in mind).
Anyone interested in doing this should read the documentation of the Django Sessions framework. It stores a session ID in the user's cookies, but maps all the cookies-like data to your database. This is an improvement on the typical cookies-based workflow for HTTP requests.
Here is an example with a Django view ...
def homepage(request):
request.session.setdefault('how_many_visits', 0)
request.session['how_many_visits'] += 1
print(request.session['how_many_visits'])
return render(request, 'home.html', {})
If you keep visiting the page over and over, you'll see the value start incrementing up from 1 until you clear your cookies, visit on a new browser, go incognito, or do anything else that sidesteps Django's Session ID cookie.
In addition to jujule's answer below you can find a solution that shows how to set a cookie to Class Based Views responses. You can apply this solution to your view classes that extends from TemplateView, ListView or View.
Below a modified version of jujule's persistent cookie setter method:
import datetime
from django.http import HttpResponse
def set_cookie(
response: HttpResponse,
key: str,
value: str,
cookie_host: str,
days_expire: int = 365,
):
max_age = days_expire * 24 * 60 * 60
expires = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(days=days_expire), "%a, %d-%b-%Y %H:%M:%S GMT",
)
domain = cookie_host.split(":")[0]
response.set_cookie(
key,
value,
max_age=max_age,
expires=expires,
domain=domain,
secure=False,
)
And sample class based view example that adds a cookie using persistent cookie setter
class BaseView(TemplateView):
template_name = "YOUR_TEMPLATE_FILE_PATH"
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
set_cookie(
response=response,
key="COOKIE_KEY",
value="COOKIE_VALUE",
cookie_host=request.get_host(),
days_expire=7,
)
return response