I am leveraging django-allauth to provide Google authentication for my property management application. Here is the registration workflow I'm looking for:
A new manager goes to a registration page with a "Signup With Google" button.
They click the button and sign into Google.
On the call back from Google they are presented with a form for additional info.
When they submit this form their user account and manager profile is created and they are redirected to their company's homepage.
I have attempted to handle this "redirect to company's homepage" through adapters. The problem I am having is that the "get_login_redirect_url" function is executed after the initial Google sign-in, but before the signup form where I collect their work schedule, so I get a DoesNotExist on the adapter redirect because it was called before the managerprofile was created.
What is the proper way to do these redirects?
settings.py
ACCOUNT_SIGNUP_FORM_CLASS = 'managers.signup.ManagerProfileSignupForm'
SOCIALACCOUNT_AUTO_SIGNUP = False
SOCIALACCOUNT_ADAPTER = 'managers.adapter.ManagerSocialAccountAdapter'
ACCOUNT_ADAPTER = 'managers.adapter.ManagerAccountAdapter'
adapters.py
class ManagerSocialAccountAdapter(DefaultSocialAccountAdapter):
def get_connect_redirect_url(self, request, socialaccount):
return reverse('company_details', args=(request.user.managerprofile.company.pk,))
class ManagerAccountAdapter(DefaultAccountAdapter):
def get_login_redirect_url(self, request):
return reverse('company_details', args=(request.user.managerprofile.company.pk,))
Set the LOGIN_REDIRECT_URL on the settings.py of your application:
I have this value, to redirect to the home page:
LOGIN_REDIRECT_URL = '/'
you need to define a decorator which consists of a function which runs before the account is created. Here take a look
#imports necessary for decorator call
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.signals import pre_social_login
from allauth.account.utils import perform_login
from django.dispatch import receiver
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.models import SocialLogin
# defining class to run through authentication
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
pass
# reciever defining function to hold the account before making it registered
#receiver(pre_social_login)
def link_to_local_user(sender, request, sociallogin, **kwargs):
socialemail = sociallogin.user.email
socialuname = socialemail.split('#')[0]
sociallogin.user.username = socialuname+str(sociallogin.user.pk)
if User.objects.filter(email=sociallogin.user.email).exists():
user = User.objects.get(email=sociallogin.user.email)
if user:
perform_login(request, user, email_verification='optional')
raise ImmediateHttpResponse(redirect('homePage'))
else:
SocialLogin.save(sociallogin, request, connect=False)
user = User.objects.get(email=sociallogin.user.email)
perform_login(request, user, email_verification='optional')
raise ImmediateHttpResponse(redirect('homePage'))
This is assuming you have created a signal over User model instance creation which directly creates a profile model instance also mapping the user. If not, below the model for ManagerProfile, use this:
def create_profile(sender, instance, created, **kwargs):
if created:
<ManagerProfileModel>.objects.create(userID=instance)
post_save.connect(create_profile, sender=<UserModelWhereMainAccountIsCreated>)
Related
My Django app has an option for login/register using CustomForm (inherited from the UserCreationForm) as well as Outh. Now the problem is if a user has already signed up using the CustomForm and if the next time he tries to log in using google Oauth then instead of logging in, google Oauth is redirecting to some other signup form (not created by me) which looks like:
But as the user is already registered, if he enters the same username/email here then it displays says username taken. So how can I resolve this issue? I mean I want the user to be able to login using Oauth even if he has signed up using the CustomForm, how can I implement that? Or even if I ask the user to fill this form to be able to use OAuth login, the problem is that his email/username are already present in the db, so he won't be able to sign up using this form.
Edit:
If that's difficult to implement then instead how can I just show a message when the user tries to login using oauth after signing up with the CustomForm, something like "You signed up using username rather than google account", rather than taking him to the socialaccount signup form?
My register function in views.py:
def register(request):
if request.method == 'POST':
form = CustomForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('login')
else:
return redirect('register')
else:
return render(request, 'accounts/register.html')
forms.py looks something like this:
class CustomForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ("username", "email")
You can use pre_social_login signal
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.signals import pre_social_login
from allauth.account.utils import perform_login
from allauth.utils import get_user_model
from django.dispatch import receiver
from django.shortcuts import redirect
from django.conf import settings
#receiver(pre_social_login)
def link_to_local_user(sender, request, sociallogin, **kwargs):
email_address = sociallogin.account.extra_data['email']
User = get_user_model()
users = User.objects.filter(email=email_address)
if users:
perform_login(request, users[0], email_verification=settings.EMAIL_VERIFICATION)
raise ImmediateHttpResponse(redirect(settings.LOGIN_REDIRECT_URL))
See https://github.com/pennersr/django-allauth/issues/215
I have seen plenty of solutions online, however all of them addressed more complex apps which allow external users to create accounts. In my case the only user will be the admin. How do I secure the /admin routes created by Flask-Admin in an efficient way?
You can use Flask-Login for that. I usually add a route to the AdminIndexView class that handles the login if the user isn't logged in, yet. Otherwise the default admin page will be shown.
from flask import Flask
from flask_login import LoginManager
from flask_admin import Admin
app = Flask(__name__)
login_manager = LoginManager(app)
login_manager.session_protection = 'strong'
login_manager.login_view = 'admin.login'
admin = Admin(app, index_view=MyIndexView())
The definition of MyAdminView can look like this:
from flask_admin import AdminIndexView, expose, helpers
class FlaskyAdminIndexView(AdminIndexView):
#expose('/')
def index(self):
if not login.current_user.is_authenticated:
return redirect(url_for('.login'))
return super(MyAdminIndexView, self).index()
#expose('/login', methods=['GET', 'POST'])
def login(self):
form = LoginForm(request.form)
if helpers.validate_form_on_submit(form):
user = form.get_user()
if user is not None and user.verify_password(form.password.data):
login.login_user(user)
else:
flash('Invalid username or password.')
if login.current_user.is_authenticated:
return redirect(url_for('.index'))
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/logout')
#login_required
def logout(self):
login.logout_user()
return redirect(url_for('.login'))
This integrates Flask-Login unobtrusively in the Flask-Admin interface. You will still need to implement the user and password verification like described in the Flask-Login documentation.
EDIT
To prevent unauthorized access to your admin routes create a ModelView class for each view and add a function is_accessible() with the following code:
def is_accessible(self):
if (not login.current_user.is_active or not
login.current_user.is_authenticated):
return False
return True
A default behavior of django-allauth is redirect to Singup form when the email retrieved from a social profile matches already existing user's emailid.
Instead, I would like to redirect the user back to the login page with the following message in case of the matching emailid on social login:
An account already exists with this "EMAIL#ADDRESS.COM" e-mail address. Please sign in to that account first, then connect your "PROVIDER" account. You can sign in using "LIST OF LINKED TO THAT EMAIL PROVIDERS".
Has anybody made something similar? Any help is appreciated.
To redirect user before sign-up you need to override allauth.socialaccount.views.SignupView. Simply alter you main urls.py settings:
# urls.py
from users.views import CustomSignupView
urlpatterns = [
# ...
# Your custom SignupView:
url(r'^accounts/social/signup/$', CustomSignupView.as_view(), name='socialaccount_signup'),
url(r'^accounts/', include('allauth.urls')),
# ...
]
CustomSignupView may look like this:
# your_app.views.py
from allauth.socialaccount.views import SignupView
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import ugettext as _
class CustomSignupView(SignupView):
http_method_names = ['get']
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
social_login_email: str = self.sociallogin.user.email
provider: str = self.sociallogin.account.provider
messages.warning(self.request, _(f"An account already exists with this "
f"\"{social_login_email}\" e-mail address. "
f"Please sign in to that account first, "
f"then connect your \"{provider.capitalize()}\" account."))
return redirect(reverse("account_login"), permanent=False)
This custom view simply forbids all HTTP methods except the GET method (which redirects the user).
This solution works only when SOCIALACCOUNT_AUTO_SIGNUP is set to True (True is set by default, see https://django-allauth.readthedocs.io/en/latest/configuration.html). With this setting, the sign-up form is displayed only when there is e-mail address conflict -> then redirect the user when SignupView is loaded.
Some background: I had a similar problem as mentioned by #horbor in the comment. After the user enters another email (if an email from an external provider is already in use), I get IntegrityError on SignupView. I still do not know if there is a problem with allauth or with my application. However, bypassing signup is the best solution for me.
I have a Tastypie ModelResource defining various endpoints which work as expected.
I've now configured this ModelResource to have BasicAuthentication:
class Meta:
authentication = BasicAuthentication()
I've defined a couple of test users through the Django Admin Interface.
As per the Django 1.7 Documentation, I've created a signals.py in which I register a couple of test signals:
from django.core.signals import request_finished
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
#receiver(user_logged_in)
def on_login(sender, request, user, **kwargs):
print('******* LOGIN DETECTED *********')
#receiver(request_finished)
def on_request_finished(sender, **kwargs):
print('******* REQUEST FINISHED *******')
This is loaded successfully by my AppConfig in apps.py:
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myapp'
verbose_name = 'verbose description of myapp'
def ready(self):
import myapp.signals
I use the Requests library to successfully communicate with my API, providing basic authentication credentials for one of my test users:
auth = HTTPBasicAuth(username, getpass('Enter password: '))
response = requests.get(self.url(endpoint), auth=self.auth, params = params)
The REQUEST FINISHED print shows in the Django server's output, but LOGIN DETECTED does not.
Do we have to manually fire a login signal when using Tastypie, or use some other inbuilt/custom Authentication class besides BasicAuthentication? In other words, is it expected that the user_logged_in signal wouldn't fire automatically?
Any info would be greatly appreciated.
Having inspected the Tastypie source code, it ties into the Django auth backend by calling the authenticate method, thus doesn't trigger the usual login cycle of which authenticate is one component. Consequently the login method is never called, and thus the user_logged_in signal never fires.
I ended up providing the signal myself by extending BasicAuthentication and overriding is_authenticated like so:
class MyBasicAuthentication(BasicAuthentication):
def is_authenticated(self, request, **kwargs):
orig_user = request.user
has_authenticated = super(MyBasicAuthentication, self).is_authenticated(request, **kwargs)
if has_authenticated:
was_authenticated = orig_user == request.user
if not was_authenticated:
user_logged_in.send(sender=self.__class__, request=request, user=request.user)
return has_authenticated
I am developing an Django application using django auth module and would like to prevent multiple login using the same user name and password.
It should prevent multiple logins on different machines using the same user name and password. How do I achieve this in Django?
We have to keep following things in mind:
If user close the browser without logging out
If the session times out
You may try this, it logs out the first user and logs in the second. Add middleware.py in your app directory (same level as models, views etc) and add this code. Useful when the same person is using more than one device. Make sure you add this to your middleware classes: 'myapp.middleware.UserRestrict',
class UserRestrict(object):
def process_request(self, request):
"""
Checks if different session exists for user and deletes it.
"""
if request.user.is_authenticated():
cache = get_cache('default')
cache_timeout = 86400
cache_key = "user_pk_%s_restrict" % request.user.pk
cache_value = cache.get(cache_key)
if cache_value is not None:
if request.session.session_key != cache_value:
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_key=cache_value)
session.delete()
cache.set(cache_key, request.session.session_key,
cache_timeout)
else:
cache.set(cache_key, request.session.session_key, cache_timeout)
Out of the box, Django doesn't provide you with a way to prevent concurrent sessions for the same user account, and that isn't a trivial thing to do. However, here's another question with some suggestions about how you might make this happen: How can I detect multiple logins into a Django web application from different locations?
i solve the problem with a new model, a custom decorator and custom login page
1) i created a additional model for users eg:
class SessionKey(models.Model):
user = models.OneToOneField(User,primary_key=True)
key = models.CharField(max_length=255)
2) i created custom decorator to check session key is equal or not last key.
i changed the original source code django decorators
from functools import wraps
from django.conf import settings
from django.utils.decorators import available_attrs
from django.contrib.auth.decorators import login_required
from django.shortcuts import resolve_url
from users.models import SessionKey #my additional model
def unique_login_required(view_func):
#wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
r = False
...
#check session key is equal to last one
...
if r:
return view_func(request, *args, **kwargs)
else:
from django.contrib.auth.views import redirect_to_login
path = request.build_absolute_uri()
resolved_login_url = resolve_url(settings.LOGIN_URL)
return redirect_to_login(path,resolved_login_url)
return _wrapped_view
3) in custom login page, i updated the session key. last login always updates the stored session key.
finally, in the views, i call my decorator
from users.decorators import unique_login_required
#unique_login_required
def index(request):
...