summaryrefslogtreecommitdiffstats
path: root/authentication/authlib.py
diff options
context:
space:
mode:
Diffstat (limited to 'authentication/authlib.py')
-rw-r--r--authentication/authlib.py167
1 files changed, 167 insertions, 0 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 @@
1import json
2from collections import defaultdict
3from unittest.mock import patch
4
5from authlib.common.security import generate_token as generate_random_token
6from authlib.oauth2 import AuthorizationServer as BaseAuthorizationServer, OAuth2Request
7from authlib.oauth2.rfc6749 import AuthorizationCodeGrant as BaseAuthorizationCodeGrant, \
8 RefreshTokenGrant as BaseRefreshTokenGrant, OAuth2Payload
9from authlib.oauth2.rfc6750 import BearerTokenGenerator
10from authlib.oauth2.rfc7636 import CodeChallenge
11from sqlalchemy.exc import NoResultFound
12
13from sqlmodel import select
14from fastapi import Request, Response
15
16from db.models import ClientApplication, AuthToken, AuthCode, User
17from db.session import get_session_context
18
19
20class 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
64class 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
85async 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
91class 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
109class 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
134class 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