diff --git a/.env b/.env new file mode 100644 index 0000000..a1f6457 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +OAUTH2_CLIENT_ID=eccd08d6736b7999a32a +OAUTH2_CLIENT_SECRET=642999c1c5f2b3df8b877afdc78252ef5b594d31 +OAUTH2_CALLBACK_URL=http://127.0.0.1:8000/oauth2/token + +JWT_SECRET=secret +JWT_ALGORITHM=HS256 +JWT_EXPIRES=900 diff --git a/README.md b/README.md index fce1c10..2d06d4e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # fastapi-oauth2 -Easy to setup social authentication mechanism with support for several auth providers. +Easy to setup OAuth2 social authentication mechanism with support for several auth providers. -## Examples +## Demo -- [airnominal](./examples/airnominal) - [fastapi-sso](https://github.com/tomasvotava/fastapi-sso) based implementation -- [dogeapi](./examples/DogeAPI) - [fastapi-allauth](https://github.com/K-villain/fastapi-allauth) based implementation +This sample application is made to demonstrate the use of the [**fastapi-oauth2**](./fastapi_oauth2) package. -Both can be run using the following command: +## Running the application ```bash uvicorn main:app --reload @@ -15,4 +14,15 @@ uvicorn main:app --reload ## TODO -- Segregate the prototype of the `fastapi-oauth2` core. +- Make the [**fastapi-oauth2**](./fastapi_oauth2) depend + on (overuse) the [**social-core**](https://github.com/python-social-auth/social-core) + +## Features + +- Integrate with any existing FastAPI project (no dependencies of the project should stop the work of + the `fastapi-oauth2`) + * Implementation must allow to provide a context for configurations (also, see how it is done in another projects) +- 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 +- Customize OAuth2 routes diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b8d3a87 --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# last version of `build` supporting Python 3.6 +pip install build==0.9.0 + +# build the wheel and install it +WHEEL_NAME=$(python -m build | grep -Po "fastapi_oauth2-.*\.whl" | tail -n 1) +pip install dist/$WHEEL_NAME \ No newline at end of file diff --git a/examples/airnominal/fastapi_sso/__init__.py b/demo/__init__.py similarity index 100% rename from examples/airnominal/fastapi_sso/__init__.py rename to demo/__init__.py diff --git a/demo/dependencies.py b/demo/dependencies.py new file mode 100644 index 0000000..52594b6 --- /dev/null +++ b/demo/dependencies.py @@ -0,0 +1,39 @@ +from typing import Optional + +from fastapi import HTTPException +from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel +from fastapi.security import OAuth2 +from fastapi.security.utils import get_authorization_scheme_param +from starlette.requests import Request +from starlette.status import HTTP_403_FORBIDDEN + + +class OAuth2PasswordBearerCookie(OAuth2): + def __init__( + self, + tokenUrl: str, + scheme_name: str = None, + scopes: dict = None, + auto_error: bool = True, + ): + flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes or {}}) + super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) + + async def __call__(self, request: Request) -> Optional[str]: + scheme, param = get_authorization_scheme_param(request.headers.get("Authorization")) + authorization = scheme.lower() == "bearer" + if not authorization: + scheme, param = get_authorization_scheme_param(request.cookies.get("Authorization")) + authorization = scheme.lower() == "bearer" + + if not authorization: + if self.auto_error: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + else: + return None + return param + + +oauth2_scheme = OAuth2PasswordBearerCookie(tokenUrl="/token") diff --git a/demo/router.py b/demo/router.py new file mode 100644 index 0000000..822dab1 --- /dev/null +++ b/demo/router.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter +from fastapi import Depends +from starlette.requests import Request + +from .dependencies import oauth2_scheme + +router = APIRouter() + + +@router.get("/user") +def user(request: Request, _: str = Depends(oauth2_scheme)): + return request.user + + +@router.post("/token") +def token(request: Request): + return request.cookies.get("Authorization") diff --git a/examples/DogeAPI/.gitignore b/examples/DogeAPI/.gitignore deleted file mode 100644 index f52fa55..0000000 --- a/examples/DogeAPI/.gitignore +++ /dev/null @@ -1,130 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal -blog.db - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/examples/DogeAPI/api/blog.py b/examples/DogeAPI/api/blog.py deleted file mode 100644 index 0473b6d..0000000 --- a/examples/DogeAPI/api/blog.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/python3 - -from fastapi import HTTPException, status -from sqlalchemy.orm import Session - -from models import models -from schema import schemas - - -def get_all(db: Session): - """ - Get all blogs - - Args: - db (Session): Database session - - Returns: - List[models.Blog]: List of blogs - """ - return db.query(models.Blog).all() - - -def create(request: schemas.Blog, db: Session): - """ - Create a new blog - - Args: - request (schemas.Blog): Blog object - db (Session): Database session - - Returns: - models.Blog: Blog object - """ - new_blog = models.Blog(title=request.title, body=request.body) - db.add(new_blog) - db.commit() - db.refresh(new_blog) - return new_blog - - -def destroy(id: int, db: Session): - """ - Delete a blog - - Args: - id (int): Blog id - db (Session): Database session - - Raises: - HTTPException: 404 not found - - Returns: - str: Success message - """ - blog_to_delete = db.query(models.Blog).filter(models.Blog.id == id) - - if not blog_to_delete.first(): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Blog with id {id} not found.", - ) - blog_to_delete.delete(synchronize_session=False) - db.commit() - return {"done"} - - -def update(id: int, request: schemas.Blog, db: Session): - """ - Update a blog - - Args: - id (int): Blog id - request (schemas.Blog): Blog object - db (Session): Database session - - Raises: - HTTPException: 404 not found - - Returns: - models.Blog: Blog object - """ - blog = db.query(models.Blog).filter(models.Blog.id == id) - if not blog.first(): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=f"Blog with id {id} not found" - ) - blog.update(request.__dict__) - db.commit() - return "updated" - - -def show(id: int, db: Session): - """ - Get a blog - - Args: - id (int): Blog id - db (Session): Database session - - Raises: - HTTPException: 404 not found - - Returns: - models.Blog: Blog object - """ - blog = db.query(models.Blog).filter(models.Blog.id == id).first() - if blog: - return blog - else: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Blog with the id {id} is not available", - ) diff --git a/examples/DogeAPI/api/user.py b/examples/DogeAPI/api/user.py deleted file mode 100644 index b472ed3..0000000 --- a/examples/DogeAPI/api/user.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/python3 - -from fastapi import HTTPException, status -from sqlalchemy.orm import Session - -from models import models -from schema import schemas -from schema.hash import Hash - - -def create(request: schemas.User, db: Session): - """ - Create a new user - - Args: - request (schemas.User): User data - db (Session): Database session - - Returns: - models.User: User created - """ - hashedPassword = Hash.bcrypt(request.password) - user = models.User(name=request.name, email=request.email, password=hashedPassword) - db.add(user) - db.commit() - db.refresh(user) - return user - - -def show(id: int, db: Session): - """ - Show a user - - Args: - id (int): User id - db (Session): Database session - - Raises: - HTTPException: User not found - - Returns: - models.User: User found - """ - user = db.query(models.User).filter(models.User.id == id).first() - if not user: - raise HTTPException( - status.HTTP_404_NOT_FOUND, detail=f"User with id {id} not found" - ) - return user - - -def get_all(db: Session): - """ - Get all users - - Args: - db (Session): Database session - - Returns: - list: List of users - """ - return db.query(models.User).all() diff --git a/examples/DogeAPI/core/auth.py b/examples/DogeAPI/core/auth.py deleted file mode 100644 index 9892da8..0000000 --- a/examples/DogeAPI/core/auth.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python3 - - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security.oauth2 import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session - -from database import configuration -from models import models -from schema import schemas -from schema.hash import Hash -from schema.token import create_access_token - -router = APIRouter(prefix="/login", tags=["Authentication"],) - - -@router.post("/") -def login( - request: OAuth2PasswordRequestForm = Depends(), - db: Session = Depends(configuration.get_db), -): - """ - Login user - - Args: - request (OAuth2PasswordRequestForm, optional): OAuth2PasswordRequestForm. - db (Session, optional): Session. Defaults to Depends(configuration.get_db). - - Raises: - HTTPException: 401 Unauthorized - HTTPException: 404 Not Found - - Returns: - Hash: Hash - """ - user: schemas.User = db.query(models.User).filter( - models.User.email == request.username - ).first() - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Invalid Credentials" - ) - - if not Hash.verify(user.password, request.password): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Incorrect password" - ) - - # access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token(data={"sub": user.email}) - - # generate JWT token and return - return {"access_token": access_token, "token_type": "bearer"} diff --git a/examples/DogeAPI/core/blog.py b/examples/DogeAPI/core/blog.py deleted file mode 100644 index f8fb1c8..0000000 --- a/examples/DogeAPI/core/blog.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/python3 - -from typing import List - -from fastapi import APIRouter, Depends, Response, status -from sqlalchemy.orm import Session - -from api import blog -from database import configuration -from schema import schemas -from schema.oa2 import get_current_user - -router = APIRouter(tags=["Blogs"], prefix="/blog") -get_db = configuration.get_db - - -@router.get("/", response_model=List[schemas.ShowBlog]) -def get_all_blogs( - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_current_user), -): - """ - Get all blogs - - Args: - db (Session, optional): Database session. Defaults to None. - current_user (schemas.User, optional): Current user. Defaults to None. - - Returns: - List[schemas.ShowBlog]: List of blogs - """ - return blog.get_all(db) - - -@router.post("/", status_code=status.HTTP_201_CREATED) -def create( - request: schemas.Blog, - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_current_user), -): - """ - Create a blog - - Args: - request (schemas.Blog): Blog to create - db (Session, optional): Database session. Defaults to None. - current_user (schemas.User, optional): Current user. Defaults to None. - - Returns: - schemas.Blog: Created blog - """ - return blog.create(request, db) - - -@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=schemas.ShowBlog) -def get_blog_by_id( - id: int, - response: Response, - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_current_user), -): - """ - Get a blog by id - - Args: - id (int): Blog id - response (Response): FastAPI response - db (Session, optional): Database session. Defaults to None. - current_user (schemas.User, optional): Current user. Defaults to None. - - Returns: - schemas.ShowBlog: Blog - """ - return blog.show(id, db) - - -@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) -def delete_blog( - id: int, - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_current_user), -): - """ - Delete a blog by id - - Args: - id (int): Blog id - db (Session, optional): Database session. Defaults to None. - current_user (schemas.User, optional): Current user. Defaults to None. - - Returns: - None: None - """ - return blog.destroy(id, db) - - -@router.put("/{id}", status_code=status.HTTP_202_ACCEPTED) -def update_blog( - id: int, - request: schemas.Blog, - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_current_user), -): - """ - Update a blog by id - - Args: - id (int): Blog id - request (schemas.Blog): Blog to update - db (Session, optional): Database session. Defaults to Depends(get_db). - current_user (schemas.User, optional): Current user. Defaults to Depends(get_current_user). - - Returns: - schemas.Blog: Updated blog - """ - return blog.update(id, request, db) diff --git a/examples/DogeAPI/core/user.py b/examples/DogeAPI/core/user.py deleted file mode 100644 index 3dbca90..0000000 --- a/examples/DogeAPI/core/user.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python3 - -from typing import List - -from fastapi import APIRouter, Depends, status -from sqlalchemy.orm import Session - -from api import user -from database import configuration -from schema import schemas - -router = APIRouter(tags=["Users"], prefix="/users") -get_db = configuration.get_db - - -@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.ShowUser) -def create_user(request: schemas.User, db: Session = Depends(get_db)): - """ - Create a new user - - Args: - request (schemas.User): User to create - db (Session, optional): Database session. - - Returns: - schemas.ShowUser: User created - """ - return user.create(request, db) - - -@router.get("/", status_code=status.HTTP_200_OK, response_model=List[schemas.ShowUser]) -def get_users(db: Session = Depends(get_db)): - """ - Get all users - - Args: - db (Session, optional): Database session. Defaults to Depends(get_db). - - Returns: - List[schemas.ShowUser]: List of users - """ - return user.get_all(db) - - -@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=schemas.ShowUser) -def get_user_by_id(id: int, db: Session = Depends(get_db)): - """ - Get a user by id - - Args: - id (int): User id - db (Session, optional): Database session. Defaults to Depends(get_db). - - Returns: - schemas.ShowUser: User - """ - return user.show(id, db) diff --git a/examples/DogeAPI/database/configuration.py b/examples/DogeAPI/database/configuration.py deleted file mode 100644 index 1d968d3..0000000 --- a/examples/DogeAPI/database/configuration.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python3 - -from decouple import config -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -SQLALCHEMY_DATABASE_URL = config( - "DATABASE_URL", default="sqlite:///./database/dogeapi.sqlite" -) -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) - -SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) -Base = declarative_base() - - -def get_db(): - """ - Get the database session - - Yields: - Session: The database session - """ - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/examples/DogeAPI/database/dogeapi.sqlite b/examples/DogeAPI/database/dogeapi.sqlite deleted file mode 100644 index c2aee60..0000000 Binary files a/examples/DogeAPI/database/dogeapi.sqlite and /dev/null differ diff --git a/examples/DogeAPI/fastapi_allauth/__init__.py b/examples/DogeAPI/fastapi_allauth/__init__.py deleted file mode 100644 index 86ddb98..0000000 --- a/examples/DogeAPI/fastapi_allauth/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .allauth_manager import AllauthManager diff --git a/examples/DogeAPI/fastapi_allauth/allauth_manager.py b/examples/DogeAPI/fastapi_allauth/allauth_manager.py deleted file mode 100644 index 85aa92a..0000000 --- a/examples/DogeAPI/fastapi_allauth/allauth_manager.py +++ /dev/null @@ -1,71 +0,0 @@ -from functools import wraps -from typing import Optional - -from fastapi import APIRouter, HTTPException -from sqlalchemy.orm import Session - -from .auth import login, register, AuthHandler -from .model import BaseUser -from .oauth.BaseOauth import BaseOauth -from .secret_handler import SecretType - - -class AllauthManager: - db: Session - user: BaseUser - secret: SecretType - lifetime_second: int = 3600 - - def __init__(self, db, user, secret, lifetime_second) -> None: - self.db = db - self.user = user - self.secret = secret - self.lifetime_second = lifetime_second - - def get_oauth_router(self, oauth: BaseOauth) -> APIRouter: - - router = APIRouter() - - @router.get("/authorize") - async def authorize(scope: Optional[str] = None): - url = await oauth.get_authorization_url(scope=scope) - return {"url": url} - - @router.get("/callback") - async def callback(code: Optional[str] = None, state: Optional[str] = None): - tokens = await oauth.get_access_token(code=code, state=state) - user_json = oauth.get_userinfo(tokens["access_token"]) - _user = self.user.create( - open_id=oauth.get_open_id(user_json=user_json), provider=oauth.provider) - - if self.get_user_by_authority(_user.authority) is None: - try: - register(self.db, _user) - except Exception("Register failed"): - pass - - return login(_user, self.secret, self.lifetime_second) - - return router - - def get_user_by_authority(self, authority: str): - return self.db.query(BaseUser).filter(BaseUser.authority == authority).first() - - def login_required(self, func): - @wraps(func) - async def wrapper(*args, **kwargs): - auth_handler = AuthHandler(self.secret, self.lifetime_second) - token = kwargs.get('authorization', False) - if token: - authority = auth_handler.decode_access_token(token) - - if not self.get_user_by_authority(authority): - raise HTTPException(status_code=401, detail="user not exist") - - else: - raise HTTPException(status_code=401, detail="token required") - - # success - return await func(*args, **kwargs) - - return wrapper diff --git a/examples/DogeAPI/fastapi_allauth/auth/__init__.py b/examples/DogeAPI/fastapi_allauth/auth/__init__.py deleted file mode 100644 index cf6bc80..0000000 --- a/examples/DogeAPI/fastapi_allauth/auth/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .authenticate import AuthHandler -from .login import login -from .register import register diff --git a/examples/DogeAPI/fastapi_allauth/auth/authenticate.py b/examples/DogeAPI/fastapi_allauth/auth/authenticate.py deleted file mode 100644 index 89ddcbe..0000000 --- a/examples/DogeAPI/fastapi_allauth/auth/authenticate.py +++ /dev/null @@ -1,88 +0,0 @@ -import time -from datetime import datetime, timedelta - -import jwt -from fastapi import HTTPException, Security -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer - -from ..secret_handler import SecretType, _get_secret_value - -INVALID = "Invalid token" - - -class AuthHandler: - secret: SecretType - lifetime: int - - def __init__(self, secret, lifetime_seconds) -> None: - self.secret = secret - self.lifetime_seconds = lifetime_seconds - - security = HTTPBearer() - - @staticmethod - def datetime_from_utc_to_local(utc_datetime): - epoch = time.mktime(utc_datetime.timetuple()) - offset = datetime.fromtimestamp( - epoch) - datetime.utcfromtimestamp(epoch) - return utc_datetime + offset - - def encode_token(self, payload: dict, type): - payload['sub'] = type - local_datetime = self.datetime_from_utc_to_local(datetime.utcnow()) - if type == "access_token": - payload.update( - {"exp": local_datetime + timedelta(seconds=self.lifetime_seconds)}) - else: - payload.update({"exp": local_datetime + timedelta(hours=720)}) - - return jwt.encode(payload, _get_secret_value(self.secret), algorithm='HS256') - - def encode_login_token(self, payload: dict): - access_token = self.encode_token(payload, "access_token") - refresh_token = self.encode_token(payload, "refresh_token") - - login_token = dict( - access_token=f"{access_token}", - refresh_token=f"{refresh_token}" - ) - return login_token - - def encode_update_token(self, payload: dict): - access_token = self.encode_token(payload, "access_token") - - update_token = dict( - access_token=f"{access_token}" - ) - return update_token - - def decode_access_token(self, token): - - try: - payload = jwt.decode(token, _get_secret_value(self.secret), algorithms=['HS256']) - if payload['sub'] != "access_token": - raise HTTPException(status_code=401, detail=INVALID) - return payload['authority'] - except jwt.ExpiredSignatureError: - raise HTTPException( - status_code=401, detail='Signature has expired') - except jwt.InvalidTokenError as e: - raise HTTPException(status_code=401, detail=INVALID) - - def decode_refresh_token(self, token): - try: - payload = jwt.decode(token, _get_secret_value(self.secret), algorithms=['HS256']) - if payload['sub'] != "refresh_token": - raise HTTPException(status_code=401, detail=INVALID) - return payload['authority'] - except jwt.ExpiredSignatureError: - raise HTTPException( - status_code=401, detail='Signature has expired') - except jwt.InvalidTokenError as e: - raise HTTPException(status_code=401, detail=INVALID) - - def auth_access_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): - return self.decode_access_token(auth.credentials) - - def auth_refresh_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): - return self.decode_refresh_token(auth.credentials) diff --git a/examples/DogeAPI/fastapi_allauth/auth/login.py b/examples/DogeAPI/fastapi_allauth/auth/login.py deleted file mode 100644 index 7d6cb87..0000000 --- a/examples/DogeAPI/fastapi_allauth/auth/login.py +++ /dev/null @@ -1,15 +0,0 @@ -from ..auth.authenticate import AuthHandler -from ..model.BaseUser import BaseUser -from ..secret_handler import SecretType, _get_secret_value - - -def login(user: BaseUser, secret: SecretType, lifetime_seconds: int): - authhandler = AuthHandler(_get_secret_value(secret), lifetime_seconds) - - _payload = {} - for key in user.payload: - _payload[key] = user[key] - - token = authhandler.encode_login_token(payload=_payload) - - return token diff --git a/examples/DogeAPI/fastapi_allauth/auth/register.py b/examples/DogeAPI/fastapi_allauth/auth/register.py deleted file mode 100644 index f300fee..0000000 --- a/examples/DogeAPI/fastapi_allauth/auth/register.py +++ /dev/null @@ -1,11 +0,0 @@ -from sqlalchemy.orm import Session - -from ..model.BaseUser import BaseUser - - -def register(db: Session, user: BaseUser): - db.add(user) - db.commit() - db.refresh(user) - - return user diff --git a/examples/DogeAPI/fastapi_allauth/model/BaseUser.py b/examples/DogeAPI/fastapi_allauth/model/BaseUser.py deleted file mode 100644 index 2c8bcd2..0000000 --- a/examples/DogeAPI/fastapi_allauth/model/BaseUser.py +++ /dev/null @@ -1,47 +0,0 @@ -import hashlib -import uuid - -from sqlalchemy import Column, String - -from database.configuration import Base - - -class BaseUser(Base): - __tablename__ = "users" - - id = Column(String, primary_key=True, index=True) - authority = Column(String) - - payload = ['id', 'authority'] - - def __init__(self, id, authority): - self.id = id - self.authority = authority - - @classmethod - def create_authority(cls, open_id, provider): - context = str(open_id) + provider - authority = hashlib.sha256(context.encode()).hexdigest() - return authority - - @classmethod - def create( - cls, - open_id: String, - provider: String, - ): - authority = cls.create_authority(open_id, provider) - id = uuid.uuid4().hex - - return cls(id=id, authority=authority) - - class Config: - orm_mode = True - - def __getitem__(self, key): - return getattr(self, key) - - -__all__ = [ - "BaseUser" -] diff --git a/examples/DogeAPI/fastapi_allauth/model/__init__.py b/examples/DogeAPI/fastapi_allauth/model/__init__.py deleted file mode 100644 index 8608a35..0000000 --- a/examples/DogeAPI/fastapi_allauth/model/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .BaseUser import BaseUser diff --git a/examples/DogeAPI/fastapi_allauth/oauth/BaseOauth.py b/examples/DogeAPI/fastapi_allauth/oauth/BaseOauth.py deleted file mode 100644 index 1dea31a..0000000 --- a/examples/DogeAPI/fastapi_allauth/oauth/BaseOauth.py +++ /dev/null @@ -1,115 +0,0 @@ -import time -from typing import Optional, List, Dict, TypeVar, Any, cast -from urllib.parse import urlencode - -import requests - -from ..secret_handler import SecretType, _get_secret_value - -T = TypeVar("T") - - -class BaseOauth: - provider: str - client_id: str - client_secret: SecretType - redirect_uri: str - authorize_url: str - access_token_url: str - refresh_token_url: Optional[str] - revoke_token_url: Optional[str] - base_scope: Optional[List[str]] - request_header: Dict[str, str] - - def __init__( - self, - provider: str, - client_id: str, - client_secret: SecretType, - redirect_uri: str, - authorize_url: str, - access_token_url: str, - refresh_token_url: Optional[str] = None, - revoke_token_url: Optional[str] = None, - base_scope: Optional[List[str]] = None, - ): - - self.provider = provider - self.client_id = client_id - self.client_secret = client_secret - self.redirect_uri = redirect_uri - self.authorize_url = authorize_url - self.access_token_url = access_token_url - self.refresh_token_url = refresh_token_url - self.revoke_token_url = revoke_token_url - self.base_scope = base_scope - - self.request_header = {"Accept": "application/json"} - - async def get_authorization_url( - self, - state: Optional[str] = None, - scope: Optional[List[str]] = None, - extras_params: Optional[T] = None, - ) -> str: - params = { - "response_type": "code", - "client_id": self.client_id, - "redirect_uri": self.redirect_uri - } - - if state is not None: - params["state"] = state - - _scope = scope or self.base_scope - if _scope is not None: - params["scope"] = " ".join(_scope) - - if extras_params is not None: - params = {**params, **extras_params} # type: ignore - return f"{self.authorize_url}?{urlencode(params)}" - - async def get_access_token(self, code: str, state: Optional[str] = None): - data = { - "grant_type": "authorization_code", - "client_id": self.client_id, - "client_secret": _get_secret_value(self.client_secret), - "code": code, - "redirect_uri": self.redirect_uri, - "state": state, - } - - response = requests.post( - self.access_token_url, data=data, headers=self.request_header) - if response.status_code >= 400: - raise Exception(response.text) - - data = cast(Dict[str, Any], response.json()) - - return OAuth2Token(data) - - async def get_userinfo(self, access_token: str): - raise NotImplementedError - - async def get_open_id(self, user_json: dict): - raise NotImplementedError - - -class OAuth2Token(Dict[str, Any]): - def __init__(self, token_dict: Dict[str, Any]): - if "expires_at" in token_dict: - token_dict["expires_at"] = int(token_dict["expires_at"]) - elif "expires_in" in token_dict: - token_dict["expires_at"] = int( - time.time()) + int(token_dict["expires_in"]) - super().__init__(token_dict) - - def is_expired(self): - if "expires_at" not in self: - return False - return time.time() > self["expires_at"] - - -__all__ = [ - "BaseOauth" -] diff --git a/examples/DogeAPI/fastapi_allauth/oauth/GithubOauth.py b/examples/DogeAPI/fastapi_allauth/oauth/GithubOauth.py deleted file mode 100644 index 039afba..0000000 --- a/examples/DogeAPI/fastapi_allauth/oauth/GithubOauth.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Optional, List - -import requests - -from .BaseOauth import BaseOauth -from ..secret_handler import SecretType - -AUTH_URL = "https://github.com/login/oauth/authorize" -TOKEN_URL = "https://github.com/login/oauth/access_token" -USER_INFO_URL = "https://api.github.com/user" - - -class GithubOauth(BaseOauth): - - def __init__( - self, - provider: str = "GITHUB", - client_id: str = "", - client_secret: SecretType = "", - redirect_uri: str = "", - scope: Optional[List[str]] = None, - refresh_token_url: Optional[str] = None, - revoke_token_url: Optional[str] = None - ): - super().__init__( - provider=provider, - client_id=client_id, - client_secret=client_secret, - redirect_uri=redirect_uri, - authorize_url=AUTH_URL, - access_token_url=TOKEN_URL, - base_scope=scope, - refresh_token_url=refresh_token_url, - revoke_token_url=revoke_token_url - ) - - def get_userinfo(self, access_token: str): - response = requests.get(USER_INFO_URL, headers={ - "authorization": f"Bearer {access_token}"}) - return response.json() - - def get_open_id(self, user_json: dict): - return user_json["id"] diff --git a/examples/DogeAPI/fastapi_allauth/oauth/__init__.py b/examples/DogeAPI/fastapi_allauth/oauth/__init__.py deleted file mode 100644 index 5eb92c7..0000000 --- a/examples/DogeAPI/fastapi_allauth/oauth/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .BaseOauth import BaseOauth -from .GithubOauth import GithubOauth diff --git a/examples/DogeAPI/fastapi_allauth/secret_handler.py b/examples/DogeAPI/fastapi_allauth/secret_handler.py deleted file mode 100644 index 8b5efe0..0000000 --- a/examples/DogeAPI/fastapi_allauth/secret_handler.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Union - -from pydantic import SecretStr - -SecretType = Union[str, SecretStr] - - -def _get_secret_value(secret: SecretStr): - if isinstance(secret, SecretStr): - return secret.get_secret_value() - return secret diff --git a/examples/DogeAPI/main.py b/examples/DogeAPI/main.py deleted file mode 100644 index 129213d..0000000 --- a/examples/DogeAPI/main.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/python3 - -from fastapi import FastAPI, Request, Depends -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from starlette.responses import HTMLResponse - -from core import auth, blog, user -from database.configuration import engine, get_db -from fastapi_allauth import AllauthManager -from fastapi_allauth.oauth import GithubOauth -from models import models -from models.models import User - -models.Base.metadata.create_all(bind=engine) - -allauthManager = AllauthManager(db=Depends(get_db), user=User, secret="secret", lifetime_second=3600) -githubOauth = GithubOauth( - client_id="eccd08d6736b7999a32a", - client_secret="642999c1c5f2b3df8b877afdc78252ef5b594d31", - redirect_uri="http://127.0.0.1:8000/github/callback", - scope=["openid", "profile"] -) - -app = FastAPI( - title="DogeAPI", - description="API with high performance built with FastAPI & SQLAlchemy, help to improve connection with your Backend Side.", - version="1.0.0", -) -app.mount("/static", StaticFiles(directory="static"), name="static") -templates = Jinja2Templates(directory="templates") - -app.include_router(auth.router) -app.include_router(blog.router) -app.include_router(user.router) -app.include_router(allauthManager.get_oauth_router(githubOauth), prefix="/github", tags=["github"]) - - -@app.get("/", response_class=HTMLResponse) -async def index(request: Request): - """ - Home page - - Args: - request (Request): Request object - - Returns: - HTMLResponse: HTML response - """ - return templates.TemplateResponse("index.html", {"request": request}) diff --git a/examples/DogeAPI/models/models.py b/examples/DogeAPI/models/models.py deleted file mode 100644 index 16d91eb..0000000 --- a/examples/DogeAPI/models/models.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/python3 -from fastapi_allauth.model import BaseUser -from sqlalchemy import Column, Integer, String - -from database.configuration import Base - - -class Blog(Base): - """ - Blog class - - Args: - Base (sqlalchemy.ext.declarative.api.Base): Base class - """ - - __tablename__ = "blogs" - id = Column(Integer, primary_key=True, index=True) - title = Column(String) - body = Column(String) - - -class User(BaseUser): - """ - User class - - Args: - Base (sqlalchemy.ext.declarative.api.Base): Base class - """ - - name = Column(String) - email = Column(String) - password = Column(String) diff --git a/examples/DogeAPI/requirements.txt b/examples/DogeAPI/requirements.txt deleted file mode 100644 index d970a80..0000000 --- a/examples/DogeAPI/requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -aiofiles -asgiref -bcrypt -cffi -click -colorama -ecdsa -fastapi -greenlet -h11 -Jinja2 -MarkupSafe -passlib -pyasn1 -pycparser -pydantic -python-decouple -python-jose -python-multipart -rsa -six -SQLAlchemy -starlette -typing-extensions -uvicorn -pre-commit \ No newline at end of file diff --git a/examples/DogeAPI/schema/hash.py b/examples/DogeAPI/schema/hash.py deleted file mode 100644 index f7598af..0000000 --- a/examples/DogeAPI/schema/hash.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python3 - -from passlib.context import CryptContext - -pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -class Hash: - @staticmethod - def bcrypt(password: str): - """ - Generate a bcrypt hashed password - - Args: - password (str): The password to hash - - Returns: - str: The hashed password - """ - return pwd_ctx.hash(password) - - def verify(hashed_password, plain_password): - """ - Verify a password against a hash - - Args: - hashed_password (bool): The hashed password - plain_password ([type]): The plain password - - Returns: - bool: True if the password matches, False otherwise - """ - return pwd_ctx.verify(plain_password, hashed_password) diff --git a/examples/DogeAPI/schema/oa2.py b/examples/DogeAPI/schema/oa2.py deleted file mode 100644 index c1fd66d..0000000 --- a/examples/DogeAPI/schema/oa2.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/python3 - -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer - -from schema.token import verify_token - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") - - -def get_current_user(token: str = Depends(oauth2_scheme)): - """ - Get the current user from the token. - - Args: - token (str, optional): The token to verify. - - Returns: - dict: The user. - """ - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "bearer"}, - ) - return verify_token(token, credentials_exception=credentials_exception) diff --git a/examples/DogeAPI/schema/schemas.py b/examples/DogeAPI/schema/schemas.py deleted file mode 100644 index e3295ac..0000000 --- a/examples/DogeAPI/schema/schemas.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python3 - -from typing import List, Optional - -from pydantic import BaseModel - - -class BlogBase(BaseModel): - title: str - body: str - - -class Blog(BlogBase): - class Config: - orm_mode = True - - -class User(BaseModel): - name: str - email: str - password: str - - -class ShowUser(BaseModel): - name: str - email: str - - class Config: - orm_mode = True - - -class ShowBlog(BaseModel): - title: str - body: str - creator: ShowUser - - class Config: - orm_mode = True - - -class Login(BaseModel): - username: str - password: str - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: Optional[str] = None diff --git a/examples/DogeAPI/schema/token.py b/examples/DogeAPI/schema/token.py deleted file mode 100644 index c244dcf..0000000 --- a/examples/DogeAPI/schema/token.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python3 - -from datetime import datetime, timedelta -from typing import Optional - -from decouple import config -from jose import JWTError, jwt - -from schema.schemas import TokenData - -# openssl rand -hex 32 -SECRET_KEY = config("SECRET_KEY", default="secret") -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = config( - "ACCESS_TOKEN_EXPIRE_MINUTES", default=60, cast=int -) - - -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): - """ - Generate JWT token - - Args: - data (dict): payload - expires_delta (Optional[timedelta]): token expiration time - - Returns: - str: JWT token - """ - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - - to_encode["exp"] = expire - return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - - -def verify_token(token: str, credentials_exception): - """ - Verify JWT token - - Args: - token (str): JWT token - credentials_exception (Exception): exception to raise if token is invalid - - Raises: - credentials_exception: if token is invalid - credentials_exception: if token is expired - """ - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=ALGORITHM) - email: str = payload.get("sub") - if email is None: - raise credentials_exception - token_data = TokenData(email=email) - except JWTError: - raise credentials_exception diff --git a/examples/DogeAPI/static/main.js b/examples/DogeAPI/static/main.js deleted file mode 100644 index 9e9d9a2..0000000 --- a/examples/DogeAPI/static/main.js +++ /dev/null @@ -1,46 +0,0 @@ -const faceButton = document.querySelector(".face-button"); -const faceContainer = document.querySelector(".face-container"); -const containerCoords = document.querySelector("#container"); -const mouseCoords = containerCoords.getBoundingClientRect(); - -faceButton.addEventListener("mousemove", function (e) { - const mouseX = e.pageX - containerCoords.offsetLeft; - const mouseY = e.pageY - containerCoords.offsetTop; - - TweenMax.to(faceButton, 0.3, { - x: ((mouseX - mouseCoords.width / 2) / mouseCoords.width) * 50, - y: ((mouseY - mouseCoords.height / 2) / mouseCoords.width) * 50, - ease: Power4.easeOut, - }); -}); - -faceButton.addEventListener("mousemove", function (e) { - const mouseX = e.pageX - containerCoords.offsetLeft; - const mouseY = e.pageY - containerCoords.offsetTop; - - TweenMax.to(faceContainer, 0.3, { - x: ((mouseX - mouseCoords.width / 2) / mouseCoords.width) * 25, - y: ((mouseY - mouseCoords.height / 2) / mouseCoords.width) * 25, - ease: Power4.easeOut, - }); -}); - -faceButton.addEventListener("mouseenter", function (e) { - TweenMax.to(faceButton, 0.3, { - scale: 0.975, - }); -}); - -faceButton.addEventListener("mouseleave", function (e) { - TweenMax.to(faceButton, 0.3, { - x: 0, - y: 0, - scale: 1, - }); - - TweenMax.to(faceContainer, 0.3, { - x: 0, - y: 0, - scale: 1, - }); -}); diff --git a/examples/DogeAPI/static/style.css b/examples/DogeAPI/static/style.css deleted file mode 100644 index 62cfd61..0000000 --- a/examples/DogeAPI/static/style.css +++ /dev/null @@ -1,120 +0,0 @@ -* { - box-sizing: border-box; -} -*::before, -*::after { - box-sizing: border-box; -} - -body { - display: flex; - align-items: center; - justify-content: center; - margin: 0; - min-height: 100vh; - background: #000000; -} - -#container { - display: flex; - align-items: center; - justify-content: center; - width: 6.25rem; - height: 6.25rem; -} - -button { - position: relative; - display: inline-block; - cursor: pointer; - outline: none; - border: 0; - vertical-align: middle; -} -button.face-button { - width: 6.25rem; - height: 6.25rem; - border-radius: 50%; - background: #fdda5f; - box-shadow: inset 2px -4px 18px #fd9744; -} - -.face-container { - position: relative; - display: block; - width: 40px; - height: 20px; - margin: auto; -} - -.eye { - position: absolute; - height: 0.5rem; - width: 0.5rem; - background: #2a2927; - border-radius: 50%; - -webkit-animation: eyeBlink 3200ms linear infinite; - animation: eyeBlink 3200ms linear infinite; -} -.eye.left { - left: 0; -} -.eye.right { - left: 2rem; -} - -.mouth { - position: absolute; - top: 1.125rem; - left: 0.8rem; - width: 1rem; - height: 0.125rem; - background: #2a2927; - border-radius: 0; -} - -.eye, -.mouth { - box-shadow: inset 1px 2px 4px #121110; -} - -.face-button:hover .mouth, -.face-button:active .mouth { - left: 1rem; - width: 0.5rem; - height: 0.4rem; - border-radius: 1rem 1rem 0.125rem 0.125rem; -} - -.face-button:hover .eye, -.face-button:active .eye { - height: 0.375rem; - width: 0.375rem; - box-shadow: 0 0 0 0.25rem #fff; -} - -@-webkit-keyframes eyeBlink { - 0%, - 30%, - 36%, - 100% { - transform: scale(1); - } - 32%, - 34% { - transform: scale(1, 0); - } -} - -@keyframes eyeBlink { - 0%, - 30%, - 36%, - 100% { - transform: scale(1); - } - 32%, - 34% { - transform: scale(1, 0); - } -} diff --git a/examples/DogeAPI/templates/index.html b/examples/DogeAPI/templates/index.html deleted file mode 100644 index a261c36..0000000 --- a/examples/DogeAPI/templates/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - -
- - - - - -This is what your JWT contains currently
+{{ json.dumps(request.user, indent=4) }}+ {% else %} +
You should sign in by clicking the GitHub's icon
+ {% endif %} +