diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..435632c
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,28 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [ published ]
+
+jobs:
+ deploy:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v3
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install setuptools wheel twine
+ - name: Build and publish
+ env:
+ TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+ run: |
+ python setup.py sdist bdist_wheel
+ twine upload dist/*
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..817762a
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,55 @@
+name: tests
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os || 'ubuntu-latest' }}
+ strategy:
+ matrix:
+ include:
+ - python: "3.6"
+ env: py36-fastapi68
+ os: ubuntu-20.04 # 3.6 is not available on ubuntu-20.04
+ - python: "3.8"
+ env: py38-fastapi68
+ - python: "3.10"
+ env: py310-fastapi68
+ - python: "3.11"
+ env: py311-fastapi68
+
+ - python: "3.7"
+ env: py37-fastapi84
+ - python: "3.9"
+ env: py39-fastapi84
+ - python: "3.10"
+ env: py310-fastapi84
+ - python: "3.11"
+ env: py311-fastapi84
+
+ - python: "3.7"
+ env: py37-fastapi99
+ - python: "3.9"
+ env: py39-fastapi99
+ - python: "3.10"
+ env: py310-fastapi99
+ - python: "3.11"
+ env: py311-fastapi99
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install dependencies
+ run: |
+ pip install --upgrade pip
+ sh build.sh
+ pip install tox tox-gh-actions
+ - name: Run tests using tox
+ run: tox -e ${{ matrix.env }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c008936
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+# caches
+.idea
+.tox
+.pytest_cache
+*.egg-info
+__pycache__
+
+# docs
+docs/node_modules
+docs/package-lock.json
+docs/.vitepress/cache
+docs/.vitepress/dist
+
+# build
+build
+dist
\ No newline at end of file
diff --git a/README.md b/README.md
index 2d06d4e..f2a7e53 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,107 @@
-# fastapi-oauth2
+# fastapi-oauth2
-Easy to setup OAuth2 social authentication mechanism with support for several auth providers.
+[](https://pypi.org/project/fastapi-oauth2/)
+[](https://pypi.org/project/fastapi-oauth2/)
+[](https://pypi.org/project/fastapi-oauth2/)
+[](https://github.com/pysnippet/fastapi-oauth2/actions/workflows/tests.yml)
+[](https://github.com/pysnippet/fastapi-oauth2/blob/master/LICENSE)
-## Demo
+FastAPI OAuth2 is a middleware-based social authentication mechanism supporting several auth providers. It depends on
+the [social-core](https://github.com/python-social-auth/social-core) authentication backends.
-This sample application is made to demonstrate the use of the [**fastapi-oauth2**](./fastapi_oauth2) package.
+## Features to be implemented
-## Running the application
+- Use multiple OAuth2 providers at the same time
+ * There need to be provided a way to configure the OAuth2 for multiple providers
+- Provide `fastapi.security.*` implementations that use cookies
+- Token -> user data, user data -> token easy conversion
+- Customizable OAuth2 routes
+- Registration support
+
+## Installation
-```bash
-uvicorn main:app --reload
+```shell
+python -m pip install fastapi-oauth2
```
-## TODO
+## Configuration
-- Make the [**fastapi-oauth2**](./fastapi_oauth2) depend
- on (overuse) the [**social-core**](https://github.com/python-social-auth/social-core)
+Configuration requires you to provide the JWT requisites and define the clients of the particular providers. The
+middleware configuration is declared with the `OAuth2Config` and `OAuth2Client` classes.
-## Features
+### OAuth2Config
-- 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
+- `allow_http` - Allow insecure HTTP requests. Defaults to `False`.
+- `jwt_secret` - The secret key used to sign the JWT. Defaults to `None`.
+- `jwt_expires` - The expiration time of the JWT in seconds. Defaults to `900`.
+- `jwt_algorithm` - The algorithm used to sign the JWT. Defaults to `HS256`.
+- `clients` - The list of the OAuth2 clients. Defaults to `[]`.
+
+### OAuth2Client
+
+- `backend` - The [social-core](https://github.com/python-social-auth/social-core) authentication backend classname.
+- `client_id` - The OAuth2 client ID for the particular provider.
+- `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 `[]`.
+
+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.client import OAuth2Client
+from fastapi_oauth2.config import OAuth2Config
+from social_core.backends.github import GithubOAuth2
+
+oauth2_config = OAuth2Config(
+ allow_http=False,
+ jwt_secret=os.getenv("JWT_SECRET"),
+ jwt_expires=os.getenv("JWT_EXPIRES"),
+ jwt_algorithm=os.getenv("JWT_ALGORITHM"),
+ clients=[
+ OAuth2Client(
+ backend=GithubOAuth2,
+ client_id=os.getenv("OAUTH2_CLIENT_ID"),
+ client_secret=os.getenv("OAUTH2_CLIENT_SECRET"),
+ redirect_uri="https://pysnippet.org/",
+ scope=["user:email"],
+ ),
+ ]
+)
+```
+
+## Integration
+
+To integrate the package into your FastAPI application, you need to add the `OAuth2Middleware` with particular configs
+in the above-represented format and include the router to the main router of the application.
+
+```python
+from fastapi import FastAPI
+from fastapi_oauth2.middleware import OAuth2Middleware
+from fastapi_oauth2.router import router as oauth2_router
+
+app = FastAPI()
+app.include_router(oauth2_router)
+app.add_middleware(OAuth2Middleware, config=oauth2_config)
+```
+
+After adding the middleware, the `user` attribute will be available in the request context. It will contain the user
+data provided by the OAuth2 provider.
+
+```jinja2
+{% if request.user.is_authenticated %}
+ Sign out
+{% else %}
+ Sign in
+{% endif %}
+```
+
+## Contribute
+
+Any contribution is welcome. If you have any ideas or suggestions, feel free to open an issue or a pull request. And
+don't forget to add tests for your changes.
+
+## License
+
+Copyright (C) 2023 Artyom Vancyan. [MIT](https://github.com/pysnippet/fastapi-oauth2/blob/master/LICENSE)
diff --git a/demo/dependencies.py b/demo/dependencies.py
deleted file mode 100644
index 52594b6..0000000
--- a/demo/dependencies.py
+++ /dev/null
@@ -1,39 +0,0 @@
-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
deleted file mode 100644
index 822dab1..0000000
--- a/demo/router.py
+++ /dev/null
@@ -1,17 +0,0 @@
-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/.env b/examples/demonstration/.env
similarity index 73%
rename from .env
rename to examples/demonstration/.env
index a1f6457..a1c0106 100644
--- a/.env
+++ b/examples/demonstration/.env
@@ -1,6 +1,5 @@
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
diff --git a/examples/demonstration/README.md b/examples/demonstration/README.md
new file mode 100644
index 0000000..6c75893
--- /dev/null
+++ b/examples/demonstration/README.md
@@ -0,0 +1,27 @@
+## Demonstration
+
+This sample application is made to demonstrate the use of
+the [**fastapi-oauth2**](https://github.com/pysnippet/fastapi-oauth2) package.
+
+## Installation
+
+You got to have `fastapi-oauth2` installed in your environment. To do so, run the following command in
+the `pyproject.toml` file's directory.
+
+### Regular install
+
+```bash
+pip install fastapi-oauth2
+```
+
+### Install in editable mode
+
+```bash
+pip install -e .
+```
+
+## Running the application
+
+```bash
+uvicorn main:app --reload
+```
diff --git a/examples/demonstration/config.py b/examples/demonstration/config.py
new file mode 100644
index 0000000..c63b136
--- /dev/null
+++ b/examples/demonstration/config.py
@@ -0,0 +1,25 @@
+import os
+
+from dotenv import load_dotenv
+from social_core.backends.github import GithubOAuth2
+
+from fastapi_oauth2.client import OAuth2Client
+from fastapi_oauth2.config import OAuth2Config
+
+load_dotenv()
+
+oauth2_config = OAuth2Config(
+ allow_http=True,
+ jwt_secret=os.getenv("JWT_SECRET"),
+ jwt_expires=os.getenv("JWT_EXPIRES"),
+ jwt_algorithm=os.getenv("JWT_ALGORITHM"),
+ clients=[
+ OAuth2Client(
+ backend=GithubOAuth2,
+ client_id=os.getenv("OAUTH2_CLIENT_ID"),
+ client_secret=os.getenv("OAUTH2_CLIENT_SECRET"),
+ # redirect_uri="http://127.0.0.1:8000/",
+ scope=["user:email"],
+ ),
+ ]
+)
diff --git a/examples/demonstration/main.py b/examples/demonstration/main.py
new file mode 100644
index 0000000..1fd1291
--- /dev/null
+++ b/examples/demonstration/main.py
@@ -0,0 +1,14 @@
+from fastapi import APIRouter
+from fastapi import FastAPI
+
+from config import oauth2_config
+from fastapi_oauth2.middleware import OAuth2Middleware
+from fastapi_oauth2.router import router as oauth2_router
+from router import router as app_router
+
+router = APIRouter()
+
+app = FastAPI()
+app.include_router(app_router)
+app.include_router(oauth2_router)
+app.add_middleware(OAuth2Middleware, config=oauth2_config)
diff --git a/examples/demonstration/router.py b/examples/demonstration/router.py
new file mode 100644
index 0000000..d592744
--- /dev/null
+++ b/examples/demonstration/router.py
@@ -0,0 +1,21 @@
+import json
+
+from fastapi import Depends
+from fastapi import Request, APIRouter
+from fastapi.responses import HTMLResponse
+from fastapi.security import OAuth2
+from fastapi.templating import Jinja2Templates
+
+oauth2 = OAuth2()
+router = APIRouter()
+templates = Jinja2Templates(directory="templates")
+
+
+@router.get("/", response_class=HTMLResponse)
+async def root(request: Request):
+ return templates.TemplateResponse("index.html", {"request": request, "user": request.user, "json": json})
+
+
+@router.get("/user")
+def user(request: Request, _: str = Depends(oauth2)):
+ return request.user
diff --git a/examples/demonstration/templates/index.html b/examples/demonstration/templates/index.html
new file mode 100644
index 0000000..c42adf1
--- /dev/null
+++ b/examples/demonstration/templates/index.html
@@ -0,0 +1,37 @@
+
+
+
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 %} +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 %} -