def validate_password_security(password, user): """ Check password reuse and similar operational security policy considerations. """ # Check reuse if not PasswordHistory.is_allowable_password_reuse(user, password): if user.is_staff: num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE'] else: num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE'] raise SecurityPolicyError(ungettext( "You are re-using a password that you have used recently. " "You must have {num} distinct password before reusing a previous password.", "You are re-using a password that you have used recently. " "You must have {num} distinct passwords before reusing a previous password.", num_distinct ).format(num=num_distinct)) # Check reset frequency if PasswordHistory.is_password_reset_too_soon(user): num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS'] raise SecurityPolicyError(ungettext( "You are resetting passwords too frequently. Due to security policies, " "{num} day must elapse between password resets.", "You are resetting passwords too frequently. Due to security policies, " "{num} days must elapse between password resets.", num_days ).format(num=num_days))
def _change_password(self, user, password): """ Helper method to change password on user and record in the PasswordHistory """ user.set_password(password) user.save() history = PasswordHistory() history.create(user)
def _update_password(self, email, new_password): """ Helper method to reset a password """ user = User.objects.get(email=email) user.set_password(new_password) user.save() history = PasswordHistory() history.create(user)
def test_pbkdf2_sha256_password_reuse(self): """ Assert against the password reuse policy but using the normal Django PBKDF2 """ user = self._user_factory_with_history() staff = self._user_factory_with_history(is_staff=True) # students need to user at least one different passwords before reuse self.assertFalse(PasswordHistory.is_allowable_password_reuse(user, "test")) self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "different")) self._change_password(user, "different") self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "test")) # staff needs to use at least two different passwords before reuse self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test")) self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "different")) self._change_password(staff, "different") self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "test")) self.assertFalse(PasswordHistory.is_allowable_password_reuse(staff, "different")) self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "third")) self._change_password(staff, "third") self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "test"))
def test_disabled_feature(self): """ Test that behavior is normal when this feature is not turned on """ user = UserFactory() staff = AdminFactory() # if feature is disabled user can keep reusing same password self.assertTrue(PasswordHistory.is_allowable_password_reuse(user, "test")) self.assertTrue(PasswordHistory.is_allowable_password_reuse(staff, "test")) self.assertFalse(PasswordHistory.should_user_reset_password_now(user)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))
def test_too_frequent_password_resets(self): """ Assert that a user should not be able to password reset too frequently """ student = self._user_factory_with_history() grandfathered_student = self._user_factory_with_history(set_initial_history=False) self.assertTrue(PasswordHistory.is_password_reset_too_soon(student)) self.assertFalse(PasswordHistory.is_password_reset_too_soon(grandfathered_student)) staff_reset_time = timezone.now() + timedelta(days=100) with freeze_time(staff_reset_time): self.assertFalse(PasswordHistory.is_password_reset_too_soon(student))
def test_disabled_too_frequent_password_resets(self): """ Verify properly default behavior when feature is disabled """ student = self._user_factory_with_history() self.assertFalse(PasswordHistory.is_password_reset_too_soon(student))
def _user_factory_with_history(self, is_staff=False, set_initial_history=True): """ Helper method to generate either an Admin or a User """ if is_staff: user = AdminFactory() else: user = UserFactory() user.date_joined = timezone.now() if set_initial_history: history = PasswordHistory() history.create(user) return user
def post(self, request): """ POST /api/user/v1/accounts/retire_misc/ { 'username': '******' } Retires the user with the given username in the LMS. """ username = request.data['username'] try: retirement = UserRetirementStatus.get_retirement_for_retirement_action( username) RevisionPluginRevision.retire_user(retirement.user) ArticleRevision.retire_user(retirement.user) PendingNameChange.delete_by_user_value(retirement.user, field='user') PasswordHistory.retire_user(retirement.user.id) ManualEnrollmentAudit.retire_manual_enrollments( retirement.user, retirement.retired_email) CreditRequest.retire_user(retirement) ApiAccessRequest.retire_user(retirement.user) CreditRequirementStatus.retire_user(retirement) # This signal allows code in higher points of LMS to retire the user as necessary USER_RETIRE_LMS_MISC.send(sender=self.__class__, user=retirement.user) # This signal allows code in higher points of LMS to unsubscribe the user # from various types of mailings. USER_RETIRE_MAILINGS.send(sender=self.__class__, email=retirement.original_email, new_email=retirement.retired_email, user=retirement.user) except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)
def _check_forced_password_reset(user): """ See if the user must reset his/her password due to any policy settings """ if user and PasswordHistory.should_user_reset_password_now(user): raise AuthFailedError(_('Your password has expired due to password policy on this account. You must ' 'reset your password before you can log in again. Please click the ' '"Forgot Password" link on this page to reset your password before logging in again.'))
def test_related_fields_ignored(self): """ Verify that we don't emit events for related fields. """ self.user.passwordhistory_set.add( PasswordHistory(password='******')) self.user.save() self.assert_no_events_were_emitted()
def post(self, request): """ POST /api/user/v1/accounts/retire_misc/ { 'username': '******' } Retires the user with the given username in the LMS. """ username = request.data['username'] try: retirement = UserRetirementStatus.get_retirement_for_retirement_action(username) RevisionPluginRevision.retire_user(retirement.user) ArticleRevision.retire_user(retirement.user) PendingNameChange.delete_by_user_value(retirement.user, field='user') PasswordHistory.retire_user(retirement.user.id) course_enrollments = CourseEnrollment.objects.filter(user=retirement.user) ManualEnrollmentAudit.retire_manual_enrollments(course_enrollments, retirement.retired_email) CreditRequest.retire_user(retirement.original_username, retirement.retired_username) ApiAccessRequest.retire_user(retirement.user) CreditRequirementStatus.retire_user(retirement.user.username) # This signal allows code in higher points of LMS to retire the user as necessary USER_RETIRE_LMS_MISC.send(sender=self.__class__, user=retirement.user) # This signal allows code in higher points of LMS to unsubscribe the user # from various types of mailings. USER_RETIRE_MAILINGS.send( sender=self.__class__, email=retirement.original_email, new_email=retirement.retired_email, user=retirement.user ) except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request): """ POST /api/user/v1/accounts/retire_misc/ { 'username': '******' } Retires the user with the given username in the LMS. """ username = request.data['username'] if is_username_retired(username): return Response(status=status.HTTP_404_NOT_FOUND) try: retirement = UserRetirementStatus.get_retirement_for_retirement_action( username) RevisionPluginRevision.retire_user(retirement.user) ArticleRevision.retire_user(retirement.user) PendingNameChange.delete_by_user_value(retirement.user, field='user') PasswordHistory.retire_user(retirement.user.id) course_enrollments = CourseEnrollment.objects.filter( user=retirement.user) ManualEnrollmentAudit.retire_manual_enrollments( course_enrollments, retirement.retired_email) CreditRequest.retire_user(retirement.original_username, retirement.retired_username) ApiAccessRequest.retire_user(retirement.user) CreditRequirementStatus.retire_user(retirement.user.username) SurveyAnswer.retire_user(retirement.user.id) except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)
def test_retirement(self): """ Verify that the user's password history contains no actual passwords after retirement is called. """ user = self._user_factory_with_history() # create multiple rows in the password history table self._change_password(user, "different") self._change_password(user, "differentagain") # ensure the rows were actually created and stored the passwords self.assertTrue(PasswordHistory.objects.filter(user_id=user.id).exists()) for row in PasswordHistory.objects.filter(user_id=user.id): self.assertFalse(row.password == "") # retire the user and ensure that the rows are still present, but with no passwords PasswordHistory.retire_user(user.id) self.assertTrue(PasswordHistory.objects.filter(user_id=user.id).exists()) for row in PasswordHistory.objects.filter(user_id=user.id): self.assertEqual(row.password, "")
def test_no_forced_password_change(self): """ Assert that if we skip configuration, then user will never have to force reset password """ student = self._user_factory_with_history() staff = self._user_factory_with_history(is_staff=True) # also create a user who doesn't have any history grandfathered_student = UserFactory() grandfathered_student.date_joined = timezone.now() self.assertFalse( PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff)) self.assertFalse( PasswordHistory.should_user_reset_password_now( grandfathered_student)) staff_reset_time = timezone.now() + timedelta(days=100) with freeze_time(staff_reset_time): self.assertFalse( PasswordHistory.should_user_reset_password_now(student)) self.assertFalse( PasswordHistory.should_user_reset_password_now( grandfathered_student)) self.assertFalse( PasswordHistory.should_user_reset_password_now(staff))
def test_retirement(self): """ Verify that the user's password history contains no actual passwords after retirement is called. """ user = self._user_factory_with_history() # create multiple rows in the password history table self._change_password(user, "different") self._change_password(user, "differentagain") # ensure the rows were actually created and stored the passwords self.assertTrue( PasswordHistory.objects.filter(user_id=user.id).exists()) for row in PasswordHistory.objects.filter(user_id=user.id): self.assertFalse(row.password == "") # retire the user and ensure that the rows are still present, but with no passwords PasswordHistory.retire_user(user.id) self.assertTrue( PasswordHistory.objects.filter(user_id=user.id).exists()) for row in PasswordHistory.objects.filter(user_id=user.id): self.assertEqual(row.password, "")
def _create_user(self, data, errors, response): """Register user and add him to a company.""" user = data.get('user') email = user.get('email') company = data.get('company') # Create the user and their profile. try: # User user = User.objects.create(**user) user.set_password(user.password) user.save() data['user_object'] = user # Profile UserProfile.objects.create(user=user, name=u'{} {}'.format( user.first_name, user.last_name)) # Notifications if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'): enable_notifications(user) # Password History password_history_entry = PasswordHistory() password_history_entry.create(user) except Exception as exc: self._add_error(errors, str(exc.message), _('Registering Participant'), email) else: response['user_id'] = user.id AUDIT_LOG.info( u"API::New account created with user-id - {0}".format(user.id)) # Associate with company. try: company.users.add(user) except Exception as exc: self._add_error(errors, str(exc.message), _('Enrolling Participant in Company'), email)
def test_no_forced_password_change(self): """ Assert that if we skip configuration, then user will never have to force reset password """ student = self._user_factory_with_history() staff = self._user_factory_with_history(is_staff=True) # also create a user who doesn't have any history grandfathered_student = UserFactory() grandfathered_student.date_joined = timezone.now() self.assertFalse(PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff)) self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student)) staff_reset_time = timezone.now() + timedelta(days=100) with freeze_time(staff_reset_time): self.assertFalse(PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))
def post(self, request): response_data = {} # Add some rate limiting here by re-using the RateLimitMixin as a helper class limiter = BadRequestRateLimiter() if limiter.is_rate_limit_exceeded(request): response_data['message'] = _('Rate limit exceeded in api login.') return Response(response_data, status=status.HTTP_403_FORBIDDEN) base_uri = generate_base_uri(request) try: existing_user = User.objects.get(username=request.DATA['username']) except ObjectDoesNotExist: existing_user = None # see if account has been locked out due to excessive login failures if existing_user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _('This account has been temporarily locked due to excessive login failures. ' 'Try again later.') return Response(response_data, status=response_status) # see if the user must reset his/her password due to any policy settings if existing_user and PasswordHistory.should_user_reset_password_now(existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _( 'Your password has expired due to password policy on this account. ' 'You must reset your password before you can log in again.' ) return Response(response_data, status=response_status) if existing_user: user = authenticate(username=existing_user.username, password=request.DATA['password']) if user is not None: # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) if user.is_active: # # Create a new session directly with the SESSION_ENGINE # We don't call the django.contrib.auth login() method # because it is bound with the HTTP request. # # Since we are a server-to-server API, we shouldn't # be stateful with respect to the HTTP request # and anything that might come with it, as it could # violate our RESTfulness # engine = import_module(settings.SESSION_ENGINE) new_session = engine.SessionStore() new_session.create() # These values are expected to be set in any new session new_session[SESSION_KEY] = user.id new_session[BACKEND_SESSION_KEY] = user.backend new_session.save() response_data['token'] = new_session.session_key response_data['expires'] = new_session.get_expiry_age() user_dto = UserSerializer(user) response_data['user'] = user_dto.data response_data['uri'] = '{}/{}'.format(base_uri, new_session.session_key) response_status = status.HTTP_201_CREATED # generate a CSRF tokens for any web clients that may need to # call into the LMS via Ajax (for example Notifications) response_data['csrftoken'] = RequestContext(request, {}).get('csrf_token') # update the last_login fields in the auth_user table for this user user.last_login = timezone.now() user.save() # add to audit log AUDIT_LOG.info(u"API::User logged in successfully with user-id - {0}".format(user.id)) else: response_status = status.HTTP_403_FORBIDDEN else: limiter.tick_bad_request_counter(request) # tick the failed login counters if the user exists in the database if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(existing_user) response_status = status.HTTP_401_UNAUTHORIZED AUDIT_LOG.warn(u"API::User authentication failed with user-id - {0}".format(existing_user.id)) else: AUDIT_LOG.warn(u"API::Failed login attempt with unknown email/username") response_status = status.HTTP_404_NOT_FOUND return Response(response_data, status=response_status)
def test_forced_password_change(self): """ Assert when passwords must be reset """ student = self._user_factory_with_history() staff = self._user_factory_with_history(is_staff=True) grandfathered_student = self._user_factory_with_history(set_initial_history=False) self.assertFalse(PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff)) self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student)) staff_reset_time = timezone.now() + timedelta(days=1) with freeze_time(staff_reset_time): self.assertFalse(PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student)) self.assertTrue(PasswordHistory.should_user_reset_password_now(staff)) self._change_password(staff, 'Different') self.assertFalse(PasswordHistory.should_user_reset_password_now(staff)) student_reset_time = timezone.now() + timedelta(days=5) with freeze_time(student_reset_time): self.assertTrue(PasswordHistory.should_user_reset_password_now(student)) self.assertTrue(PasswordHistory.should_user_reset_password_now(grandfathered_student)) self.assertTrue(PasswordHistory.should_user_reset_password_now(staff)) self._change_password(student, 'Different') self.assertFalse(PasswordHistory.should_user_reset_password_now(student)) self._change_password(grandfathered_student, 'Different') self.assertFalse(PasswordHistory.should_user_reset_password_now(grandfathered_student)) self._change_password(staff, 'Different') self.assertFalse(PasswordHistory.should_user_reset_password_now(staff))
def test_forced_password_change(self): """ Assert when passwords must be reset """ student = self._user_factory_with_history() staff = self._user_factory_with_history(is_staff=True) grandfathered_student = self._user_factory_with_history( set_initial_history=False) self.assertFalse( PasswordHistory.should_user_reset_password_now(student)) self.assertFalse(PasswordHistory.should_user_reset_password_now(staff)) self.assertFalse( PasswordHistory.should_user_reset_password_now( grandfathered_student)) staff_reset_time = timezone.now() + timedelta(days=1) with freeze_time(staff_reset_time): self.assertFalse( PasswordHistory.should_user_reset_password_now(student)) self.assertFalse( PasswordHistory.should_user_reset_password_now( grandfathered_student)) self.assertTrue( PasswordHistory.should_user_reset_password_now(staff)) self._change_password(staff, 'Different') self.assertFalse( PasswordHistory.should_user_reset_password_now(staff)) student_reset_time = timezone.now() + timedelta(days=5) with freeze_time(student_reset_time): self.assertTrue( PasswordHistory.should_user_reset_password_now(student)) self.assertTrue( PasswordHistory.should_user_reset_password_now( grandfathered_student)) self.assertTrue( PasswordHistory.should_user_reset_password_now(staff)) self._change_password(student, 'Different') self.assertFalse( PasswordHistory.should_user_reset_password_now(student)) self._change_password(grandfathered_student, 'Different') self.assertFalse( PasswordHistory.should_user_reset_password_now( grandfathered_student)) self._change_password(staff, 'Different') self.assertFalse( PasswordHistory.should_user_reset_password_now(staff))
def _do_create_account_custom(form, custom_form=None): """ Given cleaned post variables, create the User and UserProfile objects, as well as the registration for this user. Returns a tuple (User, UserProfile, Registration). Note: this function is also used for creating test users. """ errors = {} errors.update(form.errors) if custom_form: errors.update(custom_form.errors) if errors: raise ValidationError(errors) user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"], is_active=False) user.set_password(form.cleaned_data["password"]) registration = Registration() # TODO: Rearrange so that if part of the process fails, the whole process fails. # Right now, we can have e.g. no registration e-mail sent out and a zombie account try: with transaction.atomic(): user.save() custom_model = custom_form.save(user=user, commit=True) # Fix: recall user.save to avoid transaction management related exception, if we call user.save under atomic block # (in custom_from.save )a random transaction exception generated if custom_model.organization: custom_model.organization.save() user.save() except IntegrityError: # Figure out the cause of the integrity error if len(User.objects.filter(username=user.username)) > 0: raise AccountValidationError(_( "An account with the Public Username '{username}' already exists." ).format(username=user.username), field="username") elif len(User.objects.filter(email=user.email)) > 0: raise AccountValidationError(_( "An account with the Email '{email}' already exists.").format( email=user.email), field="email") else: raise # add this account creation to password history # NOTE, this will be a NOP unless the feature has been turned on in configuration password_history_entry = PasswordHistory() password_history_entry.create(user) registration.register(user) profile_fields = [ "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals", "year_of_birth" ] profile = UserProfile( user=user, **{key: form.cleaned_data.get(key) for key in profile_fields}) extended_profile = form.cleaned_extended_profile if extended_profile: profile.meta = json.dumps(extended_profile) try: profile.save() except Exception: # pylint: disable=broad-except log.exception( "UserProfile creation failed for user {id}.".format(id=user.id)) raise return (user, profile, registration)
def login_user(request, session_id=None): """ Create a new session and login the user, or upgrade an existing session """ response_data = {} # Add some rate limiting here by re-using the RateLimitMixin as a helper class limiter = BadRequestRateLimiter() if limiter.is_rate_limit_exceeded(request): response_data['message'] = _('Rate limit exceeded in api login.') return Response(response_data, status=status.HTTP_403_FORBIDDEN) base_uri = generate_base_uri(request) try: existing_user = User.objects.get(username=request.DATA['username']) except ObjectDoesNotExist: existing_user = None # see if account has been locked out due to excessive login failures if existing_user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _( 'This account has been temporarily locked due to excessive login failures. ' # pylint: disable=C0301 'Try again later.') return Response(response_data, status=response_status) # see if the user must reset his/her password due to any policy settings if existing_user and PasswordHistory.should_user_reset_password_now( existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _( 'Your password has expired due to password policy on this account. ' 'You must reset your password before you can log in again.') return Response(response_data, status=response_status) if existing_user: user = authenticate(username=existing_user.username, password=request.DATA['password']) if user is not None: # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) if user.is_active: # # Create a new session directly with the SESSION_ENGINE # We don't call the django.contrib.auth login() method # because it is bound with the HTTP request. # # Since we are a server-to-server API, we shouldn't # be stateful with respect to the HTTP request # and anything that might come with it, as it could # violate our RESTfulness # engine = import_module(settings.SESSION_ENGINE) if session_id is None: session = engine.SessionStore() session.create() success_status = status.HTTP_201_CREATED else: session = engine.SessionStore(session_id) success_status = status.HTTP_200_OK if SESSION_KEY in session: # Someone is already logged in. The user ID of whoever is logged in # now might be different than the user ID we've been asked to login, # which would be bad. But even if it is the same user, we should not # be asked to login a user who is already logged in. This likely # indicates some sort of programming/validation error and possibly # even a potential security issue - so return 403. return Response({}, status=status.HTTP_403_FORBIDDEN) # These values are expected to be set in any new session session[SESSION_KEY] = user.id session[BACKEND_SESSION_KEY] = user.backend session.save() response_data['token'] = session.session_key response_data['expires'] = session.get_expiry_age() user_dto = UserSerializer(user) response_data['user'] = user_dto.data response_data['uri'] = '{}/{}'.format( base_uri, session.session_key) response_status = success_status # generate a CSRF tokens for any web clients that may need to # call into the LMS via Ajax (for example Notifications) response_data['csrftoken'] = RequestContext( request, {}).get('csrf_token') # update the last_login fields in the auth_user table for this user user.last_login = timezone.now() user.save() # add to audit log AUDIT_LOG.info( u"API::User logged in successfully with user-id - {0}". format(user.id)) # pylint: disable=W1202 else: response_status = status.HTTP_403_FORBIDDEN else: limiter.tick_bad_request_counter(request) # tick the failed login counters if the user exists in the database if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(existing_user) response_status = status.HTTP_401_UNAUTHORIZED AUDIT_LOG.warn( u"API::User authentication failed with user-id - {0}". format(existing_user.id)) # pylint: disable=W1202 else: AUDIT_LOG.warn( u"API::Failed login attempt with unknown email/username") response_status = status.HTTP_404_NOT_FOUND return Response(response_data, status=response_status)
def do_create_account(form, custom_form=None): """ Given cleaned post variables, create the User and UserProfile objects, as well as the registration for this user. Returns a tuple (User, UserProfile, Registration). Note: this function is also used for creating test users. """ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation if not configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)): raise PermissionDenied() errors = {} errors.update(form.errors) if custom_form: errors.update(custom_form.errors) if errors: raise ValidationError(errors) user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"], is_active=False) user.set_password(form.cleaned_data["password"]) registration = Registration() # TODO: Rearrange so that if part of the process fails, the whole process fails. # Right now, we can have e.g. no registration e-mail sent out and a zombie account try: with transaction.atomic(): user.save() if custom_form: custom_model = custom_form.save(commit=False) custom_model.user = user custom_model.save() except IntegrityError: # Figure out the cause of the integrity error # TODO duplicate email is already handled by form.errors above as a ValidationError. # The checks for duplicate email/username should occur in the same place with an # AccountValidationError and a consistent user message returned (i.e. both should # return "It looks like {username} belongs to an existing account. Try again with a # different username.") if len(User.objects.filter(username=user.username)) > 0: raise AccountValidationError(_( "An account with the Public Username '{username}' already exists." ).format(username=user.username), field="username") elif len(User.objects.filter(email=user.email)) > 0: raise AccountValidationError(_( "An account with the Email '{email}' already exists.").format( email=user.email), field="email") else: raise # add this account creation to password history # NOTE, this will be a NOP unless the feature has been turned on in configuration password_history_entry = PasswordHistory() password_history_entry.create(user) registration.register(user) profile_fields = [ "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals", "year_of_birth" ] profile = UserProfile( user=user, **{key: form.cleaned_data.get(key) for key in profile_fields}) extended_profile = form.cleaned_extended_profile if extended_profile: profile.meta = json.dumps(extended_profile) try: profile.save() except Exception: # pylint: disable=broad-except log.exception( "UserProfile creation failed for user {id}.".format(id=user.id)) raise return user, profile, registration
def password_reset_confirm_wrapper(request, uidb36=None, token=None): """ A wrapper around django.contrib.auth.views.password_reset_confirm. Needed because we want to set the user as active at this step. We also optionally do some additional password policy checks. """ # convert old-style base36-encoded user id to base64 uidb64 = uidb36_to_uidb64(uidb36) platform_name = { "platform_name": configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME) } try: uid_int = base36_to_int(uidb36) user = User.objects.get(id=uid_int) except (ValueError, User.DoesNotExist): # if there's any error getting a user, just let django's # password_reset_confirm function handle it. return password_reset_confirm(request, uidb64=uidb64, token=token, extra_context=platform_name) if UserRetirementRequest.has_user_requested_retirement(user): # Refuse to reset the password of any user that has requested retirement. context = { 'validlink': True, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': _('Error in resetting your password.'), } context.update(platform_name) return TemplateResponse(request, 'registration/password_reset_confirm.html', context) if waffle().is_enabled(PREVENT_AUTH_USER_WRITES): context = { 'validlink': False, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': SYSTEM_MAINTENANCE_MSG, } context.update(platform_name) return TemplateResponse(request, 'registration/password_reset_confirm.html', context) if request.method == 'POST': password = request.POST['new_password1'] try: validate_password(password, user=user) except ValidationError as err: # We have a password reset attempt which violates some security # policy, or any other validation. Use the existing Django template to communicate that # back to the user. context = { 'validlink': True, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': err.message, } context.update(platform_name) return TemplateResponse( request, 'registration/password_reset_confirm.html', context) # remember what the old password hash is before we call down old_password_hash = user.password response = password_reset_confirm(request, uidb64=uidb64, token=token, extra_context=platform_name) # If password reset was unsuccessful a template response is returned (status_code 200). # Check if form is invalid then show an error to the user. # Note if password reset was successful we get response redirect (status_code 302). if response.status_code == 200: form_valid = response.context_data['form'].is_valid( ) if response.context_data['form'] else False if not form_valid: log.warning( u'Unable to reset password for user [%s] because form is not valid. ' u'A possible cause is that the user had an invalid reset token', user.username, ) response.context_data['err_msg'] = _( 'Error in resetting your password. Please try again.') return response # get the updated user updated_user = User.objects.get(id=uid_int) # did the password hash change, if so record it in the PasswordHistory if updated_user.password != old_password_hash: entry = PasswordHistory() entry.create(updated_user) else: response = password_reset_confirm(request, uidb64=uidb64, token=token, extra_context=platform_name) response_was_successful = response.context_data.get('validlink') if response_was_successful and not user.is_active: user.is_active = True user.save() return response
def post(request, error=""): # pylint: disable-msg=too-many-statements,unused-argument """AJAX request to log in the user.""" backend_name = None email = None password = None redirect_url = None response = None running_pipeline = None third_party_auth_requested = settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request) third_party_auth_successful = False trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password')) user = None if 'email' not in request.POST or 'password' not in request.POST: return JsonResponse({ "success": False, "value": _('There was an error receiving your login information. Please email us.'), # TODO: User error message }) # TODO: this should be status code 400 # pylint: disable=fixme email = request.POST['email'] password = request.POST['password'] try: user = User.objects.get(email=email) except User.DoesNotExist: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u"Login failed - Unknown user email") else: AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email)) # see if account has been locked out due to excessive login failures user_found_by_email_lookup = user if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user_found_by_email_lookup): return JsonResponse({ "success": False, "value": _('This account has been temporarily locked due to excessive login failures. Try again later.'), }) # TODO: this should be status code 429 # pylint: disable=fixme # see if the user must reset his/her password due to any policy settings if PasswordHistory.should_user_reset_password_now(user_found_by_email_lookup): return JsonResponse({ "success": False, "value": _('Your password has expired due to password policy on this account. You must ' 'reset your password before you can log in again. Please click the ' '"Forgot Password" link on this page to reset your password before logging in again.'), }) # TODO: this should be status code 403 # pylint: disable=fixme # if the user doesn't exist, we want to set the username to an invalid # username so that authentication is guaranteed to fail and we can take # advantage of the ratelimited backend username = user.username if user else "" if not third_party_auth_successful: try: user = authenticate(username=username, password=password, request=request) # this occurs when there are too many attempts from the same IP address except RateLimitException: return JsonResponse({ "success": False, "value": _('Too many failed login attempts. Try again later.'), }) # TODO: this should be status code 429 # pylint: disable=fixme if user is None: # tick the failed login counters if the user exists in the database if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user_found_by_email_lookup) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if username != "": if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user_found_by_email_lookup.id if user_found_by_email_lookup else "<unknown>" AUDIT_LOG.warning(u"Login failed - password for user.id: {0} is invalid".format(loggable_id)) else: AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email)) return JsonResponse({ "success": False, "value": _('Email or password is incorrect.'), }) # TODO: this should be status code 400 # pylint: disable=fixme # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) if user is not None and user.is_active: try: # We do not log here, because we have a handler registered # to perform logging on successful logins. login(request, user) if request.POST.get('remember') == 'true': request.session.set_expiry(604800) log.debug("Setting user session to never expire") else: request.session.set_expiry(0) except Exception as e: AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?") log.critical("Login failed - Could not create session. Is memcached running?") log.exception(e) raise response = JsonResponse({ "success": True, "username": user.username, "email": user.email }) # set the login cookie for the edx marketing site # we want this cookie to be accessed via javascript # so httponly is set to None if request.session.get_expire_at_browser_close(): max_age = None expires = None else: max_age = request.session.get_expiry_age() expires_time = time.time() + max_age expires = cookie_date(expires_time) response.set_cookie( settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, path='/', secure=None, httponly=None, ) return response if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id)) else: AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username)) reactivation_email_for_user(user) not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your e-mail for the activation instructions.") return JsonResponse({ "success": False, "value": not_activated_msg, }) # TODO: this should be status code 400 # pylint: disable=fixme
def password_reset_confirm_wrapper(request, uidb36=None, token=None): """ A wrapper around django.contrib.auth.views.password_reset_confirm. Needed because we want to set the user as active at this step. We also optionally do some additional password policy checks. """ # convert old-style base36-encoded user id to base64 uidb64 = uidb36_to_uidb64(uidb36) platform_name = { "platform_name": configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME) } try: uid_int = base36_to_int(uidb36) user = User.objects.get(id=uid_int) except (ValueError, User.DoesNotExist): # if there's any error getting a user, just let django's # password_reset_confirm function handle it. return password_reset_confirm( request, uidb64=uidb64, token=token, extra_context=platform_name ) if UserRetirementRequest.has_user_requested_retirement(user): # Refuse to reset the password of any user that has requested retirement. context = { 'validlink': True, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': _('Error in resetting your password.'), } context.update(platform_name) return TemplateResponse( request, 'registration/password_reset_confirm.html', context ) if waffle().is_enabled(PREVENT_AUTH_USER_WRITES): context = { 'validlink': False, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': SYSTEM_MAINTENANCE_MSG, } context.update(platform_name) return TemplateResponse( request, 'registration/password_reset_confirm.html', context ) if request.method == 'POST': password = request.POST['new_password1'] try: validate_password(password, user=user) except ValidationError as err: # We have a password reset attempt which violates some security # policy, or any other validation. Use the existing Django template to communicate that # back to the user. context = { 'validlink': True, 'form': None, 'title': _('Password reset unsuccessful'), 'err_msg': err.message, } context.update(platform_name) return TemplateResponse( request, 'registration/password_reset_confirm.html', context ) # remember what the old password hash is before we call down old_password_hash = user.password response = password_reset_confirm( request, uidb64=uidb64, token=token, extra_context=platform_name ) # If password reset was unsuccessful a template response is returned (status_code 200). # Check if form is invalid then show an error to the user. # Note if password reset was successful we get response redirect (status_code 302). if response.status_code == 200: form_valid = response.context_data['form'].is_valid() if response.context_data['form'] else False if not form_valid: log.warning( u'Unable to reset password for user [%s] because form is not valid. ' u'A possible cause is that the user had an invalid reset token', user.username, ) response.context_data['err_msg'] = _('Error in resetting your password. Please try again.') return response # get the updated user updated_user = User.objects.get(id=uid_int) # did the password hash change, if so record it in the PasswordHistory if updated_user.password != old_password_hash: entry = PasswordHistory() entry.create(updated_user) else: response = password_reset_confirm( request, uidb64=uidb64, token=token, extra_context=platform_name ) response_was_successful = response.context_data.get('validlink') if response_was_successful and not user.is_active: user.is_active = True user.save() return response
def post(self, request): response_data = {} # Add some rate limiting here by re-using the RateLimitMixin as a helper class limiter = BadRequestRateLimiter() if limiter.is_rate_limit_exceeded(request): response_data['message'] = _('Rate limit exceeded in api login.') return Response(response_data, status=status.HTTP_403_FORBIDDEN) base_uri = generate_base_uri(request) try: existing_user = User.objects.get(username=request.DATA['username']) except ObjectDoesNotExist: existing_user = None # see if account has been locked out due to excessive login failures if existing_user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _('This account has been temporarily locked due to excessive login failures. ' 'Try again later.') return Response(response_data, status=response_status) # see if the user must reset his/her password due to any policy settings if existing_user and PasswordHistory.should_user_reset_password_now(existing_user): response_status = status.HTTP_403_FORBIDDEN response_data['message'] = _( 'Your password has expired due to password policy on this account. ' 'You must reset your password before you can log in again.' ) return Response(response_data, status=response_status) if existing_user: user = authenticate(username=existing_user.username, password=request.DATA['password']) if user is not None: # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) if user.is_active: login(request, user) response_data['token'] = request.session.session_key response_data['expires'] = request.session.get_expiry_age() user_dto = UserSerializer(user) response_data['user'] = user_dto.data response_data['uri'] = '{}/{}'.format(base_uri, request.session.session_key) response_status = status.HTTP_201_CREATED # add to audit log AUDIT_LOG.info(u"API::User logged in successfully with user-id - {0}".format(user.id)) else: response_status = status.HTTP_403_FORBIDDEN else: limiter.tick_bad_request_counter(request) # tick the failed login counters if the user exists in the database if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(existing_user) response_status = status.HTTP_401_UNAUTHORIZED AUDIT_LOG.warn(u"API::User authentication failed with user-id - {0}".format(existing_user.id)) else: AUDIT_LOG.warn(u"API::Failed login attempt with unknown email/username") response_status = status.HTTP_404_NOT_FOUND return Response(response_data, status=response_status)
def login_user_custom(request, error=""): # pylint: disable=too-many-statements,unused-argument """AJAX request to log in the user.""" backend_name = None email = None password = None redirect_url = None response = None running_pipeline = None third_party_auth_requested = third_party_auth.is_enabled( ) and pipeline.running(request) third_party_auth_successful = False trumped_by_first_party_auth = bool(request.POST.get('email')) or bool( request.POST.get('password')) user = None platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) if third_party_auth_requested and not trumped_by_first_party_auth: # The user has already authenticated via third-party auth and has not # asked to do first party auth by supplying a username or password. We # now want to put them through the same logging and cookie calculation # logic as with first-party auth. running_pipeline = pipeline.get(request) username = running_pipeline['kwargs'].get('username') backend_name = running_pipeline['backend'] third_party_uid = running_pipeline['kwargs']['uid'] requested_provider = provider.Registry.get_from_pipeline( running_pipeline) try: user = pipeline.get_authenticated_user(requested_provider, username, third_party_uid) third_party_auth_successful = True except User.DoesNotExist: AUDIT_LOG.warning( u"Login failed - user with username {username} has no social auth " "with backend_name {backend_name}".format( username=username, backend_name=backend_name)) message = _( "You've successfully logged into your {provider_name} account, " "but this account isn't linked with an {platform_name} account yet." ).format( platform_name=platform_name, provider_name=requested_provider.name, ) message += "<br/><br/>" message += _( "Use your {platform_name} username and password to log into {platform_name} below, " "and then link your {platform_name} account with {provider_name} from your dashboard." ).format( platform_name=platform_name, provider_name=requested_provider.name, ) message += "<br/><br/>" message += _( "If you don't have an {platform_name} account yet, " "click <strong>Register</strong> at the top of the page." ).format(platform_name=platform_name) return HttpResponse(message, content_type="text/plain", status=403) else: if 'email' not in request.POST or 'password' not in request.POST: return JsonResponse({ "success": False, # TODO: User error message "value": _('There was an error receiving your login information. Please email us.' ), }) # TODO: this should be status code 400 email = request.POST['email'] password = request.POST['password'] try: user = User.objects.get(email=email) except User.DoesNotExist: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u"Login failed - Unknown user email") else: AUDIT_LOG.warning( u"Login failed - Unknown user email: {0}".format(email)) # check if the user has a linked shibboleth account, if so, redirect the user to shib-login # This behavior is pretty much like what gmail does for shibboleth. Try entering some @stanford.edu # address into the Gmail login. if settings.FEATURES.get('AUTH_USE_SHIB') and user: try: eamap = ExternalAuthMap.objects.get(user=user) if eamap.external_domain.startswith( openedx.core.djangoapps.external_auth.views. SHIBBOLETH_DOMAIN_PREFIX): return JsonResponse({ "success": False, "redirect": reverse('shib-login'), }) # TODO: this should be status code 301 # pylint: disable=fixme except ExternalAuthMap.DoesNotExist: # This is actually the common case, logging in user without external linked login AUDIT_LOG.info(u"User %s w/o external auth attempting login", user) # see if account has been locked out due to excessive login failures user_found_by_email_lookup = user if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user_found_by_email_lookup): lockout_message = _( 'This account has been temporarily locked due ' 'to excessive login failures. Try again later.') return JsonResponse({ "success": False, "value": lockout_message, }) # TODO: this should be status code 429 # pylint: disable=fixme # see if the user must reset his/her password due to any policy settings if user_found_by_email_lookup and PasswordHistory.should_user_reset_password_now( user_found_by_email_lookup): return JsonResponse({ "success": False, "value": _('Your password has expired due to password policy on this account. You must ' 'reset your password before you can log in again. Please click the ' '"Forgot Password" link on this page to reset your password before logging in again.' ), }) # TODO: this should be status code 403 # pylint: disable=fixme # if the user doesn't exist, we want to set the username to an invalid # username so that authentication is guaranteed to fail and we can take # advantage of the ratelimited backend username = user.username if user else "" if not third_party_auth_successful: try: user = authenticate(username=username, password=password, request=request) # this occurs when there are too many attempts from the same IP address except RateLimitException: return JsonResponse({ "success": False, "value": _('Too many failed login attempts. Try again later.'), }) # TODO: this should be status code 429 # pylint: disable=fixme if user is None: # tick the failed login counters if the user exists in the database if user_found_by_email_lookup and LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user_found_by_email_lookup) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if username != "": if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user_found_by_email_lookup.id if user_found_by_email_lookup else "<unknown>" AUDIT_LOG.warning( u"Login failed - password for user.id: {0} is invalid". format(loggable_id)) else: AUDIT_LOG.warning( u"Login failed - password for {0} is invalid".format( email)) return JsonResponse({ "success": False, "value": _('Email or password is incorrect.'), }) # TODO: this should be status code 400 # pylint: disable=fixme # successful login, clear failed login attempts counters, if applicable if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) # Track the user's sign in if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.identify( user.id, { 'email': email, 'username': username }, { # Disable MailChimp because we don't want to update the user's email # and username in MailChimp on every page load. We only need to capture # this data on registration/activation. 'MailChimp': False }) analytics.track(user.id, "edx.bi.user.account.authenticated", { 'category': "conversion", 'label': request.POST.get('course_id'), 'provider': None }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) if user is not None and user.is_active: try: # We do not log here, because we have a handler registered # to perform logging on successful logins. login(request, user) if request.POST.get('remember') == 'true': request.session.set_expiry(604800) log.debug("Setting user session to never expire") else: request.session.set_expiry(0) except Exception as exc: # pylint: disable=broad-except AUDIT_LOG.critical( "Login failed - Could not create session. Is memcached running?" ) log.critical( "Login failed - Could not create session. Is memcached running?" ) log.exception(exc) raise redirect_url = None # The AJAX method calling should know the default destination upon success if third_party_auth_successful: redirect_url = pipeline.get_complete_url(backend_name) response = JsonResponse({ "success": True, "redirect_url": redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. return set_logged_in_cookies(request, response, user) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Account not active for user.id: {0}, resending activation" .format(user.id)) else: AUDIT_LOG.warning( u"Login failed - Account not active for user {0}, resending activation" .format(username)) reactivation_email_for_user_custom(request, user) not_activated_msg = _( "Before you sign in, you need to activate your account. We have sent you an " "email message with instructions for activating your account.") return JsonResponse({ "success": False, "value": not_activated_msg, }) # TODO: this should be status code 400 # pylint: disable=fixme
def do_create_account(form, custom_form=None): """ Given cleaned post variables, create the User and UserProfile objects, as well as the registration for this user. Returns a tuple (User, UserProfile, Registration). Note: this function is also used for creating test users. """ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation if not configuration_helpers.get_value( 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True) ): raise PermissionDenied() errors = {} errors.update(form.errors) if custom_form: errors.update(custom_form.errors) if errors: raise ValidationError(errors) proposed_username = form.cleaned_data["username"] user = User( username=proposed_username, email=form.cleaned_data["email"], is_active=False ) user.set_password(form.cleaned_data["password"]) registration = Registration() # TODO: Rearrange so that if part of the process fails, the whole process fails. # Right now, we can have e.g. no registration e-mail sent out and a zombie account try: with transaction.atomic(): user.save() if custom_form: custom_model = custom_form.save(commit=False) custom_model.user = user custom_model.save() except IntegrityError: # Figure out the cause of the integrity error # TODO duplicate email is already handled by form.errors above as a ValidationError. # The checks for duplicate email/username should occur in the same place with an # AccountValidationError and a consistent user message returned (i.e. both should # return "It looks like {username} belongs to an existing account. Try again with a # different username.") if User.objects.filter(username=user.username): raise AccountValidationError( USERNAME_EXISTS_MSG_FMT.format(username=proposed_username), field="username" ) elif email_exists_or_retired(user.email): raise AccountValidationError( _("An account with the Email '{email}' already exists.").format(email=user.email), field="email" ) else: raise # add this account creation to password history # NOTE, this will be a NOP unless the feature has been turned on in configuration password_history_entry = PasswordHistory() password_history_entry.create(user) registration.register(user) profile_fields = [ "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals", "year_of_birth" ] profile = UserProfile( user=user, **{key: form.cleaned_data.get(key) for key in profile_fields} ) extended_profile = form.cleaned_extended_profile if extended_profile: profile.meta = json.dumps(extended_profile) try: profile.save() except Exception: log.exception("UserProfile creation failed for user {id}.".format(id=user.id)) raise return user, profile, registration