diff options
Diffstat (limited to 'authentication')
| -rw-r--r-- | authentication/authlib.py | 167 | ||||
| -rw-r--r-- | authentication/backend.py | 40 | ||||
| -rw-r--r-- | authentication/endpoints.py | 88 | ||||
| -rw-r--r-- | authentication/mixins.py | 54 | ||||
| -rw-r--r-- | authentication/templates/authorize.html | 5 | ||||
| -rw-r--r-- | authentication/templates/login.html | 2 | ||||
| -rw-r--r-- | authentication/utils.py | 10 |
7 files changed, 327 insertions, 39 deletions
diff --git a/authentication/authlib.py b/authentication/authlib.py new file mode 100644 index 0000000..8472062 --- /dev/null +++ b/authentication/authlib.py | |||
| @@ -0,0 +1,167 @@ | |||
| 1 | import json | ||
| 2 | from collections import defaultdict | ||
| 3 | from unittest.mock import patch | ||
| 4 | |||
| 5 | from authlib.common.security import generate_token as generate_random_token | ||
| 6 | from authlib.oauth2 import AuthorizationServer as BaseAuthorizationServer, OAuth2Request | ||
| 7 | from authlib.oauth2.rfc6749 import AuthorizationCodeGrant as BaseAuthorizationCodeGrant, \ | ||
| 8 | RefreshTokenGrant as BaseRefreshTokenGrant, OAuth2Payload | ||
| 9 | from authlib.oauth2.rfc6750 import BearerTokenGenerator | ||
| 10 | from authlib.oauth2.rfc7636 import CodeChallenge | ||
| 11 | from sqlalchemy.exc import NoResultFound | ||
| 12 | |||
| 13 | from sqlmodel import select | ||
| 14 | from fastapi import Request, Response | ||
| 15 | |||
| 16 | from db.models import ClientApplication, AuthToken, AuthCode, User | ||
| 17 | from db.session import get_session_context | ||
| 18 | |||
| 19 | |||
| 20 | class AuthorizationCodeGrant(BaseAuthorizationCodeGrant): | ||
| 21 | TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] | ||
| 22 | |||
| 23 | def save_authorization_code(self, code: str, request: OAuth2Request): | ||
| 24 | with get_session_context() as db_session: | ||
| 25 | db_session.add(request.user) | ||
| 26 | |||
| 27 | code_challenge = request.payload.data.get('code_challenge') | ||
| 28 | code_challenge_method = request.payload.data.get('code_challenge_method') | ||
| 29 | |||
| 30 | auth_code = AuthCode( | ||
| 31 | code=code, | ||
| 32 | client_id=request.client.client_id, | ||
| 33 | redirect_uri=request.payload.redirect_uri, | ||
| 34 | response_type=request.payload.response_type, | ||
| 35 | scope=request.payload.scope, | ||
| 36 | user_id=request.user.id, | ||
| 37 | code_challenge=code_challenge, | ||
| 38 | code_challenge_method=code_challenge_method, | ||
| 39 | ) | ||
| 40 | db_session.add(auth_code) | ||
| 41 | |||
| 42 | return auth_code | ||
| 43 | |||
| 44 | def query_authorization_code(self, code: str, client: ClientApplication) -> AuthCode | None: | ||
| 45 | try: | ||
| 46 | with get_session_context() as db_session: | ||
| 47 | auth_code = db_session.exec(select(AuthCode).where(AuthCode.code == code, AuthCode.client_id == client.client_id)).one() | ||
| 48 | except NoResultFound: | ||
| 49 | return None | ||
| 50 | |||
| 51 | if auth_code.is_expired(): | ||
| 52 | return None | ||
| 53 | |||
| 54 | return auth_code | ||
| 55 | |||
| 56 | def delete_authorization_code(self, code: AuthCode): | ||
| 57 | with get_session_context() as db_session: | ||
| 58 | db_session.delete(code) | ||
| 59 | |||
| 60 | def authenticate_user(self, authentication_code: AuthCode) -> User: | ||
| 61 | return authentication_code.user | ||
| 62 | |||
| 63 | |||
| 64 | class RefreshTokenGrant(BaseRefreshTokenGrant): | ||
| 65 | def authenticate_refresh_token(self, refresh_token) -> AuthToken | None: | ||
| 66 | try: | ||
| 67 | with get_session_context() as db_session: | ||
| 68 | token = db_session.exec(select(AuthToken).where(AuthToken.refresh_token == refresh_token)).one() | ||
| 69 | except NoResultFound: | ||
| 70 | return None | ||
| 71 | |||
| 72 | if not token.is_refresh_token_active(): | ||
| 73 | return None | ||
| 74 | |||
| 75 | return token | ||
| 76 | |||
| 77 | def authenticate_user(self, credential: AuthToken): | ||
| 78 | return credential.user | ||
| 79 | |||
| 80 | def revoke_old_credential(self, credential: AuthToken): | ||
| 81 | with get_session_context() as db_session: | ||
| 82 | credential.revoked = True | ||
| 83 | db_session.add(credential) | ||
| 84 | |||
| 85 | async def prepare_oauth_request(request: Request) -> Request: | ||
| 86 | async with request.form() as form: | ||
| 87 | request.state.form_data = dict(form) | ||
| 88 | return request | ||
| 89 | |||
| 90 | |||
| 91 | class FastApiOAuth2Payload(OAuth2Payload): | ||
| 92 | def __init__(self, request: Request): | ||
| 93 | self._request = request | ||
| 94 | |||
| 95 | @property | ||
| 96 | def data(self): | ||
| 97 | return { | ||
| 98 | **self._request.query_params, | ||
| 99 | **getattr(self._request.state, 'form_data', {}), | ||
| 100 | } | ||
| 101 | |||
| 102 | @property | ||
| 103 | def datalist(self): | ||
| 104 | values = defaultdict(list) | ||
| 105 | for k in self.data: | ||
| 106 | values[k].extend([self.data[k]]) | ||
| 107 | return values | ||
| 108 | |||
| 109 | class FastApiOAuth2Request(OAuth2Request): | ||
| 110 | def __init__(self, request: Request): | ||
| 111 | with patch('authlib.oauth2.rfc6749.errors.InsecureTransportError'): | ||
| 112 | super().__init__( | ||
| 113 | method=request.method, | ||
| 114 | uri=str(request.url), | ||
| 115 | headers=request.headers, | ||
| 116 | ) | ||
| 117 | self.method = request.method | ||
| 118 | self.uri = str(request.url) | ||
| 119 | self.headers = request.headers | ||
| 120 | self.payload = FastApiOAuth2Payload(request) | ||
| 121 | self.user = request.user | ||
| 122 | |||
| 123 | self._request = request | ||
| 124 | |||
| 125 | @property | ||
| 126 | def args(self): | ||
| 127 | return self._request.query_params | ||
| 128 | |||
| 129 | @property | ||
| 130 | def form(self): | ||
| 131 | return getattr(self._request.state, 'form_data', {}) | ||
| 132 | |||
| 133 | |||
| 134 | class AuthorizationServer(BaseAuthorizationServer): | ||
| 135 | def __init__(self, *args, **kwargs): | ||
| 136 | super().__init__(*args, **kwargs) | ||
| 137 | self.register_grant(AuthorizationCodeGrant, extensions=[CodeChallenge()]) | ||
| 138 | self.register_grant(RefreshTokenGrant) | ||
| 139 | self.register_token_generator("default", BearerTokenGenerator( | ||
| 140 | access_token_generator=lambda *args, **kwargs: generate_random_token(42), | ||
| 141 | refresh_token_generator=lambda *args, **kwargs: generate_random_token(48), | ||
| 142 | )) | ||
| 143 | |||
| 144 | def query_client(self, client_id: str) -> ClientApplication: | ||
| 145 | with get_session_context() as db_session: | ||
| 146 | return db_session.exec(select(ClientApplication).where(ClientApplication.client_id == client_id)).one() | ||
| 147 | |||
| 148 | def save_token(self, token: dict, request: OAuth2Request): | ||
| 149 | with get_session_context() as db_session: | ||
| 150 | db_session.add(AuthToken( | ||
| 151 | user_id=request.user.id, | ||
| 152 | client_id=request.client.client_id, | ||
| 153 | **token | ||
| 154 | )) | ||
| 155 | |||
| 156 | def create_oauth2_request(self, request: Request) -> OAuth2Request: | ||
| 157 | return FastApiOAuth2Request(request) | ||
| 158 | |||
| 159 | def handle_response(self, status, body, headers) -> Response: | ||
| 160 | return Response( | ||
| 161 | status_code=status, | ||
| 162 | content=json.dumps(body) if isinstance(body, dict) else body, | ||
| 163 | headers=dict(headers), | ||
| 164 | ) | ||
| 165 | |||
| 166 | def send_signal(self, name, *args, **kwargs): | ||
| 167 | pass | ||
diff --git a/authentication/backend.py b/authentication/backend.py index 29408e3..9434906 100644 --- a/authentication/backend.py +++ b/authentication/backend.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | from datetime import datetime, UTC | 1 | import re |
| 2 | from typing import Optional | 2 | from typing import Optional |
| 3 | 3 | ||
| 4 | from sqlalchemy import update | 4 | from sqlalchemy import update |
| @@ -6,11 +6,11 @@ from sqlmodel import select | |||
| 6 | from starlette.authentication import AuthenticationBackend, AuthCredentials, BaseUser, UnauthenticatedUser | 6 | from starlette.authentication import AuthenticationBackend, AuthCredentials, BaseUser, UnauthenticatedUser |
| 7 | from starlette.requests import HTTPConnection | 7 | from starlette.requests import HTTPConnection |
| 8 | 8 | ||
| 9 | from db.models import Session, User | 9 | from db.models import Session, User, AuthToken |
| 10 | from db.session import get_session_context | 10 | from db.session import get_session_context |
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | class AuthBackend(AuthenticationBackend): | 13 | class SessionAuthBackend(AuthenticationBackend): |
| 14 | async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: | 14 | async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: |
| 15 | if "id" not in request.session: | 15 | if "id" not in request.session: |
| 16 | return None | 16 | return None |
| @@ -33,16 +33,38 @@ class AuthBackend(AuthenticationBackend): | |||
| 33 | ) | 33 | ) |
| 34 | db_session.commit() | 34 | db_session.commit() |
| 35 | 35 | ||
| 36 | |||
| 36 | return ( | 37 | return ( |
| 37 | AuthCredentials( | 38 | AuthCredentials( |
| 38 | [ | 39 | ["authenticated", "session"] |
| 39 | "authenticated", | ||
| 40 | *[ | ||
| 41 | connection.type for connection in session.user.app_connections | ||
| 42 | ] | ||
| 43 | ] | ||
| 44 | if session is not None | 40 | if session is not None |
| 45 | else [] | 41 | else [] |
| 46 | ), | 42 | ), |
| 47 | session.user if session is not None else UnauthenticatedUser() | 43 | session.user if session is not None else UnauthenticatedUser() |
| 48 | ) | 44 | ) |
| 45 | |||
| 46 | |||
| 47 | class BearerTokenAuthBackend(AuthenticationBackend): | ||
| 48 | regex = re.compile(r"^[Bb]earer\s(?P<token>\S+)$") | ||
| 49 | async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: | ||
| 50 | token = None | ||
| 51 | if "Authorization" in request.headers: | ||
| 52 | match = self.regex.match(request.headers["Authorization"]) | ||
| 53 | |||
| 54 | query = ( | ||
| 55 | select(AuthToken) | ||
| 56 | .join(User) | ||
| 57 | .where(AuthToken.access_token == match.group('token')) | ||
| 58 | ) | ||
| 59 | |||
| 60 | with get_session_context() as db_session: | ||
| 61 | token: AuthToken = db_session.exec(query).first() | ||
| 62 | |||
| 63 | return ( | ||
| 64 | AuthCredentials( | ||
| 65 | ["authenticated", "token"] | ||
| 66 | if token is not None | ||
| 67 | else [] | ||
| 68 | ), | ||
| 69 | token.user if token is not None else UnauthenticatedUser() | ||
| 70 | ) | ||
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 | } | ||
diff --git a/authentication/mixins.py b/authentication/mixins.py new file mode 100644 index 0000000..3d6a91f --- /dev/null +++ b/authentication/mixins.py | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | import json | ||
| 2 | import time | ||
| 3 | |||
| 4 | from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin as BaseOAuth2ClientMixin, OAuth2AuthorizationCodeMixin as BaseOAuth2AuthorizationCodeMixin, OAuth2TokenMixin as BaseOAuth2TokenMixin | ||
| 5 | from sqlalchemy.sql.sqltypes import Text | ||
| 6 | from sqlmodel import Field | ||
| 7 | |||
| 8 | |||
| 9 | class OAuth2ClientMixin(BaseOAuth2ClientMixin): | ||
| 10 | client_id: str = Field(max_length=48, index=True, nullable=True) | ||
| 11 | client_secret: str = Field(max_length=120, nullable=True) | ||
| 12 | client_id_issued_at: int = Field(nullable=False, default=0) | ||
| 13 | client_secret_expires_at: int = Field(nullable=False, default=0) | ||
| 14 | client_metadata_field: str = Field(sa_type=Text, sa_column_kwargs={ | ||
| 15 | 'name': 'client_metadata' | ||
| 16 | }, nullable=True) | ||
| 17 | |||
| 18 | _client_metadata = None | ||
| 19 | |||
| 20 | @property | ||
| 21 | def client_metadata(self) -> dict: | ||
| 22 | if self.client_metadata_field: | ||
| 23 | return json.loads(self.client_metadata_field) | ||
| 24 | return {} | ||
| 25 | |||
| 26 | def set_client_metadata(self, metadata: dict): | ||
| 27 | self.client_metadata_field = json.dumps(metadata) | ||
| 28 | |||
| 29 | |||
| 30 | class OAuth2AuthorizationCodeMixin(BaseOAuth2AuthorizationCodeMixin): | ||
| 31 | code: str = Field(max_length=120, unique=True, nullable=False) | ||
| 32 | client_id: str = Field(max_length=48, nullable=True) | ||
| 33 | redirect_uri: str = Field(sa_type=Text, default="", nullable=True) | ||
| 34 | response_type: str = Field(sa_type=Text, default="", nullable=True) | ||
| 35 | scope: str = Field(sa_type=Text, default="", nullable=True) | ||
| 36 | nonce: str = Field(sa_type=Text, nullable=True) | ||
| 37 | auth_time: int = Field(nullable=False, default_factory=lambda: int(time.time())) | ||
| 38 | acr: str = Field(sa_type=Text, nullable=True) | ||
| 39 | amr: str = Field(sa_type=Text, nullable=True) | ||
| 40 | |||
| 41 | code_challenge: str = Field(sa_type=Text, nullable=True) | ||
| 42 | code_challenge_method: str = Field(max_length=48, nullable=True) | ||
| 43 | |||
| 44 | |||
| 45 | class OAuth2TokenMixin(BaseOAuth2TokenMixin): | ||
| 46 | client_id: str = Field(max_length=48, nullable=True) | ||
| 47 | token_type: str = Field(max_length=40, nullable=True) | ||
| 48 | access_token: str = Field(max_length=255, unique=True, nullable=False) | ||
| 49 | refresh_token: str = Field(max_length=255, index=True, nullable=True) | ||
| 50 | scope: str = Field(sa_type=Text, default="", nullable=True) | ||
| 51 | issued_at: int = Field(nullable=False, default_factory=lambda: int(time.time())) | ||
| 52 | access_token_revoked_at: int = Field(nullable=False, default=0) | ||
| 53 | refresh_token_revoked_at: int = Field(nullable=False, default=0) | ||
| 54 | expires_in: int = Field(nullable=False, default=0) | ||
diff --git a/authentication/templates/authorize.html b/authentication/templates/authorize.html deleted file mode 100644 index 1ed6506..0000000 --- a/authentication/templates/authorize.html +++ /dev/null | |||
| @@ -1,5 +0,0 @@ | |||
| 1 | {% extends "./base.html" %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <h1>AUTH</h1> | ||
| 5 | {% endblock %} | ||
diff --git a/authentication/templates/login.html b/authentication/templates/login.html index fcba5cd..2db2512 100644 --- a/authentication/templates/login.html +++ b/authentication/templates/login.html | |||
| @@ -3,7 +3,7 @@ | |||
| 3 | {% block content %} | 3 | {% block content %} |
| 4 | <form method="post"> | 4 | <form method="post"> |
| 5 | 5 | ||
| 6 | <input type="email" name="username" {% if form %}value="{{ form.username }}"{% endif %} placeholder="username" /> | 6 | <input type="text" name="username" {% if form %}value="{{ form.username }}"{% endif %} placeholder="username" /> |
| 7 | {% if errors and errors.username %} | 7 | {% if errors and errors.username %} |
| 8 | {{ errors.username }} | 8 | {{ errors.username }} |
| 9 | {% endif %} | 9 | {% endif %} |
diff --git a/authentication/utils.py b/authentication/utils.py index f383969..4f19bc3 100644 --- a/authentication/utils.py +++ b/authentication/utils.py | |||
| @@ -1,6 +1,9 @@ | |||
| 1 | import base64 | 1 | import base64 |
| 2 | import hashlib | 2 | import hashlib |
| 3 | from urllib.parse import urlencode | ||
| 4 | |||
| 3 | import bcrypt | 5 | import bcrypt |
| 6 | from fastapi import FastAPI | ||
| 4 | 7 | ||
| 5 | 8 | ||
| 6 | def bcrypt_sha256_hash(password: str) -> str: | 9 | def bcrypt_sha256_hash(password: str) -> str: |
| @@ -13,3 +16,10 @@ def bcrypt_sha256_verify(password: str, hashed: str) -> bool: | |||
| 13 | digest = hashlib.sha256(password.encode("utf-8")).digest() | 16 | digest = hashlib.sha256(password.encode("utf-8")).digest() |
| 14 | encoded = base64.b64encode(digest) | 17 | encoded = base64.b64encode(digest) |
| 15 | return bcrypt.checkpw(encoded, hashed.encode('utf-8')) | 18 | return bcrypt.checkpw(encoded, hashed.encode('utf-8')) |
| 19 | |||
| 20 | |||
| 21 | def get_url_path_with_query(app: FastAPI, name: str, params: dict | None = None, /, **path_params: str): | ||
| 22 | base = app.url_path_for(name, **path_params) | ||
| 23 | return base + '?' + urlencode(params) if params is not None else base | ||
| 24 | |||
| 25 | |||
