add fingerprint httpbearer schema

This commit is contained in:
IluaAir
2025-09-21 21:35:46 +03:00
parent 408e32d05f
commit 3dc36a2f25
4 changed files with 31 additions and 18 deletions

View File

@@ -1,7 +1,11 @@
from typing import Annotated from typing import Annotated
from fastapi import Depends, HTTPException, Path from fastapi import Depends, HTTPException, Path
from fastapi.security import HTTPBearer, OAuth2PasswordBearer from fastapi.security import (
HTTPAuthorizationCredentials,
HTTPBearer,
OAuth2PasswordBearer,
)
from jwt import InvalidTokenError from jwt import InvalidTokenError
from src.api.dependacies.db_dep import sessionDep from src.api.dependacies.db_dep import sessionDep
@@ -12,8 +16,10 @@ from src.services.tasks import TaskService
from src.services.users import UserService from src.services.users import UserService
http_bearer = HTTPBearer(auto_error=False) http_bearer = HTTPBearer(auto_error=False)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
AccessTokenDep = Annotated[str, Depends(oauth2_scheme)] # AccessTokenDep = Annotated[str, Depends(oauth2_scheme)]
AccessTokenDep = Annotated[HTTPAuthorizationCredentials, Depends(http_bearer)]
async def get_current_user( async def get_current_user(
@@ -25,7 +31,7 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = AuthManager.decode_access_token(token, verify_exp) payload = AuthManager.decode_access_token(token.credentials, verify_exp)
if payload is None: if payload is None:
raise credentials_exception raise credentials_exception
user = TokenData(**payload) user = TokenData(**payload)

View File

@@ -1,6 +1,6 @@
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response from fastapi import APIRouter, Body, Cookie, Depends, Form, HTTPException, Response
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from src.api.dependacies.db_dep import sessionDep from src.api.dependacies.db_dep import sessionDep
@@ -27,15 +27,16 @@ async def login(
session: sessionDep, session: sessionDep,
credential: Annotated[OAuth2PasswordRequestForm, Depends()], credential: Annotated[OAuth2PasswordRequestForm, Depends()],
response: Response, response: Response,
fingerprint: str = Form(),
): ):
result = await AuthService(session).login(credential.username, credential.password) result = await AuthService(session).login(credential.username, credential.password, fingerprint=fingerprint)
response.set_cookie( response.set_cookie(
key="refresh_token", key="refresh_token",
value=result["refresh_token"], value=result["refresh_token"],
httponly=True, httponly=True,
samesite="lax", samesite="lax",
path=settings.api.v1.auth, path=settings.api.v1.auth,
max_age=60 * 60 * 24 * 7, max_age=60 * 60 * 24 * settings.refresh_token.expire_days,
) )
return result return result
@@ -43,20 +44,21 @@ async def login(
@router.post(path="/refresh", response_model=Token) @router.post(path="/refresh", response_model=Token)
async def refresh( async def refresh(
session: sessionDep, session: sessionDep,
response: Response,
current_user: RefreshUser, current_user: RefreshUser,
response: Response,
fingerprint: str = Body(),
refresh_token: Annotated[str | None, Cookie(name="refresh_token")] = None, refresh_token: Annotated[str | None, Cookie(name="refresh_token")] = None,
): ):
if refresh_token is None: if refresh_token is None:
raise HTTPException(status_code=401, detail="No refresh token") raise HTTPException(status_code=401, detail="No refresh token")
result = await AuthService(session).refresh_tokens(refresh_token, current_user) result = await AuthService(session).refresh_tokens(refresh_token, current_user, fingerprint)
response.set_cookie( response.set_cookie(
key="refresh_token", key="refresh_token",
value=result["refresh_token"], value=result["refresh_token"],
httponly=True, httponly=True,
samesite="lax", samesite="lax",
path=settings.api.v1.auth, path=settings.api.v1.auth,
max_age=60 * 60 * 24 * 7, max_age=60 * 60 * 24 * settings.refresh_token.expire_days,
) )
return result return result

View File

@@ -1,8 +1,8 @@
"""expire and fingerprintjs token """expire and fingerprintjs token
Revision ID: f015c2db2185 Revision ID: 5821f37941a8
Revises: b879d3502c37 Revises: b879d3502c37
Create Date: 2025-09-21 19:52:28.292685 Create Date: 2025-09-21 20:16:48.289050
""" """
from typing import Sequence, Union from typing import Sequence, Union
@@ -10,7 +10,8 @@ from typing import Sequence, Union
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
revision: str = 'f015c2db2185' # revision identifiers, used by Alembic.
revision: str = '5821f37941a8'
down_revision: Union[str, None] = 'b879d3502c37' down_revision: Union[str, None] = 'b879d3502c37'
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
@@ -18,11 +19,15 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
"""Upgrade schema.""" """Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('refresh_tokens', sa.Column('fingerprint', sa.String(length=255), nullable=False)) op.add_column('refresh_tokens', sa.Column('fingerprint', sa.String(length=255), nullable=False))
op.add_column('refresh_tokens', sa.Column('expired_at', sa.TIMESTAMP(timezone=True), server_default=sa.text("(datetime('now', '+7 days'))"), nullable=False)) op.add_column('refresh_tokens', sa.Column('expired_at', sa.TIMESTAMP(timezone=True), server_default=sa.text("(datetime('now', '+7 days'))"), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema.""" """Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('refresh_tokens', 'expired_at') op.drop_column('refresh_tokens', 'expired_at')
op.drop_column('refresh_tokens', 'fingerprint') op.drop_column('refresh_tokens', 'fingerprint')
# ### end Alembic commands ###

View File

@@ -19,7 +19,7 @@ class AuthService(BaseService):
await self.session.commit() await self.session.commit()
return User.model_validate(result) return User.model_validate(result)
async def login(self, username: str, password: str): async def login(self, username: str, password: str, fingerprint: str) -> dict:
result = await self.session.user.get_one_or_none(username=username) result = await self.session.user.get_one_or_none(username=username)
if result is None: if result is None:
raise HTTPException( raise HTTPException(
@@ -27,7 +27,7 @@ class AuthService(BaseService):
detail="Incorrect username or password", detail="Incorrect username or password",
) )
user = UserWithHashedPass.model_validate(result) user = UserWithHashedPass.model_validate(result)
user_token = TokenData.model_validate(user.model_dump()) token_data = TokenData.model_validate(user.model_dump())
verify = AuthManager.verify_password( verify = AuthManager.verify_password(
plain_password=password, hashed_password=user.hashed_password plain_password=password, hashed_password=user.hashed_password
) )
@@ -36,12 +36,12 @@ class AuthService(BaseService):
status_code=401, status_code=401,
detail="Incorrect username or password", detail="Incorrect username or password",
) )
access_token = AuthManager.create_access_token(user_token.model_dump()) access_token = AuthManager.create_access_token(token_data.model_dump())
refresh_token = AuthManager.create_refresh_token() refresh_token = AuthManager.create_refresh_token()
await self.session.auth.create_one({ await self.session.auth.create_one({
"token": refresh_token, "token": refresh_token,
"user_id": user.id, "user_id": user.id,
"fingerprint": "default" # TODO: Implement proper fingerprint generation "fingerprint": fingerprint
}) })
await self.session.commit() await self.session.commit()
return { return {
@@ -54,7 +54,7 @@ class AuthService(BaseService):
await self.session.auth.delete_one(token=token) await self.session.auth.delete_one(token=token)
await self.session.commit() await self.session.commit()
async def refresh_tokens(self, refresh_token: str, user_data: TokenData): async def refresh_tokens(self, refresh_token: str, user_data: TokenData, fingerprint: str) -> dict:
token_record = await self.session.auth.get_one_or_none(token=refresh_token) token_record = await self.session.auth.get_one_or_none(token=refresh_token)
if not token_record or token_record.user_id != user_data.id: if not token_record or token_record.user_id != user_data.id:
raise HTTPException(status_code=401, detail="Invalid refresh token") raise HTTPException(status_code=401, detail="Invalid refresh token")
@@ -64,7 +64,7 @@ class AuthService(BaseService):
await self.session.auth.create_one({ await self.session.auth.create_one({
"token": new_refresh_token, "token": new_refresh_token,
"user_id": user_data.id, "user_id": user_data.id,
"fingerprint": "default" # TODO: Implement proper fingerprint generation "fingerprint": fingerprint
}) })
await self.session.commit() await self.session.commit()
return { return {