Skip to content

Commit

Permalink
Merge pull request #153 from Edraak/logistration/decode-token
Browse files Browse the repository at this point in the history
Add token decoding functionality to login/registration views
  • Loading branch information
devalih committed Mar 5, 2020
2 parents 7a221d5 + 3e283cd commit 0b43609
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 22 deletions.
80 changes: 60 additions & 20 deletions common/djangoapps/student/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import uuid
import warnings
import jwt
from urlparse import parse_qs, urlsplit, urlunsplit

import analytics
Expand Down Expand Up @@ -142,14 +143,14 @@ def _do_third_party_auth(request):
raise AuthFailedError(message)


def _get_user_by_email(request):
def _get_user_by_email(request, post_data):
"""
Finds a user object in the database based on the given request, ignores all fields except for email.
"""
if 'email' not in request.POST or 'password' not in request.POST:
if 'email' not in post_data or 'password' not in post_data:
raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))

email = request.POST['email']
email = post_data['email']

try:
return User.objects.exclude(groups__name=CHILD_USER_PERMISSION_GROUP).get(email=email)
Expand Down Expand Up @@ -196,9 +197,9 @@ def _check_forced_password_reset(user):
'"Forgot Password" link on this page to reset your password before logging in again.'))


def _enforce_password_policy_compliance(request, user):
def _enforce_password_policy_compliance(request, post_data, user):
try:
password_policy_compliance.enforce_compliance_on_login(user, request.POST.get('password'))
password_policy_compliance.enforce_compliance_on_login(user, post_data.get('password'))
except password_policy_compliance.NonCompliantPasswordWarning as e:
# Allow login, but warn the user that they will be required to reset their password soon.
PageLevelMessages.register_warning_message(request, e.message)
Expand Down Expand Up @@ -256,7 +257,7 @@ def _log_and_raise_inactive_user_auth_error(unauthenticated_user):
raise AuthFailedError(_generate_not_activated_message(unauthenticated_user))


def _authenticate_first_party(request, unauthenticated_user):
def _authenticate_first_party(request, post_data, unauthenticated_user):
"""
Use Django authentication on the given request, using rate limiting if configured
"""
Expand All @@ -268,7 +269,7 @@ def _authenticate_first_party(request, unauthenticated_user):
try:
return authenticate(
username=username,
password=request.POST.get('password', None),
password=post_data.get('password', None),
request=request)

# This occurs when there are too many attempts from the same IP address
Expand Down Expand Up @@ -298,18 +299,18 @@ def _handle_failed_authentication(user):
raise AuthFailedError(_('Email or password is incorrect.'))


def _handle_successful_authentication_and_login(user, request):
def _handle_successful_authentication_and_login(user, request, post_data):
"""
Handles clearing the failed login counter, login tracking, and setting session timeout.
"""
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)

_track_user_login(user, request)
_track_user_login(user, post_data)

try:
django_login(request, user)
if request.POST.get('remember') == 'true':
if post_data.get('remember') == 'true':
request.session.set_expiry(604800)
log.debug("Setting user session to never expire")
else:
Expand All @@ -321,7 +322,7 @@ def _handle_successful_authentication_and_login(user, request):
raise


def _track_user_login(user, request):
def _track_user_login(user, post_data):
"""
Sends a tracking event for a successful login.
"""
Expand All @@ -330,7 +331,7 @@ def _track_user_login(user, request):
analytics.identify(
user.id,
{
'email': request.POST['email'],
'email': post_data['email'],
'username': user.username
},
{
Expand All @@ -346,7 +347,7 @@ def _track_user_login(user, request):
"edx.bi.user.account.authenticated",
{
'category': "conversion",
'label': request.POST.get('course_id'),
'label': post_data.get('course_id'),
'provider': None
},
context={
Expand Down Expand Up @@ -434,8 +435,47 @@ def login_user(request):
"""
AJAX request to log in the user.
"""
post_data = request.POST.copy()

# Decrypt form data if it is encrypted
if 'data_token' in request.POST:
data_token = request.POST.get('data_token')

try:
decoded_data = jwt.decode(data_token,
settings.EDRAAK_LOGISTRATION_SECRET_KEY,
algorithms=[settings.EDRAAK_LOGISTRATION_SIGNING_ALGORITHM])
post_data.update(decoded_data)

except jwt.ExpiredSignatureError:
err_msg = u"The provided data_token has been expired"
AUDIT_LOG.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

except jwt.DecodeError:
err_msg = u"Signature verification failed"
AUDIT_LOG.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

except (jwt.InvalidTokenError, ValueError):
err_msg = u"Invalid token"
AUDIT_LOG.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request)
trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password'))
trumped_by_first_party_auth = bool(post_data.get('email')) or bool(post_data.get('password'))
was_authenticated_third_party = False
parent_user = None
child_user = None
Expand All @@ -455,8 +495,8 @@ def login_user(request):
except AuthFailedError as e:
return HttpResponse(e.value, content_type="text/plain", status=403)

elif 'child_user_id' in request.POST:
child_user_id = request.POST['child_user_id']
elif 'child_user_id' in post_data:
child_user_id = post_data['child_user_id']
try:
child_user = User.objects.get(id=child_user_id)
except User.DoesNotExist:
Expand All @@ -466,7 +506,7 @@ def login_user(request):
AUDIT_LOG.warning(u"Child login failed - Unknown child user id: {0}".format(child_user_id))

else:
email_user = _get_user_by_email(request)
email_user = _get_user_by_email(request, post_data=post_data)

if child_user:
parent_user = request.user
Expand All @@ -481,15 +521,15 @@ def login_user(request):
possibly_authenticated_user = email_user

if not was_authenticated_third_party:
possibly_authenticated_user = _authenticate_first_party(request, email_user)
possibly_authenticated_user = _authenticate_first_party(request, post_data, email_user)
if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login():
# Important: This call must be made AFTER the user was successfully authenticated.
_enforce_password_policy_compliance(request, possibly_authenticated_user)
_enforce_password_policy_compliance(request, post_data, possibly_authenticated_user)

if possibly_authenticated_user is None or not possibly_authenticated_user.is_active:
_handle_failed_authentication(email_user)

_handle_successful_authentication_and_login(possibly_authenticated_user, request)
_handle_successful_authentication_and_login(possibly_authenticated_user, request, post_data)
if parent_user:
request.session['parent_user'] = json.dumps({
'user_id': parent_user.id,
Expand Down
4 changes: 4 additions & 0 deletions lms/envs/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@
REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED')
REGISTRATION_FIELD_ORDER = ENV_TOKENS.get('REGISTRATION_FIELD_ORDER', REGISTRATION_FIELD_ORDER)

# EDRAAK LOGISTRATION KEYS
EDRAAK_LOGISTRATION_SECRET_KEY = ENV_TOKENS.get("EDRAAK_LOGISTRATION_SECRET_KEY", "edraak2020")
EDRAAK_LOGISTRATION_SIGNING_ALGORITHM = ENV_TOKENS.get("EDRAAK_LOGISTRATION_SIGNING_ALGORITHM", "HS256")

# Set the names of cookies shared with the marketing site
# These have the same cookie domain as the session, which in production
# usually includes subdomains.
Expand Down
33 changes: 33 additions & 0 deletions openedx/core/djangoapps/user_api/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,39 @@ def _wrapped(*args, **_kwargs): # pylint: disable=missing-docstring
return _decorator


def require_any_post_params(required_params):
"""
View decorator that ensures any of the required POST params are
present. If not, returns an HTTP response with status 400.
Args:
required_params (list): The required parameter keys.
i.e. [['email', 'password'], ['token']]
Returns:
HttpResponse
"""
def _decorator(func): # pylint: disable=missing-docstring
@wraps(func)
def _wrapped(*args, **_kwargs): # pylint: disable=missing-docstring
request = args[0]

for params in required_params:
missing_params = set(params) - set(request.POST.keys())

if not missing_params:
return func(request)

msg = u"Missing POST parameters: {missing}".format(
missing=' or '.join(['[' + ', '.join(params) + ']' for params in required_params])
)
return HttpResponseBadRequest(msg)

return _wrapped
return _decorator


class InvalidFieldError(Exception):
"""The provided field definition is not valid. """

Expand Down
48 changes: 46 additions & 2 deletions openedx/core/djangoapps/user_api/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""HTTP end-points for the User API. """

import logging
import jwt

from django.contrib.auth.models import User
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.db import transaction
from django.conf import settings
from django.db.utils import DatabaseError
from django.http import HttpResponse, HttpResponseForbidden
from django.utils.decorators import method_decorator
Expand All @@ -29,7 +32,11 @@
get_login_session_form,
get_password_reset_form
)
from openedx.core.djangoapps.user_api.helpers import require_post_params, shim_student_view
from openedx.core.djangoapps.user_api.helpers import (
require_post_params,
require_any_post_params,
shim_student_view
)
from openedx.core.djangoapps.user_api.models import UserPreference
from student.models import UserProfile
from openedx.core.djangoapps.user_api.preferences.api import get_country_time_zones, update_email_opt_in
Expand All @@ -55,7 +62,7 @@ class LoginSessionView(APIView):
def get(self, request):
return HttpResponse(get_login_session_form(request).to_json(), content_type="application/json")

@method_decorator(require_post_params(["email", "password"]))
@method_decorator(require_any_post_params([["email", "password"], ["data_token"]]))
@method_decorator(csrf_protect)
def post(self, request):
"""Log in a user.
Expand Down Expand Up @@ -130,6 +137,43 @@ def post(self, request):
"""
data = request.POST.copy()

# Decrypt form data if it is encrypted
if 'data_token' in request.POST:
data_token = request.POST.get('data_token')

try:
decoded_data = jwt.decode(data_token,
settings.EDRAAK_LOGISTRATION_SECRET_KEY,
algorithms=[settings.EDRAAK_LOGISTRATION_SIGNING_ALGORITHM])
data.update(decoded_data)

except jwt.ExpiredSignatureError:
err_msg = u"The provided data_token has been expired"
log.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

except jwt.DecodeError:
err_msg = u"Signature verification failed"
log.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

except (jwt.InvalidTokenError, ValueError):
err_msg = u"Invalid token"
log.warning(err_msg)

return JsonResponse({
"success": False,
"value": err_msg,
}, status=400)

email = data.get('email')
username = data.get('username')

Expand Down

0 comments on commit 0b43609

Please sign in to comment.