diff --git a/examples/fitbit.py b/examples/fitbit.py index fbd6b5a..d5c555c 100644 --- a/examples/fitbit.py +++ b/examples/fitbit.py @@ -1,10 +1,12 @@ -"""Fitbit Login Example -""" +"""Fitbit Login Example.""" + import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.fitbit import FitbitSSO + +from fastapi_sso import FitbitSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/generic.py b/examples/generic.py index c09b0a0..067ec0b 100644 --- a/examples/generic.py +++ b/examples/generic.py @@ -2,12 +2,13 @@ """ from typing import Any, Dict, Union -from httpx import AsyncClient + import uvicorn from fastapi import FastAPI, HTTPException +from httpx import AsyncClient from starlette.requests import Request -from fastapi_sso.sso.base import DiscoveryDocument, OpenID -from fastapi_sso.sso.generic import create_provider + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, factories app = FastAPI() @@ -36,7 +37,9 @@ def convert_openid(response: Dict[str, Any], _client: Union[AsyncClient, None]) "userinfo_endpoint": "http://localhost:9090/me", } -GenericSSO = create_provider(name="oidc", discovery_document=discovery_document, response_convertor=convert_openid) +GenericSSO = factories.create_provider( + name="oidc", discovery_document=discovery_document, response_convertor=convert_openid +) sso = GenericSSO( client_id="test", client_secret="secret", redirect_uri="http://localhost:8080/callback", allow_insecure_http=True diff --git a/examples/github.py b/examples/github.py index bbfa115..ecbe202 100644 --- a/examples/github.py +++ b/examples/github.py @@ -1,10 +1,12 @@ -"""Github Login Example -""" +"""Github Login Example.""" + import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.github import GithubSSO + +from fastapi_sso import GithubSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/gitlab.py b/examples/gitlab.py index a702705..2fc79dc 100644 --- a/examples/gitlab.py +++ b/examples/gitlab.py @@ -1,10 +1,12 @@ -"""Github Login Example -""" +"""Github Login Example.""" + import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.gitlab import GitlabSSO + +from fastapi_sso import GitlabSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/google.py b/examples/google.py index 5bd6fca..5ac5e56 100644 --- a/examples/google.py +++ b/examples/google.py @@ -1,10 +1,11 @@ -"""Google Login Example -""" +"""Google Login Example.""" import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.google import GoogleSSO + +from fastapi_sso import GoogleSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/kakao.py b/examples/kakao.py index b04899c..5f22ddb 100644 --- a/examples/kakao.py +++ b/examples/kakao.py @@ -1,10 +1,11 @@ -"""Kakao Login Example -""" +"""Kakao Login Example.""" import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.kakao import KakaoSSO + +from fastapi_sso import KakaoSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/microsoft.py b/examples/microsoft.py index 7b1e450..56e7cc7 100644 --- a/examples/microsoft.py +++ b/examples/microsoft.py @@ -1,10 +1,11 @@ -"""Microsoft Login Example -""" +"""Microsoft Login Example.""" import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.microsoft import MicrosoftSSO + +from fastapi_sso import MicrosoftSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/naver.py b/examples/naver.py index 75002f9..cf74923 100644 --- a/examples/naver.py +++ b/examples/naver.py @@ -1,10 +1,11 @@ -"""Naver Login Example -""" +"""Naver Login Example.""" import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.naver import NaverSSO + +from fastapi_sso import NaverSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/examples/notion.py b/examples/notion.py index fd9b84a..f55ea6b 100644 --- a/examples/notion.py +++ b/examples/notion.py @@ -2,9 +2,11 @@ """ import os + import uvicorn from fastapi import FastAPI, Request -from fastapi_sso.sso.notion import NotionSSO + +from fastapi_sso import NotionSSO CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] diff --git a/fastapi_sso/__init__.py b/fastapi_sso/__init__.py index 865c504..9975d4d 100644 --- a/fastapi_sso/__init__.py +++ b/fastapi_sso/__init__.py @@ -2,15 +2,5 @@ (such as Facebook login, Google login and login via Microsoft Office 365 account) """ -from .sso.base import OpenID, SSOBase, SSOLoginError -from .sso.facebook import FacebookSSO -from .sso.fitbit import FitbitSSO -from .sso.generic import create_provider -from .sso.github import GithubSSO -from .sso.gitlab import GitlabSSO -from .sso.google import GoogleSSO -from .sso.kakao import KakaoSSO -from .sso.microsoft import MicrosoftSSO -from .sso.naver import NaverSSO -from .sso.notion import NotionSSO -from .sso.spotify import SpotifySSO +from .infrastructure import * # noqa: F401, F403 +from .sso import * # noqa: F401, F403 diff --git a/fastapi_sso/infrastructure/__init__.py b/fastapi_sso/infrastructure/__init__.py new file mode 100644 index 0000000..2d32aa0 --- /dev/null +++ b/fastapi_sso/infrastructure/__init__.py @@ -0,0 +1,16 @@ +"""The infrastructure package defines all the shared logic +that could be used across the project. +""" + +__all__ = ( + "factories", + "OpenID", + "SSOBase", + "SSOLoginError", + "DiscoveryDocument", + "ReusedOauthClientWarning", + "UnsetStateWarning", +) + +from . import factories +from .openid import DiscoveryDocument, OpenID, ReusedOauthClientWarning, SSOBase, SSOLoginError, UnsetStateWarning diff --git a/fastapi_sso/infrastructure/factories/__init__.py b/fastapi_sso/infrastructure/factories/__init__.py new file mode 100644 index 0000000..14955b8 --- /dev/null +++ b/fastapi_sso/infrastructure/factories/__init__.py @@ -0,0 +1,5 @@ +"""This pacakge includes all the shared SSO factories.""" + +__all__ = ("create_provider",) + +from .provider import create_provider diff --git a/fastapi_sso/sso/generic.py b/fastapi_sso/infrastructure/factories/provider.py similarity index 81% rename from fastapi_sso/sso/generic.py rename to fastapi_sso/infrastructure/factories/provider.py index 57f72d7..d2536d7 100644 --- a/fastapi_sso/sso/generic.py +++ b/fastapi_sso/infrastructure/factories/provider.py @@ -1,14 +1,12 @@ -"""A generic OAuth client that can be used to quickly create support for any OAuth provider -with close to no code +"""A generic OAuth client that can be used to quickly create support for any OAuth provider with close to no code. """ import logging -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union +from typing import Any, Callable, Dict, List, Optional, Type, Union -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +import httpx -if TYPE_CHECKING: - import httpx +from ..openid import DiscoveryDocument, OpenID, SSOBase logger = logging.getLogger(__name__) @@ -18,7 +16,7 @@ def create_provider( name: str = "generic", default_scope: Optional[List[str]] = None, discovery_document: Union[DiscoveryDocument, Callable[[SSOBase], DiscoveryDocument]], - response_convertor: Optional[Callable[[Dict[str, Any], Optional["httpx.AsyncClient"]], OpenID]] = None + response_convertor: Optional[Callable[[Dict[str, Any], Optional[httpx.AsyncClient]], OpenID]] = None ) -> Type[SSOBase]: """A factory to create a generic OAuth client usable with almost any OAuth provider. Returns a class. @@ -33,7 +31,7 @@ def create_provider( Example: ```python - from fastapi_sso.sso.generic import create_provider + from fastapi_sso.infrastructure import factories discovery = { "authorization_endpoint": "http://localhost:9090/auth", @@ -41,13 +39,14 @@ def create_provider( "userinfo_endpoint": "http://localhost:9090/me", } - SSOProvider = create_provider(name="oidc", discovery_document=discovery) + SSOProvider = factories.create_provider(name="oidc", discovery_document=discovery) + sso = SSOProvider( client_id="test", client_secret="secret", redirect_uri="http://localhost:8080/callback", allow_insecure_http=True - ) + ) ``` """ @@ -64,7 +63,7 @@ async def get_discovery_document(self) -> DiscoveryDocument: return discovery_document(self) return discovery_document - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: if not response_convertor: logger.warning("No response convertor was provided, returned OpenID will always be empty") return OpenID( diff --git a/fastapi_sso/sso/base.py b/fastapi_sso/infrastructure/openid.py similarity index 99% rename from fastapi_sso/sso/base.py rename to fastapi_sso/infrastructure/openid.py index c2289f6..c45dbee 100644 --- a/fastapi_sso/sso/base.py +++ b/fastapi_sso/infrastructure/openid.py @@ -1,7 +1,7 @@ -"""SSO login base dependency -""" +"""SSO login base dependency.""" # pylint: disable=too-few-public-methods + import json import os import sys @@ -360,6 +360,7 @@ async def process_login( headers.update(additional_headers) auth = httpx.BasicAuth(self.client_id, self.client_secret) + async with httpx.AsyncClient() as session: response = await session.post(token_url, headers=headers, content=body, auth=auth) content = response.json() diff --git a/fastapi_sso/sso/__init__.py b/fastapi_sso/sso/__init__.py index e69de29..f4724b2 100644 --- a/fastapi_sso/sso/__init__.py +++ b/fastapi_sso/sso/__init__.py @@ -0,0 +1,27 @@ +"""This package includes all the concrete SSO implementations. +All of them must inherit from SSOBase class. +""" + +__all__ = ( + "FacebookSSO", + "FitbitSSO", + "GithubSSO", + "GitlabSSO", + "GoogleSSO", + "KakaoSSO", + "MicrosoftSSO", + "NaverSSO", + "NotionSSO", + "SpotifySSO", +) + +from .facebook import FacebookSSO +from .fitbit import FitbitSSO +from .github import GithubSSO +from .gitlab import GitlabSSO +from .google import GoogleSSO +from .kakao import KakaoSSO +from .microsoft import MicrosoftSSO +from .naver import NaverSSO +from .notion import NotionSSO +from .spotify import SpotifySSO diff --git a/fastapi_sso/sso/facebook.py b/fastapi_sso/sso/facebook.py index cb37f92..420335c 100644 --- a/fastapi_sso/sso/facebook.py +++ b/fastapi_sso/sso/facebook.py @@ -1,12 +1,11 @@ -"""Facebook SSO Login Helper -""" +"""Facebook SSO Login Helper.""" -from typing import TYPE_CHECKING, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +from typing import Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class FacebookSSO(SSOBase): @@ -18,6 +17,7 @@ class FacebookSSO(SSOBase): async def get_discovery_document(self) -> DiscoveryDocument: """Get document containing handy urls""" + return { "authorization_endpoint": "https://www.facebook.com/v9.0/dialog/oauth", "token_endpoint": f"{self.base_url}/oauth/access_token", @@ -26,6 +26,7 @@ async def get_discovery_document(self) -> DiscoveryDocument: async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: """Return OpenID from user information provided by Facebook""" + return OpenID( email=response.get("email", ""), first_name=response.get("first_name"), diff --git a/fastapi_sso/sso/fitbit.py b/fastapi_sso/sso/fitbit.py index 7957223..11c9b4a 100644 --- a/fastapi_sso/sso/fitbit.py +++ b/fastapi_sso/sso/fitbit.py @@ -1,12 +1,11 @@ -"""Fitbit OAuth Login Helper -""" +"""Fitbit OAuth Login Helper.""" -from typing import TYPE_CHECKING, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginError +from typing import Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase, SSOLoginError class FitbitSSO(SSOBase): @@ -17,9 +16,10 @@ class FitbitSSO(SSOBase): async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: """Return OpenID from user information provided by Google""" - info = response.get("user") - if not info: + + if not (info := response.get("user")): raise SSOLoginError(401, "Failed to process login via Fitbit") + return OpenID( id=info["encodedId"], first_name=info["fullName"], @@ -30,6 +30,7 @@ async def openid_from_response(self, response: dict, session: Optional["httpx.As async def get_discovery_document(self) -> DiscoveryDocument: """Get document containing handy urls""" + return { "authorization_endpoint": "https://www.fitbit.com/oauth2/authorize?response_type=code", "token_endpoint": "https://api.fitbit.com/oauth2/token", diff --git a/fastapi_sso/sso/github.py b/fastapi_sso/sso/github.py index e6fd9af..93fcd91 100644 --- a/fastapi_sso/sso/github.py +++ b/fastapi_sso/sso/github.py @@ -1,11 +1,11 @@ -"""Github SSO Oauth Helper class""" +"""Github SSO Oauth Helper class.""" -from typing import TYPE_CHECKING, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +from typing import Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class GithubSSO(SSOBase): @@ -26,15 +26,21 @@ async def get_discovery_document(self) -> DiscoveryDocument: async def _get_primary_email(self, session: Optional["httpx.AsyncClient"] = None) -> Optional[str]: """Attempt to get primary email from Github for a current user. The session received must be authenticated.""" + if not session: return None + response = await session.get(self.emails_endpoint) + if response.status_code != 200: return None + emails = response.json() + for email in emails: if email["primary"]: return email["email"] + return None async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: diff --git a/fastapi_sso/sso/gitlab.py b/fastapi_sso/sso/gitlab.py index 9df46ae..3bf872d 100644 --- a/fastapi_sso/sso/gitlab.py +++ b/fastapi_sso/sso/gitlab.py @@ -1,11 +1,11 @@ -"""Gitlab SSO Oauth Helper class""" +"""Gitlab SSO Oauth Helper class.""" -from typing import TYPE_CHECKING, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +from typing import Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class GitlabSSO(SSOBase): diff --git a/fastapi_sso/sso/google.py b/fastapi_sso/sso/google.py index fb372ff..0a78c29 100644 --- a/fastapi_sso/sso/google.py +++ b/fastapi_sso/sso/google.py @@ -1,11 +1,11 @@ -"""Google SSO Login Helper -""" +"""Google SSO Login Helper.""" + from typing import Optional import httpx -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginError +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase, SSOLoginError class GoogleSSO(SSOBase): @@ -15,22 +15,25 @@ class GoogleSSO(SSOBase): provider = "google" scope = ["openid", "email", "profile"] - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: """Return OpenID from user information provided by Google""" - if response.get("email_verified"): - return OpenID( - email=response.get("email", ""), - provider=self.provider, - id=response.get("sub"), - first_name=response.get("given_name"), - last_name=response.get("family_name"), - display_name=response.get("name"), - picture=response.get("picture"), - ) - raise SSOLoginError(401, f"User {response.get('email')} is not verified with Google") + + if not response.get("email_verified"): + raise SSOLoginError(401, f"User {response.get('email')} is not verified with Google") + + return OpenID( + email=response.get("email", ""), + provider=self.provider, + id=response.get("sub"), + first_name=response.get("given_name"), + last_name=response.get("family_name"), + display_name=response.get("name"), + picture=response.get("picture"), + ) async def get_discovery_document(self) -> DiscoveryDocument: """Get document containing handy urls""" + async with httpx.AsyncClient() as session: response = await session.get(self.discovery_url) content = response.json() diff --git a/fastapi_sso/sso/kakao.py b/fastapi_sso/sso/kakao.py index 8804a5a..b4cdbad 100644 --- a/fastapi_sso/sso/kakao.py +++ b/fastapi_sso/sso/kakao.py @@ -1,11 +1,11 @@ """Kakao SSO Oauth Helper class""" -from typing import TYPE_CHECKING, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +from typing import Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class KakaoSSO(SSOBase): @@ -22,5 +22,5 @@ async def get_discovery_document(self) -> DiscoveryDocument: "userinfo_endpoint": f"https://kapi.kakao.com/{self.version}/user/me", } - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: return OpenID(display_name=response["properties"]["nickname"], provider=self.provider) diff --git a/fastapi_sso/sso/microsoft.py b/fastapi_sso/sso/microsoft.py index 30a38f7..eca2fd5 100644 --- a/fastapi_sso/sso/microsoft.py +++ b/fastapi_sso/sso/microsoft.py @@ -1,13 +1,12 @@ -"""Microsoft SSO Oauth Helper class""" +"""Microsoft SSO Oauth Helper class.""" -from typing import TYPE_CHECKING, List, Optional, Union -import pydantic +from typing import List, Optional, Union -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +import httpx +import pydantic -if TYPE_CHECKING: - import httpx +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class MicrosoftSSO(SSOBase): @@ -45,7 +44,7 @@ async def get_discovery_document(self) -> DiscoveryDocument: "userinfo_endpoint": f"https://graph.microsoft.com/{self.version}/me", } - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: return OpenID( email=response.get("mail"), display_name=response.get("displayName"), diff --git a/fastapi_sso/sso/naver.py b/fastapi_sso/sso/naver.py index 032b597..f7c7d7a 100644 --- a/fastapi_sso/sso/naver.py +++ b/fastapi_sso/sso/naver.py @@ -1,11 +1,11 @@ -"""Naver SSO Oauth Helper class""" +"""Naver SSO Oauth Helper class.""" -from typing import TYPE_CHECKING, List, Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +from typing import List, Optional -if TYPE_CHECKING: - import httpx +import httpx + +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class NaverSSO(SSOBase): @@ -22,5 +22,5 @@ async def get_discovery_document(self) -> DiscoveryDocument: "userinfo_endpoint": "https://openapi.naver.com/v1/nid/me", } - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: return OpenID(display_name=response["properties"]["nickname"], provider=self.provider) diff --git a/fastapi_sso/sso/notion.py b/fastapi_sso/sso/notion.py index 923b96f..ed9a585 100644 --- a/fastapi_sso/sso/notion.py +++ b/fastapi_sso/sso/notion.py @@ -1,11 +1,10 @@ """Notion SSO Oauth Helper class""" -from typing import TYPE_CHECKING, Optional +from typing import Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginError +import httpx -if TYPE_CHECKING: - import httpx # pragma: no cover +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase, SSOLoginError class NotionSSO(SSOBase): @@ -22,7 +21,7 @@ async def get_discovery_document(self) -> DiscoveryDocument: "userinfo_endpoint": "https://api.notion.com/v1/users/me", } - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: owner = response["bot"]["owner"] if owner["type"] != "user": raise SSOLoginError(401, f"Notion login failed, owner is not a user but {response['bot']['owner']['type']}") diff --git a/fastapi_sso/sso/spotify.py b/fastapi_sso/sso/spotify.py index 9906b6b..f3d3a70 100644 --- a/fastapi_sso/sso/spotify.py +++ b/fastapi_sso/sso/spotify.py @@ -1,17 +1,15 @@ -"""Spotify SSO Login Helper -""" +"""Spotify SSO Login Helper.""" -from typing import TYPE_CHECKING, Optional +from typing import Optional -from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase +import httpx -if TYPE_CHECKING: - import httpx +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, SSOBase class SpotifySSO(SSOBase): - """Class providing login via Spotify OAuth""" + """Class providing login via Spotify OAuth.""" provider = "spotify" scope = ["user-read-private", "user-read-email"] @@ -24,12 +22,14 @@ async def get_discovery_document(self) -> DiscoveryDocument: "userinfo_endpoint": "https://api.spotify.com/v1/me", } - async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID: - """Return OpenID from user information provided by Spotify""" - if response.get("images", []): - picture = response["images"][0]["url"] + async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID: + """Return OpenID from user information provided by Spotify.""" + + if response.get("images") and len(response["images"]) > 0: + picture: Optional[str] = response["images"][0]["url"] else: picture = None + return OpenID( email=response.get("email", ""), display_name=response.get("display_name"), diff --git a/pyproject.toml b/pyproject.toml index 811c68b..095ceb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi-sso" -version = "0.10.0" +version = "0.10.1" description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)" authors = ["Tomas Votava "] readme = "README.md" diff --git a/tests/test_base.py b/tests/test_base.py index 1e1fce5..d7dac50 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,10 +1,11 @@ # type: ignore import os + import pytest from utils import Request -from fastapi_sso.sso.base import SSOBase, SSOLoginError, UnsetStateWarning +from fastapi_sso.infrastructure import SSOBase, SSOLoginError, UnsetStateWarning class TestSSOBase: diff --git a/tests/test_generic_provider.py b/tests/test_generic_provider.py index ebc24a9..b3989b2 100644 --- a/tests/test_generic_provider.py +++ b/tests/test_generic_provider.py @@ -1,7 +1,6 @@ -from fastapi_sso.sso.base import OpenID -from fastapi_sso.sso.generic import create_provider +from fastapi_sso.infrastructure import DiscoveryDocument, OpenID, factories -DISCOVERY = { +DISCOVERY: DiscoveryDocument = { "authorization_endpoint": "http://localhost:9090/auth", "token_endpoint": "http://localhost:9090/token", "userinfo_endpoint": "http://localhost:9090/me", @@ -10,26 +9,26 @@ class TestGenericProvider: async def test_discovery_document_static(self): - Provider = create_provider(discovery_document=DISCOVERY) + Provider = factories.create_provider(discovery_document=DISCOVERY) sso = Provider("client_id", "client_secret") document = await sso.get_discovery_document() assert document == DISCOVERY async def test_discovery_document_callable(self): - Provider = create_provider(discovery_document=lambda _: DISCOVERY) + Provider = factories.create_provider(discovery_document=lambda _: DISCOVERY) sso = Provider("client_id", "client_secret") document = await sso.get_discovery_document() assert document == DISCOVERY async def test_empty_response_convertor(self): - Provider = create_provider(discovery_document=DISCOVERY) + Provider = factories.create_provider(discovery_document=DISCOVERY) sso = Provider("client_id", "client_secret") openid = await sso.openid_from_response({}) assert openid.provider == Provider.provider assert openid.id is None async def test_response_convertor(self): - Provider = create_provider( + Provider = factories.create_provider( discovery_document=DISCOVERY, response_convertor=lambda response, _: OpenID( id=response["id"], email=response["email"], display_name=response["display_name"] diff --git a/tests/test_providers.py b/tests/test_providers.py index 4c9732a..9900989 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -8,20 +8,21 @@ from fastapi.responses import RedirectResponse from utils import AnythingDict, Request, Response, make_fake_async_client -from fastapi_sso.sso.base import OpenID, SSOBase -from fastapi_sso.sso.facebook import FacebookSSO -from fastapi_sso.sso.fitbit import FitbitSSO -from fastapi_sso.sso.generic import create_provider -from fastapi_sso.sso.github import GithubSSO -from fastapi_sso.sso.gitlab import GitlabSSO -from fastapi_sso.sso.google import GoogleSSO -from fastapi_sso.sso.kakao import KakaoSSO -from fastapi_sso.sso.microsoft import MicrosoftSSO -from fastapi_sso.sso.naver import NaverSSO -from fastapi_sso.sso.spotify import SpotifySSO -from fastapi_sso.sso.notion import NotionSSO - -GenericProvider = create_provider( +from fastapi_sso import ( + FacebookSSO, + FitbitSSO, + GithubSSO, + GitlabSSO, + GoogleSSO, + KakaoSSO, + MicrosoftSSO, + NaverSSO, + NotionSSO, + SpotifySSO, +) +from fastapi_sso.infrastructure import OpenID, SSOBase, factories + +GenericProvider = factories.create_provider( name="generic", discovery_document={ "authorization_endpoint": "https://example.com/auth", diff --git a/tox.ini b/tox.ini index cb4de51..b10617f 100644 --- a/tox.ini +++ b/tox.ini @@ -13,3 +13,4 @@ commands_pre = commands = poetry run poe test poetry run poe coverage + poetry run poe lint