def login(self, ctx, _ok=status_code.ok, _now=datetime.utcnow, _timedelta=timedelta, _dummy_password=uuid4().hex, is_logged_in_ext=True): """ Logs a user in, returning session info on success or raising ValidationError on any error. """ # type: (LoginCtx, unicode, datetime, timedelta, unicode, bool) -> SessionInfo # Look up user and raise exception if not found by username with closing(self.odb_session_func()) as session: if ctx.input.get('username'): user = get_user_by_username( session, ctx.input['username']) # type: SSOUser else: user = get_user_by_id(session, ctx.input['user_id']) # type: SSOUser # If the user is already logged in externally, this flag will be True, # in which case we do not check the credentials - we already know they are valid # because they were checked externally and user_id is the SSO user linked to the # already validated external credentials. if not is_logged_in_ext: # Check credentials first to make sure that attackers do not learn about any sort # of metadata (e.g. is the account locked) if they do not know username and password. if not self._check_credentials( ctx, user.password if user else _dummy_password): raise ValidationError(status_code.auth.not_allowed, False) # Check input TOTP key if two-factor authentication is enabled if user.is_totp_enabled: input_totp_code = ctx.input.get('totp_code') if not input_totp_code: logger.warn('Missing TOTP code; user `%s`', user.username) raise ValidationError(status_code.auth.not_allowed, False) else: user_totp_key = self.decrypt_func(user.totp_key) if not CryptoManager.verify_totp_code( user_totp_key, input_totp_code): logger.warn('Invalid TOTP code; user `%s`', user.username) raise ValidationError(status_code.auth.not_allowed, False) # It must be possible to log into the application requested (CRM above) self._check_login_to_app_allowed(ctx) # Common auth checks self._run_user_checks(ctx, user) # We assume that we will not have to warn about an approaching password expiry has_w_about_to_exp = False # If applicable, password may be about to expire (this must be after checking that it has not already). # Note that it may return a specific status to return (warning or error) _about_status = self._check_password_about_to_expire(user) if _about_status is not True: if _about_status == status_code.warning: has_w_about_to_exp = True else: raise ValidationError(status_code.password.e_about_to_exp, False, _about_status) # If password is marked as requiring a change upon next login but a new one was not sent, reject the request. self._check_must_send_new_password(ctx, user) # If new password is required, we need to validate and save it before session can be created. # Note that at this point we already know that the old password was correct so it is safe to set the new one # if it is confirmed to be valid. We also know that there is some new password on input because otherwise # the check above would have raised a ValidationError. if user.password_must_change: try: validate_password(self.sso_conf, ctx.input.get('new_password')) except ValidationError as e: if e.return_status: raise ValidationError(e.sub_status, e.return_status, e.status) else: set_password(self.odb_session_func, self.encrypt_func, self.hash_func, self.sso_conf, user.user_id, ctx.input['new_password'], False) # All validated, we can create a session object now creation_time = _now() expiration_time = creation_time + timedelta( minutes=self.sso_conf.session.expiry) ust = new_user_session_token() # Create current interaction details for this session session_state_change_list = [] self.update_session_state_change_list(session_state_change_list, ctx.remote_addr, ctx.user_agent, 'login', creation_time) opaque = {'session_state_change_list': session_state_change_list} session.execute(SessionModelInsert().values({ 'ust': ust, 'creation_time': creation_time, 'expiration_time': expiration_time, 'user_id': user.id, 'auth_type': const.auth_type.default, 'auth_principal': user.username, 'remote_addr': ', '.join(str(elem) for elem in ctx.remote_addr), 'user_agent': ctx.user_agent, 'ext_session_id': ctx.ext_session_id, GENERIC.ATTR_NAME: dumps(opaque) })) session.commit() info = SessionInfo() info.username = user.username info.user_id = user.user_id info.ust = self.encrypt_func(ust.encode('utf8')) info.creation_time = creation_time info.expiration_time = expiration_time info.has_w_about_to_exp = has_w_about_to_exp return info
def login(self, ctx, _ok=status_code.ok, _now=datetime.utcnow, _timedelta=timedelta): """ Logs a user in, returning session info on success or raising ValidationError on any error. """ # Look up user and raise exception if not found by username with closing(self.odb_session_func()) as session: user = get_user_by_username(session, ctx.input['username']) if not user: raise ValidationError(status_code.auth.not_allowed, False) # Check credentials first to make sure that attackers do not learn about any sort # of metadata (e.g. is the account locked) if they do not know username and password. if not self._check_credentials(ctx, user): raise ValidationError(status_code.auth.not_allowed, False) # It must be possible to log into the application requested (CRM above) self._check_login_to_app_allowed(ctx) # Common auth checks self._run_user_checks(ctx, user) # If applicable, password may not be about to expire (this must be after checking that it has not already). # Note that it may return a specific status to return (warning or error) _about_status = self._check_password_about_to_expire(user) if _about_status is not True: if _about_status == status_code.warning: _status_code = status_code.password.w_about_to_exp inform = True else: _status_code = status_code.password.e_about_to_exp inform = False raise ValidationError(_status_code, inform, _about_status) # If password is marked as as requiring a change upon next login but a new one was not sent, reject the request. self._check_must_send_new_password(ctx, user) # If new password is required, we need to validate and save it before session can be created. # Note that at this point we already know that the old password was correct so it is safe to set the new one # if it is confirmed to be valid. We also know that there is some new password on input because otherwise # the check above would have raised a ValidationError. if user.password_must_change: try: validate_password(self.sso_conf, ctx.input['new_password']) except ValidationError as e: if e.return_status: raise ValidationError(e.sub_status, e.return_status, e.status) else: set_password(self.odb_session_func, self.encrypt_func, self.hash_func, self.sso_conf, user.user_id, ctx.input['new_password'], False) # All validated, we can create a session object now creation_time = _now() expiration_time = creation_time + timedelta( minutes=self.sso_conf.session.expiry) ust = new_user_session_token() session.execute(SessionModelInsert().values({ 'ust': ust, 'creation_time': creation_time, 'expiration_time': expiration_time, 'user_id': user.id, 'remote_addr': ', '.join(str(elem) for elem in ctx.remote_addr), 'user_agent': ctx.user_agent, })) session.commit() info = SessionInfo() info.username = user.username info.user_id = user.user_id info.ust = self.encrypt_func(ust.encode('utf8')) info.creation_time = creation_time info.expiration_time = expiration_time return info