Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 792bf222de | |||
| 03725fc679 | |||
| c82de18830 | |||
| df42971011 | |||
| 45618e6834 | |||
| 0c75fc15da | |||
| c3f4e91ed4 | |||
| 6fdd1cd8c8 | |||
| 7348388341 | |||
| 172ada0349 | |||
| fde5ef1419 | |||
| c97375d63f | |||
| b502337c06 |
@@ -316,6 +316,7 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
|
||||
trigger:
|
||||
branch:
|
||||
- add_some_branch_you_need
|
||||
- chore/fix-vulnerability
|
||||
event:
|
||||
- push
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 默认:自动识别文本,统一用 LF 存库
|
||||
* text=auto eol=lf
|
||||
|
||||
# 明确常见文本文件用 LF
|
||||
*.py text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.json text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.cfg text eol=lf
|
||||
*.ini text eol=lf
|
||||
|
||||
# Windows 脚本保留 CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# 二进制:禁止任何换行转换和 diff
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.tar binary
|
||||
*.gz binary
|
||||
*.7z binary
|
||||
*.mp4 binary
|
||||
*.docx binary
|
||||
*.xlsx binary
|
||||
*.pptx binary
|
||||
@@ -10,8 +10,5 @@ RUN poetry update --without dev
|
||||
# patch langchain-openai lib. remove this when langchain-openai support reasoning_content
|
||||
RUN patch -p1 < /app/bisheng/patches/langchain_openai.patch /usr/local/lib/python3.10/site-packages/langchain_openai/chat_models/base.py
|
||||
|
||||
# patch fastapi-jwt-auth lib. remove this when remove fastapi-jwt-auth
|
||||
# fix fastapi-jwt-auth not support pydantic:v2
|
||||
RUN patch -p1 < /app/bisheng/patches/fastapi_jwt_auth.patch /usr/local/lib/python3.10/site-packages/fastapi_jwt_auth/config.py
|
||||
|
||||
CMD ["sh entrypoint.sh"]
|
||||
|
||||
+32
-36
@@ -2,53 +2,49 @@ FROM python:3.10-slim
|
||||
|
||||
ARG PANDOC_ARCH=amd64
|
||||
ENV PANDOC_ARCH=$PANDOC_ARCH
|
||||
ENV PATH="${PATH}:/root/.local/bin"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo \
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib \
|
||||
deb-src https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib \
|
||||
deb https://mirrors.aliyun.com/debian-security/ bookworm-security main \
|
||||
deb-src https://mirrors.aliyun.com/debian-security/ bookworm-security main \
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib \
|
||||
deb-src https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib \
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib \
|
||||
deb-src https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib \
|
||||
> /etc/apt/sources.list
|
||||
# 使用国内源 + 安装依赖(合并指令、清理缓存、禁用推荐包)
|
||||
RUN echo "\
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm main non-free non-free-firmware contrib\n\
|
||||
deb https://mirrors.aliyun.com/debian-security/ bookworm-security main\n\
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free non-free-firmware contrib\n\
|
||||
deb https://mirrors.aliyun.com/debian/ bookworm-backports main non-free non-free-firmware contrib" \
|
||||
> /etc/apt/sources.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gcc g++ curl build-essential postgresql-server-dev-all libreoffice \
|
||||
wget procps vim fonts-wqy-zenhei \
|
||||
libglib2.0-0 libsm6 libxrender1 libxext6 libgl1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# 安装 pandoc
|
||||
RUN mkdir -p /opt/pandoc && \
|
||||
cd /opt/pandoc && \
|
||||
wget https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz && \
|
||||
tar xvf pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz && \
|
||||
cp pandoc-3.6.4/bin/pandoc /usr/bin/ && \
|
||||
rm -rf /opt/pandoc
|
||||
|
||||
# Install lib
|
||||
RUN apt-get update && apt-get install gcc g++ curl build-essential postgresql-server-dev-all wget libreoffice -y
|
||||
RUN apt-get update && apt-get install procps -y
|
||||
|
||||
# Install pandoc
|
||||
RUN mkdir -p /opt/pandoc \
|
||||
&& cd /opt/pandoc \
|
||||
&& wget https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz \
|
||||
&& tar xvf pandoc-3.6.4-linux-${PANDOC_ARCH}.tar.gz \
|
||||
&& cd pandoc-3.6.4 \
|
||||
&& cp bin/pandoc /usr/bin/ \
|
||||
&& cd ..
|
||||
|
||||
# Install font
|
||||
RUN apt install vim fonts-wqy-zenhei -y
|
||||
# opencv
|
||||
RUN apt-get update && apt-get install -y libglib2.0-0 libsm6 libxrender1 libxext6 libgl1
|
||||
# 安装 Poetry
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.2
|
||||
# # Add Poetry to PATH
|
||||
ENV PATH="${PATH}:/root/.local/bin"
|
||||
|
||||
# 拷贝项目依赖文件
|
||||
COPY ./pyproject.toml ./
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN python -m pip install --upgrade pip && \
|
||||
pip install shapely==2.0.1
|
||||
pip install shapely==2.0.1 && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --no-interaction --no-ansi --without dev
|
||||
|
||||
# Install dependencies
|
||||
RUN poetry config virtualenvs.create false
|
||||
RUN poetry install --no-interaction --no-ansi --without dev
|
||||
# 安装 NLTK 数据
|
||||
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger'); nltk.download('averaged_perceptron_tagger_eng')"
|
||||
|
||||
# install nltk_data
|
||||
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger'); nltk.download('averaged_perceptron_tagger_eng'); "
|
||||
COPY . .
|
||||
|
||||
CMD ["sh", "entrypoint.sh"]
|
||||
|
||||
CMD ["sh entrypoint.sh"]
|
||||
|
||||
@@ -13,9 +13,9 @@ from langchain_core.language_models import BaseLanguageModel
|
||||
from langchain_core.prompts import HumanMessagePromptTemplate, PromptTemplate
|
||||
|
||||
try:
|
||||
from llama_index.node_parser import SimpleNodeParser
|
||||
from llama_index.readers.schema import Document as LlamaindexDocument
|
||||
from llama_index.schema import BaseNode
|
||||
from llama_index.core.node_parser import SimpleNodeParser
|
||||
from llama_index.core.schema import Document as LlamaindexDocument
|
||||
from llama_index.core.schema import BaseNode
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"llama_index must be installed to use this function. "
|
||||
|
||||
@@ -8,10 +8,10 @@ import pandas as pd
|
||||
nest_asyncio.apply()
|
||||
from collections import defaultdict
|
||||
|
||||
from llama_index import ServiceContext
|
||||
from llama_index.core import ServiceContext
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
from llama_index.evaluation import CorrectnessEvaluator
|
||||
from llama_index.llms import OpenAI
|
||||
from llama_index.core.evaluation import CorrectnessEvaluator
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from tqdm import tqdm
|
||||
|
||||
openai_api_key = os.environ.get('OPENAI_API_KEY', '')
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""FastAPI extension that provides JWT Auth support (secure, easy to use and lightweight)"""
|
||||
|
||||
__version__ = "0.5.0"
|
||||
|
||||
from .auth_jwt import AuthJWT
|
||||
@@ -0,0 +1,111 @@
|
||||
from fastapi_jwt_auth.config import LoadConfig
|
||||
from pydantic import ValidationError
|
||||
from typing import Callable, List
|
||||
from datetime import timedelta
|
||||
|
||||
class AuthConfig:
|
||||
_token = None
|
||||
_token_location = {'headers'}
|
||||
|
||||
_secret_key = None
|
||||
_public_key = None
|
||||
_private_key = None
|
||||
_algorithm = "HS256"
|
||||
_decode_algorithms = None
|
||||
_decode_leeway = 0
|
||||
_encode_issuer = None
|
||||
_decode_issuer = None
|
||||
_decode_audience = None
|
||||
_denylist_enabled = False
|
||||
_denylist_token_checks = {'access','refresh'}
|
||||
_header_name = "Authorization"
|
||||
_header_type = "Bearer"
|
||||
_token_in_denylist_callback = None
|
||||
_access_token_expires = timedelta(minutes=15)
|
||||
_refresh_token_expires = timedelta(days=30)
|
||||
|
||||
# option for create cookies
|
||||
_access_cookie_key = "access_token_cookie"
|
||||
_refresh_cookie_key = "refresh_token_cookie"
|
||||
_access_cookie_path = "/"
|
||||
_refresh_cookie_path = "/"
|
||||
_cookie_max_age = None
|
||||
_cookie_domain = None
|
||||
_cookie_secure = False
|
||||
_cookie_samesite = None
|
||||
|
||||
# option for double submit csrf protection
|
||||
_cookie_csrf_protect = True
|
||||
_access_csrf_cookie_key = "csrf_access_token"
|
||||
_refresh_csrf_cookie_key = "csrf_refresh_token"
|
||||
_access_csrf_cookie_path = "/"
|
||||
_refresh_csrf_cookie_path = "/"
|
||||
_access_csrf_header_name = "X-CSRF-Token"
|
||||
_refresh_csrf_header_name = "X-CSRF-Token"
|
||||
_csrf_methods = {'POST','PUT','PATCH','DELETE'}
|
||||
|
||||
@property
|
||||
def jwt_in_cookies(self) -> bool:
|
||||
return 'cookies' in self._token_location
|
||||
|
||||
@property
|
||||
def jwt_in_headers(self) -> bool:
|
||||
return 'headers' in self._token_location
|
||||
|
||||
@classmethod
|
||||
def load_config(cls, settings: Callable[...,List[tuple]]) -> "AuthConfig":
|
||||
try:
|
||||
config = LoadConfig(**{key.lower():value for key,value in settings()})
|
||||
|
||||
cls._token_location = config.authjwt_token_location
|
||||
cls._secret_key = config.authjwt_secret_key
|
||||
cls._public_key = config.authjwt_public_key
|
||||
cls._private_key = config.authjwt_private_key
|
||||
cls._algorithm = config.authjwt_algorithm
|
||||
cls._decode_algorithms = config.authjwt_decode_algorithms
|
||||
cls._decode_leeway = config.authjwt_decode_leeway
|
||||
cls._encode_issuer = config.authjwt_encode_issuer
|
||||
cls._decode_issuer = config.authjwt_decode_issuer
|
||||
cls._decode_audience = config.authjwt_decode_audience
|
||||
cls._denylist_enabled = config.authjwt_denylist_enabled
|
||||
cls._denylist_token_checks = config.authjwt_denylist_token_checks
|
||||
cls._header_name = config.authjwt_header_name
|
||||
cls._header_type = config.authjwt_header_type
|
||||
cls._access_token_expires = config.authjwt_access_token_expires
|
||||
cls._refresh_token_expires = config.authjwt_refresh_token_expires
|
||||
# option for create cookies
|
||||
cls._access_cookie_key = config.authjwt_access_cookie_key
|
||||
cls._refresh_cookie_key = config.authjwt_refresh_cookie_key
|
||||
cls._access_cookie_path = config.authjwt_access_cookie_path
|
||||
cls._refresh_cookie_path = config.authjwt_refresh_cookie_path
|
||||
cls._cookie_max_age = config.authjwt_cookie_max_age
|
||||
cls._cookie_domain = config.authjwt_cookie_domain
|
||||
cls._cookie_secure = config.authjwt_cookie_secure
|
||||
cls._cookie_samesite = config.authjwt_cookie_samesite
|
||||
# option for double submit csrf protection
|
||||
cls._cookie_csrf_protect = config.authjwt_cookie_csrf_protect
|
||||
cls._access_csrf_cookie_key = config.authjwt_access_csrf_cookie_key
|
||||
cls._refresh_csrf_cookie_key = config.authjwt_refresh_csrf_cookie_key
|
||||
cls._access_csrf_cookie_path = config.authjwt_access_csrf_cookie_path
|
||||
cls._refresh_csrf_cookie_path = config.authjwt_refresh_csrf_cookie_path
|
||||
cls._access_csrf_header_name = config.authjwt_access_csrf_header_name
|
||||
cls._refresh_csrf_header_name = config.authjwt_refresh_csrf_header_name
|
||||
cls._csrf_methods = config.authjwt_csrf_methods
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception:
|
||||
raise TypeError("Config must be pydantic 'BaseSettings' or list of tuple")
|
||||
|
||||
@classmethod
|
||||
def token_in_denylist_loader(cls, callback: Callable[...,bool]) -> "AuthConfig":
|
||||
"""
|
||||
This decorator sets the callback function that will be called when
|
||||
a protected endpoint is accessed and will check if the JWT has been
|
||||
been revoked. By default, this callback is not used.
|
||||
|
||||
*HINT*: The callback must be a function that takes decrypted_token argument,
|
||||
args for object AuthJWT and this is not used, decrypted_token is decode
|
||||
JWT (python dictionary) and returns *`True`* if the token has been deny,
|
||||
or *`False`* otherwise.
|
||||
"""
|
||||
cls._token_in_denylist_callback = callback
|
||||
@@ -0,0 +1,848 @@
|
||||
import jwt, re, uuid, hmac
|
||||
from jwt.algorithms import requires_cryptography, has_crypto
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Union, Sequence
|
||||
from fastapi import Request, Response, WebSocket
|
||||
from fastapi_jwt_auth.auth_config import AuthConfig
|
||||
from fastapi_jwt_auth.exceptions import (
|
||||
InvalidHeaderError,
|
||||
CSRFError,
|
||||
JWTDecodeError,
|
||||
RevokedTokenError,
|
||||
MissingTokenError,
|
||||
AccessTokenRequired,
|
||||
RefreshTokenRequired,
|
||||
FreshTokenRequired
|
||||
)
|
||||
|
||||
class AuthJWT(AuthConfig):
|
||||
def __init__(self,req: Request = None, res: Response = None):
|
||||
"""
|
||||
Get jwt header from incoming request or get
|
||||
request and response object if jwt in the cookie
|
||||
|
||||
:param req: all incoming request
|
||||
:param res: response from endpoint
|
||||
"""
|
||||
if res and self.jwt_in_cookies:
|
||||
self._response = res
|
||||
|
||||
if req:
|
||||
# get request object when cookies in token location
|
||||
if self.jwt_in_cookies:
|
||||
self._request = req
|
||||
# get jwt in headers when headers in token location
|
||||
if self.jwt_in_headers:
|
||||
auth = req.headers.get(self._header_name.lower())
|
||||
if auth: self._get_jwt_from_headers(auth)
|
||||
|
||||
def _get_jwt_from_headers(self,auth: str) -> "AuthJWT":
|
||||
"""
|
||||
Get token from the headers
|
||||
|
||||
:param auth: value from HeaderName
|
||||
"""
|
||||
header_name, header_type = self._header_name, self._header_type
|
||||
|
||||
parts = auth.split()
|
||||
|
||||
# Make sure the header is in a valid format that we are expecting, ie
|
||||
if not header_type:
|
||||
# <HeaderName>: <JWT>
|
||||
if len(parts) != 1:
|
||||
msg = "Bad {} header. Expected value '<JWT>'".format(header_name)
|
||||
raise InvalidHeaderError(status_code=422,message=msg)
|
||||
self._token = parts[0]
|
||||
else:
|
||||
# <HeaderName>: <HeaderType> <JWT>
|
||||
if not re.match(r"{}\s".format(header_type),auth) or len(parts) != 2:
|
||||
msg = "Bad {} header. Expected value '{} <JWT>'".format(header_name,header_type)
|
||||
raise InvalidHeaderError(status_code=422,message=msg)
|
||||
self._token = parts[1]
|
||||
|
||||
def _get_jwt_identifier(self) -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def _get_int_from_datetime(self,value: datetime) -> int:
|
||||
"""
|
||||
:param value: datetime with or without timezone, if don't contains timezone
|
||||
it will managed as it is UTC
|
||||
:return: Seconds since the Epoch
|
||||
"""
|
||||
if not isinstance(value, datetime): # pragma: no cover
|
||||
raise TypeError('a datetime is required')
|
||||
return int(value.timestamp())
|
||||
|
||||
def _get_secret_key(self, algorithm: str, process: str) -> str:
|
||||
"""
|
||||
Get key with a different algorithm
|
||||
|
||||
:param algorithm: algorithm for decode and encode token
|
||||
:param process: for indicating get key for encode or decode token
|
||||
|
||||
:return: plain text or RSA depends on algorithm
|
||||
"""
|
||||
symmetric_algorithms, asymmetric_algorithms = {"HS256","HS384","HS512"}, requires_cryptography
|
||||
|
||||
if algorithm not in symmetric_algorithms and algorithm not in asymmetric_algorithms:
|
||||
raise ValueError("Algorithm {} could not be found".format(algorithm))
|
||||
|
||||
if algorithm in symmetric_algorithms:
|
||||
if not self._secret_key:
|
||||
raise RuntimeError(
|
||||
"authjwt_secret_key must be set when using symmetric algorithm {}".format(algorithm)
|
||||
)
|
||||
|
||||
return self._secret_key
|
||||
|
||||
if algorithm in asymmetric_algorithms and not has_crypto:
|
||||
raise RuntimeError(
|
||||
"Missing dependencies for using asymmetric algorithms. run 'pip install fastapi-jwt-auth[asymmetric]'"
|
||||
)
|
||||
|
||||
if process == "encode":
|
||||
if not self._private_key:
|
||||
raise RuntimeError(
|
||||
"authjwt_private_key must be set when using asymmetric algorithm {}".format(algorithm)
|
||||
)
|
||||
|
||||
return self._private_key
|
||||
|
||||
if process == "decode":
|
||||
if not self._public_key:
|
||||
raise RuntimeError(
|
||||
"authjwt_public_key must be set when using asymmetric algorithm {}".format(algorithm)
|
||||
)
|
||||
|
||||
return self._public_key
|
||||
|
||||
def _create_token(
|
||||
self,
|
||||
subject: Union[str,int],
|
||||
type_token: str,
|
||||
exp_time: Optional[int],
|
||||
fresh: Optional[bool] = False,
|
||||
algorithm: Optional[str] = None,
|
||||
headers: Optional[Dict] = None,
|
||||
issuer: Optional[str] = None,
|
||||
audience: Optional[Union[str,Sequence[str]]] = None,
|
||||
user_claims: Optional[Dict] = {}
|
||||
) -> str:
|
||||
"""
|
||||
Create token for access_token and refresh_token (utf-8)
|
||||
|
||||
:param subject: Identifier for who this token is for example id or username from database.
|
||||
:param type_token: indicate token is access_token or refresh_token
|
||||
:param exp_time: Set the duration of the JWT
|
||||
:param fresh: Optional when token is access_token this param required
|
||||
:param algorithm: algorithm allowed to encode the token
|
||||
:param headers: valid dict for specifying additional headers in JWT header section
|
||||
:param issuer: expected issuer in the JWT
|
||||
:param audience: expected audience in the JWT
|
||||
:param user_claims: Custom claims to include in this token. This data must be dictionary
|
||||
|
||||
:return: Encoded token
|
||||
"""
|
||||
# Validation type data
|
||||
if not isinstance(subject, (str,int)):
|
||||
raise TypeError("subject must be a string or integer")
|
||||
if not isinstance(fresh, bool):
|
||||
raise TypeError("fresh must be a boolean")
|
||||
if audience and not isinstance(audience, (str, list, tuple, set, frozenset)):
|
||||
raise TypeError("audience must be a string or sequence")
|
||||
if algorithm and not isinstance(algorithm, str):
|
||||
raise TypeError("algorithm must be a string")
|
||||
if user_claims and not isinstance(user_claims, dict):
|
||||
raise TypeError("user_claims must be a dictionary")
|
||||
|
||||
# Data section
|
||||
reserved_claims = {
|
||||
"sub": subject,
|
||||
"iat": self._get_int_from_datetime(datetime.now(timezone.utc)),
|
||||
"nbf": self._get_int_from_datetime(datetime.now(timezone.utc)),
|
||||
"jti": self._get_jwt_identifier()
|
||||
}
|
||||
|
||||
custom_claims = {"type": type_token}
|
||||
|
||||
# for access_token only fresh needed
|
||||
if type_token == 'access':
|
||||
custom_claims['fresh'] = fresh
|
||||
# if cookie in token location and csrf protection enabled
|
||||
if self.jwt_in_cookies and self._cookie_csrf_protect:
|
||||
custom_claims['csrf'] = self._get_jwt_identifier()
|
||||
|
||||
if exp_time:
|
||||
reserved_claims['exp'] = exp_time
|
||||
if issuer:
|
||||
reserved_claims['iss'] = issuer
|
||||
if audience:
|
||||
reserved_claims['aud'] = audience
|
||||
|
||||
algorithm = algorithm or self._algorithm
|
||||
|
||||
try:
|
||||
secret_key = self._get_secret_key(algorithm,"encode")
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
return jwt.encode(
|
||||
{**reserved_claims, **custom_claims, **user_claims},
|
||||
secret_key,
|
||||
algorithm=algorithm,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
def _has_token_in_denylist_callback(self) -> bool:
|
||||
"""
|
||||
Return True if token denylist callback set
|
||||
"""
|
||||
return self._token_in_denylist_callback is not None
|
||||
|
||||
def _check_token_is_revoked(self, raw_token: Dict[str,Union[str,int,bool]]) -> None:
|
||||
"""
|
||||
Ensure that AUTHJWT_DENYLIST_ENABLED is true and callback regulated, and then
|
||||
call function denylist callback with passing decode JWT, if true
|
||||
raise exception Token has been revoked
|
||||
"""
|
||||
if not self._denylist_enabled:
|
||||
return
|
||||
|
||||
if not self._has_token_in_denylist_callback():
|
||||
raise RuntimeError("A token_in_denylist_callback must be provided via "
|
||||
"the '@AuthJWT.token_in_denylist_loader' if "
|
||||
"authjwt_denylist_enabled is 'True'")
|
||||
|
||||
if self._token_in_denylist_callback.__func__(raw_token):
|
||||
raise RevokedTokenError(status_code=401,message="Token has been revoked")
|
||||
|
||||
def _get_expired_time(
|
||||
self,
|
||||
type_token: str,
|
||||
expires_time: Optional[Union[timedelta,int,bool]] = None
|
||||
) -> Union[None,int]:
|
||||
"""
|
||||
Dynamic token expired, if expires_time is False exp claim not created
|
||||
|
||||
:param type_token: indicate token is access_token or refresh_token
|
||||
:param expires_time: duration expired jwt
|
||||
|
||||
:return: duration exp claim jwt
|
||||
"""
|
||||
if expires_time and not isinstance(expires_time, (timedelta,int,bool)):
|
||||
raise TypeError("expires_time must be between timedelta, int, bool")
|
||||
|
||||
if expires_time is not False:
|
||||
if type_token == 'access':
|
||||
expires_time = expires_time or self._access_token_expires
|
||||
if type_token == 'refresh':
|
||||
expires_time = expires_time or self._refresh_token_expires
|
||||
|
||||
if expires_time is not False:
|
||||
if isinstance(expires_time, bool):
|
||||
if type_token == 'access':
|
||||
expires_time = self._access_token_expires
|
||||
if type_token == 'refresh':
|
||||
expires_time = self._refresh_token_expires
|
||||
if isinstance(expires_time, timedelta):
|
||||
expires_time = int(expires_time.total_seconds())
|
||||
|
||||
return self._get_int_from_datetime(datetime.now(timezone.utc)) + expires_time
|
||||
else:
|
||||
return None
|
||||
|
||||
def create_access_token(
|
||||
self,
|
||||
subject: Union[str,int],
|
||||
fresh: Optional[bool] = False,
|
||||
algorithm: Optional[str] = None,
|
||||
headers: Optional[Dict] = None,
|
||||
expires_time: Optional[Union[timedelta,int,bool]] = None,
|
||||
audience: Optional[Union[str,Sequence[str]]] = None,
|
||||
user_claims: Optional[Dict] = {}
|
||||
) -> str:
|
||||
"""
|
||||
Create a access token with 15 minutes for expired time (default),
|
||||
info for param and return check to function create token
|
||||
|
||||
:return: hash token
|
||||
"""
|
||||
return self._create_token(
|
||||
subject=subject,
|
||||
type_token="access",
|
||||
exp_time=self._get_expired_time("access",expires_time),
|
||||
fresh=fresh,
|
||||
algorithm=algorithm,
|
||||
headers=headers,
|
||||
audience=audience,
|
||||
user_claims=user_claims,
|
||||
issuer=self._encode_issuer
|
||||
)
|
||||
|
||||
def create_refresh_token(
|
||||
self,
|
||||
subject: Union[str,int],
|
||||
algorithm: Optional[str] = None,
|
||||
headers: Optional[Dict] = None,
|
||||
expires_time: Optional[Union[timedelta,int,bool]] = None,
|
||||
audience: Optional[Union[str,Sequence[str]]] = None,
|
||||
user_claims: Optional[Dict] = {}
|
||||
) -> str:
|
||||
"""
|
||||
Create a refresh token with 30 days for expired time (default),
|
||||
info for param and return check to function create token
|
||||
|
||||
:return: hash token
|
||||
"""
|
||||
return self._create_token(
|
||||
subject=subject,
|
||||
type_token="refresh",
|
||||
exp_time=self._get_expired_time("refresh",expires_time),
|
||||
algorithm=algorithm,
|
||||
headers=headers,
|
||||
audience=audience,
|
||||
user_claims=user_claims
|
||||
)
|
||||
|
||||
def _get_csrf_token(self,encoded_token: str) -> str:
|
||||
"""
|
||||
Returns the CSRF double submit token from an encoded JWT.
|
||||
|
||||
:param encoded_token: The encoded JWT
|
||||
:return: The CSRF double submit token
|
||||
"""
|
||||
return self._verified_token(encoded_token)['csrf']
|
||||
|
||||
def set_access_cookies(
|
||||
self,
|
||||
encoded_access_token: str,
|
||||
response: Optional[Response] = None,
|
||||
max_age: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
Configures the response to set access token in a cookie.
|
||||
this will also set the CSRF double submit values in a separate cookie
|
||||
|
||||
:param encoded_access_token: The encoded access token to set in the cookies
|
||||
:param response: The FastAPI response object to set the access cookies in
|
||||
:param max_age: The max age of the cookie value should be the number of seconds (integer)
|
||||
"""
|
||||
if not self.jwt_in_cookies:
|
||||
raise RuntimeWarning(
|
||||
"set_access_cookies() called without 'authjwt_token_location' configured to use cookies"
|
||||
)
|
||||
|
||||
if max_age and not isinstance(max_age,int):
|
||||
raise TypeError("max_age must be a integer")
|
||||
if response and not isinstance(response,Response):
|
||||
raise TypeError("The response must be an object response FastAPI")
|
||||
|
||||
response = response or self._response
|
||||
|
||||
# Set the access JWT in the cookie
|
||||
response.set_cookie(
|
||||
self._access_cookie_key,
|
||||
encoded_access_token,
|
||||
max_age=max_age or self._cookie_max_age,
|
||||
path=self._access_cookie_path,
|
||||
domain=self._cookie_domain,
|
||||
secure=self._cookie_secure,
|
||||
httponly=True,
|
||||
samesite=self._cookie_samesite
|
||||
)
|
||||
|
||||
# If enabled, set the csrf double submit access cookie
|
||||
if self._cookie_csrf_protect:
|
||||
response.set_cookie(
|
||||
self._access_csrf_cookie_key,
|
||||
self._get_csrf_token(encoded_access_token),
|
||||
max_age=max_age or self._cookie_max_age,
|
||||
path=self._access_csrf_cookie_path,
|
||||
domain=self._cookie_domain,
|
||||
secure=self._cookie_secure,
|
||||
httponly=False,
|
||||
samesite=self._cookie_samesite
|
||||
)
|
||||
|
||||
def set_refresh_cookies(
|
||||
self,
|
||||
encoded_refresh_token: str,
|
||||
response: Optional[Response] = None,
|
||||
max_age: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
Configures the response to set refresh token in a cookie.
|
||||
this will also set the CSRF double submit values in a separate cookie
|
||||
|
||||
:param encoded_refresh_token: The encoded refresh token to set in the cookies
|
||||
:param response: The FastAPI response object to set the refresh cookies in
|
||||
:param max_age: The max age of the cookie value should be the number of seconds (integer)
|
||||
"""
|
||||
if not self.jwt_in_cookies:
|
||||
raise RuntimeWarning(
|
||||
"set_refresh_cookies() called without 'authjwt_token_location' configured to use cookies"
|
||||
)
|
||||
|
||||
if max_age and not isinstance(max_age,int):
|
||||
raise TypeError("max_age must be a integer")
|
||||
if response and not isinstance(response,Response):
|
||||
raise TypeError("The response must be an object response FastAPI")
|
||||
|
||||
response = response or self._response
|
||||
|
||||
# Set the refresh JWT in the cookie
|
||||
response.set_cookie(
|
||||
self._refresh_cookie_key,
|
||||
encoded_refresh_token,
|
||||
max_age=max_age or self._cookie_max_age,
|
||||
path=self._refresh_cookie_path,
|
||||
domain=self._cookie_domain,
|
||||
secure=self._cookie_secure,
|
||||
httponly=True,
|
||||
samesite=self._cookie_samesite
|
||||
)
|
||||
|
||||
# If enabled, set the csrf double submit refresh cookie
|
||||
if self._cookie_csrf_protect:
|
||||
response.set_cookie(
|
||||
self._refresh_csrf_cookie_key,
|
||||
self._get_csrf_token(encoded_refresh_token),
|
||||
max_age=max_age or self._cookie_max_age,
|
||||
path=self._refresh_csrf_cookie_path,
|
||||
domain=self._cookie_domain,
|
||||
secure=self._cookie_secure,
|
||||
httponly=False,
|
||||
samesite=self._cookie_samesite
|
||||
)
|
||||
|
||||
def unset_jwt_cookies(self,response: Optional[Response] = None) -> None:
|
||||
"""
|
||||
Unset (delete) all jwt stored in a cookie
|
||||
|
||||
:param response: The FastAPI response object to delete the JWT cookies in.
|
||||
"""
|
||||
self.unset_access_cookies(response)
|
||||
self.unset_refresh_cookies(response)
|
||||
|
||||
def unset_access_cookies(self,response: Optional[Response] = None) -> None:
|
||||
"""
|
||||
Remove access token and access CSRF double submit from the response cookies
|
||||
|
||||
:param response: The FastAPI response object to delete the access cookies in.
|
||||
"""
|
||||
if not self.jwt_in_cookies:
|
||||
raise RuntimeWarning(
|
||||
"unset_access_cookies() called without 'authjwt_token_location' configured to use cookies"
|
||||
)
|
||||
|
||||
if response and not isinstance(response,Response):
|
||||
raise TypeError("The response must be an object response FastAPI")
|
||||
|
||||
response = response or self._response
|
||||
|
||||
response.delete_cookie(
|
||||
self._access_cookie_key,
|
||||
path=self._access_cookie_path,
|
||||
domain=self._cookie_domain
|
||||
)
|
||||
|
||||
if self._cookie_csrf_protect:
|
||||
response.delete_cookie(
|
||||
self._access_csrf_cookie_key,
|
||||
path=self._access_csrf_cookie_path,
|
||||
domain=self._cookie_domain
|
||||
)
|
||||
|
||||
def unset_refresh_cookies(self,response: Optional[Response] = None) -> None:
|
||||
"""
|
||||
Remove refresh token and refresh CSRF double submit from the response cookies
|
||||
|
||||
:param response: The FastAPI response object to delete the refresh cookies in.
|
||||
"""
|
||||
if not self.jwt_in_cookies:
|
||||
raise RuntimeWarning(
|
||||
"unset_refresh_cookies() called without 'authjwt_token_location' configured to use cookies"
|
||||
)
|
||||
|
||||
if response and not isinstance(response,Response):
|
||||
raise TypeError("The response must be an object response FastAPI")
|
||||
|
||||
response = response or self._response
|
||||
|
||||
response.delete_cookie(
|
||||
self._refresh_cookie_key,
|
||||
path=self._refresh_cookie_path,
|
||||
domain=self._cookie_domain
|
||||
)
|
||||
|
||||
if self._cookie_csrf_protect:
|
||||
response.delete_cookie(
|
||||
self._refresh_csrf_cookie_key,
|
||||
path=self._refresh_csrf_cookie_path,
|
||||
domain=self._cookie_domain
|
||||
)
|
||||
|
||||
def _verify_and_get_jwt_optional_in_cookies(
|
||||
self,
|
||||
request: Union[Request,WebSocket],
|
||||
csrf_token: Optional[str] = None,
|
||||
) -> "AuthJWT":
|
||||
"""
|
||||
Optionally check if cookies have a valid access token. if an access token present in
|
||||
cookies, self._token will set. raises exception error when an access token is invalid
|
||||
or doesn't match with CSRF token double submit
|
||||
|
||||
:param request: for identity get cookies from HTTP or WebSocket
|
||||
:param csrf_token: the CSRF double submit token
|
||||
"""
|
||||
if not isinstance(request,(Request,WebSocket)):
|
||||
raise TypeError("request must be an instance of 'Request' or 'WebSocket'")
|
||||
|
||||
cookie_key = self._access_cookie_key
|
||||
cookie = request.cookies.get(cookie_key)
|
||||
if not isinstance(request, WebSocket):
|
||||
csrf_token = request.headers.get(self._access_csrf_header_name)
|
||||
|
||||
if cookie and self._cookie_csrf_protect and not csrf_token:
|
||||
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
|
||||
raise CSRFError(status_code=401,message="Missing CSRF Token")
|
||||
|
||||
# set token from cookie and verify jwt
|
||||
self._token = cookie
|
||||
self._verify_jwt_optional_in_request(self._token)
|
||||
|
||||
decoded_token = self.get_raw_jwt()
|
||||
|
||||
if decoded_token and self._cookie_csrf_protect and csrf_token:
|
||||
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
|
||||
if 'csrf' not in decoded_token:
|
||||
raise JWTDecodeError(status_code=422,message="Missing claim: csrf")
|
||||
if not hmac.compare_digest(csrf_token,decoded_token['csrf']):
|
||||
raise CSRFError(status_code=401,message="CSRF double submit tokens do not match")
|
||||
|
||||
def _verify_and_get_jwt_in_cookies(
|
||||
self,
|
||||
type_token: str,
|
||||
request: Union[Request,WebSocket],
|
||||
csrf_token: Optional[str] = None,
|
||||
fresh: Optional[bool] = False,
|
||||
) -> "AuthJWT":
|
||||
"""
|
||||
Check if cookies have a valid access or refresh token. if an token present in
|
||||
cookies, self._token will set. raises exception error when an access or refresh token
|
||||
is invalid or doesn't match with CSRF token double submit
|
||||
|
||||
:param type_token: indicate token is access or refresh token
|
||||
:param request: for identity get cookies from HTTP or WebSocket
|
||||
:param csrf_token: the CSRF double submit token
|
||||
:param fresh: check freshness token if True
|
||||
"""
|
||||
if type_token not in ['access','refresh']:
|
||||
raise ValueError("type_token must be between 'access' or 'refresh'")
|
||||
if not isinstance(request,(Request,WebSocket)):
|
||||
raise TypeError("request must be an instance of 'Request' or 'WebSocket'")
|
||||
|
||||
if type_token == 'access':
|
||||
cookie_key = self._access_cookie_key
|
||||
cookie = request.cookies.get(cookie_key)
|
||||
if not isinstance(request, WebSocket):
|
||||
csrf_token = request.headers.get(self._access_csrf_header_name)
|
||||
if type_token == 'refresh':
|
||||
cookie_key = self._refresh_cookie_key
|
||||
cookie = request.cookies.get(cookie_key)
|
||||
if not isinstance(request, WebSocket):
|
||||
csrf_token = request.headers.get(self._refresh_csrf_header_name)
|
||||
|
||||
if not cookie:
|
||||
raise MissingTokenError(status_code=401,message="Missing cookie {}".format(cookie_key))
|
||||
|
||||
if self._cookie_csrf_protect and not csrf_token:
|
||||
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
|
||||
raise CSRFError(status_code=401,message="Missing CSRF Token")
|
||||
|
||||
# set token from cookie and verify jwt
|
||||
self._token = cookie
|
||||
self._verify_jwt_in_request(self._token,type_token,'cookies',fresh)
|
||||
|
||||
decoded_token = self.get_raw_jwt()
|
||||
|
||||
if self._cookie_csrf_protect and csrf_token:
|
||||
if isinstance(request, WebSocket) or request.method in self._csrf_methods:
|
||||
if 'csrf' not in decoded_token:
|
||||
raise JWTDecodeError(status_code=422,message="Missing claim: csrf")
|
||||
if not hmac.compare_digest(csrf_token,decoded_token['csrf']):
|
||||
raise CSRFError(status_code=401,message="CSRF double submit tokens do not match")
|
||||
|
||||
def _verify_jwt_optional_in_request(self,token: str) -> None:
|
||||
"""
|
||||
Optionally check if this request has a valid access token
|
||||
|
||||
:param token: The encoded JWT
|
||||
"""
|
||||
if token: self._verifying_token(token)
|
||||
|
||||
if token and self.get_raw_jwt(token)['type'] != 'access':
|
||||
raise AccessTokenRequired(status_code=422,message="Only access tokens are allowed")
|
||||
|
||||
def _verify_jwt_in_request(
|
||||
self,
|
||||
token: str,
|
||||
type_token: str,
|
||||
token_from: str,
|
||||
fresh: Optional[bool] = False
|
||||
) -> None:
|
||||
"""
|
||||
Ensure that the requester has a valid token. this also check the freshness of the access token
|
||||
|
||||
:param token: The encoded JWT
|
||||
:param type_token: indicate token is access or refresh token
|
||||
:param token_from: indicate token from headers cookies, websocket
|
||||
:param fresh: check freshness token if True
|
||||
"""
|
||||
if type_token not in ['access','refresh']:
|
||||
raise ValueError("type_token must be between 'access' or 'refresh'")
|
||||
if token_from not in ['headers','cookies','websocket']:
|
||||
raise ValueError("token_from must be between 'headers', 'cookies', 'websocket'")
|
||||
|
||||
if not token:
|
||||
if token_from == 'headers':
|
||||
raise MissingTokenError(status_code=401,message="Missing {} Header".format(self._header_name))
|
||||
if token_from == 'websocket':
|
||||
raise MissingTokenError(status_code=1008,message="Missing {} token from Query or Path".format(type_token))
|
||||
|
||||
# verify jwt
|
||||
issuer = self._decode_issuer if type_token == 'access' else None
|
||||
self._verifying_token(token,issuer)
|
||||
|
||||
if self.get_raw_jwt(token)['type'] != type_token:
|
||||
msg = "Only {} tokens are allowed".format(type_token)
|
||||
if type_token == 'access':
|
||||
raise AccessTokenRequired(status_code=422,message=msg)
|
||||
if type_token == 'refresh':
|
||||
raise RefreshTokenRequired(status_code=422,message=msg)
|
||||
|
||||
if fresh and not self.get_raw_jwt(token)['fresh']:
|
||||
raise FreshTokenRequired(status_code=401,message="Fresh token required")
|
||||
|
||||
def _verifying_token(self,encoded_token: str, issuer: Optional[str] = None) -> None:
|
||||
"""
|
||||
Verified token and check if token is revoked
|
||||
|
||||
:param encoded_token: token hash
|
||||
:param issuer: expected issuer in the JWT
|
||||
"""
|
||||
raw_token = self._verified_token(encoded_token,issuer)
|
||||
if raw_token['type'] in self._denylist_token_checks:
|
||||
self._check_token_is_revoked(raw_token)
|
||||
|
||||
def _verified_token(self,encoded_token: str, issuer: Optional[str] = None) -> Dict[str,Union[str,int,bool]]:
|
||||
"""
|
||||
Verified token and catch all error from jwt package and return decode token
|
||||
|
||||
:param encoded_token: token hash
|
||||
:param issuer: expected issuer in the JWT
|
||||
|
||||
:return: raw data from the hash token in the form of a dictionary
|
||||
"""
|
||||
algorithms = self._decode_algorithms or [self._algorithm]
|
||||
|
||||
try:
|
||||
unverified_headers = self.get_unverified_jwt_headers(encoded_token)
|
||||
except Exception as err:
|
||||
raise InvalidHeaderError(status_code=422,message=str(err))
|
||||
|
||||
try:
|
||||
secret_key = self._get_secret_key(unverified_headers['alg'],"decode")
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
try:
|
||||
return jwt.decode(
|
||||
encoded_token,
|
||||
secret_key,
|
||||
issuer=issuer,
|
||||
audience=self._decode_audience,
|
||||
leeway=self._decode_leeway,
|
||||
algorithms=algorithms
|
||||
)
|
||||
except Exception as err:
|
||||
raise JWTDecodeError(status_code=422,message=str(err))
|
||||
|
||||
def jwt_required(
|
||||
self,
|
||||
auth_from: str = "request",
|
||||
token: Optional[str] = None,
|
||||
websocket: Optional[WebSocket] = None,
|
||||
csrf_token: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Only access token can access this function
|
||||
|
||||
:param auth_from: for identity get token from HTTP or WebSocket
|
||||
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
|
||||
authorization and get token from Query Url or Path
|
||||
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
|
||||
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
|
||||
its must be passing csrf_token manually and can achieve by Query Url or Path
|
||||
"""
|
||||
if auth_from == "websocket":
|
||||
if websocket: self._verify_and_get_jwt_in_cookies('access',websocket,csrf_token)
|
||||
else: self._verify_jwt_in_request(token,'access','websocket')
|
||||
|
||||
if auth_from == "request":
|
||||
if len(self._token_location) == 2:
|
||||
if self._token and self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'access','headers')
|
||||
if not self._token and self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('access',self._request)
|
||||
else:
|
||||
if self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'access','headers')
|
||||
if self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('access',self._request)
|
||||
|
||||
def jwt_optional(
|
||||
self,
|
||||
auth_from: str = "request",
|
||||
token: Optional[str] = None,
|
||||
websocket: Optional[WebSocket] = None,
|
||||
csrf_token: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
If an access token in present in the request you can get data from get_raw_jwt() or get_jwt_subject(),
|
||||
If no access token is present in the request, this endpoint will still be called, but
|
||||
get_raw_jwt() or get_jwt_subject() will return None
|
||||
|
||||
:param auth_from: for identity get token from HTTP or WebSocket
|
||||
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
|
||||
authorization and get token from Query Url or Path
|
||||
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
|
||||
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
|
||||
its must be passing csrf_token manually and can achieve by Query Url or Path
|
||||
"""
|
||||
if auth_from == "websocket":
|
||||
if websocket: self._verify_and_get_jwt_optional_in_cookies(websocket,csrf_token)
|
||||
else: self._verify_jwt_optional_in_request(token)
|
||||
|
||||
if auth_from == "request":
|
||||
if len(self._token_location) == 2:
|
||||
if self._token and self.jwt_in_headers:
|
||||
self._verify_jwt_optional_in_request(self._token)
|
||||
if not self._token and self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_optional_in_cookies(self._request)
|
||||
else:
|
||||
if self.jwt_in_headers:
|
||||
self._verify_jwt_optional_in_request(self._token)
|
||||
if self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_optional_in_cookies(self._request)
|
||||
|
||||
def jwt_refresh_token_required(
|
||||
self,
|
||||
auth_from: str = "request",
|
||||
token: Optional[str] = None,
|
||||
websocket: Optional[WebSocket] = None,
|
||||
csrf_token: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
This function will ensure that the requester has a valid refresh token
|
||||
|
||||
:param auth_from: for identity get token from HTTP or WebSocket
|
||||
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
|
||||
authorization and get token from Query Url or Path
|
||||
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
|
||||
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
|
||||
its must be passing csrf_token manually and can achieve by Query Url or Path
|
||||
"""
|
||||
if auth_from == "websocket":
|
||||
if websocket: self._verify_and_get_jwt_in_cookies('refresh',websocket,csrf_token)
|
||||
else: self._verify_jwt_in_request(token,'refresh','websocket')
|
||||
|
||||
if auth_from == "request":
|
||||
if len(self._token_location) == 2:
|
||||
if self._token and self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'refresh','headers')
|
||||
if not self._token and self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('refresh',self._request)
|
||||
else:
|
||||
if self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'refresh','headers')
|
||||
if self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('refresh',self._request)
|
||||
|
||||
def fresh_jwt_required(
|
||||
self,
|
||||
auth_from: str = "request",
|
||||
token: Optional[str] = None,
|
||||
websocket: Optional[WebSocket] = None,
|
||||
csrf_token: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
This function will ensure that the requester has a valid access token and fresh token
|
||||
|
||||
:param auth_from: for identity get token from HTTP or WebSocket
|
||||
:param token: the encoded JWT, it's required if the protected endpoint use WebSocket to
|
||||
authorization and get token from Query Url or Path
|
||||
:param websocket: an instance of WebSocket, it's required if protected endpoint use a cookie to authorization
|
||||
:param csrf_token: the CSRF double submit token. since WebSocket cannot add specifying additional headers
|
||||
its must be passing csrf_token manually and can achieve by Query Url or Path
|
||||
"""
|
||||
if auth_from == "websocket":
|
||||
if websocket: self._verify_and_get_jwt_in_cookies('access',websocket,csrf_token,True)
|
||||
else: self._verify_jwt_in_request(token,'access','websocket',True)
|
||||
|
||||
if auth_from == "request":
|
||||
if len(self._token_location) == 2:
|
||||
if self._token and self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'access','headers',True)
|
||||
if not self._token and self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('access',self._request,fresh=True)
|
||||
else:
|
||||
if self.jwt_in_headers:
|
||||
self._verify_jwt_in_request(self._token,'access','headers',True)
|
||||
if self.jwt_in_cookies:
|
||||
self._verify_and_get_jwt_in_cookies('access',self._request,fresh=True)
|
||||
|
||||
def get_raw_jwt(self,encoded_token: Optional[str] = None) -> Optional[Dict[str,Union[str,int,bool]]]:
|
||||
"""
|
||||
this will return the python dictionary which has all of the claims of the JWT that is accessing the endpoint.
|
||||
If no JWT is currently present, return None instead
|
||||
|
||||
:param encoded_token: The encoded JWT from parameter
|
||||
:return: claims of JWT
|
||||
"""
|
||||
token = encoded_token or self._token
|
||||
|
||||
if token:
|
||||
return self._verified_token(token)
|
||||
return None
|
||||
|
||||
def get_jti(self,encoded_token: str) -> str:
|
||||
"""
|
||||
Returns the JTI (unique identifier) of an encoded JWT
|
||||
|
||||
:param encoded_token: The encoded JWT from parameter
|
||||
:return: string of JTI
|
||||
"""
|
||||
return self._verified_token(encoded_token)['jti']
|
||||
|
||||
def get_jwt_subject(self) -> Optional[Union[str,int]]:
|
||||
"""
|
||||
this will return the subject of the JWT that is accessing this endpoint.
|
||||
If no JWT is present, `None` is returned instead.
|
||||
|
||||
:return: sub of JWT
|
||||
"""
|
||||
if self._token:
|
||||
return self._verified_token(self._token)['sub']
|
||||
return None
|
||||
|
||||
def get_unverified_jwt_headers(self,encoded_token: Optional[str] = None) -> dict:
|
||||
"""
|
||||
Returns the Headers of an encoded JWT without verifying the actual signature of JWT
|
||||
|
||||
:param encoded_token: The encoded JWT to get the Header from
|
||||
:return: JWT header parameters as a dictionary
|
||||
"""
|
||||
encoded_token = encoded_token or self._token
|
||||
|
||||
return jwt.get_unverified_header(encoded_token)
|
||||
@@ -0,0 +1,85 @@
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Union, List
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
validator,
|
||||
StrictBool,
|
||||
StrictInt,
|
||||
StrictStr
|
||||
)
|
||||
|
||||
class LoadConfig(BaseModel):
|
||||
authjwt_token_location: Optional[List[StrictStr]] = {'headers'}
|
||||
authjwt_secret_key: Optional[StrictStr] = None
|
||||
authjwt_public_key: Optional[StrictStr] = None
|
||||
authjwt_private_key: Optional[StrictStr] = None
|
||||
authjwt_algorithm: Optional[StrictStr] = "HS256"
|
||||
authjwt_decode_algorithms: Optional[List[StrictStr]] = None
|
||||
authjwt_decode_leeway: Optional[Union[StrictInt,timedelta]] = 0
|
||||
authjwt_encode_issuer: Optional[StrictStr] = None
|
||||
authjwt_decode_issuer: Optional[StrictStr] = None
|
||||
authjwt_decode_audience: Optional[Union[StrictStr,List[StrictStr]]] = None
|
||||
authjwt_denylist_enabled: Optional[StrictBool] = False
|
||||
authjwt_denylist_token_checks: Optional[List[StrictStr]] = {'access','refresh'}
|
||||
authjwt_header_name: Optional[StrictStr] = "Authorization"
|
||||
authjwt_header_type: Optional[StrictStr] = "Bearer"
|
||||
authjwt_access_token_expires: Optional[Union[StrictBool,StrictInt,timedelta]] = timedelta(minutes=15)
|
||||
authjwt_refresh_token_expires: Optional[Union[StrictBool,StrictInt,timedelta]] = timedelta(days=30)
|
||||
# option for create cookies
|
||||
authjwt_access_cookie_key: Optional[StrictStr] = "access_token_cookie"
|
||||
authjwt_refresh_cookie_key: Optional[StrictStr] = "refresh_token_cookie"
|
||||
authjwt_access_cookie_path: Optional[StrictStr] = "/"
|
||||
authjwt_refresh_cookie_path: Optional[StrictStr] = "/"
|
||||
authjwt_cookie_max_age: Optional[StrictInt] = None
|
||||
authjwt_cookie_domain: Optional[StrictStr] = None
|
||||
authjwt_cookie_secure: Optional[StrictBool] = False
|
||||
authjwt_cookie_samesite: Optional[StrictStr] = None
|
||||
# option for double submit csrf protection
|
||||
authjwt_cookie_csrf_protect: Optional[StrictBool] = True
|
||||
authjwt_access_csrf_cookie_key: Optional[StrictStr] = "csrf_access_token"
|
||||
authjwt_refresh_csrf_cookie_key: Optional[StrictStr] = "csrf_refresh_token"
|
||||
authjwt_access_csrf_cookie_path: Optional[StrictStr] = "/"
|
||||
authjwt_refresh_csrf_cookie_path: Optional[StrictStr] = "/"
|
||||
authjwt_access_csrf_header_name: Optional[StrictStr] = "X-CSRF-Token"
|
||||
authjwt_refresh_csrf_header_name: Optional[StrictStr] = "X-CSRF-Token"
|
||||
authjwt_csrf_methods: Optional[List[StrictStr]] = {'POST','PUT','PATCH','DELETE'}
|
||||
|
||||
@validator('authjwt_access_token_expires')
|
||||
def validate_access_token_expires(cls, v):
|
||||
if v is True:
|
||||
raise ValueError("The 'authjwt_access_token_expires' only accept value False (bool)")
|
||||
return v
|
||||
|
||||
@validator('authjwt_refresh_token_expires')
|
||||
def validate_refresh_token_expires(cls, v):
|
||||
if v is True:
|
||||
raise ValueError("The 'authjwt_refresh_token_expires' only accept value False (bool)")
|
||||
return v
|
||||
|
||||
@validator('authjwt_denylist_token_checks', each_item=True)
|
||||
def validate_denylist_token_checks(cls, v):
|
||||
if v not in ['access','refresh']:
|
||||
raise ValueError("The 'authjwt_denylist_token_checks' must be between 'access' or 'refresh'")
|
||||
return v
|
||||
|
||||
@validator('authjwt_token_location', each_item=True)
|
||||
def validate_token_location(cls, v):
|
||||
if v not in ['headers','cookies']:
|
||||
raise ValueError("The 'authjwt_token_location' must be between 'headers' or 'cookies'")
|
||||
return v
|
||||
|
||||
@validator('authjwt_cookie_samesite')
|
||||
def validate_cookie_samesite(cls, v):
|
||||
if v not in ['strict','lax','none']:
|
||||
raise ValueError("The 'authjwt_cookie_samesite' must be between 'strict', 'lax', 'none'")
|
||||
return v
|
||||
|
||||
@validator('authjwt_csrf_methods', each_item=True)
|
||||
def validate_csrf_methods(cls, v):
|
||||
if v.upper() not in {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}:
|
||||
raise ValueError("The 'authjwt_csrf_methods' must be between http request methods")
|
||||
return v.upper()
|
||||
|
||||
class Config:
|
||||
str_min_length = 1
|
||||
str_strip_whitespace = True
|
||||
@@ -0,0 +1,72 @@
|
||||
class AuthJWTException(Exception):
|
||||
"""
|
||||
Base except which all fastapi_jwt_auth errors extend
|
||||
"""
|
||||
pass
|
||||
|
||||
class InvalidHeaderError(AuthJWTException):
|
||||
"""
|
||||
An error getting jwt in header or jwt header information from a request
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class JWTDecodeError(AuthJWTException):
|
||||
"""
|
||||
An error decoding a JWT
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class CSRFError(AuthJWTException):
|
||||
"""
|
||||
An error with CSRF protection
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class MissingTokenError(AuthJWTException):
|
||||
"""
|
||||
Error raised when token not found
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class RevokedTokenError(AuthJWTException):
|
||||
"""
|
||||
Error raised when a revoked token attempt to access a protected endpoint
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class AccessTokenRequired(AuthJWTException):
|
||||
"""
|
||||
Error raised when a valid, non-access JWT attempt to access an endpoint
|
||||
protected by jwt_required, jwt_optional, fresh_jwt_required
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class RefreshTokenRequired(AuthJWTException):
|
||||
"""
|
||||
Error raised when a valid, non-refresh JWT attempt to access an endpoint
|
||||
protected by jwt_refresh_token_required
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
class FreshTokenRequired(AuthJWTException):
|
||||
"""
|
||||
Error raised when a valid, non-fresh JWT attempt to access an endpoint
|
||||
protected by fresh_jwt_required
|
||||
"""
|
||||
def __init__(self,status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
+18
-11
@@ -47,28 +47,28 @@ beautifulsoup4 = "^4.12.2"
|
||||
google-search-results = "^2.4.1"
|
||||
google-api-python-client = "^2.79.0"
|
||||
typer = "^0.9.0"
|
||||
gunicorn = "^20.1.0"
|
||||
gunicorn = "23.0.0"
|
||||
pandas = "^2.0.0"
|
||||
chromadb = "^0.3.21"
|
||||
rich = "^13.4.2"
|
||||
networkx = "^3.1"
|
||||
unstructured = "^0.17.2"
|
||||
pypdf = "*" # 和llama-index包冲突
|
||||
pypdf = "*"
|
||||
lxml = "^4.9.2"
|
||||
pysrt = "^1.1.2"
|
||||
fake-useragent = "^1.1.3"
|
||||
docstring-parser = "^0.15"
|
||||
psycopg2-binary = "^2.9.6"
|
||||
pyarrow = "^12.0.0"
|
||||
pyarrow = "14.0.1"
|
||||
tiktoken = "*"
|
||||
wikipedia = "^1.4.0"
|
||||
qdrant-client = "^1.3.0"
|
||||
websockets = "^10.3"
|
||||
weaviate-client = "^3.21.0"
|
||||
cohere = "^4.11.0"
|
||||
python-multipart = "^0.0.6"
|
||||
python-multipart = "0.0.18"
|
||||
sqlmodel = "^0.0.14"
|
||||
pymysql = ">=1.0,<2.0"
|
||||
pymysql = "1.1.1"
|
||||
pymilvus = "2.5.10"
|
||||
elasticsearch = "^8.9.0"
|
||||
orjson = "^3.9.1"
|
||||
@@ -77,7 +77,7 @@ cachetools = "^5.3.1"
|
||||
types-cachetools = "^5.3.0.5"
|
||||
appdirs = "^1.4.4"
|
||||
supabase = "^2.4.0"
|
||||
certifi = "^2023.5.7"
|
||||
certifi = ">=2024.7.4"
|
||||
psycopg = "^3.1.9"
|
||||
psycopg-binary = "^3.1.9"
|
||||
emoji = "^2.10.1"
|
||||
@@ -86,17 +86,15 @@ scipy = "*"
|
||||
arxiv = "2.1.0"
|
||||
matplotlib = "3.8.4"
|
||||
cchardet = "^2.1.7"
|
||||
llama-index = "0.9.48"
|
||||
tenacity = "<8.4.0"
|
||||
bisheng-ragas = "^1.0.2"
|
||||
qianfan = "^0.4.4"
|
||||
dashscope = "^1.20.3"
|
||||
blobfile = "^3.0.0"
|
||||
mcp = "^1.6.0"
|
||||
mcp = "1.10.0"
|
||||
numpy = "^1.26.2"
|
||||
pyjwt = "^1.7.1"
|
||||
pyjwt = "2.4.0"
|
||||
tencentcloud-sdk-python = "^3.0.1373"
|
||||
fastapi-jwt-auth = "^0.5.0"
|
||||
langchain-openai = "^0.3.16"
|
||||
markdownify = "^1.1.0"
|
||||
readability = "^0.3.2"
|
||||
@@ -111,11 +109,20 @@ tabulate = "^0.9.0"
|
||||
zhipuai = "^1.0.7"
|
||||
websocket-client = "^1.8.0"
|
||||
opencv-python = "4.5.5.64"
|
||||
Pillow = "9.5.0"
|
||||
Pillow = "10.3.0"
|
||||
filetype = "1.2.0"
|
||||
aiomysql = "^0.2.0"
|
||||
sse-starlette = "^2.3.6"
|
||||
transformers = "^4.53.0"
|
||||
llama-index = "0.13.0"
|
||||
llama-index-core = ">=0.13.0,<0.14"
|
||||
llama-index-cli = ">=0.5.0,<0.6"
|
||||
llama-index-llms-openai = ">=0.5,<0.6"
|
||||
llama-index-embeddings-openai = ">=0.5,<0.6"
|
||||
llama-index-readers-file = ">=0.5,<0.6"
|
||||
llama-index-readers-llama-parse = "0.5.0"
|
||||
llama-index-indices-managed-llama-cloud = "0.9.0"
|
||||
llama-cloud = "0.1.35"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^23.1.0"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { saveSop, startLinsight } from '~/api/linsight';
|
||||
import { useLinsightManager, useLinsightSessionManager } from '~/hooks/useLinsightManager';
|
||||
import { Button, Textarea } from '../ui';
|
||||
import CopyButton from './components/CopyButton';
|
||||
import { LoadingDots } from './components/sopLoading';
|
||||
import { LoadingDots } from './components/SopLoading';
|
||||
import SopMarkdown from './SopMarkdown';
|
||||
|
||||
export const enum SopStatus {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useGetLinsightToolList, useGetOrgToolList, useGetPersonalToolList } fro
|
||||
import { useGenerateSop, useLinsightManager } from '~/hooks/useLinsightManager';
|
||||
import { formatTime } from '~/utils';
|
||||
import { SopCase } from './case';
|
||||
import { LoadingBox } from './components/sopLoading';
|
||||
import { LoadingBox } from './components/SopLoading';
|
||||
import { Header } from './Header';
|
||||
import { SOPEditor, SopStatus } from './SOPEditor';
|
||||
import { TaskFlow } from './TaskFlow';
|
||||
|
||||
Reference in New Issue
Block a user