aboutsummaryrefslogtreecommitdiffstats
path: root/authentication
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2026-09-04 17:09:24 +0200
committerGravatar Tom van der Lee <tom@vanderlee.io>2026-09-04 17:09:24 +0200
commit6b6f20a032eb31d747366c8bc270ca81731eaf6a (patch)
treeada48080495267bebde4ee1251bbbe77d629745f /authentication
parent2d22158dd95f76399f5716d556be60c77da04a46 (diff)
downloadserver-v3.tar.gz
server-v3.tar.bz2
server-v3.zip
Fixed pre-commit issuesv3
Diffstat (limited to 'authentication')
-rw-r--r--authentication/authlib.py77
-rw-r--r--authentication/backend.py49
-rw-r--r--authentication/endpoints.py177
-rw-r--r--authentication/mixins.py12
-rw-r--r--authentication/utils.py10
5 files changed, 197 insertions, 128 deletions
diff --git a/authentication/authlib.py b/authentication/authlib.py
index 8472062..a37d68f 100644
--- a/authentication/authlib.py
+++ b/authentication/authlib.py
@@ -1,31 +1,37 @@
1import json 1import json
2from collections import defaultdict 2from collections import defaultdict
3from typing import ClassVar
3from unittest.mock import patch 4from unittest.mock import patch
4 5
5from authlib.common.security import generate_token as generate_random_token 6from authlib.common.security import generate_token as generate_random_token
6from authlib.oauth2 import AuthorizationServer as BaseAuthorizationServer, OAuth2Request 7from authlib.oauth2 import AuthorizationServer as BaseAuthorizationServer
7from authlib.oauth2.rfc6749 import AuthorizationCodeGrant as BaseAuthorizationCodeGrant, \ 8from authlib.oauth2 import OAuth2Request
8 RefreshTokenGrant as BaseRefreshTokenGrant, OAuth2Payload 9from authlib.oauth2.rfc6749 import AuthorizationCodeGrant as BaseAuthorizationCodeGrant
10from authlib.oauth2.rfc6749 import OAuth2Payload
11from authlib.oauth2.rfc6749 import RefreshTokenGrant as BaseRefreshTokenGrant
9from authlib.oauth2.rfc6750 import BearerTokenGenerator 12from authlib.oauth2.rfc6750 import BearerTokenGenerator
10from authlib.oauth2.rfc7636 import CodeChallenge 13from authlib.oauth2.rfc7636 import CodeChallenge
14from fastapi import Request, Response
11from sqlalchemy.exc import NoResultFound 15from sqlalchemy.exc import NoResultFound
12
13from sqlmodel import select 16from sqlmodel import select
14from fastapi import Request, Response
15 17
16from db.models import ClientApplication, AuthToken, AuthCode, User 18from db.models import AuthCode, AuthToken, ClientApplication, User
17from db.session import get_session_context 19from db.session import get_session_context
18 20
19 21
20class AuthorizationCodeGrant(BaseAuthorizationCodeGrant): 22class AuthorizationCodeGrant(BaseAuthorizationCodeGrant):
21 TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] 23 TOKEN_ENDPOINT_AUTH_METHODS: ClassVar[list[str]] = [
24 "client_secret_basic",
25 "client_secret_post",
26 "none",
27 ]
22 28
23 def save_authorization_code(self, code: str, request: OAuth2Request): 29 def save_authorization_code(self, code: str, request: OAuth2Request):
24 with get_session_context() as db_session: 30 with get_session_context() as db_session:
25 db_session.add(request.user) 31 db_session.add(request.user)
26 32
27 code_challenge = request.payload.data.get('code_challenge') 33 code_challenge = request.payload.data.get("code_challenge")
28 code_challenge_method = request.payload.data.get('code_challenge_method') 34 code_challenge_method = request.payload.data.get("code_challenge_method")
29 35
30 auth_code = AuthCode( 36 auth_code = AuthCode(
31 code=code, 37 code=code,
@@ -41,10 +47,16 @@ class AuthorizationCodeGrant(BaseAuthorizationCodeGrant):
41 47
42 return auth_code 48 return auth_code
43 49
44 def query_authorization_code(self, code: str, client: ClientApplication) -> AuthCode | None: 50 def query_authorization_code(
51 self, code: str, client: ClientApplication
52 ) -> AuthCode | None:
45 try: 53 try:
46 with get_session_context() as db_session: 54 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() 55 auth_code = db_session.exec(
56 select(AuthCode).where(
57 AuthCode.code == code, AuthCode.client_id == client.client_id
58 )
59 ).one()
48 except NoResultFound: 60 except NoResultFound:
49 return None 61 return None
50 62
@@ -65,7 +77,9 @@ class RefreshTokenGrant(BaseRefreshTokenGrant):
65 def authenticate_refresh_token(self, refresh_token) -> AuthToken | None: 77 def authenticate_refresh_token(self, refresh_token) -> AuthToken | None:
66 try: 78 try:
67 with get_session_context() as db_session: 79 with get_session_context() as db_session:
68 token = db_session.exec(select(AuthToken).where(AuthToken.refresh_token == refresh_token)).one() 80 token = db_session.exec(
81 select(AuthToken).where(AuthToken.refresh_token == refresh_token)
82 ).one()
69 except NoResultFound: 83 except NoResultFound:
70 return None 84 return None
71 85
@@ -82,6 +96,7 @@ class RefreshTokenGrant(BaseRefreshTokenGrant):
82 credential.revoked = True 96 credential.revoked = True
83 db_session.add(credential) 97 db_session.add(credential)
84 98
99
85async def prepare_oauth_request(request: Request) -> Request: 100async def prepare_oauth_request(request: Request) -> Request:
86 async with request.form() as form: 101 async with request.form() as form:
87 request.state.form_data = dict(form) 102 request.state.form_data = dict(form)
@@ -96,7 +111,7 @@ class FastApiOAuth2Payload(OAuth2Payload):
96 def data(self): 111 def data(self):
97 return { 112 return {
98 **self._request.query_params, 113 **self._request.query_params,
99 **getattr(self._request.state, 'form_data', {}), 114 **getattr(self._request.state, "form_data", {}),
100 } 115 }
101 116
102 @property 117 @property
@@ -106,9 +121,10 @@ class FastApiOAuth2Payload(OAuth2Payload):
106 values[k].extend([self.data[k]]) 121 values[k].extend([self.data[k]])
107 return values 122 return values
108 123
124
109class FastApiOAuth2Request(OAuth2Request): 125class FastApiOAuth2Request(OAuth2Request):
110 def __init__(self, request: Request): 126 def __init__(self, request: Request):
111 with patch('authlib.oauth2.rfc6749.errors.InsecureTransportError'): 127 with patch("authlib.oauth2.rfc6749.errors.InsecureTransportError"):
112 super().__init__( 128 super().__init__(
113 method=request.method, 129 method=request.method,
114 uri=str(request.url), 130 uri=str(request.url),
@@ -128,7 +144,7 @@ class FastApiOAuth2Request(OAuth2Request):
128 144
129 @property 145 @property
130 def form(self): 146 def form(self):
131 return getattr(self._request.state, 'form_data', {}) 147 return getattr(self._request.state, "form_data", {})
132 148
133 149
134class AuthorizationServer(BaseAuthorizationServer): 150class AuthorizationServer(BaseAuthorizationServer):
@@ -136,22 +152,33 @@ class AuthorizationServer(BaseAuthorizationServer):
136 super().__init__(*args, **kwargs) 152 super().__init__(*args, **kwargs)
137 self.register_grant(AuthorizationCodeGrant, extensions=[CodeChallenge()]) 153 self.register_grant(AuthorizationCodeGrant, extensions=[CodeChallenge()])
138 self.register_grant(RefreshTokenGrant) 154 self.register_grant(RefreshTokenGrant)
139 self.register_token_generator("default", BearerTokenGenerator( 155 self.register_token_generator(
140 access_token_generator=lambda *args, **kwargs: generate_random_token(42), 156 "default",
141 refresh_token_generator=lambda *args, **kwargs: generate_random_token(48), 157 BearerTokenGenerator(
142 )) 158 access_token_generator=lambda *args, **kwargs: generate_random_token(
159 42
160 ),
161 refresh_token_generator=lambda *args, **kwargs: generate_random_token(
162 48
163 ),
164 ),
165 )
143 166
144 def query_client(self, client_id: str) -> ClientApplication: 167 def query_client(self, client_id: str) -> ClientApplication:
145 with get_session_context() as db_session: 168 with get_session_context() as db_session:
146 return db_session.exec(select(ClientApplication).where(ClientApplication.client_id == client_id)).one() 169 return db_session.exec(
170 select(ClientApplication).where(
171 ClientApplication.client_id == client_id
172 )
173 ).one()
147 174
148 def save_token(self, token: dict, request: OAuth2Request): 175 def save_token(self, token: dict, request: OAuth2Request):
149 with get_session_context() as db_session: 176 with get_session_context() as db_session:
150 db_session.add(AuthToken( 177 db_session.add(
151 user_id=request.user.id, 178 AuthToken(
152 client_id=request.client.client_id, 179 user_id=request.user.id, client_id=request.client.client_id, **token
153 **token 180 )
154 )) 181 )
155 182
156 def create_oauth2_request(self, request: Request) -> OAuth2Request: 183 def create_oauth2_request(self, request: Request) -> OAuth2Request:
157 return FastApiOAuth2Request(request) 184 return FastApiOAuth2Request(request)
diff --git a/authentication/backend.py b/authentication/backend.py
index 9434906..58842de 100644
--- a/authentication/backend.py
+++ b/authentication/backend.py
@@ -1,52 +1,49 @@
1import re 1import re
2from typing import Optional
3 2
4from sqlalchemy import update 3from sqlalchemy import update
5from sqlmodel import select 4from sqlmodel import select
6from starlette.authentication import AuthenticationBackend, AuthCredentials, BaseUser, UnauthenticatedUser 5from starlette.authentication import (
6 AuthCredentials,
7 AuthenticationBackend,
8 BaseUser,
9 UnauthenticatedUser,
10)
7from starlette.requests import HTTPConnection 11from starlette.requests import HTTPConnection
8 12
9from db.models import Session, User, AuthToken 13from db.models import AuthToken, Session, User
10from db.session import get_session_context 14from db.session import get_session_context
11 15
12 16
13class SessionAuthBackend(AuthenticationBackend): 17class SessionAuthBackend(AuthenticationBackend):
14 async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: 18 async def authenticate(
19 self, request: HTTPConnection
20 ) -> tuple[AuthCredentials, BaseUser] | None:
15 if "id" not in request.session: 21 if "id" not in request.session:
16 return None 22 return None
17 23
18 query = ( 24 query = select(Session).join(User).where(Session.id == request.session["id"])
19 select(Session)
20 .join(User)
21 .where(
22 Session.id == request.session['id']
23 )
24 )
25
26 25
27 with get_session_context() as db_session: 26 with get_session_context() as db_session:
28 session: Session = db_session.exec(query).first() 27 session: Session = db_session.exec(query).first()
29 28
30 if session is not None: 29 if session is not None:
31 db_session.execute( 30 db_session.execute(update(Session).where(Session.id == session.id))
32 update(Session).where(Session.id == session.id)
33 )
34 db_session.commit() 31 db_session.commit()
35 32
36
37 return ( 33 return (
38 AuthCredentials( 34 AuthCredentials(
39 ["authenticated", "session"] 35 ["authenticated", "session"] if session is not None else []
40 if session is not None
41 else []
42 ), 36 ),
43 session.user if session is not None else UnauthenticatedUser() 37 session.user if session is not None else UnauthenticatedUser(),
44 ) 38 )
45 39
46 40
47class BearerTokenAuthBackend(AuthenticationBackend): 41class BearerTokenAuthBackend(AuthenticationBackend):
48 regex = re.compile(r"^[Bb]earer\s(?P<token>\S+)$") 42 regex = re.compile(r"^[Bb]earer\s(?P<token>\S+)$")
49 async def authenticate(self, request: HTTPConnection) -> Optional[tuple[AuthCredentials, BaseUser]]: 43
44 async def authenticate(
45 self, request: HTTPConnection
46 ) -> tuple[AuthCredentials, BaseUser] | None:
50 token = None 47 token = None
51 if "Authorization" in request.headers: 48 if "Authorization" in request.headers:
52 match = self.regex.match(request.headers["Authorization"]) 49 match = self.regex.match(request.headers["Authorization"])
@@ -54,17 +51,13 @@ class BearerTokenAuthBackend(AuthenticationBackend):
54 query = ( 51 query = (
55 select(AuthToken) 52 select(AuthToken)
56 .join(User) 53 .join(User)
57 .where(AuthToken.access_token == match.group('token')) 54 .where(AuthToken.access_token == match.group("token"))
58 ) 55 )
59 56
60 with get_session_context() as db_session: 57 with get_session_context() as db_session:
61 token: AuthToken = db_session.exec(query).first() 58 token: AuthToken = db_session.exec(query).first()
62 59
63 return ( 60 return (
64 AuthCredentials( 61 AuthCredentials(["authenticated", "token"] if token is not None else []),
65 ["authenticated", "token"] 62 token.user if token is not None else UnauthenticatedUser(),
66 if token is not None
67 else []
68 ),
69 token.user if token is not None else UnauthenticatedUser()
70 ) 63 )
diff --git a/authentication/endpoints.py b/authentication/endpoints.py
index 3af49fc..17d49d7 100644
--- a/authentication/endpoints.py
+++ b/authentication/endpoints.py
@@ -4,8 +4,8 @@ from functools import partial
4from typing import Annotated, Self 4from typing import Annotated, Self
5from uuid import uuid7 5from uuid import uuid7
6 6
7from fastapi import FastAPI, Request, Form, Query 7from fastapi import FastAPI, Form, Query, Request
8from pydantic import BaseModel, ValidationError, model_validator, EmailStr 8from pydantic import BaseModel, EmailStr, ValidationError, model_validator
9from sqlalchemy import func 9from sqlalchemy import func
10from sqlmodel import select 10from sqlmodel import select
11from starlette.authentication import requires 11from starlette.authentication import requires
@@ -15,10 +15,14 @@ from starlette.responses import HTMLResponse, RedirectResponse
15from starlette.templating import Jinja2Templates 15from starlette.templating import Jinja2Templates
16 16
17import conf 17import conf
18from authentication.authlib import AuthorizationServer, AuthorizationCodeGrant, prepare_oauth_request 18from authentication.authlib import (
19 AuthorizationCodeGrant,
20 AuthorizationServer,
21 prepare_oauth_request,
22)
19from authentication.backend import SessionAuthBackend 23from authentication.backend import SessionAuthBackend
20from authentication.utils import bcrypt_sha256_verify, get_url_path_with_query 24from authentication.utils import bcrypt_sha256_verify, get_url_path_with_query
21from db.models import User, Session, ClientApplication 25from db.models import ClientApplication, Session, User
22from db.session import get_session_context 26from db.session import get_session_context
23 27
24authentication = FastAPI() 28authentication = FastAPI()
@@ -31,10 +35,7 @@ oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY)
31auth_server = AuthorizationServer() 35auth_server = AuthorizationServer()
32code_grant = partial(AuthorizationCodeGrant, server=auth_server) 36code_grant = partial(AuthorizationCodeGrant, server=auth_server)
33 37
34templates = Jinja2Templates(directory=[ 38templates = Jinja2Templates(directory=["templates", "authentication/templates"])
35 'templates',
36 'authentication/templates'
37])
38 39
39 40
40class LoginForm(BaseModel): 41class LoginForm(BaseModel):
@@ -42,57 +43,75 @@ class LoginForm(BaseModel):
42 password: str 43 password: str
43 44
44 45
45async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): 46async def base_login(
46 return templates.TemplateResponse(request, 'login.html', context={ 47 request: Request,
47 'form': form, 48 form: LoginForm | None = None,
48 'errors': errors, 49 errors: dict[str, str] | None = None,
49 }) 50):
51 return templates.TemplateResponse(
52 request,
53 "login.html",
54 context={
55 "form": form,
56 "errors": errors,
57 },
58 )
50 59
51 60
52@authentication.get('/login/', response_class=HTMLResponse) 61@authentication.get("/login/", response_class=HTMLResponse)
53async def get_login(request: Request, next: Annotated[str, Query()] = None): 62async def get_login(request: Request, next: Annotated[str | None, Query()] = None):
54 return await base_login(request) 63 return await base_login(request)
55 64
56 65
57@authentication.post('/login/', response_class=HTMLResponse) 66@authentication.post("/login/", response_class=HTMLResponse)
58async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', 67async def post_login(
59 next: Annotated[str, Query()] = None): 68 request: Request,
69 username: Annotated[str, Form()] = "",
70 password: Annotated[str, Form()] = "",
71 next: Annotated[str | None, Query()] = None,
72):
60 form = None 73 form = None
61 errors = {} 74 errors = {}
62 75
63 try: 76 try:
64 form = LoginForm.model_validate({ 77 form = LoginForm.model_validate({"username": username, "password": password})
65 'username': username,
66 'password': password
67 })
68 78
69 with get_session_context() as db_session: 79 with get_session_context() as db_session:
70 user = db_session.query(User).filter(User.username == form.username).first() 80 user = db_session.query(User).filter(User.username == form.username).first()
71 81
72 if not bcrypt_sha256_verify(password, user.password if user is not None else '') or user is None: 82 if (
73 errors.update({'password': 'Invalid username or password'}) 83 not bcrypt_sha256_verify(
84 password, user.password if user is not None else ""
85 )
86 or user is None
87 ):
88 errors.update({"password": "Invalid username or password"})
74 else: 89 else:
75 with get_session_context() as db_session: 90 with get_session_context() as db_session:
76 session = Session(id=uuid7(), user=user) 91 session = Session(id=uuid7(), user=user)
77 db_session.add(session) 92 db_session.add(session)
78 93
79 request.session.update({'id': str(session.id)}) 94 request.session.update({"id": str(session.id)})
80 95
81 if next is not None: 96 if next is not None:
82 return RedirectResponse(next, status_code=303) 97 return RedirectResponse(next, status_code=303)
83 except ValidationError as exc: 98 except ValidationError as exc:
84 errors.update({e['loc'][0]: e['msg'] for e in exc.errors()}) 99 errors.update({e["loc"][0]: e["msg"] for e in exc.errors()})
85 100
86 return await base_login(request, form, errors) 101 return await base_login(request, form, errors)
87 102
88 103
89@authentication.get('/logout/') 104@authentication.get("/logout/")
90def logout(request: Request): 105def logout(request: Request):
91 with get_session_context() as db_session: 106 with get_session_context() as db_session:
92 db_session.query(Session).filter(Session.id == request.session.get('id')).delete() 107 db_session.query(Session).filter(
108 Session.id == request.session.get("id")
109 ).delete()
93 110
94 request.session.clear() 111 request.session.clear()
95 return RedirectResponse(request.headers.get('Referer', '/management/dashboard/'), status_code=303) 112 return RedirectResponse(
113 request.headers.get("Referer", "/management/dashboard/"), status_code=303
114 )
96 115
97 116
98class RegisterForm(BaseModel): 117class RegisterForm(BaseModel):
@@ -100,10 +119,10 @@ class RegisterForm(BaseModel):
100 password: str 119 password: str
101 verify_password: str 120 verify_password: str
102 121
103 @model_validator(mode='after') 122 @model_validator(mode="after")
104 def check_passwords_match(self) -> Self: 123 def check_passwords_match(self) -> Self:
105 if self.password != self.verify_password: 124 if self.password != self.verify_password:
106 raise ValueError('Passwords do not match') 125 raise ValueError("Passwords do not match")
107 return self 126 return self
108 127
109 128
@@ -113,31 +132,42 @@ def is_first_run() -> bool:
113 return user_count.first() == 0 132 return user_count.first() == 0
114 133
115 134
116async def base_register(request: Request, form: RegisterForm | None = None, errors: dict[str, str] | None = None): 135async def base_register(
117 return templates.TemplateResponse(request, 'register.html', context={ 136 request: Request,
118 'form': form, 137 form: RegisterForm | None = None,
119 'errors': errors, 138 errors: dict[str, str] | None = None,
120 'is_first_run': is_first_run(), 139):
121 }) 140 return templates.TemplateResponse(
141 request,
142 "register.html",
143 context={
144 "form": form,
145 "errors": errors,
146 "is_first_run": is_first_run(),
147 },
148 )
122 149
123 150
124@authentication.get('/register/', response_class=HTMLResponse) 151@authentication.get("/register/", response_class=HTMLResponse)
125async def get_register(request: Request, next: Annotated[str, Query()] = None): 152async def get_register(request: Request, next: Annotated[str | None, Query()] = None):
126 return await base_register(request) 153 return await base_register(request)
127 154
128 155
129@authentication.post('/register/', response_class=HTMLResponse) 156@authentication.post("/register/", response_class=HTMLResponse)
130async def post_register(request: Request, email: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', 157async def post_register(
131 verify_password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): 158 request: Request,
159 email: Annotated[str, Form()] = "",
160 password: Annotated[str, Form()] = "",
161 verify_password: Annotated[str, Form()] = "",
162 next: Annotated[str | None, Query()] = None,
163):
132 form = None 164 form = None
133 errors = {} 165 errors = {}
134 166
135 try: 167 try:
136 form = RegisterForm.model_validate({ 168 form = RegisterForm.model_validate(
137 'email': email, 169 {"email": email, "password": password, "verify_password": verify_password}
138 'password': password, 170 )
139 'verify_password': verify_password
140 })
141 171
142 with get_session_context() as db_session: 172 with get_session_context() as db_session:
143 user = User(username=email, email=email) 173 user = User(username=email, email=email)
@@ -147,13 +177,18 @@ async def post_register(request: Request, email: Annotated[str, Form()] = '', pa
147 session = Session(id=uuid7(), user=user) 177 session = Session(id=uuid7(), user=user)
148 db_session.add(session) 178 db_session.add(session)
149 179
150 request.session.update({'id': str(session.id)}) 180 request.session.update({"id": str(session.id)})
151 181
152 if next is not None: 182 if next is not None:
153 return RedirectResponse(next, status_code=303) 183 return RedirectResponse(next, status_code=303)
154 184
155 except ValidationError as exc: 185 except ValidationError as exc:
156 errors.update({(e['loc'][0] if e['loc'] else 'verify_password'): e['msg'] for e in exc.errors()}) 186 errors.update(
187 {
188 (e["loc"][0] if e["loc"] else "verify_password"): e["msg"]
189 for e in exc.errors()
190 }
191 )
157 192
158 return await base_register(request, form, errors) 193 return await base_register(request, form, errors)
159 194
@@ -162,49 +197,59 @@ async def base_authorize(request: Request, user: User | None = None):
162 return auth_server.create_authorization_response(request, request.user, grant=None) 197 return auth_server.create_authorization_response(request, request.user, grant=None)
163 198
164 199
165@oauth.get('/authorize/') 200@oauth.get("/authorize/")
166@requires(['authenticated', 'session'], redirect='get_login') 201@requires(["authenticated", "session"], redirect="get_login")
167async def get_authorize(request: Request): 202async def get_authorize(request: Request):
168 return await base_authorize(request, request.user) 203 return await base_authorize(request, request.user)
169 204
170 205
171@oauth.post('/authorize/') 206@oauth.post("/authorize/")
172@requires(['authenticated', 'session'], redirect='get_login') 207@requires(["authenticated", "session"], redirect="get_login")
173async def post_authorize(request: Request): 208async def post_authorize(request: Request):
174 return await base_authorize(request, request.user) 209 return await base_authorize(request, request.user)
175 210
176 211
177@oauth.post('/token/') 212@oauth.post("/token/")
178async def post_token(request: Request): 213async def post_token(request: Request):
179 await prepare_oauth_request(request) 214 await prepare_oauth_request(request)
180 return auth_server.create_token_response(request) 215 return auth_server.create_token_response(request)
181 216
182 217
183@authentication.get('/connect/') 218@authentication.get("/connect/")
184async def connect(request: Request): 219async def connect(request: Request):
185 origin = request.url.scheme + '://' + request.url.netloc 220 origin = request.url.scheme + "://" + request.url.netloc
186 221
187 with get_session_context() as session: 222 with get_session_context() as session:
188 app = session.exec(select(ClientApplication)).first() 223 app = session.exec(select(ClientApplication)).first()
189 224
190 return RedirectResponse( 225 return RedirectResponse(
191 '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { 226 "/oauth"
192 'client_id': app.client_id, 227 + get_url_path_with_query(
193 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), 228 oauth,
194 'response_type': 'code', 229 "get_authorize",
195 'state': b64encode(json.dumps({ 230 {
196 'client_id': app.client_id, 231 "client_id": app.client_id,
197 }).encode()).decode(), 232 "redirect_uri": f"{origin}/oauth"
198 }), 233 + get_url_path_with_query(oauth, "callback"),
234 "response_type": "code",
235 "state": b64encode(
236 json.dumps(
237 {
238 "client_id": app.client_id,
239 }
240 ).encode()
241 ).decode(),
242 },
243 ),
199 status_code=303, 244 status_code=303,
200 ) 245 )
201 246
202 247
203@oauth.get('/config/') 248@oauth.get("/config/")
204async def get_config(request: Request): 249async def get_config(request: Request):
205 with get_session_context() as session: 250 with get_session_context() as session:
206 app = session.exec(select(ClientApplication)).first() 251 app = session.exec(select(ClientApplication)).first()
207 252
208 return { 253 return {
209 'client_id': app.client_id, 254 "client_id": app.client_id,
210 } 255 }
diff --git a/authentication/mixins.py b/authentication/mixins.py
index 3d6a91f..70816a1 100644
--- a/authentication/mixins.py
+++ b/authentication/mixins.py
@@ -1,7 +1,11 @@
1import json 1import json
2import time 2import time
3 3
4from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin as BaseOAuth2ClientMixin, OAuth2AuthorizationCodeMixin as BaseOAuth2AuthorizationCodeMixin, OAuth2TokenMixin as BaseOAuth2TokenMixin 4from authlib.integrations.sqla_oauth2 import (
5 OAuth2AuthorizationCodeMixin as BaseOAuth2AuthorizationCodeMixin,
6)
7from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin as BaseOAuth2ClientMixin
8from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin as BaseOAuth2TokenMixin
5from sqlalchemy.sql.sqltypes import Text 9from sqlalchemy.sql.sqltypes import Text
6from sqlmodel import Field 10from sqlmodel import Field
7 11
@@ -11,9 +15,9 @@ class OAuth2ClientMixin(BaseOAuth2ClientMixin):
11 client_secret: str = Field(max_length=120, nullable=True) 15 client_secret: str = Field(max_length=120, nullable=True)
12 client_id_issued_at: int = Field(nullable=False, default=0) 16 client_id_issued_at: int = Field(nullable=False, default=0)
13 client_secret_expires_at: int = Field(nullable=False, default=0) 17 client_secret_expires_at: int = Field(nullable=False, default=0)
14 client_metadata_field: str = Field(sa_type=Text, sa_column_kwargs={ 18 client_metadata_field: str = Field(
15 'name': 'client_metadata' 19 sa_type=Text, sa_column_kwargs={"name": "client_metadata"}, nullable=True
16 }, nullable=True) 20 )
17 21
18 _client_metadata = None 22 _client_metadata = None
19 23
diff --git a/authentication/utils.py b/authentication/utils.py
index 4f19bc3..b0c9989 100644
--- a/authentication/utils.py
+++ b/authentication/utils.py
@@ -15,11 +15,11 @@ def bcrypt_sha256_hash(password: str) -> str:
15def bcrypt_sha256_verify(password: str, hashed: str) -> bool: 15def bcrypt_sha256_verify(password: str, hashed: str) -> bool:
16 digest = hashlib.sha256(password.encode("utf-8")).digest() 16 digest = hashlib.sha256(password.encode("utf-8")).digest()
17 encoded = base64.b64encode(digest) 17 encoded = base64.b64encode(digest)
18 return bcrypt.checkpw(encoded, hashed.encode('utf-8')) 18 return bcrypt.checkpw(encoded, hashed.encode("utf-8"))
19 19
20 20
21def get_url_path_with_query(app: FastAPI, name: str, params: dict | None = None, /, **path_params: str): 21def get_url_path_with_query(
22 app: FastAPI, name: str, params: dict | None = None, /, **path_params: str
23):
22 base = app.url_path_for(name, **path_params) 24 base = app.url_path_for(name, **path_params)
23 return base + '?' + urlencode(params) if params is not None else base 25 return base + "?" + urlencode(params) if params is not None else base
24
25