I am trying to implement a Twitter sign in using Django Social Auth. I have added a partial pipeline where I gather extra details from the user using a form (DOB, email, etc).
My problem is that I want to skip this pipeline if the user already exists. However, when I try to do this I get an AuthTokenError "Token error: Missing unauthorized token" and I cannot figure out why.
Here is code that is causing a problem:
def gather_extra_data(backend, details, uid, request, user=None, *args, **kwargs):
social_user = UserSocialAuth.get_social_auth(backend.name, uid)
if social_user:
return redirect('socialauth_complete', backend.name)
if not details.get('email'):
if not request.session.get('saved_email'):
return redirect(request_extra, backend=backend.name)
else:
details['email'] = request.session['saved_email']
details['password'] = request.session['password']
details['first_name'] = request.session['first_name']
details['last_name'] = request.session['last_name']
details['dob'] = request.session['dob']
details['gender'] = request.session['gender']
details['avatar_url'] = request.session['avatar_url']
You should put your pipeline entry after the user is created and the social account is associated (after social_auth.backends.pipeline.social.associate_user entry), then you can try with this code:
def gather_extra_data(social_auth, user, details, request, is_new=False, *args, **kwargs):
if is_new:
if request.session.get('saved_email') is None:
return redirect(request_extra, backend=backend.name)
else:
details['email'] = request.session['saved_email']
details['password'] = request.session['password']
details['first_name'] = request.session['first_name']
details['last_name'] = request.session['last_name']
details['dob'] = request.session['dob']
details['gender'] = request.session['gender']
details['avatar_url'] = request.session['avatar_url']
Remember to put social_auth.backends.pipeline.misc.save_status_to_session before your entry.
Try this way:
def gather_extra_data(backend, details, uid, request, user=None, is_new=False, *args, **kwargs):
If not user is None and is_new: #is_new is your missing argument.
if not details.get('email'):
if not request.session.get('saved_email'):
return redirect(request_extra, backend=backend.name)
else:
details['email'] = request.session['saved_email']
details['password'] = request.session['password']
details['first_name'] = request.session['first_name']
details['last_name'] = request.session['last_name']
details['dob'] = request.session['dob']
details['gender'] = request.session['gender']
details['avatar_url'] = request.session['avatar_url']
Related
I'm new to coding and I feel really stuck. I decided to unit test my micro Flask app. I use "Google sign in" to authenticate my users, but I'm not able to get through the Google authentication and assert what is really on the page. I have no idea if I need to mock, play with the session or application context.
Here is the code for authentication process:
def login_required(f):
#wraps(f)
def decorated_function(*args, **kwargs):
user = dict(session).get('profile', None)
if user is not None:
email = session['profile']['email']
else:
email = None
if user:
return f(*args, **kwargs)
return render_template('login.html', email=email)
return decorated_function
#app.route('/login/')
def login():
google = oauth.create_client('google') # create the google oauth client
redirect_uri = url_for('authorize', _external=True)
return google.authorize_redirect(redirect_uri)
#app.route('/authorize')
def authorize():
google = oauth.create_client('google') # create the google oauth client
token = google.authorize_access_token() # Access token from google (needed to get user info)
resp = google.get('userinfo') # userinfo specificed in the scrope
user_info = resp.json()
# Loading and cleaning all user emails
auth_users = db.session.query(users.email).all()
auth_users = [str(i).strip("(),'") for i in auth_users]
# Checking if user is in the list
if user_info['email'] in auth_users:
flash("Boli ste úspešne prihlásený.", category="success")
else:
flash("Nemáte práva na prístup.", category="primary")
return redirect('/')
session['profile'] = user_info
# make the session pernament so it keeps existing after broweser gets closed
session.permanent = True
return redirect('/')
The test structure look like this:
class Uvo_page(unittest.TestCase):
def setUp(self):
self.client = app.test_client(self)
self.user_info = {'email': 'xxx'}
self.auth_users = ['xxx', 'yyy']
# UVO page shows results
def test_uvo_page_show_results(self):
response = self.client.get('/show/1',
content_type='html/text',
follow_redirects=True)
test_string = bytes('Sledované stránky', 'utf-8')
self.assertTrue(test_string in response.data)
I thought that by defining the "user_info" it will be used for authentication evaluation. Instead of it it does not continue to "/authorize" and authentication, but stays on the login page.
I created the userdefined decorator to check the session is active or not. Following is the function defination
def session_required(func):
"""
Decorator to check the session is active or not for logged in user
:param func: Name of function for which you have to check the session is active or not
:return:
"""
def wrap(request, *args, **kwargs):
"""
Wrapper function for the decorator
:param request: request parameter for called URL
:return:
"""
if not request.session.get("admin_id"):
return redirect("/")
func_return = func(request, *args, **kwargs)
return func_return
return wrap
I am using this decorator on the respective function based view. At some places it works absolutely fine but when I do some POST or PUT operation then it gives me error
Forbidden (CSRF token missing or incorrect.):
My function based view is like
#csrf_exempt
#session_required
def mover_profile_handler(request):
"""
mover profile handler function for viewing and editing the details
:param request:
:return:
"""
try:
if request.method == "GET":
login_id = request.session.get("admin_id")
login_info_obj = Login.objects.get(id=login_id)
mover_info_obj = Mover.objects.get(fk_login=login_info_obj)
country_obj = Country.objects.all()
currency_obj = CurrencyType.objects.all()
subscription_detail = SubscriptionMoverDetail.objects.filter(fk_mover=mover_info_obj).order_by("-id")
# Extracting data for showing the subscription package details
current_subscription_detail = {}
subscription_detail_history = []
for index, item in enumerate(subscription_detail):
subscription_master_detail = SubscriptionMaster.objects.get(id=item.fk_subscription_master_id)
subscription_detail_json = {
"plan_name": subscription_master_detail.subscription_plan_name,
"subscription_start_date": item.subscription_date,
"subscription_end_date": item.subscription_end_date,
"amount_paid": item.amount_paid,
"users": subscription_master_detail.customer_permitted_count
}
if index == 0:
current_subscription_detail = subscription_detail_json
else:
subscription_detail_history.append(subscription_detail_json)
return render(request, "mover_profile.html", {
"mover_info_obj": mover_info_obj,
"country_obj": country_obj,
"currency_obj": currency_obj,
"login_info_obj": login_info_obj,
"current_subscription_detail": current_subscription_detail,
"subscription_detail_history": subscription_detail_history
})
elif request.method == "PUT":
request = convert_method_put_to_post(request)
mover_id = request.POST.get("id")
if Mover.objects.filter(id=mover_id).exists():
mover_info_obj = Mover.objects.get(id=mover_id)
mover_info_obj.mover_name = request.POST.get("name")
mover_info_obj.address = request.POST.get("address")
mover_info_obj.phone_no = request.POST.get("phone")
mover_info_obj.mover_size = request.POST.get("size")
mover_info_obj.reg_no = request.POST.get("reg_no")
mover_info_obj.website = request.POST.get("website")
mover_info_obj.fk_country_id = request.POST.get("country")
mover_info_obj.fk_currency_id = request.POST.get("currency")
operational_countries = request.POST.getlist("operational_countries[]")
mover_info_obj.countries_in_operation.set(operational_countries)
mover_info_obj.save()
return HttpResponse("success")
except Exception as e:
error_save(str(traceback.format_exc()))
return redirect('error_handler_500')
I tried with
#csrf_protect #csrf_exempt
in view and also tried {% csrf_token %} in html file
without using #session_required code is working absolutely fine.
So please tell me what is wrong with this stuff!!
I want to send a login link to the users.
I know there are some OneTimePassword apps out there with thousands of features. But I just want some easy and barebon way to login user via login link.
My question is if this is a correct way to go about this. Like best practice and DRY code.
So I've set up a table that stores three rows.
1. 'user' The user
2. 'autogeneratedkey' A autogenerated key
3. 'created_at' A Timestamp
When they login, the'll be sent a mail containing a login link valid for nn minutes.
So the login would be something like
https://example.net/login/?username=USERNAME&autogeneratedkey=KEY
The tricky part for me is to figure out a good way to check this and log in the user.
I'm just guessing here. But would this be a good approach?
class login(generic.CreateView):
def get(self, request, *args, **kwargs):
try:
autgeneratedkey = self.request.GET.get('autgeneratedkey', '')
username = self.request.GET.get('username', '')
obj_key = Login.objects.filter(autgeneratedkey=autgeneratedkey)[0]
obj_user = Login.objects.filter(userusername=username)[0]
try:
if obj_user == obj_key: #Compare the objects if same
if datetime.datetime.now() < (obj_key.created_at + datetime.timedelta(minutes=10)): #Check so the key is not older than 10min
u = CustomUser.objects.get(pk=obj_user.user_id)
login(request, u)
Login.objects.filter(autgeneratedkey=autgeneratedkey).delete()
else:
return login_fail
else:
return login_fail
except:
return login_fail
return redirect('index')
def login_fail(self, request, *args, **kwargs):
return render(request, 'login/invalid_login.html')
It feels sloppy to call the same post using first the autogeneratedkey then using the username. Also stacking if-else feels tacky.
I would not send the username in the get request. Just send an autogenerated key.
http://example.com/login?key=random-long-string
Then this db schema (it's a new table because I don't know if Login is already being used.
LoginKey ( id [PK], user [FK(CustomUser)], key [Unique], expiry )
When a user provides an email, you create a new LoginKey.
Then do something like this:
def get(self, request, *args, **kwargs):
key = request.GET.get('key', '')
if not key:
return login_fail
login_key = LoginKey.objects.get(key=key)
if login_key is None or datetime.datetime.now() > login_key.expiry:
return login_fail
u = login_key.user
login(request, u)
login_key.delete()
return redirect('index')
Probably you can optimize the code like this:
First assuming you have relationship between User and Login Model like this:
class Login(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
Then you can use a view like this:
class LoginView(generic.View):
def get(self, request, *args, **kwargs):
try:
autgeneratedkey = self.request.GET.get('autgeneratedkey', '')
username = self.request.GET.get('username', '')
user = CustomUser.objects.get(login__autgeneratedkey=autgeneratedkey, username=username, login__created_at__gte=datetime.now()-datetime.timedelta(minutes=10))
login(request, user)
user.login_set.all().delete() # delete all login objects
except CustomUser.DoesNotExist:
return login_fail
return redirect('index')
Just another thing, it is not a good practice to use GET method where the database is updated. GET methods should be idempotent. Its better to use a post method here. Just allow user to click the link(which will be handled by a different template view), then from that template, use ajax to make a POST request to this view.
This question is directly related to this question, but that one is now outdated it seems.
I am trying to test a view without having to access the database. To do that I need to Mock a RelatedManager on the user.
I am using pytest and pytest-mock.
models.py
# truncated for brevity, taken from django-rest-knox
class AuthToken(models.Model):
user = models.ForeignKey(
User,
null=False,
blank=False,
related_name='auth_token_set',
on_delete=models.CASCADE
)
views.py
class ChangeEmail(APIView):
permission_classes = [permissions.IsAdmin]
serializer_class = serializers.ChangeEmail
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = request.user
user.email = request.validated_data['email']
user.save()
# Logout user from all devices
user.auth_token_set.all().delete() # <--- How do I mock this?
return Response(status=status.HTTP_200_OK)
test_views.py
def test_valid(mocker, user_factory):
user = user_factory.build()
user.id = 1
data = {
'email': 'foo#example.com'
}
factory = APIRequestFactory()
request = factory.post('/', data=data)
force_authenticate(request, user)
mocker.patch.object(user, "save")
related_manager = mocker.patch(
'django.db.models.fields.related.ReverseManyToOneDescriptor.__set__',
return_vaue=mocker.MagicMock()
)
related_manager.all = mocker.MagicMock()
related_manager.all.delete = mocker.MagicMock()
response = ChangeEmail.as_view()(request)
assert response.status_code == status.HTTP_200_OK
Drawing from the answer in the linked question I tried to patch the ReverseManyToOneDescriptor. However, it does not appear to actually get mocked because the test is still trying to connect to the database when it tries to delete the user's auth_token_set.
You'll need to mock the return value of the create_reverse_many_to_one_manager factory function. Example:
def test_valid(mocker):
mgr = mocker.MagicMock()
mocker.patch(
'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
return_value=mgr
)
user = user_factory.build()
user.id = 1
...
mgr.assert_called()
Beware that the above example will mock the rev manager for all models. If you need a more fine-grained approach (e.g. patch User.auth_token's rev manager only, leave the rest unpatched), provide a custom factory impl, e.g.
def test_valid(mocker):
mgr = mocker.MagicMock()
factory_orig = related_descriptors.create_reverse_many_to_one_manager
def my_factory(superclass, rel):
if rel.model == User and rel.name == 'auth_token_set':
return mgr
else:
return factory_orig(superclass, rel)
mocker.patch(
'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
my_factory
)
user = user_factory.build()
user.id = 1
...
mgr.assert_called()
I accomplish this doing this(Django 1.11.5)
#patch("django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager")
def test_reverse_mock_count(self, reverse_mock):
instance = mommy.make(DjangoModel)
manager_mock = MagicMock
count_mock = MagicMock()
manager_mock.count = count_mock()
reverse_mock.return_value = manager_mock
instance.related_manager.count()
self.assertTrue(count_mock.called)
hope this help!
If you use django's APITestCase, this becomes relatively simple.
class TestChangeEmail(APITestCase):
def test_valid(self):
user = UserFactory()
auth_token = AuthToken.objects.create(user=user)
response = self.client.post(
reverse('your endpoint'),
data={'email': 'foo#example.com'}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(AuthToken.objects.filter(user=user).exists())
This avoids mocking altogether and gives a more accurate representation of your logic.
unittest.PropertyMock can be used to mock descriptors in a way that doesn't require mocking internal implementation details:
def test_valid(mocker, user_factory):
user = user_factory.build()
user.id = 1
data = {
'email': 'foo#example.com'
}
factory = APIRequestFactory()
request = factory.post('/', data=data)
force_authenticate(request, user)
mocker.patch.object(user, "save")
with mocker.patch('app.views.User.auth_token_set', new_callable=PropertyMock) as mock_auth_token_set:
mock_delete = mocker.MagicMock()
mock_auth_token_set.return_value.all.return_value.delete = mock_delete
response = ChangeEmail.as_view()(request)
assert response.status_code == status.HTTP_200_OK
assert mock_delete.call_count == 1
I've been playing with Django Two Factor authentication for the last two days or so, and I have it partially working. I am trying to figure out a way to remove the QR Token Generator. I have tried subclassing the setup view, but the form wizard is causing me some grief. The wizard is confusing me. I know in a regular form, how to remove radio buttons, but in this case, I can't seem to locate the source of the Token Generator.
The SetupView...
#class_view_decorator(never_cache)
#class_view_decorator(login_required)
class SetupView(IdempotentSessionWizardView):
"""
View for handling OTP setup using a wizard.
The first step of the wizard shows an introduction text, explaining how OTP
works and why it should be enabled. The user has to select the verification
method (generator / call / sms) in the second step. Depending on the method
selected, the third step configures the device. For the generator method, a
QR code is shown which can be scanned using a mobile phone app and the user
is asked to provide a generated token. For call and sms methods, the user
provides the phone number which is then validated in the final step.
"""
success_url = 'two_factor:setup_complete'
qrcode_url = 'two_factor:qr'
template_name = 'two_factor/core/setup.html'
session_key_name = 'django_two_factor-qr_secret_key'
initial_dict = {}
form_list = (
('welcome', Form),
('method', MethodForm),
('generator', TOTPDeviceForm),
('sms', PhoneNumberForm),
('call', PhoneNumberForm),
('validation', DeviceValidationForm),
('yubikey', YubiKeyDeviceForm),
)
condition_dict = {
'generator': lambda self: self.get_method() == 'generator',
'call': lambda self: self.get_method() == 'call',
'sms': lambda self: self.get_method() == 'sms',
'validation': lambda self: self.get_method() in ('sms', 'call'),
'yubikey': lambda self: self.get_method() == 'yubikey',
}
idempotent_dict = {
'yubikey': False,
}
def get_method(self):
method_data = self.storage.validated_step_data.get('method', {})
return method_data.get('method', None)
def get(self, request, *args, **kwargs):
"""
Start the setup wizard. Redirect if already enabled.
"""
if default_device(self.request.user):
return redirect(self.success_url)
return super(SetupView, self).get(request, *args, **kwargs)
def get_form_list(self):
"""
Check if there is only one method, then skip the MethodForm from form_list
"""
form_list = super(SetupView, self).get_form_list()
available_methods = get_available_methods()
if len(available_methods) == 1:
form_list.pop('method', None)
method_key, _ = available_methods[0]
self.storage.validated_step_data['method'] = {'method': method_key}
return form_list
def render_next_step(self, form, **kwargs):
"""
In the validation step, ask the device to generate a challenge.
"""
next_step = self.steps.next
if next_step == 'validation':
try:
self.get_device().generate_challenge()
kwargs["challenge_succeeded"] = True
except Exception:
logger.exception("Could not generate challenge")
kwargs["challenge_succeeded"] = False
return super(SetupView, self).render_next_step(form, **kwargs)
def done(self, form_list, **kwargs):
"""
Finish the wizard. Save all forms and redirect.
"""
# Remove secret key used for QR code generation
try:
del self.request.session[self.session_key_name]
except KeyError:
pass
# TOTPDeviceForm
if self.get_method() == 'generator':
form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0]
device = form.save()
# PhoneNumberForm / YubiKeyDeviceForm
elif self.get_method() in ('call', 'sms', 'yubikey'):
device = self.get_device()
device.save()
else:
raise NotImplementedError("Unknown method '%s'" % self.get_method())
django_otp.login(self.request, device)
return redirect(self.success_url)
def get_form_kwargs(self, step=None):
kwargs = {}
if step == 'generator':
kwargs.update({
'key': self.get_key(step),
'user': self.request.user,
})
if step in ('validation', 'yubikey'):
kwargs.update({
'device': self.get_device()
})
metadata = self.get_form_metadata(step)
if metadata:
kwargs.update({
'metadata': metadata,
})
return kwargs
def get_device(self, **kwargs):
"""
Uses the data from the setup step and generated key to recreate device.
Only used for call / sms -- generator uses other procedure.
"""
method = self.get_method()
kwargs = kwargs or {}
kwargs['name'] = 'default'
kwargs['user'] = self.request.user
if method in ('call', 'sms'):
kwargs['method'] = method
kwargs['number'] = self.storage.validated_step_data\
.get(method, {}).get('number')
return PhoneDevice(key=self.get_key(method), **kwargs)
if method == 'yubikey':
kwargs['public_id'] = self.storage.validated_step_data\
.get('yubikey', {}).get('token', '')[:-32]
try:
kwargs['service'] = ValidationService.objects.get(name='default')
except ValidationService.DoesNotExist:
raise KeyError("No ValidationService found with name 'default'")
except ValidationService.MultipleObjectsReturned:
raise KeyError("Multiple ValidationService found with name 'default'")
return RemoteYubikeyDevice(**kwargs)
def get_key(self, step):
self.storage.extra_data.setdefault('keys', {})
if step in self.storage.extra_data['keys']:
return self.storage.extra_data['keys'].get(step)
key = random_hex(20).decode('ascii')
self.storage.extra_data['keys'][step] = key
return key
def get_context_data(self, form, **kwargs):
context = super(SetupView, self).get_context_data(form, **kwargs)
if self.steps.current == 'generator':
key = self.get_key('generator')
rawkey = unhexlify(key.encode('ascii'))
b32key = b32encode(rawkey).decode('utf-8')
self.request.session[self.session_key_name] = b32key
context.update({
'QR_URL': reverse(self.qrcode_url)
})
elif self.steps.current == 'validation':
context['device'] = self.get_device()
context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL)
return context
def process_step(self, form):
if hasattr(form, 'metadata'):
self.storage.extra_data.setdefault('forms', {})
self.storage.extra_data['forms'][self.steps.current] = form.metadata
return super(SetupView, self).process_step(form)
def get_form_metadata(self, step):
self.storage.extra_data.setdefault('forms', {})
return self.storage.extra_data['forms'].get(step, None)
Seems to reference a MethodForm...
class MethodForm(forms.Form):
method = forms.ChoiceField(label=_("Method"),
initial='generator',
widget=forms.RadioSelect)
def __init__(self, **kwargs):
super(MethodForm, self).__init__(**kwargs)
self.fields['method'].choices = get_available_methods()
I can't seem to track back to where the list of choices is defined, clearly in this case it is saying generator is the initial choice in setup, but I can't figure out how to remove the generator option as a list of valid choices. I also tried to remove generator from the form_list but this didn't seem to make a difference either.
If there's an easier way to remove the Token Generator option and a different approach that's altogether different, I'm open to that too.
Thanks in advance for any thoughts.
Found it. It was in the models.py....
def get_available_methods():
methods = [('generator', _('Token generator'))]
methods.extend(get_available_phone_methods())
methods.extend(get_available_yubikey_methods())
return methods
I removed the ('generator', _('Token generator')) reference from inside the brackets ( list ) and it removed the Token Generator option.