diff options
Diffstat (limited to 'authentication/authlib.py')
| -rw-r--r-- | authentication/authlib.py | 167 |
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 @@ | |||
| 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 | ||
