require_dependency 'carto/oauth/github/config' require_dependency 'carto/oauth/google/config' require_dependency 'carto/saml_service' require_dependency 'carto/username_proposer' require_dependency 'carto/email_cleaner' require_relative '../../lib/user_account_creator' require_relative '../../lib/cartodb/stats/authentication' class SessionsController < ApplicationController include ActionView::Helpers::DateHelper include LoginHelper include Carto::EmailCleaner SESSION_EXPIRED = 'session_expired'.freeze PASSWORD_LOCKED = 'password_locked'.freeze MULTIFACTOR_AUTHENTICATION_INACTIVITY = 'multifactor_authentication_inactivity'.freeze MAX_MULTIFACTOR_AUTHENTICATION_INACTIVITY = 120.seconds layout 'frontend' ssl_required :new, :create, :destroy, :show, :unauthenticated, :account_token_authentication_error, :ldap_user_not_at_cartodb, :saml_user_not_in_carto, :password_expired, :password_change, :password_locked, :multifactor_authentication, :multifactor_authentication_verify_code skip_before_filter :ensure_org_url_if_org_user # Don't force org urls # Disables CSRF protection for the login view (create). I *think* this is safe # since the only transaction that a user can be tricked into doing is logging in # and login won't be accepted if the ADFS server's fingerprint is wrong / missing. # If SAML data isn't passed at all, then authentication is manually failed. # In case of fallback on SAML authorization failed, it will be manually checked. skip_before_filter :verify_authenticity_token, only: [:create], if: :saml_authentication? # We want the password expiration related methods to be executed regardless of CSRF token authenticity skip_before_filter :verify_authenticity_token, only: [:password_expired], if: :json_formatted_request? skip_before_filter :ensure_account_has_been_activated, only: [:account_token_authentication_error, :ldap_user_not_at_cartodb, :saml_user_not_in_carto] before_filter :load_organization before_filter :initialize_oauth_config before_filter :api_authorization_required, only: :show after_action :set_last_mfa_activity, only: [:multifactor_authentication, :multifactor_authentication_verify_code] PLEASE_LOGIN = 'Please, log in to continue using CARTO.'.freeze def new if current_viewer redirect_to(CartoDB.url(self, 'dashboard', params: { trailing_slash: true }, user: current_viewer)) elsif saml_authentication? && !user # Automatically trigger SAML request on login view load -- could easily trigger this elsewhere redirect_to(saml_service.authentication_request) elsif central_enabled? && !@organization.try(:auth_enabled?) url = Cartodb::Central.new.login_url url += "?error=#{params[:error]}" if params[:error].present? redirect_to(url) else if params[:error] == SESSION_EXPIRED @flash_login_error = 'Your session has expired. ' + PLEASE_LOGIN elsif params[:error] == PASSWORD_LOCKED wait_text = time_ago_in_words(Time.now + params[:retry_after].to_i.seconds, include_seconds: true) @flash_login_error = 'Too many failed login attempts.' + " Please, wait #{wait_text} or reset your password to continue using CARTO." elsif params[:error] == MULTIFACTOR_AUTHENTICATION_INACTIVITY @flash_login_error = 'You\'ve been logged out due to a long time of inactivity. ' + PLEASE_LOGIN end render end end def create strategies, username = saml_strategy_username || ldap_strategy_username || credentials_strategy_username unless strategies return saml_authentication? ? render_403 : render(action: 'new') end candidate_user = Carto::User.where(username: username).first if central_enabled? && @organization && candidate_user && !candidate_user.belongs_to_organization?(@organization) @flash_login_error = 'The user is not part of the organization' @user_login_url = Cartodb::Central.new.login_url return render(action: 'new') end user = authenticate!(*strategies, scope: username) CartoDB::Stats::Authentication.instance.increment_login_counter(user.email) redirect_to after_login_url(user) end def destroy saml_authentication? && saml_service.try(:logout_url_configured?) ? saml_logout : do_logout end def show render :json => {:email => current_user.email, :uid => current_user.id, :username => current_user.username} end def multifactor_authentication @user = current_viewer return redirect_to after_login_url(@user) unless multifactor_authentication_required? @mfa = @user.active_multifactor_authentication render action: 'multifactor_authentication' rescue Carto::UnauthorizedError, Warden::NotAuthenticated unauthenticated end def multifactor_authentication_verify_code user = ::User.where(id: params[:user_id]).first url = after_login_url(user) if params[:skip] == "true" && user.active_multifactor_authentication.needs_setup? disable_mfa(user.id) else return multifactor_authentication_inactivity if mfa_inactivity_period_expired?(user) retry_after = user.password_login_attempt if retry_after != ::User::LOGIN_NOT_RATE_LIMITED cdb_logout return password_locked(retry_after) end user.active_multifactor_authentication.verify!(params[:code]) user.reset_password_rate_limit end warden.session(user.username)[:multifactor_authentication_performed] = true redirect_to url rescue Carto::UnauthorizedError, Warden::NotAuthenticated unauthenticated end def unauthenticated username = extract_username(request, params) CartoDB::Stats::Authentication.instance.increment_failed_login_counter(username) # Use an instance variable to show the error instead of the flash hash. Setting the flash here means setting # the flash for the next request and we want to show the message only in the current one @login_error = if mfa_request? 'Verification code is not valid' elsif params[:email].blank? && params[:password].blank? 'Can\'t be blank' else 'Your account or your password is not ok' end respond_to do |format| format.html do return multifactor_authentication if mfa_request? return render action: 'new' end format.json do head :unauthorized end end end def account_token_authentication_error warden.custom_failure! user_id = warden.env['warden.options'][:user_id] if warden.env['warden.options'] @user = ::User.where(id: user_id).first if user_id end # Meant to be called always from warden LDAP authentication def ldap_user_not_at_cartodb render action: 'new' and return unless verify_warden_failure username = warden.env['warden.options'][:cartodb_username] organization_id = warden.env['warden.options'][:organization_id] email = warden.env['warden.options'][:ldap_email].blank? ? nil : warden.env['warden.options'][:ldap_email] created_via = Carto::UserCreation::CREATED_VIA_LDAP create_user(username, organization_id, email, created_via) end def saml_user_not_in_carto # ensure to be called only from warden SAML authentication unless verify_warden_failure render action: 'new' return end saml_email = warden.env['warden.options'][:saml_email] username = CartoDB::UserAccountCreator.email_to_username(saml_email) unique_username = Carto::UsernameProposer.find_unique(username) organization_id = warden.env['warden.options'][:organization_id] created_via = Carto::UserCreation::CREATED_VIA_SAML create_user(unique_username, organization_id, saml_email, created_via) end def verify_warden_failure warden.custom_failure! warden.env['warden.options'] end def password_change username = warden.env['warden.options'][:username] if warden.env['warden.options'] redirect_to edit_password_change_url(username) if username end def password_locked(retry_after = warden.env['warden.options'][:retry_after]) warden.custom_failure! redirect_to login_url + "?error=#{PASSWORD_LOCKED}&retry_after=#{retry_after}" end def password_expired warden.custom_failure! cdb_logout session[:return_to] = request.original_url respond_to do |format| format.html do url = central_enabled? && !@organization.try(:auth_enabled?) ? Cartodb::Central.new.login_url : login_url redirect_to(url + "?error=#{SESSION_EXPIRED}") end format.json do render(json: { error: SESSION_EXPIRED }, status: 403) end end end def multifactor_authentication_inactivity warden.custom_failure! cdb_logout redirect_to login_url + "?error=#{MULTIFACTOR_AUTHENTICATION_INACTIVITY}" end def create_user(username, organization_id, email, created_via, &config_account_creator_block) @organization = ::Organization.where(id: organization_id).first account_creator = CartoDB::UserAccountCreator.new(created_via) account_creator.with_organization(@organization) .with_username(username) account_creator.with_email(email) unless email.nil? # Allows externals gears to override this method and add further configuration to the # account creator config_account_creator_block.call(account_creator) if config_account_creator_block.present? if account_creator.valid? creation_data = account_creator.enqueue_creation(self) flash.now[:success] = 'User creation in progress' @user_creation_id = creation_data[:id] @user_name = creation_data[:id] @redirect_url = CartoDB.url(self, 'login') render 'shared/signup_confirmation' else errors = account_creator.validation_errors CartoDB.notify_debug('User not valid at signup', { errors: errors } ) @signup_source = created_via.upcase @signup_errors = errors render 'shared/signup_issue' end rescue StandardError => e new_user = account_creator.nil? ? "account_creator nil" : account_creator.user.inspect CartoDB.report_exception(e, "Creating user", new_user: new_user) flash.now[:error] = e.message render action: 'new' end protected def initialize_oauth_config @oauth_configs = [google_config, github_config].compact end def google_config unless @organization && !@organization.auth_google_enabled Carto::Oauth::Google::Config.instance(form_authenticity_token, google_oauth_url, invitation_token: params[:invitation_token], organization_name: @organization.try(:name)) end end def github_config unless @organization && !@organization.auth_github_enabled Carto::Oauth::Github::Config.instance(form_authenticity_token, github_url, invitation_token: params[:invitation_token], organization_name: @organization.try(:name)) end end private def mfa_request? params[:code].presence || params[:skip].presence end def set_last_mfa_activity user = ::User.where(id: params[:user_id]).first || current_viewer warden.session(user.username)[:multifactor_authentication_last_activity] = Time.now.to_i if user rescue Warden::NotAuthenticated end def mfa_inactivity_period_expired?(user) return false unless warden.session(user.username)[:multifactor_authentication_last_activity] time_inactive = Time.now.to_i - warden.session(user.username)[:multifactor_authentication_last_activity] time_inactive > MAX_MULTIFACTOR_AUTHENTICATION_INACTIVITY rescue Warden::NotAuthenticated end def after_login_url(user) return login_url unless user session.delete('return_to') || (user.public_url + CartoDB.path(self, 'dashboard', trailing_slash: true)) end def central_enabled? Cartodb::Central.sync_data_with_cartodb_central? end def extract_username(request, params) # params[:email] can contain a username email = params[:email] username = if email.present? email.include?('@') ? username_from_user_by_email(params[:email]) : email else CartoDB.extract_subdomain(request) end username.strip.downcase if username end def username_from_user_by_email(email) ::User.where(email: clean_email(email)).first.try(:username) end def ldap_strategy_username if ldap_authentication? username = params[:user_domain].present? ? params[:user_domain] : params[:email] # INFO: LDAP allows characters that we don't [[:ldap, :password], Carto::Ldap::Manager.sanitize_for_cartodb(username)] end end def saml_strategy_username if saml_authentication? email = saml_service.get_user_email(params[:SAMLResponse]) if email [:saml, username_from_user_by_email(email)] else # This stops trying other strategies. Important because CSRF is not checked for SAML. [nil, nil] end end end def credentials_strategy_username [:password, extract_username(request, params)] if user_password_authentication? end def user_password_authentication? params && params['email'].present? && params['password'].present? end def ldap_authentication? Carto::Ldap::Manager.new.configuration_present? end def saml_authentication? saml_service.try(:enabled?) end def saml_service if load_organization @saml_service ||= Carto::SamlService.new(load_organization) end end def load_organization return @organization if @organization if current_viewer @organization = current_viewer.organization else subdomain = CartoDB.extract_subdomain(request) @organization = Carto::Organization.where(name: subdomain).first if subdomain end end def do_logout # Make sure sessions are destroyed on both scopes: username and default cdb_logout redirect_to default_logout_url end def saml_logout if params[:SAMLRequest] # If we're given a logout request, handle it in the IdP logout initiated method redirect_to saml_service.idp_logout_request(params[:SAMLRequest], params[:RelayState]) { cdb_logout } elsif params[:SAMLResponse] # We've been given a response back from the IdP, process it begin saml_service.process_logout_response(params[:SAMLResponse]) rescue StandardError => e log_warning(exception: e, message: 'Error proccessing SAML logout') ensure cdb_logout end redirect_to default_logout_url else # Initiate SLO (send Logout Request) redirect_to saml_service.sp_logout_request(current_user) end end def default_logout_url # User could've been just deleted username = CartoDB.extract_subdomain(request) if username && (Carto::User.exists?(username: username) || Carto::Organization.exists?(name: username)) CartoDB.url(self, 'public_visualizations_home') elsif Cartodb::Central.sync_data_with_cartodb_central? "https://carto.com" else "/404.html" end end def disable_mfa(user_id) service = Carto::UserMultifactorAuthUpdateService.new(user_id: user_id) service.update(enabled: false) end end