flask_dance: Cannot get OAuth token without an associated user - flask

I want to migrate flask_dance with my application to make the user authorize using google and another social networks.
I am getting this error:
Cannot get OAuth token without an associated user
Before i do the connection between the blueprint and sqlalchemy backend, the application worked just fine, if i removed the google_blueprint.backend line the error disappear.
Here is my __init__.py:
import os
from flask import Flask, redirect, url_for, current_app
from flask_login import current_user
from develop.models import (
db,
User,
OAuth
)
from flask_dance.contrib.google import make_google_blueprint
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend
from flask_dance.consumer import oauth_authorized
from sqlalchemy.orm.exc import NoResultFound
def create_app(config_object):
app = Flask(__name__)
app.config.from_object(config_object)
db.init_app(app)
login_manager.init_app(app)
google_blueprint = make_google_blueprint(
client_id=app.config['GOOGLE_CLIENT_ID'],
client_secret=app.config['GOOGLE_CLIENT_SECRET'],
scope=["profile", "email"]
)
app.register_blueprint(google_blueprint, url_prefix='/login')
#oauth_authorized.connect_via(google_blueprint)
def google_logged_in(blueprint, token):
resp = blueprint.session.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
email = account_info_json['email']
query = User.query.filter_by(email=email)
try:
user = query.one()
except NoResultFound:
user = User()
user.image = account_info_json['picture']
user.fullname = account_info_json['name']
user.username = account_info_json['given_name']
user.email = account_info_json['email']
db.session.add(user)
db.session.commit()
login_user(get_user, remember=True)
identity_changed.send(
current_app._get_current_object(),
identity=Identity(get_user.id)
)
#login_manager.user_loader
def load_user(userid):
return User.query.get(userid)
google_blueprint.backend = SQLAlchemyBackend(OAuth, db.session, user=current_user)
return app
Here is also my tables how i organized them in models.py:
class User(db.Model, UserMixin):
id = db.Column(db.Integer(), primary_key=True)
image = db.Column(db.String(), nullable=True)
fullname = db.Column(db.String())
username = db.Column(db.String(), unique=True)
password = db.Column(db.String())
email = db.Column(db.String(), unique=True)
class OAuth(OAuthConsumerMixin, db.Model):
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User)
Please any help would be appreciated :)

TL;DR: You can disable this exception by setting user_required=False on the SQLAlchemyStorage object. However, the exception is being raised for a reason, and if you simply disable it like this, your database may get into an unexpected state where some OAuth tokens are not linked to users. There's a better way to solve this problem. Read on for details.
I am the author of Flask-Dance. This Cannot get OAuth token without an associated user exception is only present in version 0.13.0 and above of Flask-Dance. (CHANGELOG is here.) The pull request introducing this change has some more context for why the change was made.
There are several different ways to use OAuth. Here are some example use cases, all of which Flask-Dance supports:
I want to build a bot that can connect to one specific service, such as a Twitter bot that tweets to a specific account, or a Slack bot that connects to a specific Slack team. I want this bot to respond to HTTP requests, so it has to run as a website, even though I don't expect people to actually use this website directly.
I want to build a website where users can log in. Users need to create an account on my website using a username and password. After they have created an account, users may decide to link their account to other OAuth providers, like Google or Facebook, to unlock additional functionality.
I want to build a website where users can log in. Users should be able to create their account simply by logging in with GitHub (or any other OAuth provider). Users should not need to create a new password for my website.
Use case 1 is the simplest: do not pass a user or user_id argument to your SQLAlchemyStorage, and it will assume that your application does not use multiple user accounts. This means that your website can only link to one particular account on the remote service: only one Twitter account, only one Slack team, etc.
Use case 2 is also pretty simple: pass a user or user_id argument to your SQLAlchemyStorage. Flask-Dance will save the OAuth token into your database automatically, and link it to the user that is currently logged in.
Use case 3 is more complex, since it involves automatically creating both the OAuth token and the local user account at the same time. Different applications have different requirements for creating user accounts, and there's no way for Flask-Dance to know what those requirements are. As a result, Flask-Dance cannot handle this use case automatically. You must hook into the oauth_authorized signal, create the user account and associate it with the OAuth token manually, and return False to tell Flask-Dance to not attempt to handle the OAuth token automatically.
Before version 0.13.0, it was possible to accidentally create OAuth tokens in the database that were not linked with any users at all. In use case 3, the OAuth token is created before a local user account exists for that user, so Flask-Dance would save the OAuth token to the database without any linked local user account. You could use the oauth_authorized handler to associate the OAuth token with a local user account afterwards, but if your code is buggy and raises an exception, then the OAuth token could remain in your database, forever unlinked to any users.
Starting in version 0.13.0, Flask-Dance detects this problem and raises an exception, instead of saving an OAuth token to your database without an associated local user account. There are two ways to resolve this problem:
Rewrite your code to manually create the user account and associate it with the OAuth token. The documentation contains some example code you can use for this.
Disable this check, and tell Flask-Dance that it's OK to create OAuth tokens without associated users. You can disable this check by setting user_required=False on the SQLAlchemyStorage object.
I believe that option 1 is the better solution by far, but it requires more understanding of what Flask-Dance is actually doing behind the scenes. I've written some documentation that describes how to handle multi-user setups, which discusses this problem as well.

Related

how to use keycloak in django rest framework with custom authentication

A better title for this question would be: how to use keycloak in django grpc framework.
I want to handle user registration with keycloak. this is the my service without keycloak:
class UserService(Service):
def Create(self, request, context):
serializer = UserProtoSerializer(message=request)
serializer.is_valid(raise_exception=True)
serializer.save()
# some code for sending activation email
return serializer.message
and I'm using a custom user.
I have created a realm and client in keycloak admin console. what is the best way of relating these two? shall I try using drf-keycloak-auth or python-keycloak?
with a custom authentication it's better to use python-keycloak . you can create a user object both in keycloak and your database and save different information about user in them.
the documentation is almost clear if you have followed the steps in keycloak documentation. the thing I couldn't quite understand was input arguments for keycloak_admin.create_user to create a new user in a realm named 'test'. this the confusing code in documentation:
keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/",
username='example-admin',
password='secret',
realm_name="master",
user_realm_name="only_if_other_realm_than_master",
client_secret_key="client-secret",
verify=True)
now the thing you should know before jumping from keycloak documentations to python-keycloak documentations is that you should give one user in your new realm administration role. check this link.
keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/",
username='test#user.com',
password='123',
realm_name="test",
verify=True)
to generate client secret check: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/clients/oidc/confidential.html

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 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.

REST login with Django social_auth

I've been asked to provide a "Login with Facebook" functionality to an iOS app I am creating.
The app connects to a REST api created with Piston, the web application is created with Django and uses social_auth. The application also has a Facebook login.
My thought was to create a service 'FBLogin' providing just the Facebook profile UID (separate FB login procedure on iPhone to get the ID). Using the SocialAuth models I can query the DB with uid and provider to fetch the user... but how can i use the authentication mechanism to get this user instance authenticated?
Any ideas on getting this right?
This just doesn't feel good ... getting the user instance authenticated is a pain...
The username password authentication is already implemented ... without a problem.
Btw, don't have django experience ... do have a lot of other development experience so understanding python and django isn't that hard :)
Tx
Y
It doesn't really seem to be documented anywhere, but you can do this in your REST handler:
from social_auth.backends.pipeline.social import associate_user
from social_auth.backends.facebook import FacebookBackend
from social_auth.models import UserSocialAuth
myextra_data = {
'access_token' : 'jfkdlfsdgeyejfghfdsjdfpoweipuo',
'id' : 123456789,
}
usa, created = UserSocialAuth.objects.get_or_create(provider = 'facebook',
uid=123456789)
usa.user = user
usa.extra_data = myextra_data
usa.save()
if created:
associate_user(backend=FacebookBackend, user=user, uid=usa.uid)
These get pretty vendor-specific in terms of how data gets formatted in extra_data so YMMV

How can I detect multiple logins into a Django web application from different locations?

I want to only allow one authenticated session at a time for an individual login in my Django application. So if a user is logged into the webpage on a given IP address, and those same user credentials are used to login from a different IP address I want to do something (either logout the first user or deny access to the second user.)
Not sure if this is still needed but thought I would share my solution:
1) Install django-tracking (thankyou for that tip Van Gale Google Maps + GeoIP is amazing!)
2) Add this middleware:
from django.contrib.sessions.models import Session
from tracking.models import Visitor
from datetime import datetime
class UserRestrictMiddleware(object):
"""
Prevents more than one user logging in at once from two different IPs
"""
def process_request(self, request):
ip_address = request.META.get('REMOTE_ADDR','')
try:
last_login = request.user.last_login
except:
last_login = 0
if unicode(last_login)==unicode(datetime.now())[:19]:
previous_visitors = Visitor.objects.filter(user=request.user).exclude(ip_address=ip_address)
for visitor in previous_visitors:
Session.objects.filter(session_key=visitor.session_key).delete()
visitor.user = None
visitor.save()
3) Make sure it goes after the VisitorTrackingMiddleware and you should find previous logins are automatically bumped when someone new logs in :)
If you're already using django-tracking as suggested here, there's a much easier way to implement this:
Define a signal handler:
# myapp/signals.py
def kick_my_other_sessions(sender, request=None, user=None, **kwargs):
from tracking.models import Visitor
from django.contrib.sessions.models import Session
keys = [v.session_key for v in Visitor.objects.filter(user=request.user).exclude(session_key=request.session.session_key)]
Session.objects.filter(session_key__in=keys).delete()
Create a listener for the user_logged_in signal:
# myapp/__init__.py
from myapp.signals import kick_my_other_sessions
from django.contrib.auth.signals import user_logged_in
user_logged_in.connect(kick_my_other_sessions, sender=User)
This will institute a sort of "last user to login wins" system. If you want to allow multiple logins by the same user from the same ip, you can add an .exclude() to the Visitors lookup.
Django's middleware will probably help you achieve this. The issue is that you will probably want to allow multiple anonymous sessions from the same IP address, even authenticated sessions for different users, but not authenticated sessions for the same user.
You'll want to:
Create a user profile model to store the IP address of a user's last login. See Django's Storing additional information about users documentation.
Implement a custom authentication backend. This backend, when triggered and successfully authenticating a user (just call super) would wipe out the user's last login IP in the profile model.
Implement a subclass of Django's django.contrib.sessions.SessionMiddleware class. Implement process_request. If the request.user object's profile model has no IP address, set it and allow the request. If it has an IP, and the IP is different from the current request's IP (request.META.REMOTE_ADDR), then do whatever you like to either log out the other user, or return an error to the requestor.
Update your settings.py file so that your custom auth backend is processed first, and so that your custom session middleware is also processed first. This involves updating settings.AUTHENTICATION_BACKENDS and settings.MIDDLEWARE_CLASSES.
You'll need to do this with custom middleware.
In your middleware process_request() method you will have access to the request object so you can do something like the following:
session_key = request.session.session_key
ip_address = request.META.get('REMOTE_ADDR', '')
Now you know the IP address, so check a model you create that (roughly) would look like this:
class SessionIPS(models.Model):
session = models.ForeignKey(Session)
IP = models.CharField(max_length=20)
So when a session is created or deleted you will modifiy your session ip's table accordingly, and when a request comes in make sure the IP address isn't being used for another session. If if is, then return a Http404 (or something like it) from the middleware.
A pluggable app that can show you a lot more detail (and even includes IP address in its own model) is django-tracking.