From c2bef4813c302510f6462e3777c609ea6acd5a9d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 13 Mar 2025 15:23:26 +0800 Subject: [PATCH] Optimize the fastapi oauth2 integration --- fastapi_oauth20/clients/feishu.py | 1 - fastapi_oauth20/clients/gitee.py | 1 - fastapi_oauth20/clients/github.py | 1 - fastapi_oauth20/clients/google.py | 1 - fastapi_oauth20/clients/linuxdo.py | 1 - fastapi_oauth20/clients/oschina.py | 1 - fastapi_oauth20/errors.py | 6 +-- fastapi_oauth20/integrations/fastapi.py | 70 ++++++++++++++++++++----- fastapi_oauth20/oauth20.py | 3 -- pyproject.toml | 2 +- 10 files changed, 61 insertions(+), 26 deletions(-) diff --git a/fastapi_oauth20/clients/feishu.py b/fastapi_oauth20/clients/feishu.py index f63415d..95ed41f 100644 --- a/fastapi_oauth20/clients/feishu.py +++ b/fastapi_oauth20/clients/feishu.py @@ -14,7 +14,6 @@ def __init__(self, client_id: str, client_secret: str): authorize_endpoint='https://passport.feishu.cn/suite/passport/oauth/authorize', access_token_endpoint='https://passport.feishu.cn/suite/passport/oauth/token', refresh_token_endpoint='https://passport.feishu.cn/suite/passport/oauth/authorize', - oauth_callback_route_name='feishu', default_scopes=[ 'contact:user.employee_id:readonly', 'contact:user.base:readonly', diff --git a/fastapi_oauth20/clients/gitee.py b/fastapi_oauth20/clients/gitee.py index 4df22d9..8265e28 100644 --- a/fastapi_oauth20/clients/gitee.py +++ b/fastapi_oauth20/clients/gitee.py @@ -14,7 +14,6 @@ def __init__(self, client_id: str, client_secret: str): authorize_endpoint='https://gitee.com/oauth/authorize', access_token_endpoint='https://gitee.com/oauth/token', refresh_token_endpoint='https://gitee.com/oauth/token', - oauth_callback_route_name='gitee', default_scopes=['user_info'], ) diff --git a/fastapi_oauth20/clients/github.py b/fastapi_oauth20/clients/github.py index 819fb1f..40bc6ce 100644 --- a/fastapi_oauth20/clients/github.py +++ b/fastapi_oauth20/clients/github.py @@ -13,7 +13,6 @@ def __init__(self, client_id: str, client_secret: str): client_secret=client_secret, authorize_endpoint='https://github.com/login/oauth/authorize', access_token_endpoint='https://github.com/login/oauth/access_token', - oauth_callback_route_name='github', default_scopes=['user', 'user:email'], ) diff --git a/fastapi_oauth20/clients/google.py b/fastapi_oauth20/clients/google.py index b951352..8f5c151 100644 --- a/fastapi_oauth20/clients/google.py +++ b/fastapi_oauth20/clients/google.py @@ -15,7 +15,6 @@ def __init__(self, client_id: str, client_secret: str): access_token_endpoint='https://oauth2.googleapis.com/token', refresh_token_endpoint='https://oauth2.googleapis.com/token', revoke_token_endpoint='https://accounts.google.com/o/oauth2/revoke', - oauth_callback_route_name='google', default_scopes=['email', 'openid', 'profile'], ) diff --git a/fastapi_oauth20/clients/linuxdo.py b/fastapi_oauth20/clients/linuxdo.py index b63e25d..fc65d98 100644 --- a/fastapi_oauth20/clients/linuxdo.py +++ b/fastapi_oauth20/clients/linuxdo.py @@ -14,7 +14,6 @@ def __init__(self, client_id: str, client_secret: str): authorize_endpoint='https://connect.linux.do/oauth2/authorize', access_token_endpoint='https://connect.linux.do/oauth2/token', refresh_token_endpoint='https://connect.linux.do/oauth2/token', - oauth_callback_route_name='linuxdo', token_endpoint_basic_auth=True, ) diff --git a/fastapi_oauth20/clients/oschina.py b/fastapi_oauth20/clients/oschina.py index 90e84ef..c34fde2 100644 --- a/fastapi_oauth20/clients/oschina.py +++ b/fastapi_oauth20/clients/oschina.py @@ -14,7 +14,6 @@ def __init__(self, client_id: str, client_secret: str): authorize_endpoint='https://www.oschina.net/action/oauth2/authorize', access_token_endpoint='https://www.oschina.net/action/openapi/token', refresh_token_endpoint='https://www.oschina.net/action/openapi/token', - oauth_callback_route_name='oschina', ) async def get_userinfo(self, access_token: str) -> dict: diff --git a/fastapi_oauth20/errors.py b/fastapi_oauth20/errors.py index 5407d3a..d28c0cd 100644 --- a/fastapi_oauth20/errors.py +++ b/fastapi_oauth20/errors.py @@ -3,8 +3,8 @@ import httpx -class FastAPIOAuth20BaseError(Exception): - """The fastapi-oauth20 base error.""" +class OAuth20BaseError(Exception): + """The oauth2 base error.""" msg: str @@ -13,7 +13,7 @@ def __init__(self, msg: str) -> None: super().__init__(msg) -class OAuth20RequestError(FastAPIOAuth20BaseError): +class OAuth20RequestError(OAuth20BaseError): """OAuth2 httpx request error""" def __init__(self, msg: str, response: httpx.Response | None = None) -> None: diff --git a/fastapi_oauth20/integrations/fastapi.py b/fastapi_oauth20/integrations/fastapi.py index 8533a19..4e23e57 100644 --- a/fastapi_oauth20/integrations/fastapi.py +++ b/fastapi_oauth20/integrations/fastapi.py @@ -1,36 +1,80 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from fastapi import Request +from typing import Any -from fastapi_oauth20.errors import RedirectURIError +import httpx + +from fastapi import HTTPException, Request + +from fastapi_oauth20.errors import AccessTokenError, HTTPXOAuth20Error, OAuth20BaseError from fastapi_oauth20.oauth20 import OAuth20Base +class OAuth20AuthorizeCallbackError(HTTPException, OAuth20BaseError): + """The OAuth2 authorization callback error.""" + + def __init__( + self, + status_code: int, + detail: Any = None, + headers: dict[str, str] | None = None, + response: httpx.Response | None = None, + ) -> None: + self.response = response + super().__init__(status_code=status_code, detail=detail, headers=headers) + + class FastAPIOAuth20: def __init__( self, client: OAuth20Base, redirect_uri: str | None = None, - oauth_callback_route_name: str | None = None, + oauth2_callback_route_name: str | None = None, ): + """ + OAuth2 authorization callback dependency injection + + :param client: A client base on OAuth20Base. + :param redirect_uri: OAuth2 callback full URL. + :param oauth2_callback_route_name: OAuth2 callback route name, as defined by the route decorator 'name' parameter. + """ + assert (redirect_uri is None and oauth2_callback_route_name is not None) or ( + redirect_uri is not None and oauth2_callback_route_name is None + ), 'FastAPIOAuth20 redirect_uri and oauth2_callback_route_name cannot be defined at the same time.' self.client = client - self.oauth_callback_route_name = oauth_callback_route_name self.redirect_uri = redirect_uri + self.oauth2_callback_route_name = oauth2_callback_route_name async def __call__( self, request: Request, - code: str, + code: str | None = None, state: str | None = None, code_verifier: str | None = None, + error: str | None = None, ) -> tuple[dict, str]: - if self.redirect_uri is None: - if self.oauth_callback_route_name is None: - raise RedirectURIError('redirect_uri is required') - self.redirect_uri = str(request.url_for(self.oauth_callback_route_name)) - - access_token = await self.client.get_access_token( - code=code, redirect_uri=self.redirect_uri, code_verifier=code_verifier - ) + if code is None or error is not None: + raise OAuth20AuthorizeCallbackError( + status_code=400, + detail=error if error is not None else None, + ) + + if self.oauth2_callback_route_name: + redirect_url = str(request.url_for(self.oauth2_callback_route_name)) + else: + redirect_url = self.redirect_uri + + try: + access_token = await self.client.get_access_token( + code=code, + redirect_uri=redirect_url, + code_verifier=code_verifier, + ) + except (HTTPXOAuth20Error, AccessTokenError) as e: + raise OAuth20AuthorizeCallbackError( + status_code=500, + detail=e.msg, + response=e.response, + ) from e return access_token, state diff --git a/fastapi_oauth20/oauth20.py b/fastapi_oauth20/oauth20.py index b74bf48..33f9be0 100644 --- a/fastapi_oauth20/oauth20.py +++ b/fastapi_oauth20/oauth20.py @@ -27,7 +27,6 @@ def __init__( access_token_endpoint: str, refresh_token_endpoint: str | None = None, revoke_token_endpoint: str | None = None, - oauth_callback_route_name: str = 'oauth20', default_scopes: list[str] | None = None, token_endpoint_basic_auth: bool = False, revoke_token_endpoint_basic_auth: bool = False, @@ -41,7 +40,6 @@ def __init__( :param access_token_endpoint: The access token endpoint URL. :param refresh_token_endpoint: The refresh token endpoint URL. :param revoke_token_endpoint: The revoke token endpoint URL. - :param oauth_callback_route_name: :param default_scopes: :param token_endpoint_basic_auth: :param revoke_token_endpoint_basic_auth: @@ -52,7 +50,6 @@ def __init__( self.access_token_endpoint = access_token_endpoint self.refresh_token_endpoint = refresh_token_endpoint self.revoke_token_endpoint = revoke_token_endpoint - self.oauth_callback_route_name = oauth_callback_route_name self.default_scopes = default_scopes self.token_endpoint_basic_auth = token_endpoint_basic_auth self.revoke_token_endpoint_basic_auth = revoke_token_endpoint_basic_auth diff --git a/pyproject.toml b/pyproject.toml index cae72d5..6d26c04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "fastapi_oauth20" +name = "fastapi-oauth20" description = "在 FastAPI 中异步授权 OAuth 2.0 客户端" dynamic = [ "version",