From 9475ca4319f4a5fccbf175fc129ef76ff1f303b3 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 2 Aug 2023 16:03:58 +0400 Subject: [PATCH 01/19] GH-8: Add typed property definitions --- src/fastapi_oauth2/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fastapi_oauth2/core.py b/src/fastapi_oauth2/core.py index a9e7291..475d5e3 100644 --- a/src/fastapi_oauth2/core.py +++ b/src/fastapi_oauth2/core.py @@ -46,9 +46,10 @@ class OAuth2Core: client_id: str = None client_secret: str = None - callback_url: Optional[str] = None scope: Optional[List[str]] = None claims: Optional[Claims] = None + provider: str = None + redirect_uri: str = None backend: BaseOAuth2 = None _oauth_client: Optional[WebApplicationClient] = None From 4524e0d131db3c248e90d8bac36e3f00dd619107 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 2 Aug 2023 16:05:45 +0400 Subject: [PATCH 02/19] GH-8: Hold the instance of provider in `Auth` context --- src/fastapi_oauth2/middleware.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/fastapi_oauth2/middleware.py b/src/fastapi_oauth2/middleware.py index c921f7b..5dd5eb1 100644 --- a/src/fastapi_oauth2/middleware.py +++ b/src/fastapi_oauth2/middleware.py @@ -6,7 +6,6 @@ from typing import Dict from typing import List from typing import Optional -from typing import Sequence from typing import Tuple from typing import Union @@ -39,16 +38,15 @@ class Auth(AuthCredentials): scopes: List[str] clients: Dict[str, OAuth2Core] = {} - provider: str - default_provider: str = "local" + _provider: OAuth2Core = None - def __init__( - self, - scopes: Optional[Sequence[str]] = None, - provider: str = default_provider, - ) -> None: - super().__init__(scopes) - self.provider = provider + @property + def provider(self) -> Union[OAuth2Core, None]: + return self._provider + + @provider.setter + def provider(self, identifier) -> None: + self._provider = self.clients.get(identifier) @classmethod def set_http(cls, http: bool) -> None: @@ -145,18 +143,16 @@ async def authenticate(self, request: Request) -> Optional[Tuple[Auth, User]]: return Auth(), User() user = User(Auth.jwt_decode(param)) - user.update(provider=user.get("provider", Auth.default_provider)) - auth = Auth(user.pop("scope", []), user.get("provider")) - client = Auth.clients.get(auth.provider) - claims = client.claims if client else Claims() - user = user.use_claims(claims) + auth = Auth(user.pop("scope", [])) + auth.provider = user.get("provider") + claims = auth.provider.claims if auth.provider else {} # Call the callback function on authentication if callable(self.callback): - coroutine = self.callback(auth, user) + coroutine = self.callback(auth, user.use_claims(claims)) if issubclass(type(coroutine), Awaitable): await coroutine - return auth, user + return auth, user.use_claims(claims) class OAuth2Middleware: From e77a32f33fbc1073c2b5af982cbc40d4d7600765 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 2 Aug 2023 16:07:14 +0400 Subject: [PATCH 03/19] Use the claim mapped `User` properties --- examples/demonstration/main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/demonstration/main.py b/examples/demonstration/main.py index e657bf1..8c8ca7e 100644 --- a/examples/demonstration/main.py +++ b/examples/demonstration/main.py @@ -24,12 +24,13 @@ async def on_auth(auth: Auth, user: User): db: Session = next(get_db()) query = db.query(UserModel) if user.identity and not query.filter_by(identity=user.identity).first(): + # create a local user by OAuth2 user's data if it does not exist yet UserModel(**{ - "identity": user.get("identity"), - "username": user.get("username"), - "image": user.get("image"), - "email": user.get("email"), - "name": user.get("name"), + "identity": user.identity, # User property + "username": user.get("username"), # custom attribute + "name": user.display_name, # User property + "image": user.picture, # User property + "email": user.email, # User property }).save(db) From a03f24034cf5d710b0713c7eaf8b2db98e80faa9 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 2 Aug 2023 16:08:13 +0400 Subject: [PATCH 04/19] Add indicator for checking the current provider --- examples/demonstration/templates/index.html | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/demonstration/templates/index.html b/examples/demonstration/templates/index.html index 9a8b81d..50b953c 100644 --- a/examples/demonstration/templates/index.html +++ b/examples/demonstration/templates/index.html @@ -21,11 +21,13 @@ Simulate Login - - - - - + {% for provider in request.auth.clients %} + + + + + + {% endfor %} {% endif %} @@ -33,6 +35,14 @@ style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: calc(100vh - 70px);"> {% if request.user.is_authenticated %}

Hi, {{ request.user.display_name }}

+

+ You're signed in using + {% if request.auth.provider %} + external {{ request.auth.provider.provider }} OAuth2 provider. + {% else %} + local authentication system. + {% endif %} +

This is what your JWT contains currently

{{ json.dumps(request.user, indent=4) }}
{% else %} From 187fcd03bfcb5293d5366e1bb6dd34d9f2a70f31 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Sun, 6 Aug 2023 21:30:04 +0400 Subject: [PATCH 05/19] Fix the IDE highlighting on OAuth2 classes of the `security` module --- src/fastapi_oauth2/security.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/fastapi_oauth2/security.py b/src/fastapi_oauth2/security.py index 0f5d3b3..fddc067 100644 --- a/src/fastapi_oauth2/security.py +++ b/src/fastapi_oauth2/security.py @@ -1,8 +1,4 @@ -from typing import Any -from typing import Callable -from typing import Dict from typing import Optional -from typing import Tuple from typing import Type from fastapi.security import OAuth2 as FastAPIOAuth2 @@ -12,32 +8,29 @@ from starlette.requests import Request -def use_cookies(cls: Type[FastAPIOAuth2]) -> Callable[[Tuple[Any], Dict[str, Any]], FastAPIOAuth2]: - """OAuth2 classes wrapped with this decorator will use cookies for the Authorization header.""" +class OAuth2Cookie(type): + """OAuth2 classes using this metaclass will use cookies for the Authorization header.""" + + def __new__(metacls, name, bases, attrs) -> Type: + instance = super().__new__(metacls, name, bases, attrs) - def _use_cookies(*args, **kwargs) -> FastAPIOAuth2: async def __call__(self: FastAPIOAuth2, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization", request.cookies.get("Authorization")) if authorization: request._headers = Headers({**request.headers, "Authorization": authorization}) - return await super(cls, self).__call__(request) - - cls.__call__ = __call__ - return cls(*args, **kwargs) + return await instance.__base__.__call__(self, request) - return _use_cookies + instance.__call__ = __call__ + return instance -@use_cookies -class OAuth2(FastAPIOAuth2): +class OAuth2(FastAPIOAuth2, metaclass=OAuth2Cookie): """Wrapper class of the `fastapi.security.OAuth2` class.""" -@use_cookies -class OAuth2PasswordBearer(FastAPIPasswordBearer): +class OAuth2PasswordBearer(FastAPIPasswordBearer, metaclass=OAuth2Cookie): """Wrapper class of the `fastapi.security.OAuth2PasswordBearer` class.""" -@use_cookies -class OAuth2AuthorizationCodeBearer(FastAPICodeBearer): +class OAuth2AuthorizationCodeBearer(FastAPICodeBearer, metaclass=OAuth2Cookie): """Wrapper class of the `fastapi.security.OAuth2AuthorizationCodeBearer` class.""" From 64fd834285ecc09249f278e73966ab02d9b64f08 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Mon, 7 Aug 2023 21:37:41 +0400 Subject: [PATCH 06/19] Upgrade the development status --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ed46db8..81b6a1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ license_files = LICENSE platforms = unix, linux, osx, win32 classifiers = Operating System :: OS Independent - Development Status :: 2 - Pre-Alpha + Development Status :: 3 - Alpha Framework :: FastAPI Programming Language :: Python Programming Language :: Python :: 3 From f38fb0cc472bb783cc28566f775e82cd74621bb2 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Mon, 7 Aug 2023 21:42:45 +0400 Subject: [PATCH 07/19] Split tests by topics --- tests/test_backends.py | 17 +++++++++++++++++ ..._oauth2_middleware.py => test_middleware.py} | 14 -------------- 2 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 tests/test_backends.py rename tests/{test_oauth2_middleware.py => test_middleware.py} (83%) diff --git a/tests/test_backends.py b/tests/test_backends.py new file mode 100644 index 0000000..47a91d6 --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,17 @@ +import pytest + +from fastapi_oauth2.client import OAuth2Client +from fastapi_oauth2.core import OAuth2Core + + +@pytest.mark.anyio +async def test_core_init_with_all_backends(backends): + for backend in backends: + try: + OAuth2Core(OAuth2Client( + backend=backend, + client_id="test_client_id", + client_secret="test_client_secret", + )) + except (NotImplementedError, Exception): + assert False diff --git a/tests/test_oauth2_middleware.py b/tests/test_middleware.py similarity index 83% rename from tests/test_oauth2_middleware.py rename to tests/test_middleware.py index 27d0752..1cc0a21 100644 --- a/tests/test_oauth2_middleware.py +++ b/tests/test_middleware.py @@ -8,7 +8,6 @@ from starlette.responses import Response from fastapi_oauth2.client import OAuth2Client -from fastapi_oauth2.core import OAuth2Core from fastapi_oauth2.middleware import OAuth2Middleware from fastapi_oauth2.router import router as oauth2_router from fastapi_oauth2.security import OAuth2 @@ -72,16 +71,3 @@ async def test_authenticated_request(): response = await client.get("/user") assert response.status_code == 200 # OK - - -@pytest.mark.anyio -async def test_core_init(backends): - for backend in backends: - try: - OAuth2Core(OAuth2Client( - backend=backend, - client_id="test_client_id", - client_secret="test_client_secret", - )) - except (NotImplementedError, Exception): - assert False From f24cfed9a33ad4245b99fdb0cc133ba841d9fd06 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Mon, 7 Aug 2023 21:47:59 +0400 Subject: [PATCH 08/19] Add a comment notation on client ID and secret --- examples/demonstration/.env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/demonstration/.env b/examples/demonstration/.env index a1c0106..ebbfcdc 100644 --- a/examples/demonstration/.env +++ b/examples/demonstration/.env @@ -1,3 +1,5 @@ +# These id and secret are generated especially for testing purposes, +# if you have your own, please use them, otherwise you can use these. OAUTH2_CLIENT_ID=eccd08d6736b7999a32a OAUTH2_CLIENT_SECRET=642999c1c5f2b3df8b877afdc78252ef5b594d31 From 126c720a1291ec0841244d7f855a03f77dc4f1cc Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Tue, 8 Aug 2023 23:59:34 +0400 Subject: [PATCH 09/19] Add more test-cases --- tests/test_middleware.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 1cc0a21..d1cc3d2 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -62,7 +62,24 @@ async def test_auth_redirect(): @pytest.mark.anyio -async def test_authenticated_request(): +async def test_token_redirect(): + async with AsyncClient(app=app, base_url="http://test") as client: + response = await client.get("/oauth2/github/token") + assert response.status_code == 400 # Bad Request + + response = await client.get("/oauth2/github/token?state=test&code=test") + assert response.status_code == 400 # Bad Request TODO: <-- Fix: this should return 4xx status code + + +@pytest.mark.anyio +async def test_logout_redirect(): + async with AsyncClient(app=app, base_url="http://test") as client: + response = await client.get("/oauth2/logout") + assert response.status_code == 307 # Redirect + + +@pytest.mark.anyio +async def test_authentication(): async with AsyncClient(app=app, base_url="http://test") as client: response = await client.get("/user") assert response.status_code == 403 # Forbidden @@ -71,3 +88,17 @@ async def test_authenticated_request(): response = await client.get("/user") assert response.status_code == 200 # OK + + +@pytest.mark.anyio +async def test_logout(): + async with AsyncClient(app=app, base_url="http://test") as client: + await client.get("/auth") # Simulate login + + response = await client.get("/user") + assert response.status_code == 200 # OK + + await client.get("/oauth2/logout") # Perform logout + + response = await client.get("/user") + assert response.status_code == 403 # Forbidden From f7718479d7ff0ac6bdf62fdcb5f351b4d1f3bf53 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 9 Aug 2023 21:33:44 +0400 Subject: [PATCH 10/19] Catch the unhandled exception for invalid query params --- src/fastapi_oauth2/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fastapi_oauth2/core.py b/src/fastapi_oauth2/core.py index 475d5e3..3a4ea18 100644 --- a/src/fastapi_oauth2/core.py +++ b/src/fastapi_oauth2/core.py @@ -10,6 +10,7 @@ import httpx from oauthlib.oauth2 import WebApplicationClient +from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error from social_core.backends.oauth import BaseOAuth2 from social_core.strategy import BaseStrategy from starlette.exceptions import HTTPException @@ -109,9 +110,12 @@ async def token_redirect(self, request: Request) -> RedirectResponse: auth = httpx.BasicAuth(self.client_id, self.client_secret) async with httpx.AsyncClient() as session: response = await session.post(token_url, headers=headers, content=content, auth=auth) - token = self.oauth_client.parse_request_body_response(json.dumps(response.json())) - token_data = self.standardize(self.backend.user_data(token.get("access_token"))) - access_token = request.auth.jwt_create(token_data) + try: + token = self.oauth_client.parse_request_body_response(json.dumps(response.json())) + token_data = self.standardize(self.backend.user_data(token.get("access_token"))) + access_token = request.auth.jwt_create(token_data) + except (CustomOAuth2Error, Exception) as e: + raise OAuth2LoginError(400, str(e)) response = RedirectResponse(self.redirect_uri or request.base_url) response.set_cookie( From ad5d07798a19c9a6c0c22a62ce7843200b6bd182 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Wed, 9 Aug 2023 21:34:12 +0400 Subject: [PATCH 11/19] Remove TODO as it's done --- tests/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index d1cc3d2..f2e3f00 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -68,7 +68,7 @@ async def test_token_redirect(): assert response.status_code == 400 # Bad Request response = await client.get("/oauth2/github/token?state=test&code=test") - assert response.status_code == 400 # Bad Request TODO: <-- Fix: this should return 4xx status code + assert response.status_code == 400 # Bad Request @pytest.mark.anyio From 06858323b9770e26c56e55cfcfd4b9c98b8bc2d6 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 13:29:23 +0400 Subject: [PATCH 12/19] Wrap the `app` into a fixture --- tests/conftest.py | 54 +++++++++++++++++++++++++++++++ tests/test_middleware.py | 69 ---------------------------------------- 2 files changed, 54 insertions(+), 69 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index aedb52a..3252235 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,18 @@ import pytest import social_core.backends as backends +from fastapi import APIRouter +from fastapi import Depends +from fastapi import FastAPI +from fastapi import Request +from social_core.backends.github import GithubOAuth2 from social_core.backends.oauth import BaseOAuth2 +from starlette.responses import Response + +from fastapi_oauth2.client import OAuth2Client +from fastapi_oauth2.middleware import OAuth2Middleware +from fastapi_oauth2.router import router as oauth2_router +from fastapi_oauth2.security import OAuth2 package_path = backends.__path__[0] @@ -24,3 +35,46 @@ def backends(): except ImportError: continue return backend_instances + + +@pytest.fixture +def app(): + oauth2 = OAuth2() + application = FastAPI() + app_router = APIRouter() + + @app_router.get("/user") + def user(request: Request, _: str = Depends(oauth2)): + return request.user + + @app_router.get("/auth") + def auth(request: Request): + access_token = request.auth.jwt_create({ + "name": "test", + "sub": "test", + "id": "test", + }) + response = Response() + response.set_cookie( + "Authorization", + value=f"Bearer {access_token}", + max_age=request.auth.expires, + expires=request.auth.expires, + httponly=request.auth.http, + ) + return response + + application.include_router(app_router) + application.include_router(oauth2_router) + application.add_middleware(OAuth2Middleware, config={ + "allow_http": True, + "clients": [ + OAuth2Client( + backend=GithubOAuth2, + client_id="test_id", + client_secret="test_secret", + ), + ], + }) + + return application diff --git a/tests/test_middleware.py b/tests/test_middleware.py index f2e3f00..77eb2b1 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,74 +1,5 @@ import pytest -from fastapi import APIRouter -from fastapi import Depends -from fastapi import FastAPI -from fastapi import Request from httpx import AsyncClient -from social_core.backends.github import GithubOAuth2 -from starlette.responses import Response - -from fastapi_oauth2.client import OAuth2Client -from fastapi_oauth2.middleware import OAuth2Middleware -from fastapi_oauth2.router import router as oauth2_router -from fastapi_oauth2.security import OAuth2 - -app = FastAPI() -oauth2 = OAuth2() -app_router = APIRouter() - - -@app_router.get("/user") -def user(request: Request, _: str = Depends(oauth2)): - return request.user - - -@app_router.get("/auth") -def auth(request: Request): - access_token = request.auth.jwt_create({ - "name": "test", - "sub": "test", - "id": "test", - }) - response = Response() - response.set_cookie( - "Authorization", - value=f"Bearer {access_token}", - max_age=request.auth.expires, - expires=request.auth.expires, - httponly=request.auth.http, - ) - return response - - -app.include_router(app_router) -app.include_router(oauth2_router) -app.add_middleware(OAuth2Middleware, config={ - "allow_http": True, - "clients": [ - OAuth2Client( - backend=GithubOAuth2, - client_id="test_id", - client_secret="test_secret", - ), - ], -}) - - -@pytest.mark.anyio -async def test_auth_redirect(): - async with AsyncClient(app=app, base_url="http://test") as client: - response = await client.get("/oauth2/github/auth") - assert response.status_code == 303 # Redirect - - -@pytest.mark.anyio -async def test_token_redirect(): - async with AsyncClient(app=app, base_url="http://test") as client: - response = await client.get("/oauth2/github/token") - assert response.status_code == 400 # Bad Request - - response = await client.get("/oauth2/github/token?state=test&code=test") - assert response.status_code == 400 # Bad Request @pytest.mark.anyio From cb593ff11402cea7b4d44d96342d70f14838a4aa Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 13:30:29 +0400 Subject: [PATCH 13/19] Move out the route related tests units --- tests/test_middleware.py | 11 ++--------- tests/test_routes.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 tests/test_routes.py diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 77eb2b1..173a4d8 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -3,14 +3,7 @@ @pytest.mark.anyio -async def test_logout_redirect(): - async with AsyncClient(app=app, base_url="http://test") as client: - response = await client.get("/oauth2/logout") - assert response.status_code == 307 # Redirect - - -@pytest.mark.anyio -async def test_authentication(): +async def test_authentication(app): async with AsyncClient(app=app, base_url="http://test") as client: response = await client.get("/user") assert response.status_code == 403 # Forbidden @@ -22,7 +15,7 @@ async def test_authentication(): @pytest.mark.anyio -async def test_logout(): +async def test_logout(app): async with AsyncClient(app=app, base_url="http://test") as client: await client.get("/auth") # Simulate login diff --git a/tests/test_routes.py b/tests/test_routes.py new file mode 100644 index 0000000..1094b97 --- /dev/null +++ b/tests/test_routes.py @@ -0,0 +1,26 @@ +import pytest +from httpx import AsyncClient + + +@pytest.mark.anyio +async def test_auth_redirect(app): + async with AsyncClient(app=app, base_url="http://test") as client: + response = await client.get("/oauth2/github/auth") + assert response.status_code == 303 # Redirect + + +@pytest.mark.anyio +async def test_token_redirect(app): + async with AsyncClient(app=app, base_url="http://test") as client: + response = await client.get("/oauth2/github/token") + assert response.status_code == 400 # Bad Request + + response = await client.get("/oauth2/github/token?state=test&code=test") + assert response.status_code == 400 # Bad Request + + +@pytest.mark.anyio +async def test_logout_redirect(app): + async with AsyncClient(app=app, base_url="http://test") as client: + response = await client.get("/oauth2/logout") + assert response.status_code == 307 # Redirect From af2889e3d50a3d42eddb66e2e274a20e01b2c79b Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 15:57:58 +0400 Subject: [PATCH 14/19] Configure a Google OAuth2 client --- examples/demonstration/.env | 7 +++++-- examples/demonstration/config.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/demonstration/.env b/examples/demonstration/.env index ebbfcdc..25f028b 100644 --- a/examples/demonstration/.env +++ b/examples/demonstration/.env @@ -1,7 +1,10 @@ # These id and secret are generated especially for testing purposes, # if you have your own, please use them, otherwise you can use these. -OAUTH2_CLIENT_ID=eccd08d6736b7999a32a -OAUTH2_CLIENT_SECRET=642999c1c5f2b3df8b877afdc78252ef5b594d31 +OAUTH2_GITHUB_CLIENT_ID=eccd08d6736b7999a32a +OAUTH2_GITHUB_CLIENT_SECRET=642999c1c5f2b3df8b877afdc78252ef5b594d31 + +OAUTH2_GOOGLE_CLIENT_ID=105851609656-uueuan570963mnnf4288nv40eieh9f5l.apps.googleusercontent.com +OAUTH2_GOOGLE_CLIENT_SECRET=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 JWT_SECRET=secret JWT_ALGORITHM=HS256 diff --git a/examples/demonstration/config.py b/examples/demonstration/config.py index 935c2b1..be64b0f 100644 --- a/examples/demonstration/config.py +++ b/examples/demonstration/config.py @@ -2,6 +2,7 @@ from dotenv import load_dotenv from social_core.backends.github import GithubOAuth2 +from social_core.backends.google import GoogleOAuth2 from fastapi_oauth2.claims import Claims from fastapi_oauth2.client import OAuth2Client @@ -17,14 +18,22 @@ clients=[ OAuth2Client( backend=GithubOAuth2, - client_id=os.getenv("OAUTH2_CLIENT_ID"), - client_secret=os.getenv("OAUTH2_CLIENT_SECRET"), - # redirect_uri="http://127.0.0.1:8000/", + client_id=os.getenv("OAUTH2_GITHUB_CLIENT_ID"), + client_secret=os.getenv("OAUTH2_GITHUB_CLIENT_SECRET"), scope=["user:email"], claims=Claims( picture="avatar_url", identity=lambda user: "%s:%s" % (user.get("provider"), user.get("id")), ), ), + OAuth2Client( + backend=GoogleOAuth2, + client_id=os.getenv("OAUTH2_GOOGLE_CLIENT_ID"), + client_secret=os.getenv("OAUTH2_GOOGLE_CLIENT_SECRET"), + scope=["openid", "profile", "email"], + claims=Claims( + identity=lambda user: "%s:%s" % (user.get("provider"), user.get("sub")), + ), + ), ] ) From 8a2cf61cc2fe186d817be86034398aca423c729d Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 16:23:55 +0400 Subject: [PATCH 15/19] Mount staticfiles for social icons --- examples/demonstration/main.py | 2 ++ examples/demonstration/static/github.svg | 5 +++++ examples/demonstration/static/google-oauth2.svg | 6 ++++++ examples/demonstration/templates/index.html | 8 +++++--- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 examples/demonstration/static/github.svg create mode 100644 examples/demonstration/static/google-oauth2.svg diff --git a/examples/demonstration/main.py b/examples/demonstration/main.py index 8c8ca7e..4b78238 100644 --- a/examples/demonstration/main.py +++ b/examples/demonstration/main.py @@ -1,5 +1,6 @@ from fastapi import APIRouter from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from config import oauth2_config @@ -37,4 +38,5 @@ async def on_auth(auth: Auth, user: User): app = FastAPI() app.include_router(app_router) app.include_router(oauth2_router) +app.mount("/static", StaticFiles(directory="static"), name="static") app.add_middleware(OAuth2Middleware, config=oauth2_config, callback=on_auth) diff --git a/examples/demonstration/static/github.svg b/examples/demonstration/static/github.svg new file mode 100644 index 0000000..75a94ed --- /dev/null +++ b/examples/demonstration/static/github.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/examples/demonstration/static/google-oauth2.svg b/examples/demonstration/static/google-oauth2.svg new file mode 100644 index 0000000..ac18388 --- /dev/null +++ b/examples/demonstration/static/google-oauth2.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/examples/demonstration/templates/index.html b/examples/demonstration/templates/index.html index 50b953c..caea8e5 100644 --- a/examples/demonstration/templates/index.html +++ b/examples/demonstration/templates/index.html @@ -23,9 +23,11 @@ {% for provider in request.auth.clients %} - - - + {{ provider }} icon {% endfor %} {% endif %} From 8330e6ceab9df1c0a4451e87fe0d205132ee350f Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 16:26:47 +0400 Subject: [PATCH 16/19] Make the `app` fixture callable with default oauth security engine --- tests/conftest.py | 76 ++++++++++++++++++++++------------------ tests/test_middleware.py | 8 ++--- tests/test_routes.py | 12 +++---- 3 files changed, 51 insertions(+), 45 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3252235..b96e6c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,43 +38,49 @@ def backends(): @pytest.fixture -def app(): - oauth2 = OAuth2() - application = FastAPI() - app_router = APIRouter() +def get_app(): + def fixture_wrapper(authentication: OAuth2 = None): + if not authentication: + authentication = OAuth2() - @app_router.get("/user") - def user(request: Request, _: str = Depends(oauth2)): - return request.user + oauth2 = authentication + application = FastAPI() + app_router = APIRouter() - @app_router.get("/auth") - def auth(request: Request): - access_token = request.auth.jwt_create({ - "name": "test", - "sub": "test", - "id": "test", + @app_router.get("/user") + def user(request: Request, _: str = Depends(oauth2)): + return request.user + + @app_router.get("/auth") + def auth(request: Request): + access_token = request.auth.jwt_create({ + "name": "test", + "sub": "test", + "id": "test", + }) + response = Response() + response.set_cookie( + "Authorization", + value=f"Bearer {access_token}", + max_age=request.auth.expires, + expires=request.auth.expires, + httponly=request.auth.http, + ) + return response + + application.include_router(app_router) + application.include_router(oauth2_router) + application.add_middleware(OAuth2Middleware, config={ + "allow_http": True, + "clients": [ + OAuth2Client( + backend=GithubOAuth2, + client_id="test_id", + client_secret="test_secret", + ), + ], }) - response = Response() - response.set_cookie( - "Authorization", - value=f"Bearer {access_token}", - max_age=request.auth.expires, - expires=request.auth.expires, - httponly=request.auth.http, - ) - return response - application.include_router(app_router) - application.include_router(oauth2_router) - application.add_middleware(OAuth2Middleware, config={ - "allow_http": True, - "clients": [ - OAuth2Client( - backend=GithubOAuth2, - client_id="test_id", - client_secret="test_secret", - ), - ], - }) + return application - return application + return fixture_wrapper diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 173a4d8..e33c6b7 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -3,8 +3,8 @@ @pytest.mark.anyio -async def test_authentication(app): - async with AsyncClient(app=app, base_url="http://test") as client: +async def test_middleware_on_authentication(get_app): + async with AsyncClient(app=get_app(), base_url="http://test") as client: response = await client.get("/user") assert response.status_code == 403 # Forbidden @@ -15,8 +15,8 @@ async def test_authentication(app): @pytest.mark.anyio -async def test_logout(app): - async with AsyncClient(app=app, base_url="http://test") as client: +async def test_middleware_on_logout(get_app): + async with AsyncClient(app=get_app(), base_url="http://test") as client: await client.get("/auth") # Simulate login response = await client.get("/user") diff --git a/tests/test_routes.py b/tests/test_routes.py index 1094b97..084f459 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -3,15 +3,15 @@ @pytest.mark.anyio -async def test_auth_redirect(app): - async with AsyncClient(app=app, base_url="http://test") as client: +async def test_auth_redirect(get_app): + async with AsyncClient(app=get_app(), base_url="http://test") as client: response = await client.get("/oauth2/github/auth") assert response.status_code == 303 # Redirect @pytest.mark.anyio -async def test_token_redirect(app): - async with AsyncClient(app=app, base_url="http://test") as client: +async def test_token_redirect(get_app): + async with AsyncClient(app=get_app(), base_url="http://test") as client: response = await client.get("/oauth2/github/token") assert response.status_code == 400 # Bad Request @@ -20,7 +20,7 @@ async def test_token_redirect(app): @pytest.mark.anyio -async def test_logout_redirect(app): - async with AsyncClient(app=app, base_url="http://test") as client: +async def test_logout_redirect(get_app): + async with AsyncClient(app=get_app(), base_url="http://test") as client: response = await client.get("/oauth2/logout") assert response.status_code == 307 # Redirect From 2ec28d9afb6032f3d90cc3b1c1374ddf479daadf Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 10 Aug 2023 16:27:19 +0400 Subject: [PATCH 17/19] Add test cases for each security engine --- tests/{test_routes.py => test_router.py} | 0 tests/test_security.py | 29 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) rename tests/{test_routes.py => test_router.py} (100%) create mode 100644 tests/test_security.py diff --git a/tests/test_routes.py b/tests/test_router.py similarity index 100% rename from tests/test_routes.py rename to tests/test_router.py diff --git a/tests/test_security.py b/tests/test_security.py new file mode 100644 index 0000000..9c8fa1f --- /dev/null +++ b/tests/test_security.py @@ -0,0 +1,29 @@ +import pytest + +from fastapi_oauth2.security import OAuth2 +from fastapi_oauth2.security import OAuth2AuthorizationCodeBearer +from fastapi_oauth2.security import OAuth2PasswordBearer + + +@pytest.mark.anyio +async def test_security_oauth2(get_app): + try: + get_app(OAuth2()) + except (TypeError, Exception): + assert False + + +@pytest.mark.anyio +async def test_security_oauth2_password_bearer(get_app): + try: + get_app(OAuth2PasswordBearer(tokenUrl="/test")) + except (TypeError, Exception): + assert False + + +@pytest.mark.anyio +async def test_security_oauth2_authentication_code_bearer(get_app): + try: + get_app(OAuth2AuthorizationCodeBearer(authorizationUrl="/test", tokenUrl="/test")) + except (TypeError, Exception): + assert False From dc3e53b4bb8e3a148ff4b89584969280d483bac9 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 11 Aug 2023 14:56:56 +0400 Subject: [PATCH 18/19] GH-8: Remove the "Features to be implemented" section --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 490cbe2..a36c6e2 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,6 @@ FastAPI OAuth2 is a middleware-based social authentication mechanism supporting several auth providers. It depends on the [social-core](https://github.com/python-social-auth/social-core) authentication backends. -## Features to be implemented - -- Use multiple OAuth2 providers at the same time - * There need to be provided a way to configure the OAuth2 for multiple providers -- Customizable OAuth2 routes - ## Installation ```shell From ff8385f77935570a2b595f865ed9c46de3c22224 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 11 Aug 2023 14:57:21 +0400 Subject: [PATCH 19/19] Upgrade the version to `alpha.2` --- src/fastapi_oauth2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastapi_oauth2/__init__.py b/src/fastapi_oauth2/__init__.py index a390618..5186ae4 100644 --- a/src/fastapi_oauth2/__init__.py +++ b/src/fastapi_oauth2/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0-alpha.1" +__version__ = "1.0.0-alpha.2"