def _handle_failed_authentication(user, authenticated_user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if authenticated_user and not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password loggable_id = user.id if user else "<unknown>" AUDIT_LOG.warning( f"Login failed - password for user.id: {loggable_id} is invalid") if user and LoginFailures.is_feature_enabled(): blocked_threshold, failure_count = LoginFailures.check_user_reset_password_threshold( user) if blocked_threshold: if not LoginFailures.is_user_locked_out(user): max_failures_allowed = settings.MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED remaining_attempts = max_failures_allowed - failure_count error_message = Text( _('Email or password is incorrect.' '{li_start}You have {remaining_attempts} more sign-in ' 'attempts before your account is temporarily locked.{li_end}' '{li_start}If you\'ve forgotten your password, click ' '{link_start}here{link_end} to reset.{li_end}') ).format(link_start=HTML( '<a http="#login" class="form-toggle" data-type="password-reset">' ), link_end=HTML('</a>'), li_start=HTML('<li>'), li_end=HTML('</li>'), remaining_attempts=remaining_attempts) raise AuthFailedError(error_message, error_code='failed-login-attempt', context={ 'remaining_attempts': remaining_attempts, }) _generate_locked_out_error_message() raise AuthFailedError(_('Email or password is incorrect.'), error_code='incorrect-email-or-password')
def _check_user_auth_flow(site, user): """ Check if user belongs to an allowed domain and not whitelisted then ask user to login through allowed domain SSO provider. """ if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled(): allowed_domain = site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower() email_parts = user.email.split('@') if len(email_parts) != 2: # User has a nonstandard email so we record their id. # we don't record their e-mail in case there is sensitive info accidentally # in there. set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id) log.warn("User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id) return user_domain = email_parts[1].strip().lower() # If user belongs to allowed domain and not whitelisted then user must login through allowed domain SSO if user_domain == allowed_domain and not AllowedAuthUser.objects.filter(site=site, email=user.email).exists(): msg = Text(_( u'As {allowed_domain} user, You must login with your {allowed_domain} ' u'{link_start}{provider} account{link_end}.' )).format( allowed_domain=allowed_domain, link_start=HTML("<a href='{tpa_provider_link}'>").format( tpa_provider_link='{dashboard_url}?tpa_hint={tpa_hint}'.format( dashboard_url=reverse('dashboard'), tpa_hint=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'), ) ), provider=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'), link_end=HTML("</a>") ) raise AuthFailedError(msg)
def _log_and_raise_inactive_user_auth_error(unauthenticated_user): """ Depending on Django version we can get here a couple of ways, but this takes care of logging an auth attempt by an inactive user, re-sending the activation email, and raising an error with the correct message. """ if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Account not active for user.id: {0}, resending activation" .format(unauthenticated_user.id)) else: AUDIT_LOG.warning( u"Login failed - Account not active for user {0}, resending activation" .format(unauthenticated_user.username)) profile = UserProfile.objects.get(user=unauthenticated_user) compose_and_send_activation_email(unauthenticated_user, profile) raise AuthFailedError( error_code='inactive-user', context={ 'platformName': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'supportLink': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK) })
def _get_user_by_email_or_username(request, api_version): """ Finds a user object in the database based on the given request, ignores all fields except for email and username. """ is_api_v2 = api_version != API_V1 login_fields = ['email', 'password'] if is_api_v2: login_fields = ['email_or_username', 'password'] if any(f not in request.POST.keys() for f in login_fields): raise AuthFailedError( _('There was an error receiving your login information. Please email us.' )) email_or_username = request.POST.get('email', None) or request.POST.get( 'email_or_username', None) user = _get_user_by_email(email_or_username) if not user and is_api_v2: # If user not found with email and API_V2, try username lookup user = _get_user_by_username(email_or_username) if not user: digest = hashlib.shake_128( email_or_username.encode('utf-8')).hexdigest(16) # pylint: disable=too-many-function-args AUDIT_LOG.warning( f"Login failed - Unknown user email or username {digest}") return user
def test_sanitize_message(self): """ Tests that AuthFailedError HTML-escapes the message.""" script_tag = '<script>alert("vulnerable")</script>' exception = AuthFailedError(script_tag) expected_value = Text(script_tag) self.assertEqual(exception.value, expected_value)
def login_user(request): """ AJAX request to log in the user. """ third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request) first_party_auth_requested = bool(request.POST.get('email')) or bool(request.POST.get('password')) is_user_third_party_authenticated = False try: if third_party_auth_requested and not first_party_auth_requested: # 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. # This nested try is due to us only returning an HttpResponse in this # one case vs. JsonResponse everywhere else. try: user = _do_third_party_auth(request) is_user_third_party_authenticated = True except AuthFailedError as e: return HttpResponse(e.value, content_type="text/plain", status=403) else: user = _get_user_by_email(request) _check_shib_redirect(user) _check_excessive_login_attempts(user) possibly_authenticated_user = user if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party(request, 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) if possibly_authenticated_user is None or not possibly_authenticated_user.is_active: _handle_failed_authentication(user, possibly_authenticated_user) if not is_edly_user_allowed_to_login(request, possibly_authenticated_user): raise AuthFailedError(_('You are not allowed to login on this site.')) _handle_successful_authentication_and_login(possibly_authenticated_user, request) redirect_url = None # The AJAX method calling should know the default destination upon success if is_user_third_party_authenticated: running_pipeline = pipeline.get(request) redirect_url = pipeline.get_complete_url(backend_name=running_pipeline['backend']) 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, possibly_authenticated_user) except AuthFailedError as error: log.exception(error.get_response()) return JsonResponse(error.get_response())
def _check_user_auth_flow(site, user): """ Check if user belongs to an allowed domain and not whitelisted then ask user to login through allowed domain SSO provider. """ if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled(): allowed_domain = site.configuration.get_value( 'THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower() user_domain = user.email.split('@')[1].strip().lower() # If user belongs to allowed domain and not whitelisted then user must login through allowed domain SSO if user_domain == allowed_domain and not AllowedAuthUser.objects.filter( site=site, email=user.email).exists(): msg = Text( _(u'As {allowed_domain} user, You must login with your {allowed_domain} ' u'{link_start}{provider} account{link_end}.') ).format( allowed_domain=allowed_domain, link_start=HTML("<a href='{tpa_provider_link}'>").format( tpa_provider_link='{dashboard_url}?tpa_hint={tpa_hint}'. format( dashboard_url=reverse('dashboard'), tpa_hint=site.configuration.get_value( 'THIRD_PARTY_AUTH_ONLY_HINT'), )), provider=site.configuration.get_value( 'THIRD_PARTY_AUTH_ONLY_PROVIDER'), link_end=HTML("</a>")) raise AuthFailedError(msg)
def _check_user_auth_flow(site, user): """ Check if user belongs to an allowed domain and not whitelisted then ask user to login through allowed domain SSO provider. """ if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled(): allowed_domain = site.configuration.get_value( 'THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower() email_parts = user.email.split('@') if len(email_parts) != 2: # User has a nonstandard email so we record their id. # we don't record their e-mail in case there is sensitive info accidentally # in there. set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id) log.warn( "User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id) return user_domain = email_parts[1].strip().lower() # If user belongs to allowed domain and not whitelisted then user must login through allowed domain SSO if user_domain == allowed_domain and not AllowedAuthUser.objects.filter( site=site, email=user.email).exists(): if not should_redirect_to_logistration_mircrofrontend(): msg = _create_message(site, None, allowed_domain) else: root_url = configuration_helpers.get_value( 'LMS_ROOT_URL', settings.LMS_ROOT_URL) msg = _create_message(site, root_url, allowed_domain) raise AuthFailedError(msg)
def _get_user_by_email(request): """ Finds a user object in the database based on the given request, ignores all fields except for email. Increase the user logic by phone number. """ if 'email' not in request.POST or 'password' not in request.POST: raise AuthFailedError( _('There was an error receiving your login information. Please email us.' )) email_or_phone = request.POST['email'] try: return User.objects.get(email=email_or_phone) except User.DoesNotExist: try: return UserProfile.objects.get(phone=email_or_phone).user except ObjectDoesNotExist: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Unknown user email or phone") else: AUDIT_LOG.warning( u"Login failed - Unknown user email or phone: {0}".format( email_or_phone))
def _authenticate_first_party(request, unauthenticated_user, third_party_auth_requested): """ Use Django authentication on the given request, using rate limiting if configured """ # 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 = unauthenticated_user.username if unauthenticated_user else "" # First time when a user login through third_party_auth account then user needs to link # third_party account with the platform account by login through email and password that's # why we need to by-pass this check when user is already authenticated by third_party_auth. if not third_party_auth_requested: _check_user_auth_flow(request.site, unauthenticated_user) try: password = normalize_password(request.POST['password']) return authenticate(username=username, password=password, request=request) # This occurs when there are too many attempts from the same IP address except RateLimitException: raise AuthFailedError( _('Too many failed login attempts. Try again later.'))
def _do_third_party_auth(request): """ User is already authenticated via 3rd party, now try to find and return their associated Django user. """ 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) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) try: return pipeline.get_authenticated_user(requested_provider, username, third_party_uid) except User.DoesNotExist: AUDIT_LOG.info( u"Login failed - user with username {username} has no social auth " u"with backend_name {backend_name}".format( username=username, backend_name=backend_name)) message = Text( _(u"You've successfully signed in to your {provider_name} account, " u"but this account isn't linked with your {platform_name} account yet. {blank_lines}" u"Use your {platform_name} username and password to sign in to {platform_name} below, " u"and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}" u"If you don't have an account on {platform_name} yet, " u"click {register_label_strong} at the top of the page.") ).format(blank_lines=HTML('<br/><br/>'), platform_name=platform_name, provider_name=requested_provider.name, register_label_strong=HTML( '<strong>{register_text}</strong>').format( register_text=_('Register'))) raise AuthFailedError(message)
def _handle_failed_authentication(self, user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user and LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) raise AuthFailedError(_('Email or password is incorrect.'))
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 _check_excessive_login_attempts(self, user): """ See if account has been locked out due to excessive login failures """ if user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user): raise AuthFailedError(_('Due to multiple login failures, the account is temporarily locked.' ' Please try again after 15 minutes.'))
def _check_excessive_login_attempts(user): """ See if account has been locked out due to excessive login failures """ if user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user): raise AuthFailedError(_('This account has been temporarily locked due ' 'to excessive login failures. Try again later.'))
def _enforce_password_policy_compliance(request, user): try: password_policy_compliance.enforce_compliance_on_login(user, request.POST.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) except password_policy_compliance.NonCompliantPasswordException as e: # Prevent the login attempt. raise AuthFailedError(e.message)
def _get_login_oauth_client(): """ Returns the configured OAuth Client/Application used for Login. """ login_client_id = settings.JWT_AUTH['JWT_LOGIN_CLIENT_ID'] try: return Application.objects.get(client_id=login_client_id) except Application.DoesNotExist: raise AuthFailedError( u"OAuth Client for the Login service, '{}', is not configured.". format(login_client_id))
def _get_login_oauth_client(): """ Returns the configured OAuth Client/Application used for Login. """ login_client_id = settings.JWT_AUTH['JWT_LOGIN_CLIENT_ID'] try: return Application.objects.get(client_id=login_client_id) except Application.DoesNotExist: raise AuthFailedError( # lint-amnesty, pylint: disable=raise-missing-from f"OAuth Client for the Login service, '{login_client_id}', is not configured." )
def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint: disable=missing-function-docstring try: password_policy_compliance.enforce_compliance_on_login(user, request.POST.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, str(e)) except password_policy_compliance.NonCompliantPasswordException as e: AUDIT_LOG.info("Password reset initiated for email %s.", user.email) send_password_reset_email_for_user(user, request) # Prevent the login attempt. raise AuthFailedError(HTML(str(e)), error_code=e.__class__.__name__) # lint-amnesty, pylint: disable=raise-missing-from
def _enforce_password_policy_compliance(request, user): try: password_policy_compliance.enforce_compliance_on_login( user, request.POST.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, six.text_type(e)) except password_policy_compliance.NonCompliantPasswordException as e: send_password_reset_email_for_user(user, request) # Prevent the login attempt. raise AuthFailedError(HTML(six.text_type(e)), error_code=e.__class__.__name__)
def _generate_locked_out_error_message(): locked_out_period_in_sec = settings.MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS raise AuthFailedError(Text(_('To protect your account, it’s been temporarily ' 'locked. Try again in {locked_out_period} minutes.' '{li_start}To be on the safe side, you can reset your ' 'password {link_start}here{link_end} before you try again.')).format( link_start=HTML('<a "#login" class="form-toggle" data-type="password-reset">'), link_end=HTML('</a>'), li_start=HTML('<li>'), li_end=HTML('</li>'), locked_out_period=int(locked_out_period_in_sec / 60)))
def _handle_failed_authentication_viatris(user,site): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if 'viatris-via' not in site: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user.id if user 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(user.email)) if 'viatris-via' in site: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link or Sign In using the OTP option.By entering you email address you will receive a OTP (One Time Password) by email that will be valid for 2 minutes and you can use it to sign-in into the VIA platform and join lectures.')) elif 'viatris-kreon' in site or 'viatris-farmaciaformacion' in site: raise AuthFailedError(_('Contrasena incorrecta. Puede restablecer su contrasena utilizando el enlace Olvide mi contrasena.')) elif 'viatris-pvp-i' in site or 'viatris.' in site or 'viatris-multimodal' in site: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link.')) elif 'viatris-atpon' in site: raise AuthFailedError(_('Niepoprawne haslo. Mozesz zresetowac swoje haslo, korzystajac z linku Nie pamietam hasla.')) elif 'viatris-norge' in site: raise AuthFailedError(_('Du kan tilbakestille passordet ved a bruke Glemt passord - koblingen eller Logg pa med OTP-alternativet.Ved a skrive inn din e-postadresse vil du motta et OTP (engangspassord) pa e-post som vil vaere gyldig i 2 minutter, og som du kan bruke til a logge deg pa norge-plattformen og bli med pa forelesninger.')) else: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link or Sign In using the OTP option.'))
def _get_user_by_email(request): """ 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: raise AuthFailedError(_('There was an error receiving your login information. Please email us.')) email = request.POST['email'] try: return USER_MODEL.objects.get(email=email) except USER_MODEL.DoesNotExist: digest = hashlib.shake_128(email.encode('utf-8')).hexdigest(16) # pylint: disable=too-many-function-args AUDIT_LOG.warning(f"Login failed - Unknown user email {digest}")
def _check_shib_redirect(user): """ See 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): raise AuthFailedError('', redirect=reverse('shib-login')) 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)
def refresh_jwt_cookies(request, response): """ Resets the JWT related cookies in the response, while expecting a refresh cookie in the request. """ try: refresh_token = request.COOKIES[jwt_cookies.jwt_refresh_cookie_name()] except KeyError: raise AuthFailedError(u"JWT Refresh Cookie not found in request.") # TODO don't extend the cookie expiration - reuse value from existing cookie cookie_settings = standard_cookie_settings(request) _create_and_set_jwt_cookies(response, request, cookie_settings, refresh_token=refresh_token) return response
def refresh_jwt_cookies(request, response): """ Resets the JWT related cookies in the response, while expecting a refresh cookie in the request. """ if JWT_COOKIES_FLAG.is_enabled(): try: refresh_token = request.COOKIES[ jwt_cookies.jwt_refresh_cookie_name()] except KeyError: raise AuthFailedError(u"JWT Refresh Cookie not found in request.") _create_and_set_jwt_cookies(response, request, refresh_token=refresh_token) return response
def _log_and_raise_inactive_user_auth_error(unauthenticated_user): """ Depending on Django version we can get here a couple of ways, but this takes care of logging an auth attempt by an inactive user, re-sending the activation email, and raising an error with the correct message. """ if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Account not active for user.id: {0}, resending activation" .format(unauthenticated_user.id)) else: AUDIT_LOG.warning( u"Login failed - Account not active for user {0}, resending activation" .format(unauthenticated_user.username)) send_reactivation_email_for_user(unauthenticated_user) raise AuthFailedError( _generate_not_activated_message(unauthenticated_user))
def _authenticate_first_party(request, unauthenticated_user): """ Use Django authentication on the given request, using rate limiting if configured """ # 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 = unauthenticated_user.username if unauthenticated_user else "" try: return authenticate( username=username, password=request.POST['password'], request=request) # This occurs when there are too many attempts from the same IP address except RateLimitException: raise AuthFailedError(_('Too many failed login attempts. Try again later.'))
def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint: disable=missing-function-docstring try: password_policy_compliance.enforce_compliance_on_login( user, request.POST.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, HTML(str(e))) except password_policy_compliance.NonCompliantPasswordException as e: # Increment the lockout counter to safguard from further brute force requests # if user's password has been compromised. if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) AUDIT_LOG.info("Password reset initiated for email %s.", user.email) send_password_reset_email_for_user(user, request) # Prevent the login attempt. raise AuthFailedError(HTML(str(e)), error_code=e.__class__.__name__) # lint-amnesty, pylint: disable=raise-missing-from
def _log_and_raise_inactive_user_auth_error(unauthenticated_user): """ Depending on Django version we can get here a couple of ways, but this takes care of logging an auth attempt by an inactive user, re-sending the activation email, and raising an error with the correct message. """ if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u"Login failed - Account not active for user.id: {0}, resending activation" .format(unauthenticated_user.id)) else: AUDIT_LOG.warning( u"Login failed - Account not active for user {0}, resending activation" .format(unauthenticated_user.username)) profile = UserProfile.objects.get(user=unauthenticated_user) compose_and_send_activation_email(unauthenticated_user, profile) raise AuthFailedError(error_code='inactive-user')