aboutsummaryrefslogtreecommitdiffstats
path: root/authentication/endpoints.py
diff options
context:
space:
mode:
Diffstat (limited to 'authentication/endpoints.py')
-rw-r--r--authentication/endpoints.py177
1 files changed, 111 insertions, 66 deletions
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 }