Django change request.path in middleware (to authenticate by token in url) - django

Dear omnoscient beings at Stackoverflow,
In Django 1.3 I am making a process_request middleware that gets a token from an url, logs the user in (if it's correct) and removes the token from the url. However:
I) Django recommends against accessing POST/GET data in middleware, I'm not really sure why so... Does the same apply to request.path ? https://docs.djangoproject.com/en/dev/topics/http/middleware/#process-view
II) I want to remove the token from the URL, so /album3/pic42/~53Cr3t70K3n/like/ -> /album3/pic42/like/. Changing request.path however does not work. The page will not be found, while
The middleware does process correctly (verified by print)
Directly entering /album3/pic42/like/ does work
The error (with token) shows Request URL: http://www.site.com/album3/pic42/like/
Is there a fix for this, or am I approaching this from the wrong angle entirely?
Thanks in advance!
I just realized that to change it client side, obviously I need a redirect (why didn't I think of that...). However, it would still be useful to be able to rewrite it just server-side without a new request, for example for accessing a personalized image.
P.s.: more details if needed, feel free to skip
I am working on a site that (will) sends personalized emails to users. I would like users to be able to click links in the email and be logged in automatically, by means of a token in the email link. This is in addition to normal login. (I know it's a less secure because people might forward an e-mail, but it's good enough for my site). The url would look something like this:
/album3/pic42/~53Cr3t70K3n/like/ (with http://www.site.com stripped, Django does that)
I am writing a middleware to match this and log the user in when appropriate, a authentication backend for accepting a token as valid credentials and a token model.
Middleware process_request function:
def process_request(self, request):
if '/~' in request.path:
result = re.search('(.*)/~(.+?)/(.*)', request.path)
(uidb36, token) = result.group(2).split('-', 2)
user = authenticate(uidb36 = uidb36, token = token)
if user: login(request, user)
return HttpResponseRedirect('%s/%s' % (result.group(1), result.group(3)) + ('?' + '&'.join('='.join(item) for item in request.GET.items()) if request.GET else ''))
return None
Right now it works with redirects, I'd like to also be able to do it internally.

If you don't want to mess up with upload handlers, I have a better solution for you:
Create a rule in your urls.py to specifically catch visits with tokens
Put it at the start of the urlpatterns to make sure that it will be evaluated first. Something like this will do:
(r'/~', 'my_app_name.my_redirect_view'),
Create a view:
def my_redirect_view(request):
#Compiled regular expressions work much faster
beloved_tokens = re.compile(r'(.*)/~(.+?)/(.*)')
result = beloved_tokens.search(request.path)
try:
(uidb36, token) = result.group(2).split('-', 2)
path_end = result.group(3)
# We use "try" to be sure that no one had
# messed with our beloved tokens:
except AttributeError:
raise Http404
else:
user = authenticate(uidb36 = uidb36, token = token)
if user:
login(request, user)
return HttpResponseRedirect('%s/%s' % (result.group(1), result.group(3)) + ('?' + '&'.join('='.join(item) for item in request.GET.items()) if request.GET else ''))
else:
raise Http404

IMO changing request.path is more bad(if it can be called bad) compared to accessing GET/POST parameters, so just pass token as GET parameter and login based on token and do not redirect or do request.path modifications
I see token as a added attribute to a valid url, hence a middleware acknowledges that and does something with that token, but url still is handled by the correct view, so middleware to me seems a very logical fit here.

Related

Flask-praetorian, not able to pass auth header to protected endpoint

im using flask-praetorian in order to add security to my app.
I got two routes: one for /login and a protected endpoint called /profile. The Login route works fine, it takes the username and password from the form, im able to pass the information to the guard object and authenticate it to get a new token, but im not been able to pass this token to the request headers for the protected endpoint.
I've tried to use the 'session" to add the header, the 'make_response' method, and the redirect(url_for()), but everytime it gets to the endpoint does it without the correct header causing the error.
Code below:
#user.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if request.method == 'POST':
username = form.username.data
password = form.password.data
user = guard.authenticate(username, password)
token = guard.pack_header_for_user(user)
resp = make_response(profile())
resp.headers['Authorization'] = token['Authorization']
return resp
else:
return render_template('login.html', form=form)
#user.route('/profile')
#auth_required
def profile():
return render_template('profile.html')
(Author of the flask-praetorian package here)
The issue here is that you are just calling the profile() method from your login() method. Calling pack_header_for_user will not actually put the correct auth headers in the current request context. Instead, it just creates a header dict that could be put into a request (but is nicely returned as json). You could jam the token into the current request by tinkering with flask's request context, but it would not be the best way.
The right way to do this is to have your login route include the token in your response payload and then to make another call from your frontend code to the profile endpoint with the token added to the request header.
Really, I think you would be better off going with a different package like flask-security. The flask-praetorian package is intended to be used with pure APIs, and I haven't done any testing or prototyping with standard flask pages.

flask-login reusable cookies

I am using Flask-login with remember=False (the only cookie is the session cookie). When copy-pasting the session cookie after logging out, for some reason the session is still valid and the user is logged in. Even though the logged out session was deleted properly in the flask logout_user() function - meaning that the ["user_id"] was deleted from the session dictionary. It seems like the session is restored from the old cookie. can someone explain?
I do not really have a right answer for this yet, as I am investigating it myself, but there are a couple of points I would like to make here:
the logout_user() from Flask-login does not really seem to be invalidating the session. It just changes the 'session' cookie's value in the client (the browser). While in the backend this session is still alive.
An experiment to prove this would be: (a simple browser plugin like CookieManager can be used to perform this exercise)
login to the app
take a note of the 'session' cookie's value post successful login
now logout
now observer the 'session' cookie's value again. And you would
notice that it has now changed.
Replace this value with the 'session'cookie's value previously noted
in step 1 above.
Try visiting an internal authenticated page again.
Result : You would successfully be able to view an internal page without re-logging in, proving that the logout_user() never really invalidated the session but just changed the 'session' cookie in the client.
Howeverm, I am myself still taking a look into flask-login logout_user() definition and trying to make sense of it.
I had This issue too. After diagnosing what i found is the decorator #login_required *does not invalidate the User in server side after logout*, which is a security threat. This will be a cake walk for a Hacker to hack your application since they can easily extract all the request and header data from developer tool of your browser and can again send request to you server from outside of the application.For ex: If you have used any API in your application the it will be very easy for Hacker to get all the request data and resend a request using POSTMAN.
I solved this issue by creating a separate decorator "#authentication_required" and used in place of "#login_required". then it worked for me,though #login_required is supposed to do the same.
So basically while logging in i generated a random string(token) and sent to database and same string(token) is added to session of flask i.e session["token"]="akhfkjdbfnd334fndf" use any random string generator function.(session object is globally available if u r using flask . u can very well add any field to session). and while logout i again generate a string(token) and update the old token with newly generated token in database. So what #authentication_required will do is it will get the token from session object and the token which is present in database and try to compare the value. if both are same then only #authentication_required will let the client access api.and dont forget to do session.clear() after logout_user().
#---------------------------------------------------------------#
##authentication_required definition
def authentication_required(f):
#wraps(f)
def wrap(*args, **kwargs):
try:
user_id=session['user_id'] #assigning "user_id" from flask session object to a variable "user_id"
user=User_table.find_first(_id=user_id)#couhdb query syntax
#comparing api_token sent to session object at the login time with api_token in sent to database at login time. If both doesn't matches then user is redirected to login page.
if not session["token"]==user.token:
return redirect(url_for('login'))
else:
return f(*args, **kwargs)
except:
app.logger.info(Response('Request Did not came through header !', 401, {'WWW-Authenticate': 'Login failed'}))
return redirect(url_for('login_to system'))
return wrap
#---------------------------------------------------------------#
-------------------------------------------------------
login api code
#app.route('/login_to_system', methods=['GET', 'POST'])
def login_to_system():
form = LoginForm()
user = User_table.find_first(username=form.username.data)
login_user(user, remember=False)
try:
#Generating an random api_token at login time and will send to db
token_string=''.join(random.choices(string.ascii_uppercase + string.digits, k=14))
user.token=token_string #assigning token_string value to field api_token in database.
user.save() #saving the value in user table(using couch Db You can follow syntax as per you DB)
except Exception as error:
app.logger.error(str(error))
app.logger.info("before setting api_token in session")
session["token"]= token_string #adding a "token" to session object
#app.logger.info("Rendering login form")
return render_template('login.html', title='Sign In', form=form)
#-------------------------------------------------------#
#-----------------------------------#
#logout api code
#app.route('/logout')
def logout():
try:
user=User_table.find_first(_id=user_id)
#Generating a random token while logging out and will overwrite eariler token sent at login time send to database.
user.token=token_string=''.join(random.choices(string.ascii_uppercase + string.digits, k=17))
user.save()
except Exception as error:
app.logger.error(str(error))
logout_user()
session.clear()#clearing session
return redirect(url_for('Home page'))
#-----------------------------------#
Note: Seems like login_required is not working fine for me thats why i had to create another decorator but login_required also does the same thing but its strange that it not working for me.

Django will delete session record in DB table, if call login without call anthencate first

In our service, sometime, we need to switch user from one (high privilige) to another without them logging in manually.
After googling, I found a solution, to call login() without authentication first, as the following code shows.
def handle_change_to_anotheruser(self, request):
user = User(id=request.GET['uid'])
if user is not None:
if user.is_active:
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
return JsonResponse({'retcode': 0, 'realname': user.teacher.realname})
Everything works well, user switching succeed, and new sessionid is created for new user. as the following http response shows
And wait, weird thing happend, in later HTTP request, DJANGO deleted the session record in DB table "django_session" . And the code of django.contrib.sessions.middleware.py shows if it cannot find session with ID in HTTP request cookie, it will set session-cookie to blank string, thus force user to logout. As following http capture shows
Why does that happen?
1.Check MIDDLEWARE_CLASSES contains'django.contrib.sessions.middleware.SessionMiddleware'.
2.Make sure 'django.contrib.sessions' is in your INSTALLED_APPS
Then,
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
if not request.session.session_key:
request.session.cycle_key()
return JsonResponse({'retcode': 0,'realname':user.teacher.realname})
This is normal, expected behaviour. If you look at the source of the login() method it does this:
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
request.session.get(HASH_SESSION_KEY) != session_auth_hash):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
i.e., it flushes the session if the user stored in the session has changed - which is exactly what you are doing.
This is a necessary security measure - otherwise it would be possible to inject one user's session into another's!
My suggestion in your case would be to either find a better way to do this (changing a user mid session while retaining all other data seems pretty risky). Alternatively grab the session data you want to retain before logging the user in, log them in, and then restore it. Something like this:
temp_session['somedata'] = request.session.get('somedata')
# So on for whatever else you want to retain
login(request, user)
# Repopulate session
for k in temp_session:
request.session[k] = temp_session[k]

Django process_view middleware resulting in 403 forbidden

I've written a small bit of middleware that catches if a user is using a temporary password and, if so, redirects them to a page that forces them to create a new password. My problem is that the page works fine when the user is logged in and NOT using a temp password (i.e. they go to the change password URL manually), but when they ARE using a temp password the redirect from the middleware yields a 403 Forbidden page.
The middleware does one other thing in process_view after the temp password check, but this is the relevant code:
class MyMiddleware( object ):
def process_view( self, request, view_func, view_args, view_kwargs ):
if request.user.is_authenticated( ):
try:
if request.user.get_profile( ).using_temp:
return HttpResponseRedirect( reverse( 'change_password' ) )
except Object.DoesNotExist:
pass
# Not using temp password, let the request process
return None
Note that rendering the template directly could be used, with something like render_to_response, to fix the problem but that will cause the browser's URL to not follow as well as it not being able to really exit the page it renders.
First, I think your indenting is off in the example, but how about the following as a solution to detect when the current path is the change_password URL? This should get rid of that infinite redirect you have going on.
class MyMiddleware( object ):
def process_view( self, request, view_func, view_args, view_kwargs ):
if request.user.is_authenticated( ):
try:
if request.user.get_profile( ).using_temp and request.path != reverse('change_password'):
return HttpResponseRedirect( reverse( 'change_password' ) )
except Object.DoesNotExist:
pass
# Not using temp password, let the request process
return None
Which version of django are you using ?
If your are using the latest beta, setting the logging may be helpful
http://docs.djangoproject.com/en/dev/topics/logging/
Django Debug Toolbar might be helpful here. It can trap redirects and show you where it's redirecting before actually going there. This helps run down broken redirects.
That said, I'd recommend using a different "change password" page for users with temporary passwords, so it can handle permissions checking differently. The page you have might have a #login_required decorator, and a temporary password might not be considered "really" logged in.

Looking for a comprehensive guide to setting up custom authentication backends in Django, or pointers

I'm trying to set up a custom backend that queries another database, for which I have created a model in the system. It uses its own rules (email instead of username, and a differently salted/hashed password) so I can't use built in authentication. I've set up a custom authentication backend like so:
class BlahBlahBackend:
def check_password():
# check password code here
return true
def authenticate(self, email=None, password=None):
import myapp.models.loginmodel
try:
person = myapp.models.loginmodel.People.objects.get(email=email)
if check_password(password, person.password):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
username=person.first_name + person.last_name
name_count = User.objects.filter(username__startswith = username).count()
if name_count:
username = '%s%s'%(username, name_count + 1)
user = User.objects.create_user(username,email)
else:
user = User.objects.create_user(username,email)
except People.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
I've added BlahBlahBackend as an authentication backend:
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',
'socialauth.auth_backends.OpenIdBackend',
'socialauth.auth_backends.TwitterBackend',
'socialauth.auth_backends.FacebookBackend',
'socialauth.auth_backends.BlahBlahBackend',
)
As you can see, I'm also using some pre-existing auth backends that are also in socialauth.
I have a submission form that points to the following view:
def blahblah_login_complete(request):
email = request.POST.get('email')
password = request.POST.get('password')
user = authenticate(email,password)
# if user is authenticated then login user
if user:
login(request, user)
else:
return HttpResponseRedirect(reverse('socialauth_login_page'))
However, when I try to login in this way, it seems like one or more of the other backends are acting as if I'm trying to log in using their method.
I read that backends are cached and so ran
Session.objects.all().delete()
to clear out the backends cache.
My main questions are:
Does the order in which items are listed in AUTHENTICATION_BACKENDS
How does the system decide/know which Backend to use? This was never made clear by any of the documentation, and I find it a bit confusing.
Is there any way to force the use of a specific authorization based on the request. In other words, if someone submits a form, is there a way to force them to use the form-login-based authentication as opposed to the login via openid or Twitter?
Update:
It works! This is very cool, thanks. I guess it just seemed like the django doc was saying "You don't have to do anything else, it just sort of works like magic" and it turns out this is absolutely the case. So long as the backend is there and the credentials are set up correctly, the authentication will work. As it turns out the real problem was a misconfiguration in the urls.py file that wasn't sending the post from the login form to the correct handler, which is why it kept trying to use another authentication method.
You're supposed to use keyword arguments to django.contrib.auth.authenticate() The names should match the names of the arguments in your backend's authenticate method. The default backend handles the names 'username' & 'password'.
Your backend can use a different name for the keyword arguments e.g.: blahblah_email and blahblah_password, and then call authenticate(blahblah_email=..., blahblah_password=...).
It's clearly described here -
django tries each backend in order
defined, if first fails to
authenticate it goes to second etc.
I believe you can load backend class dynamically and authenticate
directly through it. Look at django authenticate() function sources on how to do that.
I guess django-cas will be a good reference for you :)
And yes, the order of AUTHENTICATION_BACKENDS matters.
Django loops over the backends list and stop at the first backend that has a authenticate method accepting the credential parameters you passed to it.