Django Security issue - django

So I have just completed my site, it is a basic social network. Each user has a profile and can edit their profile. However if a logged in user switches the user id in the edit url they can edit another users profile.
So if I am logged in as user 1 I can edit my profile here:
/user/1/edit
However, if logged in as user 1 and I go to user 2's edit profile url here:
/user/2/edit
I can also edit their profile... How do I stop that??

You should compare request.user.id with id in URL args if they are not same return 403 FORBIDDEN ( HttpResponseForbidden() )
You don't even need to have URL formed like following for edit, it could be just /user/edit as you can always retrieve current logged in user in your view by using request.user. I would recommend you going with the following solution and not consuming user id through URL where scope is limited to current user

Related

Authentication url routing with Flask-Login

I am using flask-login for my project.
For my uri, I am using
/user/<string:user_id>/
when I want to access resources specific to a certain user.
The problem is that, flask-login only provides #login-required authentication using cookies. That is, I can only check if the user is logged in or not. Thus, I have to make a separate function to prevent users access urls with different user_id.
For example, using only #login-required to protect my views allows a user with id user1 to access /user/user2.
How should I make this separate authentication to check if the current user has access to the specific user url?
Flask-Login gives you a current_user global that you can use to check which user is visiting a certain endpoint and retrieve their id. Use it to decide whether a user has access to a certain page.
Your endpoint then becomes
#login_required
#app.route('/user/<string:user_id>')
def user_account(user_id):
if user_id == current_user.id:
user = fetch_user_from_storage(user_id)
return render_template('user_account.html', user=user)
else:
flash('You do not have access to this page.')
# redirect user to another page
With this implementation user #1 won't be able to access user #2's data as current_user.id will only ever match user #1's id.
See the first example in flask-login's docs for more info on current_user.
Have a look at flask-security or flask-principal if you need more advanced access control.

Django Admin use JWT

Using:
Django 1.11
Python 3.6
DRF with JWT in FE
I understand that the Django admin uses a session, and basic authentication.
What I did so far: Replaced the Django Admin authentication signin page with AWS-Cognito:
The user goes to domain/admin/*, redirected to signin in AWS
On successful signin the user is redirected to the redirect_uri, leads to a Django View
In the view I replace the code with tokens
I can't navigate to any Admin page - I am trying to redirect, but that doesn't work since I didn't login() the User
Stuck - I would like to associate the User with the fetched tokens and authenticate with every Admin page request, and when the user logs out delete the tokens
What to do next?
When I use JWT with the Front End application, every request.META has HTTP_AUTHORIZATION, and uses a suitable backend.
I know how to add backends, and potentially leverage the user.backend (I also use Cognito-JWT for other FE portions, so already wrote BE for that)
I need to find a way to replace the Django Admin sessions authentication with the fetched token
Thank you!
EDIT:
If I login() the user, and set it to a model backend that I have already I can navigate to any admin page - but using the session that I created when I logged the user in.
I would like to have the user be set to a new model backend, with authentication that uses a token (from Django backend docs):
class MyBackend:
def authenticate(self, request, token=None):
# Check the token and return a user.
...
How do I make the different Admin pages requests pass the token to the authentication?
Where do I store the token? (I could make a NewUserModel that is 1-1 with the Django User model, and place a token field there)
I am thinking of writing a middleware to capture all requests, and looking into the target URL - if Admin url, add the token to the HTTP_AUTHORIZATION once I fetch the user mentioned in #2 (the user is in every request due to DRF)
EDIT 2
My solution is getting more and more like this stack solution, I would have liked to know if there are any other options, but here is what I did so far:
I made a model that has a 1-1 user field, and a tokens field
As I am fetching/creating the user, I am also saving the tokens on the user's related model from #1 above
I created a middleware that is capturing any request in process_request, and has access to the user. I can see the tokens there as I access the user's related model from #1 above.
I am trying to set the HTTP_AUTHORIZATION header on the request, but cannot do that yet (currently stuck here)
In my backend, I am looking at the incoming request, and trying to fetch the HTTP_AUTHORIZATION - not there yet.
EDIT 3
I ended up just using the Django session as is - once the user authenticates with AWS-Cognito once, it is safe to assume that it is a legitimate User.
Then I just dump the Cognito-JWT, and login() the User.
Note: I am still interested in a solution that would drop the Django session for using the Cognito-JWT, and would love to hear suggestions.

Django csrf fails after logout login new user

Even if I understand the problem, I'm not sure how to solve this. I have a django powered api that has an endpoint that lets the user change the email. If the logged user A enters a already existing email, it checks if the logged user A entered a password that corresponds to the already existing user B object (i.e he owns another, older account). If that is the case, I have to logout the actual user A and login again the already existing B account.
...
if User.objects.filter(email=email).exists():
# If the email already belongs to another account
user = authenticate(username=email, password=password)
if user is not None:
# The user is the owner of the existing account, he has the password
# Get already existing client object
client_existing_user_obj = Client.objects.get(user=user)
# get clients actual cart
actual_cart = client.cart
# update existing clients cart with newly created cart
client_existing_user_obj.cart = actual_cart
# logout user with the old email, login already existing user
logout(request)
login(request, user)
...
The endpoint works correctly, it returns 200. However, the next post & put requests answer 403 - "detail": "CSRF Failed: CSRF token missing or incorrect."
How can I solve this? Any advice will help.
Django rotates the CSRF token when a user logs in. This is a security measure.
You'll have to refresh the token after login (e.g by refreshing the page) before you submit more POST/PUT requests.

Django AllAuth - save session data after login/signup

How do I implement some logic immediately after a user is logged using Django-AllAuth? Before I started implementing AllAuth, my login view contained this extra bit of logic after a user was logged in
...
login(request, user)
# Check if the user has a league in session
if 'league_id' in self.request.session:
# Save the league to this user's user instance
league_id = self.request.session.pop('league_id') # pop removes it from the session
league = League.objects.get(pk = league_id)
league.user = user
league.save()
(The purpose here is that I'm allowing users to create a 'league' instance before logging in, and after they login the league gets associated to their user instance via a league_id stored in session.)
I tried extending the form_valid() method LoginView provided in allauth but it appears as though the form_valid() method never even gets called.
Any ideas how I can handle this?
You could use signals.
There is a signal that is triggered right after an user is logged in: allauth.account.signals.user_logged_in

Django allauth social login: automatically linking social site profiles using the registered email

I aim to create the easiest login experience possible for the users of my Django site. I imagine something like:
Login screen is presented to user
User selects to login with Facebook or Google
User enter password in external site
User can interact with my site as an authenticated user
Ok, this part is easy, just have to install django-allauth and configure it.
But I also want to give the option to use the site with a local user. It would have another step:
Login screen is presented to user
User selects to register
User enter credentials
Site sends a verification email
User clicks in email link and can interact with my site as an authenticated user
Ok, both the default authentication and allauth can do it. But now is the million dollars question.
If they change how they do the login, how do I automatically associate their Google, FB and local accounts?
See that any way they login, I have their email address. Is it possible to do it using django-allauth? I know I can do it with user intervention. Today the default behavior is to refuse the login saying that the email is already registered.
If it isn't possible to do just with configuration, I'll accept the answer that gives me some orientation about which modifications should I make in allauth code to support this workflow.
There are a lot of reasons to do this. The users will forget which method they used to authenticate, and will sometimes use Google, sometimes FB and sometimes the local user account. We already have a lot of local user accounts and social accounts will be a new feature. I want the users to maintain their identity. I envision the possibility to ask for the user friends list, so if they logged using Google, I'd like to also have their FB account.
It is a hobby site, there isn't great security requirements, so please don't answer that this isn't a wise security implementation.
Later, I'd create a custom user model to have just the email as the login id. But I'll be happy with an answer that just let me automatically associate a accounts of the default user model that has a required username.
I'm using Django==1.5.4 and django-allauth==0.13.0
Note (2018-10-23): I'm not using this anymore. Too much magic happening. Instead I enabled SOCIALACCOUNT_EMAIL_REQUIRED and 'facebook': { 'VERIFIED_EMAIL': False, ... }. So allauth will redirect social logins on a social signup form to enter a valid email address. If it's already registered an error shows up to login first and then connect the account. Fair enough for me atm.
I'm trying to improve this kind of use case and came up with the following solution:
from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
"""
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
We're trying to solve different use cases:
- social account already exists, just go on
- social account has no email or email is unknown, just go on
- social account's email exists, link social account to existing user
"""
# Ignore existing social accounts, just do this stuff for new ones
if sociallogin.is_existing:
return
# some social logins don't have an email address, e.g. facebook accounts
# with mobile numbers only, but allauth takes care of this case so just
# ignore it
if 'email' not in sociallogin.account.extra_data:
return
# check if given email address already exists.
# Note: __iexact is used to ignore cases
try:
email = sociallogin.account.extra_data['email'].lower()
email_address = EmailAddress.objects.get(email__iexact=email)
# if it does not, let allauth take care of this new social account
except EmailAddress.DoesNotExist:
return
# if it does, connect this new social login to the existing user
user = email_address.user
sociallogin.connect(request, user)
As far as I can test it, it seems to work well. But inputs and suggestions are very welcome!
You will need to override the sociallogin adapter, specifically, the pre_social_login method, which is called after authentication with the social provider, but before this login is processed by allauth.
In my_adapter.py, do something like this
from django.contrib.auth.models import User
from allauth.account.models import EmailAccount
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class MyAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
# This isn't tested, but should work
try:
user = User.objects.get(email=sociallogin.email)
sociallogin.connect(request, user)
# Create a response object
raise ImmediateHttpResponse(response)
except User.DoesNotExist:
pass
And in your settings, change the social adapter to your adapter
SOCIALACCOUNT_ADAPTER = 'myapp.my_adapter.MyAdapter`
And you should be able to connect multiple social accounts to one user this way.
As per babus comment on this related thread, the proposed answers posted before this one (1, 2) introduce a big security hole, documented in allauth docs:
"It is not clear from the Facebook documentation whether or not the fact that the account is verified implies that the e-mail address is verified as well. For example, verification could also be done by phone or credit card. To be on the safe side, the default is to treat e-mail addresses from Facebook as unverified."
Saying so, I can signup in facebook with your email ID or change my email to yours in facebook and login to the website to get access to your account.
So taking this into consideration, and building on #sspross answer, my approach is to redirect the user to the login page, and notify her/him of the duplicate, and inviting him to log in with her/his other account, and link them once they are logged in. I acknowledge that differs from the original question, but in doing so, no security hole is introduced.
Thus, my adapter looks like:
from django.contrib.auth.models import User
from allauth.account.models import EmailAddress
from allauth.exceptions import ImmediateHttpResponse
from django.shortcuts import redirect
from django.contrib import messages
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class MyAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
"""
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
We're trying to solve different use cases:
- social account already exists, just go on
- social account has no email or email is unknown, just go on
- social account's email exists, link social account to existing user
"""
# Ignore existing social accounts, just do this stuff for new ones
if sociallogin.is_existing:
return
# some social logins don't have an email address, e.g. facebook accounts
# with mobile numbers only, but allauth takes care of this case so just
# ignore it
if 'email' not in sociallogin.account.extra_data:
return
# check if given email address already exists.
# Note: __iexact is used to ignore cases
try:
email = sociallogin.account.extra_data['email'].lower()
email_address = EmailAddress.objects.get(email__iexact=email)
# if it does not, let allauth take care of this new social account
except EmailAddress.DoesNotExist:
return
# if it does, bounce back to the login page
account = User.objects.get(email=email).socialaccount_set.first()
messages.error(request, "A "+account.provider.capitalize()+" account already exists associated to "+email_address.email+". Log in with that instead, and connect your "+sociallogin.account.provider.capitalize()+" account through your profile page to link them together.")
raise ImmediateHttpResponse(redirect('/accounts/login'))
I've just found this comment in the source code:
if account_settings.UNIQUE_EMAIL:
if email_address_exists(email):
# Oops, another user already has this address. We
# cannot simply connect this social account to the
# existing user. Reason is that the email adress may
# not be verified, meaning, the user may be a hacker
# that has added your email address to his account in
# the hope that you fall in his trap. We cannot check
# on 'email_address.verified' either, because
# 'email_address' is not guaranteed to be verified.
so, it is impossible to do by design.
If they change how they do the login, how do I automatically associate their Google, FB and local accounts?
It is possible, but you have to be careful about security issues. Check scenario:
User create account via email and password on your site. User does not have Facebook.
Attacker creates account on Facebook with user email. (Hypothetic scenario, but you do not control if social network verify email).
Attacker login to your site with Facebook and automatically get access to user original account.
But you can fix it. I describe solution to ticket https://github.com/pennersr/django-allauth/issues/1149
Happy scenario should be:
User create account via email and password on your site. User logged out.
User forget about his account and try to login via his Facebook.
System authenticate user via Facebook and find out, he already created account via other method (emails are same). System redirect user to normal login page with message "You already create your account using the email and password. Please log in this way. After you log in, you will be able to use and login using Facebook."
User login via email and password.
System automatically connect his Facebook login with his account. Next time user can use Facebook login or email and password.