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 _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.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))
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 _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: # 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)) send_reactivation_email_for_user(unauthenticated_user) raise AuthFailedError( _generate_not_activated_message(unauthenticated_user))
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 " "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 += Text(_( "If you don't have an {platform_name} account yet, " "click {register_label_strong} at the top of the page." )).format( platform_name=platform_name, register_label_strong=HTML('<strong>{register_text}</strong>').format( register_text=_('Register') ) ) raise AuthFailedError(message)
def _get_user_by_email_or_username(request): """ Finds a user object in the database based on the given request, ignores all fields except for email and username. """ if 'email_or_username' 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_username = request.POST.get('email_or_username', None) try: return USER_MODEL.objects.get( Q(username=email_or_username) | Q(email=email_or_username)) except USER_MODEL.DoesNotExist: 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 username/email {digest}")
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 _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')
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 authn_waffle.is_enabled(ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY): 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 = _( u'As an {allowed_domain} user, You must login with your {allowed_domain} {provider} account.' ).format( allowed_domain=allowed_domain, provider=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER') ) 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. """ AUDIT_LOG.warning( f"Login failed - Account not active for user.id: {unauthenticated_user.id}, resending activation" ) 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 _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 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)) raise AuthFailedError(_('Email or password is incorrect.'))
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 _authenticate_first_party(request, unauthenticated_user, third_party_auth_requested): """ Use Django authentication on the given request, using rate limiting if configured """ should_be_rate_limited = getattr(request, 'limited', False) if should_be_rate_limited: raise AuthFailedError( _('Too many failed login attempts. Try again later.')) # lint-amnesty, pylint: disable=raise-missing-from # 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) password = normalize_password(request.POST['password']) return authenticate(username=username, password=password, request=request)
def _generate_locked_out_error_message(): """ Helper function to generate error message for users consumed all login attempts. """ locked_out_period_in_sec = settings.MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS error_message = 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 http="#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)) raise AuthFailedError( error_message, error_code='account-locked-out', context={'locked_out_period': int(locked_out_period_in_sec / 60)})
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 _check_excessive_login_attempts_viatris(user,site): """ See if account has been locked out due to excessive login failures """ #log.info('site--> %s', site) if 'viatris-via' not in site: if user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user): if 'viatris-via' in site: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins. or Sign In using the One Time Password(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: #log.info('site3--> %s', site) raise AuthFailedError(_('La cuenta se ha bloqueado temporalmente debido a un numero excesivo de errores de inicio de sesion. Intentalo de nuevo despus de 5 minutos.')) elif 'viatris-pvp-i' in site or 'viatris-multimodal' in site: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins.')) elif 'viatris-atpon' in site: raise AuthFailedError(_('Konto zostalo tymczasowo zablokowane z powodu nadmiernych niepowodzen logowania. Sprobuj ponownie za 5 minut.')) elif 'viatris-norge' in site: raise AuthFailedError(_('Kontoen er midlertidig last pa grunn av store paloggingsfeil. Prov pa nytt etter 5 minutter.')) else: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins. or Sign In using the One Time Password(OTP) option.'))
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 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 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 if not should_redirect_to_logistration_mircrofrontend: # pylint: disable=no-else-raise raise AuthFailedError( 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)) else: raise AuthFailedError( Text( _('Email or password is incorrect.\n' 'You have {remaining_attempts} more sign-in ' 'attempts before your account is temporarily locked.\n' 'If you{quote}ve forgotten your password, click ' '{link_start}here{link_end} to reset.\n')). format(quote=HTML("'"), link_start=HTML('<a href="/reset" >'), link_end=HTML('</a>'), remaining_attempts=remaining_attempts)) else: _generate_locked_out_error_message() raise AuthFailedError(_('Email or password is incorrect.'))
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): if user_can_login_on_requested_edly_organization( request, possibly_authenticated_user): create_user_link_with_edly_sub_organization( request, possibly_authenticated_user) else: 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 login_user(request, api_version='v1'): # pylint: disable=too-many-statements """ AJAX request to log in the user. Arguments: request (HttpRequest) Required params: email, password Optional params: analytics: a JSON-encoded object with additional info to include in the login analytics event. The only supported field is "enroll_course_id" to indicate that the user logged in while enrolling in a particular course. Returns: HttpResponse: 200 if successful. Ex. {'success': true} HttpResponse: 400 if the request failed. Ex. {'success': false, 'value': '{'success': false, 'value: 'Email or password is incorrect.'} HttpResponse: 403 if successful authentication with a third party provider but does not have a linked account. Ex. {'success': false, 'error_code': 'third-party-auth-with-no-linked-account'} Example Usage: POST /login_ajax with POST params `email`, `password` 200 {'success': true} """ _parse_analytics_param_for_course_id(request) 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 set_custom_attribute('login_user_course_id', request.POST.get('course_id')) if is_require_third_party_auth_enabled( ) and not third_party_auth_requested: return HttpResponseForbidden( "Third party authentication is required to login. Username and password were received instead." ) possibly_authenticated_user = None 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 set_custom_attribute('login_user_tpa_success', True) except AuthFailedError as e: set_custom_attribute('login_user_tpa_success', False) set_custom_attribute('login_user_tpa_failure_msg', e.value) if e.error_code: set_custom_attribute('login_error_code', e.error_code) # user successfully authenticated with a third party provider, but has no linked Open edX account response_content = e.get_response() return JsonResponse(response_content, status=403) else: user = _get_user_by_email_or_username(request, api_version) _check_excessive_login_attempts(user) possibly_authenticated_user = user try: possibly_authenticated_user = StudentLoginRequested.run_filter( user=possibly_authenticated_user) except StudentLoginRequested.PreventLogin as exc: raise AuthFailedError( str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context, ) from exc if not is_user_third_party_authenticated: possibly_authenticated_user = _authenticate_first_party( request, user, third_party_auth_requested) 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 or settings.MARKETING_EMAILS_OPT_IN): _handle_failed_authentication(user, possibly_authenticated_user) pwned_properties = check_pwned_password_and_send_track_event( user.id, request.POST.get('password'), user.is_staff) if not is_user_third_party_authenticated else {} # Set default for third party login password_frequency = pwned_properties.get('frequency', -1) if (settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY and password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG, 'require-password-change') _handle_successful_authentication_and_login( possibly_authenticated_user, request) # The AJAX method calling should know the default destination upon success redirect_url, finish_auth_url = None, '' if third_party_auth_requested: running_pipeline = pipeline.get(request) finish_auth_url = pipeline.get_complete_url( backend_name=running_pipeline['backend']) if is_user_third_party_authenticated: redirect_url = finish_auth_url elif should_redirect_to_authn_microfrontend(): next_url, root_url = get_next_url_for_login_page(request, include_host=True) redirect_url = get_redirect_url_with_host( root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)) if (settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY and 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD): raise VulnerablePasswordError( accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG, 'nudge-password-change', redirect_url) response = JsonResponse({ 'success': True, 'redirect_url': redirect_url, }) # Ensure that the external marketing site can # detect that the user is logged in. response = set_logged_in_cookies(request, response, possibly_authenticated_user) set_custom_attribute('login_user_auth_failed_error', False) set_custom_attribute('login_user_response_status', response.status_code) set_custom_attribute('login_user_redirect_url', redirect_url) mark_user_change_as_expected(user.id) return response except AuthFailedError as error: response_content = error.get_response() log.exception(response_content) error_code = response_content.get('error_code') if error_code: set_custom_attribute('login_error_code', error_code) email_or_username_key = 'email' if api_version == API_V1 else 'email_or_username' email_or_username = request.POST.get(email_or_username_key, None) email_or_username = possibly_authenticated_user.email if possibly_authenticated_user else email_or_username response_content['email'] = email_or_username except VulnerablePasswordError as error: response_content = error.get_response() log.exception(response_content) response = JsonResponse(response_content, status=400) set_custom_attribute('login_user_auth_failed_error', True) set_custom_attribute('login_user_response_status', response.status_code) return response