Demo
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.venv/
|
||||
__pycache__
|
||||
.idea/
|
||||
@@ -0,0 +1,5 @@
|
||||
Генерируем приватный ключ в папку certs(пока что она открыта)
|
||||
openssl genrsa -out jwt-private.pem 2048
|
||||
На основе него публичный
|
||||
openssl rsa -in jwt-private.pem -outform PEM -pubout -out jwt-public.pem
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
class AuthJWT(BaseModel):
|
||||
private_key_path: Path = BASE_DIR/"certs"/"jwt-private.pem"
|
||||
public_key_path: Path = BASE_DIR/"certs"/"jwt-public.pem"
|
||||
algorithm: str = "RS256"
|
||||
access_token_expire_minutes: int = 15
|
||||
|
||||
class Settings(BaseSettings):
|
||||
auth_jwt: AuthJWT = AuthJWT()
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,95 @@
|
||||
from app.schemas.user import UserSchema
|
||||
from app.schemas.token import TokenInfo
|
||||
from app.utils.bcrypt_utils import *
|
||||
from app.utils.jwt_utlis import *
|
||||
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
http_bearer = HTTPBearer()
|
||||
|
||||
router = APIRouter(prefix="/jwt", tags=["JWT"])
|
||||
|
||||
john = UserSchema(
|
||||
username="john",
|
||||
password=hash_password("qwerty"),
|
||||
email="john@example.com"
|
||||
)
|
||||
|
||||
sam = UserSchema(
|
||||
username="sam",
|
||||
password=hash_password("secret"),
|
||||
)
|
||||
|
||||
users_db: dict[str, UserSchema] = {
|
||||
john.username: john,
|
||||
sam.username: sam
|
||||
}
|
||||
|
||||
def validate_auth_user(
|
||||
username: str = Form(),
|
||||
password: str = Form()
|
||||
):
|
||||
unauthed_exc = HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid username or password"
|
||||
)
|
||||
if not (user := users_db.get(username)):
|
||||
raise unauthed_exc
|
||||
|
||||
if validate_password(
|
||||
password=password,
|
||||
hashed_password=user.password
|
||||
):
|
||||
return user
|
||||
raise unauthed_exc
|
||||
|
||||
def get_current_token_payload(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(http_bearer)
|
||||
) -> UserSchema:
|
||||
token = credentials.credentials
|
||||
try:
|
||||
payload = decode_jwt(
|
||||
token=token
|
||||
)
|
||||
except InvalidTokenError:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid token error"
|
||||
)
|
||||
return payload
|
||||
|
||||
def get_current_auth_user(
|
||||
payload: dict = Depends(get_current_token_payload)
|
||||
) -> UserSchema:
|
||||
username: str = payload.get("username")
|
||||
if not (user := users_db.get(username)):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Token invalid" # fr user not found
|
||||
)
|
||||
return user
|
||||
|
||||
@router.post("/login/", response_model=TokenInfo)
|
||||
def auth_user_issue_jwt(
|
||||
user: UserSchema = Depends(validate_auth_user)
|
||||
):
|
||||
jwt_payload = {
|
||||
"username": user.username,
|
||||
"email": user.email
|
||||
}
|
||||
token = encode_jwt(jwt_payload)
|
||||
return TokenInfo(
|
||||
access_token=token,
|
||||
token_type="Bearer"
|
||||
)
|
||||
|
||||
@router.get("/users/me/")
|
||||
def auth_user_check_self_info(
|
||||
user: UserSchema = Depends(get_current_auth_user)
|
||||
):
|
||||
return {
|
||||
"username": user.username,
|
||||
"email": user.email
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class TokenInfo(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel, EmailStr, ConfigDict
|
||||
|
||||
|
||||
class UserSchema(BaseModel):
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
username: str
|
||||
password: bytes
|
||||
email: EmailStr | None = None
|
||||
active: bool = True
|
||||
@@ -0,0 +1,17 @@
|
||||
import bcrypt
|
||||
|
||||
def hash_password(
|
||||
password: str
|
||||
) -> bytes:
|
||||
salt = bcrypt.gensalt()
|
||||
pwd_bytes: bytes = password.encode()
|
||||
return bcrypt.hashpw(pwd_bytes, salt)
|
||||
|
||||
def validate_password(
|
||||
password: str,
|
||||
hashed_password: bytes
|
||||
) -> bool:
|
||||
return bcrypt.checkpw(
|
||||
password=password.encode(),
|
||||
hashed_password=hashed_password
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
import jwt
|
||||
from app.core.config import settings
|
||||
|
||||
def encode_jwt(payload: dict,
|
||||
private_key: str = settings.auth_jwt.private_key_path.read_text(),
|
||||
algorithm=settings.auth_jwt.algorithm,
|
||||
expire_timedelta: timedelta | None = None,
|
||||
expire_minutes: int = settings.auth_jwt.access_token_expire_minutes):
|
||||
to_encode = payload.copy()
|
||||
now = datetime.utcnow()
|
||||
if expire_timedelta:
|
||||
expire = now + timedelta
|
||||
else:
|
||||
expire = now + timedelta(minutes=expire_minutes)
|
||||
to_encode.update(
|
||||
exp=expire,
|
||||
iat=now
|
||||
)
|
||||
encoded = jwt.encode(to_encode,
|
||||
private_key,
|
||||
algorithm=algorithm)
|
||||
return encoded
|
||||
|
||||
def decode_jwt(token: str | bytes,
|
||||
public_key: str = settings.auth_jwt.public_key_path.read_text(),
|
||||
algorithm: str = settings.auth_jwt.algorithm):
|
||||
decoded = jwt.decode(token, public_key, algorithms=[algorithm])
|
||||
return decoded
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvCJK5l6y0AbeW
|
||||
1yMhbvR2LrJz0F+oXwWsx/pK+h73I48UhITWRKrktYYrnEyc/T2ip2x5EaYwYQpi
|
||||
mZ1+cX5iIqWO/7QtlKSXQfKlccU9YDXKrkK7p1lHkPnSIRXC+x8/IuicseLe8Iue
|
||||
smYR6qfIs6rt6HehtlXj8yVFmc2XVZsWEIyERCXLbxYJSka/kGKzL1OwQpS8kubu
|
||||
LToTxGhJey8rdm30mEA+oWAHqmuKJYCTfC9qwzb3D4XhNTIe2vMfyeZN/m5pSK9h
|
||||
XwaE0up8MiMMtr/r4kAcMMjGxa02G9sKa2twXvEz3swrhiEZmaJ2tsHqlH3mcHFJ
|
||||
KfrxRXS3AgMBAAECggEACRTDyaQOtMqMan1AK44X86dZx0J3XamLoAd1rlaQ+eq6
|
||||
+kO9DjTco1l7qMWTzM1V8xttBR1r5ufeykQE2zheNNwHuB/Lve9C8goQFPj0uv0Q
|
||||
rWXvIzdI6lQ7C39MVr2aq51PCw1iCbkNm8hog7BBNkHqfpKbqlgAcTKYEdorkLKT
|
||||
q1GVlCJTH1ObsBextTCB7sNYfMx9aF3r796C2PF9n75/JAn2ju29ZvaRlKnV50XO
|
||||
ETOpBVUu8soTDBMSeJibSBqF/673p0mJtHf21bQOJMfF08udl8OR1sxnY/utp4wo
|
||||
oc6qoEFkA8F2gRKHPNRcCokSNIrGFilMAkw+xysiYQKBgQDnYOuXIanFPeTwZhS6
|
||||
vtlahLHeLM8SC/ZAEqtalTEGJXxquCkKKwaxveWy0He2HBEDNn70bAWgFNRWTVP5
|
||||
2dj2wzCinwWpuZTMHOzFnuOaD0AE5aul/NsmrWB8EaXiPgDZ1zffW+b5RDWQ4GSO
|
||||
9PM9yoGn1AfIowZV0BpWPt8ZBwKBgQDBqLuLr0FV18P0DykvLerxScjCjdfH5FOB
|
||||
76LbLwtaN66P+nRcJWZJDEMcqDEOCa48W4yQtJR1xfsU2omlnXt7gK1YDm0zdqYY
|
||||
ZCsNQmn1IGmd854FwfJ0ZnflM0JkpgYPhc1BkGjFDzd24fLxdozhOYw/JQl1/qlD
|
||||
NhBzZWpK0QKBgGdRD+HcVkKM8L2n13nL3qJcvk/HEm+sh7j0nS9Fjy5/bp74vNqd
|
||||
e8/CS98c11eqEK6uluHtmlVw95/Rx725WPhXEFCNUaoIBDlgcRmNaXRRevS02YC0
|
||||
9+a9ZGgCOWBc72RWhcxm7SxBGOooSL7NAhCFbH/UXeVpZlrLIfeA+33hAoGBAJxF
|
||||
4dmteeagr6LBnTVSM3W0WcoNliPa7zw6OUvwIZQSvM24iK9XCrxFpd55JuSyESTp
|
||||
p3uaDPcg5SUF2O+JLn0R8E/PjHVr+EpUgp03i7NgDXqFfW4d/N3LVlLhU2FOzDM4
|
||||
gNK4iqMCTXqkiFwrkzGoM7E83O6XqtTuysWj/+sRAoGAQdBZPveQy2dsIw5BHrew
|
||||
5s9Xz1kpc2ZYOKmW3tcWAL0DCL1QeLdFiKLK4EjeTILsB9q2VboWExMGTGPw1OZM
|
||||
a6ohIB++/SsFVgBRaaHXJ0wixsHYS5kfnpUxtYk5eMgO/q4DvJ0X30nFZARKY2SC
|
||||
O1OjFagJYTbGmUj5Azkwhi8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwiSuZestAG3ltcjIW70
|
||||
di6yc9BfqF8FrMf6Svoe9yOPFISE1kSq5LWGK5xMnP09oqdseRGmMGEKYpmdfnF+
|
||||
YiKljv+0LZSkl0HypXHFPWA1yq5Cu6dZR5D50iEVwvsfPyLonLHi3vCLnrJmEeqn
|
||||
yLOq7eh3obZV4/MlRZnNl1WbFhCMhEQly28WCUpGv5Bisy9TsEKUvJLm7i06E8Ro
|
||||
SXsvK3Zt9JhAPqFgB6priiWAk3wvasM29w+F4TUyHtrzH8nmTf5uaUivYV8GhNLq
|
||||
fDIjDLa/6+JAHDDIxsWtNhvbCmtrcF7xM97MK4YhGZmidrbB6pR95nBxSSn68UV0
|
||||
twIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import FastAPI
|
||||
from app.routers import demo
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(demo.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", reload=True)
|
||||
@@ -0,0 +1,23 @@
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.0
|
||||
bcrypt==5.0.0
|
||||
cffi==2.0.0
|
||||
click==8.3.1
|
||||
cryptography==46.0.3
|
||||
dnspython==2.8.0
|
||||
email-validator==2.3.0
|
||||
fastapi==0.122.0
|
||||
h11==0.16.0
|
||||
idna==3.11
|
||||
pycparser==2.23
|
||||
pydantic==2.12.5
|
||||
pydantic-settings==2.12.0
|
||||
pydantic_core==2.41.5
|
||||
PyJWT==2.10.1
|
||||
python-dotenv==1.2.1
|
||||
python-multipart==0.0.20
|
||||
starlette==0.50.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
uvicorn==0.38.0
|
||||
Reference in New Issue
Block a user