This commit is contained in:
roma-dxunvrs
2025-11-30 12:38:46 +03:00
commit 27466a255f
17 changed files with 250 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.venv/
__pycache__
.idea/
+5
View File
@@ -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
View File
View File
+16
View File
@@ -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()
View File
+95
View File
@@ -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
}
View File
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class TokenInfo(BaseModel):
access_token: str
token_type: str
+10
View File
@@ -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
View File
+17
View File
@@ -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
)
+30
View File
@@ -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
+28
View File
@@ -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-----
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwiSuZestAG3ltcjIW70
di6yc9BfqF8FrMf6Svoe9yOPFISE1kSq5LWGK5xMnP09oqdseRGmMGEKYpmdfnF+
YiKljv+0LZSkl0HypXHFPWA1yq5Cu6dZR5D50iEVwvsfPyLonLHi3vCLnrJmEeqn
yLOq7eh3obZV4/MlRZnNl1WbFhCMhEQly28WCUpGv5Bisy9TsEKUvJLm7i06E8Ro
SXsvK3Zt9JhAPqFgB6priiWAk3wvasM29w+F4TUyHtrzH8nmTf5uaUivYV8GhNLq
fDIjDLa/6+JAHDDIxsWtNhvbCmtrcF7xM97MK4YhGZmidrbB6pR95nBxSSn68UV0
twIDAQAB
-----END PUBLIC KEY-----
+9
View File
@@ -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)
+23
View File
@@ -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