Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typing infrastructure improvements #120

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions examples/fitbit.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
11 changes: 7 additions & 4 deletions examples/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions examples/github.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
8 changes: 5 additions & 3 deletions examples/gitlab.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
7 changes: 4 additions & 3 deletions examples/google.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
7 changes: 4 additions & 3 deletions examples/kakao.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
7 changes: 4 additions & 3 deletions examples/microsoft.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
7 changes: 4 additions & 3 deletions examples/naver.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
4 changes: 3 additions & 1 deletion examples/notion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
14 changes: 2 additions & 12 deletions fastapi_sso/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Fixed Show fixed Hide fixed
from .sso import * # noqa: F401, F403
Fixed Show fixed Hide fixed
16 changes: 16 additions & 0 deletions fastapi_sso/infrastructure/__init__.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions fastapi_sso/infrastructure/factories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""This pacakge includes all the shared SSO factories."""

__all__ = ("create_provider",)

from .provider import create_provider
Original file line number Diff line number Diff line change
@@ -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

Fixed Show fixed Hide fixed
if TYPE_CHECKING:
import httpx
from ..openid import DiscoveryDocument, OpenID, SSOBase

logger = logging.getLogger(__name__)

Expand All @@ -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.
Expand All @@ -33,21 +31,22 @@ 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",
"token_endpoint": "http://localhost:9090/token",
"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
)
)
```

"""
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""SSO login base dependency
"""
"""SSO login base dependency."""
# pylint: disable=too-few-public-methods


import json
import os
import sys
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions fastapi_sso/sso/__init__.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions fastapi_sso/sso/facebook.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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",
Expand All @@ -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"),
Expand Down
17 changes: 9 additions & 8 deletions fastapi_sso/sso/fitbit.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -17,9 +16,10 @@

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")

Check warning on line 22 in fastapi_sso/sso/fitbit.py

View check run for this annotation

Codecov / codecov/patch

fastapi_sso/sso/fitbit.py#L20-L22

Added lines #L20 - L22 were not covered by tests
return OpenID(
id=info["encodedId"],
first_name=info["fullName"],
Expand All @@ -30,6 +30,7 @@

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",
Expand Down
Loading
Loading