def verify_user_status(cls, user: User) -> None: if not user.is_active: cls.log_event( Events.refused_login, payload={ "username": user.email, "motivation": "account not active" }, ) # Beware, frontend leverages on this exact message, # do not modified it without fix also on frontend side raise Forbidden("Sorry, this account is not active") now: Optional[datetime] = None if cls.DISABLE_UNUSED_CREDENTIALS_AFTER and user.last_login: if TESTING and user.email == cls.default_user: log.info( "Default user can't be blocked for inactivity during tests" ) else: now = get_now(user.last_login.tzinfo) if user.last_login + cls.DISABLE_UNUSED_CREDENTIALS_AFTER < now: cls.log_event( Events.refused_login, payload={ "username": user.email, "motivation": "account blocked due to inactivity", }, ) raise Forbidden( "Sorry, this account is blocked for inactivity") if user.expiration: # Reuse the now instance, if previously inizialized # tzinfo should be the same for both last_login and expiration fields if not now: now = get_now(user.expiration.tzinfo) if user.expiration < now: cls.log_event( Events.refused_login, payload={ "username": user.email, "motivation": "account expired" }, ) raise Forbidden("Sorry, this account is expired")
def count_failed_login(self, username: str) -> int: failed_logins = self.get_logins(username, only_unflushed=True) if not failed_logins: return 0 last_failed = failed_logins[-1] exp = last_failed.date + self.FAILED_LOGINS_EXPIRATION if get_now(exp.tzinfo) > exp: self.flush_failed_logins(username) return 0 return len(failed_logins)
def check_password_validity( self, user: User, totp_authentication: bool) -> Dict[str, List[str]]: # ################################################## # Check if something is missing in the authentication and ask additional actions # raises exceptions in case of errors message: Dict[str, List[str]] = {"actions": [], "errors": []} last_pwd_change = user.last_password_change if last_pwd_change is None or last_pwd_change == 0: last_pwd_change = EPOCH if self.FORCE_FIRST_PASSWORD_CHANGE and last_pwd_change == EPOCH: message["actions"].append("FIRST LOGIN") message["errors"].append("Please change your temporary password") self.log_event(Events.password_expired, user=user) if totp_authentication: message["qr_code"] = [self.get_qrcode(user)] elif self.MAX_PASSWORD_VALIDITY: valid_until = last_pwd_change + self.MAX_PASSWORD_VALIDITY # offset-naive datetime to compare with MySQL now = get_now(last_pwd_change.tzinfo) expired = last_pwd_change == EPOCH or valid_until < now if expired: message["actions"].append("PASSWORD EXPIRED") message["errors"].append( "Your password is expired, please change it") self.log_event(Events.password_expired, user=user) return message
def verify_token_validity(self, jti: str, user: User) -> bool: token_entry = self.db.Token.query.filter_by(jti=jti).first() if token_entry is None: return False if token_entry.user_id is None or token_entry.user_id != user.id: return False # offset-naive datetime to compare with MySQL now = get_now(token_entry.expiration.tzinfo) if now > token_entry.expiration: self.invalidate_token(token=token_entry.token) log.info( "This token is no longer valid: expired since {}", token_entry.expiration.strftime("%d/%m/%Y"), ) return False # Verify IP validity only after grace period is expired if token_entry.creation + self.GRACE_PERIOD < now: ip = self.get_remote_ip() if token_entry.IP != ip: log.warning( "This token is emitted for IP {}, invalid use from {}", token_entry.IP, ip, ) return False if token_entry.last_access + self.SAVE_LAST_ACCESS_EVERY < now: token_entry.last_access = now try: self.db.session.add(token_entry) self.db.session.commit() except Exception as e: # pragma: no cover log.error("DB error ({}), rolling back", e) self.db.session.rollback() return True