diff options
Diffstat (limited to 'authentication/endpoints.py')
| -rw-r--r-- | authentication/endpoints.py | 88 |
1 files changed, 64 insertions, 24 deletions
diff --git a/authentication/endpoints.py b/authentication/endpoints.py index ebdc7a1..f94d55b 100644 --- a/authentication/endpoints.py +++ b/authentication/endpoints.py | |||
| @@ -1,31 +1,40 @@ | |||
| 1 | import json | ||
| 2 | from base64 import b64encode | ||
| 3 | from functools import partial | ||
| 1 | from typing import Annotated | 4 | from typing import Annotated |
| 2 | from uuid import uuid7 | 5 | from uuid import uuid7 |
| 3 | 6 | ||
| 4 | from fastapi import FastAPI, Request, Form, Query, HTTPException | 7 | from alembic.testing import requirements |
| 5 | from pydantic import BaseModel, EmailStr, ValidationError | 8 | from fastapi import FastAPI, Request, Form, Query |
| 9 | from pydantic import BaseModel, ValidationError | ||
| 10 | from sqlmodel import select | ||
| 11 | from starlette.authentication import requires | ||
| 6 | from starlette.middleware.authentication import AuthenticationMiddleware | 12 | from starlette.middleware.authentication import AuthenticationMiddleware |
| 7 | from starlette.middleware.sessions import SessionMiddleware | 13 | from starlette.middleware.sessions import SessionMiddleware |
| 8 | from starlette.responses import HTMLResponse, RedirectResponse | 14 | from starlette.responses import HTMLResponse, RedirectResponse |
| 9 | from starlette.templating import Jinja2Templates | 15 | from starlette.templating import Jinja2Templates |
| 10 | 16 | ||
| 11 | import conf | 17 | import conf |
| 12 | from authentication.backend import AuthBackend | 18 | from authentication.authlib import AuthorizationServer, AuthorizationCodeGrant, prepare_oauth_request |
| 13 | from authentication.utils import bcrypt_sha256_verify | 19 | from authentication.backend import SessionAuthBackend |
| 20 | from authentication.utils import bcrypt_sha256_verify, get_url_path_with_query | ||
| 14 | from db.models import User, Session, ClientApplication | 21 | from db.models import User, Session, ClientApplication |
| 15 | from db.session import get_session_context | 22 | from db.session import get_session_context |
| 16 | from proxy.utils import get_path_with_query_string | ||
| 17 | 23 | ||
| 18 | authentication = FastAPI() | 24 | authentication = FastAPI() |
| 19 | authentication.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | 25 | authentication.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) |
| 20 | 26 | ||
| 21 | oauth = FastAPI() | 27 | oauth = FastAPI() |
| 22 | oauth.add_middleware(AuthenticationMiddleware, backend=AuthBackend()) | 28 | oauth.add_middleware(AuthenticationMiddleware, backend=SessionAuthBackend()) |
| 23 | oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) | 29 | oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY) |
| 24 | 30 | ||
| 31 | auth_server = AuthorizationServer() | ||
| 32 | code_grant = partial(AuthorizationCodeGrant, server=auth_server) | ||
| 33 | |||
| 25 | templates = Jinja2Templates(directory='authentication/templates') | 34 | templates = Jinja2Templates(directory='authentication/templates') |
| 26 | 35 | ||
| 27 | class LoginForm(BaseModel): | 36 | class LoginForm(BaseModel): |
| 28 | username: EmailStr | 37 | username: str |
| 29 | password: str | 38 | password: str |
| 30 | 39 | ||
| 31 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): | 40 | async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): |
| @@ -35,11 +44,11 @@ async def base_login(request: Request, form: LoginForm | None = None, errors: di | |||
| 35 | }) | 44 | }) |
| 36 | 45 | ||
| 37 | @authentication.get('/login/', response_class=HTMLResponse) | 46 | @authentication.get('/login/', response_class=HTMLResponse) |
| 38 | async def login(request: Request): | 47 | async def get_login(request: Request, next: Annotated[str, Query()] = None): |
| 39 | return await base_login(request) | 48 | return await base_login(request) |
| 40 | 49 | ||
| 41 | @authentication.post('/login/', response_class=HTMLResponse) | 50 | @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): | 51 | async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): |
| 43 | form = None | 52 | form = None |
| 44 | errors = {} | 53 | errors = {} |
| 45 | 54 | ||
| @@ -58,30 +67,61 @@ async def login(request: Request, username: Annotated[str, Form()] = '', passwor | |||
| 58 | session = Session(id=uuid7(), user=user) | 67 | session = Session(id=uuid7(), user=user) |
| 59 | db_session.add(session) | 68 | db_session.add(session) |
| 60 | 69 | ||
| 61 | if redirect is not None: | 70 | request.session.update({'id': str(session.id)}) |
| 62 | return RedirectResponse(redirect) | 71 | |
| 72 | if next is not None: | ||
| 73 | return RedirectResponse(next) | ||
| 63 | 74 | ||
| 64 | except ValidationError as exc: | 75 | except ValidationError as exc: |
| 65 | errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) | 76 | errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) |
| 66 | 77 | ||
| 67 | return await base_login(request, form, errors) | 78 | return await base_login(request, form, errors) |
| 68 | 79 | ||
| 80 | async def base_authorize(request: Request, user: User | None = None): | ||
| 81 | if not conf.IS_LOCAL: | ||
| 82 | with get_session_context() as db_session: | ||
| 83 | db_session.query(Session).filter(Session.id == request.session.get('id')).delete() | ||
| 69 | 84 | ||
| 70 | async def base_authorize(request: Request, client_id: str, redirect_uri: str): | 85 | return auth_server.create_authorization_response(request, request.user, grant=None) |
| 71 | with get_session_context() as session: | ||
| 72 | app = session.query(ClientApplication).where(ClientApplication.client_id == client_id).first() | ||
| 73 | 86 | ||
| 74 | if app is None: | 87 | @oauth.get('/authorize/') |
| 75 | raise HTTPException(status_code=404, detail='Invalid client id') | 88 | @requires(['authenticated', 'session'], redirect='get_login') |
| 89 | async def get_authorize(request: Request): | ||
| 90 | return await base_authorize(request, request.user) | ||
| 76 | 91 | ||
| 77 | if not request.user.is_authenticated: | 92 | @oauth.post('/authorize/') |
| 78 | return RedirectResponse(f'/auth/login/?redirect={get_path_with_query_string(request)}') | 93 | @requires(['authenticated', 'session'], redirect='get_login') |
| 94 | async def post_authorize(request: Request): | ||
| 95 | return await base_authorize(request, request.user) | ||
| 79 | 96 | ||
| 97 | @oauth.post('/token/') | ||
| 98 | async def post_token(request: Request): | ||
| 99 | await prepare_oauth_request(request) | ||
| 100 | return auth_server.create_token_response(request) | ||
| 80 | 101 | ||
| 81 | @oauth.get('/authorize/') | 102 | @authentication.get('/connect/') |
| 82 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | 103 | async def connect(request: Request): |
| 83 | return await base_authorize(request, client_id, redirect_uri) | 104 | origin = request.url.scheme + '://' + request.url.netloc |
| 84 | 105 | ||
| 85 | @oauth.post('/authorize/') | 106 | with get_session_context() as session: |
| 86 | async def authorize(request: Request, client_id: Annotated[str, Query()], redirect_uri: Annotated[str, Query()]): | 107 | app = session.exec(select(ClientApplication)).first() |
| 87 | return await base_authorize(request, client_id, redirect_uri) | 108 | |
| 109 | return RedirectResponse( | ||
| 110 | '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { | ||
| 111 | 'client_id': app.client_id, | ||
| 112 | 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), | ||
| 113 | 'response_type': 'code', | ||
| 114 | 'state': b64encode(json.dumps({ | ||
| 115 | 'client_id': app.client_id, | ||
| 116 | }).encode()).decode(), | ||
| 117 | }) | ||
| 118 | ) | ||
| 119 | |||
| 120 | @oauth.get('/config/') | ||
| 121 | async def get_config(request: Request): | ||
| 122 | with get_session_context() as session: | ||
| 123 | app = session.exec(select(ClientApplication)).first() | ||
| 124 | |||
| 125 | return { | ||
| 126 | 'client_id': app.client_id, | ||
| 127 | } | ||
