forcing session expiry on browser close with django-social-auth - django

I am looking for a way to force a re-login when a user who has logged in using FB/Google onto my site closes the browser. I was reading https://django-social-auth.readthedocs.org/en/latest/configuration.html, and I don't think:
SOCIAL_AUTH_EXPIRATION = 'expires'
or
SOCIAL_AUTH_SESSION_EXPIRATION = True
really does what I am looking for. I tried to add a custom pipeline this way which sets expiry time to 0 as the last thing in the pipelines:
def expire_session_on_browser_close(backend, details, response, social_user, uid, user, request, *args, **kwargs):
request.session.set_expiry(0)
SOCIAL_AUTH_PIPELINE = (
'social_auth.backends.pipeline.social.social_auth_user',
#'social_auth.backends.pipeline.associate.associate_by_email',
'social_auth.backends.pipeline.user.get_username',
'social_auth.backends.pipeline.user.create_user',
'social_auth.backends.pipeline.social.associate_user',
'social_auth.backends.pipeline.social.load_extra_data',
'social_auth.backends.pipeline.user.update_user_details',
'useraccount.pipeline.expire_session_on_browser_close',
)
But it doesn't seem to take effect. Setting
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
has no effect either.
On a similar note, my site also allows user to login "traditionally" and I am able to have
request.session.set_expiry(0)
do the trick there, and users are forced to login when they close the browsers. Just doesn't work with FB/Google logins.
Any thoughts?
Thanks!
Edit:
If I go and muck around with:
UserSocialAuthMixin::expiration_datetime()
from
db\base.py
and force it to return 0, my issue gets resolved.
But this is bad, bad hackery. Is there a better, more elegant way?
Thanks!

Oh man, I over-engineered to this to the Nth degree. All I needed to do was set:
SOCIAL_AUTH_SESSION_EXPIRATION=False
This way if the Provider's response contains 'expires' or whatever SOCIAL_AUTH_EXPIRATION contains, django-social-auth won't call set_expiry() on that parsed value.
Additionally, I also set the pipeline function (as seen in my original question) so that I could set my own expiry (0 in my case).

Related

django-channels: differentiate between different `AnonymousUser`s

Unfortunately I'm using django-channels channels 1.1.8, as I missed all the
updates to channels 2.0. Upgrading now is unrealistic as we've just
launched and this will take some time to figure out correctly.
Here's my problem:
I'm using the *message.user.id *to differentiate between authenticated
users that I need to send messages to. However, there are cases where I'll
need to send messages to un-authenticated users as well - and that message
depends on an external API call. I have done this in ws_connect():
#channel_session_user_from_http
def ws_connect(message):
# create group for user
if str(message.user) == "AnonymousUser":
user_group = "AnonymousUser" + str(uuid.uuid4())
else:
user_group = str(message.user.id)
print(f"user group is {user_group}")
Group(user_group).add(message.reply_channel)
Group(user_group).send({"accept": True})
message.channel_session['get_user'] = user_group
This is only the first part of the issue, basically I'm appending a random
string to each AnonymousUser instance. But I can't find a way to access
this string from the request object in a view, in order to determine who
I am sending the message to.
Is this even achievable? Right now I'm not able to access anything set in
the ws_connect in my view.
EDIT: Following kagronick's advice, I tried this:
#channel_session_user_from_http
def ws_connect(message):
# create group for user
if str(message.user) == "AnonymousUser":
user_group = "AnonymousUser" + str(uuid.uuid4())
else:
user_group = str(message.user.id)
Group(user_group).add(message.reply_channel)
Group(user_group).send({"accept": True})
message.channel_session['get_user'] = user_group
message.http_session['get_user'] = user_group
print(message.http_session['get_user'])
message.http_session.save()
However, http_session is None when user is AnonymousUser. Other decorators didn't help.
Yes you can save to the session and access it in the view. But you need to use the http_session and not the channel session. Use the #http_session decorator or #channel_and_http_session. You may need to call message.http_session.save() (I don't remember, I'm on Channels 2 now.). But after that you will be able to see the user's group in the view.
Also, using a group for this is kind of overkill. If the group will only ever have 1 user, put the reply_channel in the session and do something like Channel(request.session['reply_channel']).send() in the view. That way it doesn't need to look up the one user that is in the group and can send directly to the user.
If this solves your problem please mark it as accepted.
EDIT: unfortunately this only works locally but not in production. when AnonymousUser, message.http_sesssion doesn't exist.
user kagronick got me on the right track, where he pointed that message has an http_session attribute. However, it seems http_session is always None in ws_connect when user is AnonymousUser, which defeats our purpose.
I've solved it by checking if the user is Anonymous in the view, and if he is, which means he doesn't have a session (or at least channels can't see it), initialize one, and assign the key get_user the value "AnonymousUser" + str(uuid.uuid4()) (this way previously done in the consumer).
After I did this, every time ws_connect is called message will have an http_session attribute: Either the user ID when one is logged in, or AnonymousUser-uuid.uuid4().

Want to have django-two-factor-auth skip token step if user/device logged in during the last 30 days

First, if anyone has done this, please advise :)
Right now, I am thinking of subclassing LoginView method get_context_data() (from the django-two-factor-auth package).
the 1st line of the new method would be:
if self.steps.current == 'token':
(pseudo code)
if user_agent == current user_agent and last_activity < 30 days ago (from table user_sessions_session):
return context # skip the token step
I see this is a desired feature but not yet implemented
I forked the repository with changes here and put in a pull request. You can review the changes here (the coverage for the changes is 100%).
Basically, if there was valid login (with a token), it sets a signed cookie limited to the login page. Logins after that will check for that signed cookie, and if it exists and has not expired, it will allow login without a token. This is the key logic:
def token_required(self, request):
"""
if this user logged with a token in the last {{TWO_FACTOR_TRUSTED_DAYS}}
days, they can skip the token steps.
"""
end_valid_login = None
if not request.COOKIES.get('evl'):
return True
try:
end_valid_login = request.get_signed_cookie('evl',
salt=settings.TWO_FACTOR_SALT)
except (BadSignature, SignatureExpired) as e:
return True
end_valid_login_dt = datetime.strptime(end_valid_login, '%Y-%m-%d')
if datetime.today() < end_valid_login_dt:
#--- the cookie is valid and still within {{TWO_FACTOR_TRUSTED_DAYS}} ---#
return False
else:
return True
NOTE: (edit Mar 2020)
Actually, I would recommend that anyone implementing 2FA use Webauthn. I think it makes all other methods obsolete and will become the standard everywhere. Here is an explanation of the inherent weaknesses of other 2FA methods

#vary_on_cookie fails due to non-Django cookies

I am stumped on a caching issue in my Django 1.5.6 application:
#vary_on_cookie
#cache_page(24 * 60 * 60, key_prefix=':1:community')
#rendered_with("general/community.html")
#allow_http("GET")
def community(request):
...
return { ... }
Locally the caching is working correctly, but when I test this in staging, #vary_on_cookie isn't working -- I can see by the queries being executed that community() is being executed on subsequent calls to this page.
I updated my settings in my local environment to use the same Redis cache as staging to eliminate that difference, but the local environment continued to behave correctly.
Looking at the keys Redis has in its cache, I can see what the problem is -- in staging every time this page gets called, new keys are added to the cache. Compare the output from cache.keys('*community*'):
LOCAL:
First call to community page:
[u'community:1:views.decorators.cache.cache_page.:1:community.GET.b528759dd79cf1c6b405290c0bc05e39.3b7d4c38ec8d92512a4a0847f4738298.en-us.America/New_York',
u'community:1:views.decorators.cache.cache_header.:1:community.b528759dd79cf1c6b405290c0bc05e39.en-us.America/New_York']
Second call (same user):
[u'community:1:views.decorators.cache.cache_page.:1:community.GET.b528759dd79cf1c6b405290c0bc05e39.3b7d4c38ec8d92512a4a0847f4738298.en-us.America/New_York',
u'community:1:views.decorators.cache.cache_header.:1:community.b528759dd79cf1c6b405290c0bc05e39.en-us.America/New_York']
Notice there are the same number of keys in both cases.
STAGING:
First call to community page:
[u'community:1:views.decorators.cache.cache_header.:1:community.b528759dd79cf1c6b405290c0bc05e39.en-us.America/New_York',
u'community:1:views.decorators.cache.cache_page.:1:community.GET.b528759dd79cf1c6b405290c0bc05e39.559380b85dc0cdcf0ff25051df78987d.en-us.America/New_York']
Second call (same user):
[u'community:1:views.decorators.cache.cache_header.:1:community.b528759dd79cf1c6b405290c0bc05e39.en-us.America/New_York',
u'community:1:views.decorators.cache.cache_page.:1:community.GET.b528759dd79cf1c6b405290c0bc05e39.559380b85dc0cdcf0ff25051df78987d.en-us.America/New_York',
u'community:1:views.decorators.cache.cache_page.:1:community.GET.b528759dd79cf1c6b405290c0bc05e39.6ec85abcc8a14d66800228bdccc537f0.en-us.America/New_York']
Notice that an additional entry has been added to the cache though it's the same user!
I'm stumped where to go from here. Both environments are using SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'. The staging environment clearly recognizes that this is the same user in every other way. What is happening in #vary_on_cookie that is creating a difference in staging, but not locally?
I've inspected all of my staging vs. local differences, scrutinized my custom middleware, but I don't have any ideas of what to look at. Any ideas even of what to look at next would be greatly appreciated. Thanks!
UPDATE
I inspected django.utils.cache._generate_cache_key() to see how it generates that last hex section of the cache key. I naively assumed it just looked at Django's own cookies (like sessionid), but I see that it uses all of the cookies passed into HTTP_COOKIE -- that means, Django and non-Django. For me, that means cookies from Google Analytics and New Relic, neither of which I have running locally.
for header in headerlist: # headerlist = [u'HTTP_COOKIE']
value = request.META.get(header, None) # the string of all cookies, for ex: __atuvc=39%7C17%2C8%7C18; csrftoken=dPqaXS6XVGp2UUvfhEW9kS6R6WPHQlE4; sessionid=j6a83wbsq1sez9bz75n0tzl4n884umg2'
if value is not None:
ctx.update(force_bytes(value))
Can this really be true?! All of the world's Django sites using #vary_on_cookie are being thwarted by their third-party cookies?!
I created a custom decorator which hacks the HTTP headers to isolate the user's ID. (Although it sets Vary: DJANGO_USERID, Cookie in the response sent back to the browser, it doesn't include the actual ID.)
I would appreciate any feedback on this solution, since it's a bit beyond my Django comfort zone. Thanks!
def vary_on_user(view):
"""
Adapted from django.views.decorators.vary_on_cookie
"""
#wraps(view, assigned=available_attrs(view))
def inner_func(request, *args, **kwargs):
request.META['HTTP_DJANGO_USERID'] = request.user.id
response = view(request, *args, **kwargs)
patch_vary_headers(response, ('DJANGO_USERID',))
return response
return inner_func

I need to rewrite url in django

I need to rewrite url www.example.com/product/1 to www.example.com/en/product/1 when user chooses english language. (he will click on a select box that will toggle the language and set a session called 'language')
I cannot choose the django 1.4 which supports this feature. We are advised to stick with django 1.3.
Hence I tried a middleware, but as it turns out, the middleware runs for each request resulting in a endless loop.
class urlrewrite():
def process_request(self, request):
if 'i' in request.session:
if request.session.get('i','') != 0:
print "session"
request.session['i'] = request.session['i'] + 1
else:
request.session['i'] = 0
else:
request.session['i'] = 0
print "request.session['i']", request.session['i']
if request.session.get('i','') == SOME_CONSTANT and request.session.get('django_language','') == 'en':
del request.session['i']
return HttpResponseRedirect("en/"+request.META['PATH_INFO'])
Ofcourse, it doesnt work. This runs for every single request.
Kindly help me out.
Thank you
Don't write this yourself, use someone else's hard work.
Try django-cms's solution first.
==== EDIT ====
You do not need to used django-cms, just have it installed and use their Multilingual URL Middleware. This interfaces with django's regular internationalisation machinery.
http://django-cms.readthedocs.org/en/2.3.3/advanced/i18n.html
This problem can be solved by using a little trick in your urls.py file, as shown by this part of the docs: https://docs.djangoproject.com/en/1.4/ref/generic-views/#django-views-generic-simple-redirect-to.
You keep the same view, it will simple have a different URL. I think thats what you want. Make sure you choose the 1.3 version of the docs, I believe there has been some changes.

How to check user is authenticated properly in Django and Backbone.js

I want to build single page application using Backbone.js and Django.
For checking user is authenticated or not,
I wrote a method get_identity method in django side.
If request.user.is_authenticated is true it returns request.user.id otherwise it returns Http404
In backbone side, I defined a User model and periodically make ajax call to get_identity.
I think it is the most straightforward way to check user is authenticated or not.
For learning single page application, I want to do this operation more sensible and efficient than this way if it is possible.
So what is your advice about this? When I search Django+Backbone.js + User Authentication, I couldn't find any satisfactory result and I really wonder how people do this simple operation.
Any help or idea will be appreciated.
(By the way I tried to read cookie periodically but HttpOnly True flagged cookies are not reacheable in client side.)
Django views.py
def get_identity(request):
if not request.user.is_authenticated():
raise Http404
return HttpResponse(json.dumps({'identity':request.user.id}), mimetype="application/json")
Backbone.js side.
updateUser:function(){
var $self=this;
$.ajaxSetup({async:false});
$.get(
'/get_identity',
function(response){
// update model...
$self.user.id =response.identity;
//check user every five minutes...
$self.user.fetch({success: function() {
$self.user.set('is_authenticated',true);
setTimeout($self.updateUser, 1000*60*1);
}
},this);
}).fail(function(){
//clear model
$self.user.clear().set($self.user.defaults);
setTimeout($self.updateUser, 1000*60*1);
});
$.ajaxSetup({async:true});
}
I had var is_authenticated = {{request.user.is_authenticated}}; in my base.html
and used the global variable to check.
I'm in pursuit of better solution (because this breaks when you start caching).
But you might find it useful.