Skip to content

Registration #11

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

Merged
merged 29 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d38f39
Make `redirect_uri` override `base_url` if set
ArtyomVancyan Jul 15, 2023
0b1ec58
Create a `User` model and API endpoints for creating and getting
ArtyomVancyan Jul 15, 2023
cc35c8f
Add an auth simulation
ArtyomVancyan Jul 17, 2023
7606da6
Extend from `AuthCredentials` and `BaseUser` superclasses
ArtyomVancyan Jul 17, 2023
1241140
GH-9: Add `callback` argument to middleware
ArtyomVancyan Jul 17, 2023
72b0a80
GH-9: Finish up the implementation of `BaseUser` properties
ArtyomVancyan Jul 18, 2023
74a702e
GH-9: Implement a data standardizer for converting fields to the comm…
ArtyomVancyan Jul 18, 2023
3283969
Add new user fields in the user model
ArtyomVancyan Jul 18, 2023
d38d513
Fix the field names in mocked user data
ArtyomVancyan Jul 18, 2023
2b2f0a5
Showcase the usage of `callback` of the middleware
ArtyomVancyan Jul 18, 2023
e40321e
Add a TODO note for future
ArtyomVancyan Jul 20, 2023
3b2b883
Remove the existing feature from the list of 'to be'
ArtyomVancyan Jul 20, 2023
d013953
Add class description comments
ArtyomVancyan Jul 20, 2023
099ec02
Add the missing typehints
ArtyomVancyan Jul 20, 2023
6d6c149
Fix the typing for Python<3.10 versions
ArtyomVancyan Jul 21, 2023
774512c
Merge dependent methods
ArtyomVancyan Jul 22, 2023
fe95111
GH-9: Add custom user properties and remove polymorph constructor
ArtyomVancyan Jul 23, 2023
367374f
GH-9: Implement the skeleton of claim declarator
ArtyomVancyan Jul 27, 2023
afcc936
GH-9: Each client config can have its claim mappings
ArtyomVancyan Jul 27, 2023
6cdfddc
GH-9: Fix init when `Claims` obtains `Claims` as a value
ArtyomVancyan Jul 28, 2023
49115c9
GH-9: Add the `claims` attribute
ArtyomVancyan Jul 28, 2023
ead1086
GH-9: Add `provider` in the user entity
ArtyomVancyan Jul 28, 2023
86ff69f
GH-9: Handle claims mapping for certain provider
ArtyomVancyan Jul 28, 2023
6cc5b61
Showcase the usage of `claims` config
ArtyomVancyan Jul 28, 2023
ca1cc3c
GH-9: Add `Auth` to on-auth callback
ArtyomVancyan Jul 29, 2023
6d60c55
GH-9: Describe `claims` and add a usage example
ArtyomVancyan Jul 29, 2023
920d78d
GH-9: Remove 'Registration support' from to be list
ArtyomVancyan Jul 30, 2023
c5b6329
Upgrade the `fastapi` version for tox environment
ArtyomVancyan Jul 30, 2023
b876221
Upgrade the version to `alpha.1`
ArtyomVancyan Jul 30, 2023
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: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ jobs:
env: py311-fastapi84

- python: "3.7"
env: py37-fastapi99
env: py37-fastapi100
- python: "3.9"
env: py39-fastapi99
env: py39-fastapi100
- python: "3.10"
env: py310-fastapi99
env: py310-fastapi100
- python: "3.11"
env: py311-fastapi99
env: py311-fastapi100

steps:
- uses: actions/checkout@v2
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ the [social-core](https://github.com/python-social-auth/social-core) authenticat

- Use multiple OAuth2 providers at the same time
* There need to be provided a way to configure the OAuth2 for multiple providers
- Token -> user data, user data -> token easy conversion
- Customizable OAuth2 routes
- Registration support

## Installation

Expand Down Expand Up @@ -43,12 +41,14 @@ middleware configuration is declared with the `OAuth2Config` and `OAuth2Client`
- `client_secret` - The OAuth2 client secret for the particular provider.
- `redirect_uri` - The OAuth2 redirect URI to redirect to after success. Defaults to the base URL.
- `scope` - The OAuth2 scope for the particular provider. Defaults to `[]`.
- `claims` - Claims mapping for the certain provider.

It is also important to mention that for the configured clients of the auth providers, the authorization URLs are
accessible by the `/oauth2/{provider}/auth` path where the `provider` variable represents the exact value of the auth
provider backend `name` attribute.

```python
from fastapi_oauth2.claims import Claims
from fastapi_oauth2.client import OAuth2Client
from fastapi_oauth2.config import OAuth2Config
from social_core.backends.github import GithubOAuth2
Expand All @@ -65,6 +65,10 @@ oauth2_config = OAuth2Config(
client_secret=os.getenv("OAUTH2_CLIENT_SECRET"),
redirect_uri="https://pysnippet.org/",
scope=["user:email"],
claims=Claims(
picture="avatar_url",
identity=lambda user: "%s:%s" % (user.get("provider"), user.get("id")),
),
),
]
)
Expand Down
5 changes: 5 additions & 0 deletions examples/demonstration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dotenv import load_dotenv
from social_core.backends.github import GithubOAuth2

from fastapi_oauth2.claims import Claims
from fastapi_oauth2.client import OAuth2Client
from fastapi_oauth2.config import OAuth2Config

Expand All @@ -20,6 +21,10 @@
client_secret=os.getenv("OAUTH2_CLIENT_SECRET"),
# redirect_uri="http://127.0.0.1:8000/",
scope=["user:email"],
claims=Claims(
picture="avatar_url",
identity=lambda user: "%s:%s" % (user.get("provider"), user.get("id")),
),
),
]
)
21 changes: 21 additions & 0 deletions examples/demonstration/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

engine = create_engine(
"sqlite:///./database.sqlite",
connect_args={
"check_same_thread": False,
},
)

Base = declarative_base()
SessionLocal = sessionmaker(bind=engine, autoflush=False)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
27 changes: 26 additions & 1 deletion examples/demonstration/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
from fastapi import APIRouter
from fastapi import FastAPI
from sqlalchemy.orm import Session

from config import oauth2_config
from database import Base
from database import engine
from database import get_db
from fastapi_oauth2.middleware import Auth
from fastapi_oauth2.middleware import OAuth2Middleware
from fastapi_oauth2.middleware import User
from fastapi_oauth2.router import router as oauth2_router
from models import User as UserModel
from router import router as app_router

Base.metadata.create_all(bind=engine)

router = APIRouter()


async def on_auth(auth: Auth, user: User):
# perform a check for user existence in
# the database and create if not exists
db: Session = next(get_db())
query = db.query(UserModel)
if user.identity and not query.filter_by(identity=user.identity).first():
UserModel(**{
"identity": user.get("identity"),
"username": user.get("username"),
"image": user.get("image"),
"email": user.get("email"),
"name": user.get("name"),
}).save(db)


app = FastAPI()
app.include_router(app_router)
app.include_router(oauth2_router)
app.add_middleware(OAuth2Middleware, config=oauth2_config)
app.add_middleware(OAuth2Middleware, config=oauth2_config, callback=on_auth)
27 changes: 27 additions & 0 deletions examples/demonstration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.orm import Session

from database import Base


class BaseModel(Base):
__abstract__ = True

def save(self, db: Session):
db.add(self)
db.commit()
db.refresh(self)
return self


class User(BaseModel):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
username = Column(String)
email = Column(String)
name = Column(String)
image = Column(String)
identity = Column(String, unique=True) # provider_name:user_id
41 changes: 39 additions & 2 deletions examples/demonstration/router.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import json

from fastapi import APIRouter
from fastapi import Depends
from fastapi import Request
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from starlette.responses import RedirectResponse

from database import get_db
from fastapi_oauth2.security import OAuth2
from models import User

oauth2 = OAuth2()
router = APIRouter()
Expand All @@ -18,6 +22,39 @@ async def root(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "user": request.user, "json": json})


@router.get("/auth")
def sim_auth(request: Request):
access_token = request.auth.jwt_create({
"id": 1,
"identity": "demo:1",
"image": None,
"display_name": "John Doe",
"email": "john.doe@auth.sim",
"username": "JohnDoe",
"exp": 3689609839,
})
response = RedirectResponse("/")
response.set_cookie(
"Authorization",
value=f"Bearer {access_token}",
max_age=request.auth.expires,
expires=request.auth.expires,
httponly=request.auth.http,
)
return response


@router.get("/user")
def user(request: Request, _: str = Depends(oauth2)):
def user_get(request: Request, _: str = Depends(oauth2)):
return request.user


@router.get("/users")
def users_get(request: Request, db: Session = Depends(get_db), _: str = Depends(oauth2)):
return db.query(User).all()


@router.post("/users")
async def users_post(request: Request, db: Session = Depends(get_db), _: str = Depends(oauth2)):
data = await request.json()
return User(**data).save(db)
11 changes: 9 additions & 2 deletions examples/demonstration/templates/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/fastapi_oauth2/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.0-alpha"
__version__ = "1.0.0-alpha.1"
19 changes: 19 additions & 0 deletions src/fastapi_oauth2/claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Any
from typing import Callable
from typing import Union


class Claims(dict):
"""Claims configuration for a single provider."""

display_name: Union[str, Callable[[dict], Any]]
identity: Union[str, Callable[[dict], Any]]
picture: Union[str, Callable[[dict], Any]]
email: Union[str, Callable[[dict], Any]]

def __init__(self, seq=None, **kwargs) -> None:
super().__init__(seq or {}, **kwargs)
self["display_name"] = kwargs.get("display_name", self.get("display_name", "name"))
self["identity"] = kwargs.get("identity", self.get("identity", "sub"))
self["picture"] = kwargs.get("picture", self.get("picture", "picture"))
self["email"] = kwargs.get("email", self.get("email", "email"))
10 changes: 9 additions & 1 deletion src/fastapi_oauth2/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from typing import Optional
from typing import Sequence
from typing import Type
from typing import Union

from social_core.backends.oauth import BaseOAuth2

from .claims import Claims


class OAuth2Client:
"""OAuth2 client configuration for a single provider."""

backend: Type[BaseOAuth2]
client_id: str
client_secret: str
redirect_uri: Optional[str]
scope: Optional[Sequence[str]]
claims: Optional[Union[Claims, dict]]

def __init__(
self,
Expand All @@ -20,9 +26,11 @@ def __init__(
client_secret: str,
redirect_uri: Optional[str] = None,
scope: Optional[Sequence[str]] = None,
):
claims: Optional[Union[Claims, dict]] = None,
) -> None:
self.backend = backend
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.scope = scope or []
self.claims = Claims(claims)
4 changes: 3 additions & 1 deletion src/fastapi_oauth2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class OAuth2Config:
"""Configuration class of the authentication middleware."""

allow_http: bool
jwt_secret: str
jwt_expires: int
Expand All @@ -20,7 +22,7 @@ def __init__(
jwt_expires: Union[int, str] = 900,
jwt_algorithm: str = "HS256",
clients: List[OAuth2Client] = None,
):
) -> None:
if allow_http:
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
self.allow_http = allow_http
Expand Down
Loading