diff options
| author | 2026-07-03 15:49:08 +0200 | |
|---|---|---|
| committer | 2026-07-03 15:49:14 +0200 | |
| commit | 7784c60c03ec277b456db6e2709383e302d9382b (patch) | |
| tree | a008def666f04041fe77740693d42cfef89af558 /authentication/endpoints.py | |
| parent | fb28c648390464c75ba4903b0e1e0ff2642c8ffb (diff) | |
| download | server-7784c60c03ec277b456db6e2709383e302d9382b.tar.gz server-7784c60c03ec277b456db6e2709383e302d9382b.tar.bz2 server-7784c60c03ec277b456db6e2709383e302d9382b.zip | |
Added basic oauth flow
Diffstat (limited to 'authentication/endpoints.py')
| -rw-r--r-- | authentication/endpoints.py | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/authentication/endpoints.py b/authentication/endpoints.py new file mode 100644 index 0000000..ebdc7a1 --- /dev/null +++ b/authentication/endpoints.py | |||
| @@ -0,0 +1,87 @@ | |||
| 1 | from typing import Annotated | ||
| 2 | from uuid import uuid7 | ||
| 3 | |||
| 4 | from fastapi import FastAPI, Request, Form, Query, HTTPException | ||
| 5 | from pydantic import BaseModel, EmailStr, ValidationError | ||
| 6 | from starlette.middleware.authentication import AuthenticationMiddleware | ||
| 7 | from starlette.middleware.sessions import SessionMiddleware | ||
| 8 | from starlette.responses import HTMLResponse, RedirectResponse | ||
| 9 | from starlette.templating import Jinja2Templates | ||
| 10 | |||
| 11 | import conf | ||
| 12 | from authentication.backend import AuthBackend | ||
| 13 | from authentication.utils import bcrypt_sha256_verify | ||
| 14 | from db.models import User, Session, ClientApplication | ||
| 15 | from db.session import get_session_context | ||
| 16 | from proxy.utils import get_path_with_query_string | ||
| 17 | |||
| 18 | authentication = FastAPI() | ||
| 19 | authentication.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | ||
| 20 | |||
| 21 | oauth = FastAPI() | ||
| 22 | oauth.add_middleware(AuthenticationMiddleware, backend=AuthBackend()) | ||
| 23 | oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | ||
| 24 | |||
| 25 | templates = Jinja2Templates(directory='authentication/templates') | ||
| 26 | |||
| 27 | class LoginForm(BaseModel): | ||
| 28 | username: EmailStr | ||
| 29 | password: str | ||
| 30 | |||
| 31 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): | ||
| 32 | return templates.TemplateResponse(request, 'login.html', context={ | ||
| 33 | 'form': form, | ||
| 34 | 'errors': errors, | ||
| 35 | }) | ||
| 36 | |||
| 37 | @authentication.get('/login/', response_class=HTMLResponse) | ||
| 38 | async def login(request: Request): | ||
| 39 | return await base_login(request) | ||
| 40 | |||
| 41 | @authentication.post('/login/', response_class=HTMLResponse) | ||
| 42 | async def login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', redirect: Annotated[str, Query()] = None): | ||
| 43 | form = None | ||
| 44 | errors = {} | ||
| 45 | |||
| 46 | try: | ||
| 47 | form = LoginForm.model_validate({ | ||
| 48 | 'username': username, | ||
| 49 | 'password': password | ||
| 50 | }) | ||
| 51 | with get_session_context() as db_session: | ||
| 52 | user = db_session.query(User).filter(User.username == form.username).first() | ||
| 53 | |||
| 54 | if not bcrypt_sha256_verify(password, user.password if user is not None else '') or user is None: | ||
| 55 | errors.update({'password': 'Invalid username or password'}) | ||
| 56 | |||
| 57 | with get_session_context() as db_session: | ||
| 58 | session = Session(id=uuid7(), user=user) | ||
| 59 | db_session.add(session) | ||
| 60 | |||
| 61 | if redirect is not None: | ||
| 62 | return RedirectResponse(redirect) | ||
| 63 | |||
| 64 | except ValidationError as exc: | ||
| 65 | errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) | ||
| 66 | |||
| 67 | return await base_login(request, form, errors) | ||
| 68 | |||
| 69 | |||
| 70 | async def base_authorize(request: Request, client_id: str, redirect_uri: str): | ||
| 71 | with get_session_context() as session: | ||
| 72 | app = session.query(ClientApplication).where(ClientApplication.client_id == client_id).first() | ||
| 73 | |||
| 74 | if app is None: | ||
| 75 | raise HTTPException(status_code=404, detail='Invalid client id') | ||
| 76 | |||
| 77 | if not request.user.is_authenticated: | ||
| 78 | return RedirectResponse(f'/auth/login/?redirect={get_path_with_query_string(request)}') | ||
| 79 | |||
| 80 | |||
| 81 | @oauth.get('/authorize/') | ||
| 82 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | ||
| 83 | return await base_authorize(request, client_id, redirect_uri) | ||
| 84 | |||
| 85 | @oauth.post('/authorize/') | ||
| 86 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | ||
| 87 | return await base_authorize(request, client_id, redirect_uri) | ||
