diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..9c02b32
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,18 @@
+name: docs
+
+on:
+ push:
+ branches: [ master ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Run deployment script on server
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USERNAME }}
+ key: ${{ secrets.KEY_ED25519 }}
+ port: ${{ secrets.PORT }}
+ script: sh ~/fastapi-oauth2/docs/deploy.sh
diff --git a/README.md b/README.md
index a36c6e2..a0a3575 100644
--- a/README.md
+++ b/README.md
@@ -4,74 +4,17 @@
[](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)
+[](https://github.com/pysnippet/fastapi-oauth2/actions/workflows/docs.yml)
-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.
-
-## Installation
-
-```shell
-python -m pip install fastapi-oauth2
-```
-
-## Configuration
-
-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.
-
-### OAuth2Config
-
-- `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 `[]`.
-- `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
-
-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"],
- claims=Claims(
- picture="avatar_url",
- identity=lambda user: "%s:%s" % (user.get("provider"), user.get("id")),
- ),
- ),
- ]
-)
-```
+FastAPI OAuth2 is a middleware-based social authentication mechanism supporting several OAuth2 providers. It leverages
+the [social-core](https://github.com/python-social-auth/social-core) authentication backends and integrates seamlessly
+with FastAPI applications.
## 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.
+For integrating the package into an existing FastAPI application, the router with OAuth2 routes and
+the `OAuth2Middleware` with particular [configs](https://docs.pysnippet.org/fastapi-oauth2/integration/configuration)
+should be added to the application.
```python
from fastapi import FastAPI
@@ -80,24 +23,14 @@ 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 %}
+app.add_middleware(OAuth2Middleware, config=OAuth2Config(...))
```
## 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.
+Any contribution is welcome. Always feel free to open an issue or a discussion if you have any questions not covered by
+the documentation. If you have any ideas or suggestions, please, open a pull request. Your name will shine in our
+contributors' list. Be proud of what you build!
## License
diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
new file mode 100644
index 0000000..4555c23
--- /dev/null
+++ b/docs/.vitepress/config.js
@@ -0,0 +1,45 @@
+export default {
+ title: "FastAPI OAuth2",
+ description: "OAuth2 authentication with support for several identity providers",
+ head: [
+ ["link", {rel: "icon", type: "image/x-icon", href: "/logo.png"}],
+ ["link", {href: "/index.css", rel: "stylesheet"}],
+ ],
+ cleanUrls: true,
+ lang: "en-US",
+ base: "/fastapi-oauth2/",
+ themeConfig: {
+ siteTitle: "FastAPI OAuth2",
+ socialLinks: [
+ {
+ icon: "github",
+ link: "https://github.com/pysnippet/fastapi-oauth2",
+ },
+ {
+ icon: {
+ svg: ""
+ },
+ link: "https://pysnippet.org/",
+ },
+ ],
+ search: {
+ provider: "local",
+ },
+ nav: [
+ {text: "Home", link: "/"},
+ {text: "Docs", link: "/integration/", activeMatch: /integration/},
+ {text: "Contributing", link: "https://github.com/pysnippet/.github/blob/master/.github/CONTRIBUTING.md"},
+ {text: "Releases", link: "https://github.com/pysnippet/fastapi-oauth2/releases"},
+ ],
+ sidebar: [
+ {
+ text: "Integration",
+ items: [
+ {text: "Getting Started", link: "/integration/"},
+ {text: "Configuration", link: "/integration/configuration"},
+ {text: "Integration", link: "/integration/integration"},
+ ],
+ },
+ ],
+ },
+}
\ No newline at end of file
diff --git a/docs/deploy.sh b/docs/deploy.sh
new file mode 100644
index 0000000..a5bb8d6
--- /dev/null
+++ b/docs/deploy.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+cd ~/fastapi-oauth2/
+git restore .
+git pull
+sudo rm -r /var/www/docs/fastapi-oauth2/
+cd ~/fastapi-oauth2/docs/ && npm install && npm run build
+sudo cp -r ~/fastapi-oauth2/docs/.vitepress/dist/ /var/www/docs/fastapi-oauth2/
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..c5496f4
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,33 @@
+---
+layout: home
+sidebar: false
+
+title: FastAPI OAuth2
+titleTemplate: OAuth2 authentication with support for several identity providers
+
+hero:
+ name: FastAPI OAuth2
+ text: OAuth2 has never been that simple
+ tagline: Easy to integrate OAuth2 authentication with support for several identity providers.
+ image:
+ src: /logo.png
+ alt: PySnippet
+ actions:
+ - theme: brand
+ text: Get Started
+ link: /integration/
+ - theme: alt
+ text: View on GitHub
+ link: https://github.com/pysnippet/fastapi-oauth2
+
+features:
+ - icon: 🛠️
+ title: Free and open source
+ details: Enjoy the freedom of our OSS project, giving you full access to its source code and allowing you to contribute to its development.
+ - icon: 🧩
+ title: Easy to integrate
+ details: Incorporate FastAPI OAuth2 into your existing projects with its straightforward integration process, saving you time.
+ - icon: ⚡
+ title: Compatible with FastAPI 0.68.1+
+ details: The package is fully compatible with FastAPI v0.68.1 and above, ensuring smooth operation and integration with your application.
+---
\ No newline at end of file
diff --git a/docs/integration/configuration.md b/docs/integration/configuration.md
new file mode 100644
index 0000000..d0d9990
--- /dev/null
+++ b/docs/integration/configuration.md
@@ -0,0 +1,66 @@
+# Configuration
+
+The configuration for the OAuth2 clients can be provided by using the [`OAuth2Config`](#oauth2config)
+and [`OAuth2Client`](#oauth2client) classes. There is an alternate way to define the configuration by using the
+Python's `dict` type with the same structure as these two classes.
+
+## OAuth2Config
+
+The `OAuth2Config` class is used to define the middleware configuration, and it has the following attributes:
+
+- `allow_http` - Whether allow HTTP requests or not. Defaults to `False`.
+- `jwt_secret` - Secret used to sign the JWT tokens. Defaults to an empty string.
+- `jwt_expires` - JWT lifetime in seconds. Defaults to 900 (15 minutes).
+- `jwt_algorithm` - The algorithm used to sign the JWT tokens. Defaults to `HS256`.
+- `clients` - A list of [`OAuth2Client`](#oauth2client) instances. Defaults to an empty list.
+
+```python
+OAuth2Config(
+ allow_http=True,
+ jwt_secret=os.getenv("JWT_SECRET"),
+ jwt_expires=os.getenv("JWT_EXPIRES"),
+ jwt_algorithm=os.getenv("JWT_ALGORITHM"),
+ clients=[
+ OAuth2Client(...),
+ OAuth2Client(...),
+ ]
+)
+```
+
+## OAuth2Client
+
+The `OAuth2Client` class is used to define the configuration for a given OAuth2 client, and it has the following
+attributes:
+
+- `backend` - A backend class from the `social_core.backends` package.
+- `client_id` - A string value of the generated client ID.
+- `client_secret` - A string value of the generated client secret.
+- `redirect_uri` - URL to redirect to after the authentication. Defaults to the base URL.
+- `scope` - A list of the desired scopes. Defaults to an empty list.
+- `claims` - An instance of [`Claims`](#claims) with the claim mapping definitions.
+
+```python
+OAuth2Client(
+ backend=GithubOAuth2,
+ client_id=os.getenv("OAUTH2_GITHUB_CLIENT_ID"),
+ client_secret=os.getenv("OAUTH2_GITHUB_CLIENT_SECRET"),
+ redirect_uri="https://example.com/dashboard",
+ scope=["user:email"],
+ claims=Claims(...),
+)
+```
+
+## Claims
+
+The `Claims` class is used to define the claim mapping for a given OAuth2 client, and it has `display_name`, `identity`,
+`picture`, and `email` permanent attributes. It also accepts custom attributes if your case is special. Each attribute
+can have a value of a string or a callable that receives the user data and returns a string.
+
+```python
+Claims(
+ # Map the `picture` claim to the `avatar_url` key in the user data.
+ picture="avatar_url",
+ # Calculate the `identity` claim based on the user data.
+ identity=lambda user: f"{user.provider}:{user.id}",
+)
+```
diff --git a/docs/integration/index.md b/docs/integration/index.md
new file mode 100644
index 0000000..07df73a
--- /dev/null
+++ b/docs/integration/index.md
@@ -0,0 +1,34 @@
+# FastAPI OAuth2
+
+FastAPI OAuth2 is a middleware-based social authentication mechanism supporting several OAuth2 providers. It leverages
+the [social-core](https://github.com/python-social-auth/social-core) authentication backends and integrates seamlessly
+with FastAPI applications.
+
+## Installation
+
+This package is compatible with Python 3.7+ and FastAPI 0.68.1+ versions and is available on The Python Package Index.
+So you can install it using the package installer for Python.
+
+```bash
+python -m pip install fastapi-oauth2
+```
+
+## Upgrade
+
+Make sure you are using the latest version of FastAPI OAuth2 for getting the latest features and the best performance.
+You can check the latest releases on its [PyPI page](https://pypi.org/project/fastapi-oauth2/).
+
+```bash
+python -m pip install --upgrade fastapi-oauth2
+```
+
+## Dependencies
+
+FastAPI OAuth2 depends on the following packages.
+
+- [fastapi](https://github.com/tiangolo/fastapi)
+- [httpx](https://github.com/encode/httpx)
+- [oauthlib](https://github.com/oauthlib/oauthlib)
+- [python-jose](https://github.com/mpdavis/python-jose)
+- [social-auth-core](https://github.com/python-social-auth/social-core)
+- [starlette](https://github.com/encode/starlette)
diff --git a/docs/integration/integration.md b/docs/integration/integration.md
new file mode 100644
index 0000000..46d0dea
--- /dev/null
+++ b/docs/integration/integration.md
@@ -0,0 +1,76 @@
+---
+outline: deep
+---
+
+# Integration
+
+In the previous section, were described the configuration components of the OAuth2 authentication middleware and this
+section covers its integration into a FastAPI app.
+
+## OAuth2Middleware
+
+The `OAuth2Middleware` is an authentication middleware which means that its usage makes the `user` and `auth` attributes
+available in the [request](https://www.starlette.io/requests/) context. It has a mandatory argument `config` of
+[`OAuth2Config`](/integration/configuration#oauth2config) instance that has been discussed at the previous section and
+an optional argument `callback` which is a callable that is called when the authentication succeeds.
+
+```python
+app: FastAPI
+
+def on_auth_success(auth: Auth, user: User):
+ """This could be async function as well."""
+
+app.add_middleware(
+ OAuth2Middleware,
+ config=OAuth2Config(...),
+ callback=on_auth_success,
+)
+```
+
+### Auth context
+
+This is extended version of Starlette's [`AuthCredentials`](https://www.starlette.io/authentication/#authcredentials)
+and the difference is that the `Auth` has additionally the list of the `clients` that can be used in the Jinja templates
+to display them dynamically, and the `provider` is an item of the `clients` that was used to authenticate the current
+user. Also, there are some methods for managing the JWT tokens: `jwt_encode`, `jwt_decode`, and `jwt_create`.
+
+### User context
+
+This is the extended version of Starlette's [`BaseUser`](https://www.starlette.io/authentication/#users) and apart from
+the default `is_authenticated` and `display_name` and the extended `identity`, `picture`, and `email` properties, it
+also contains all attributes of the user received from a certain provider.
+
+### Callback
+
+The `callback` is called with the [`Auth`](#auth-context) and [`User`](#user-context) arguments when the authentication
+succeeds. This can be used for migrating an external user into the system of the existing application. Apart from other
+OAuth2 solutions that force using their base user models, certain architectural designs, or a database from a limited
+set of choices, this kind of solution gives developers freedom.
+
+## Router
+
+Router defines the endpoints that are used for the authentication and logout. The authentication is done by
+the `/oauth2/{provider}/auth` endpoint and the logout is done by the `/oauth2/logout` endpoint. The `{provider}` is the
+name of the provider that is going to be used for the authentication and coincides with the `name` attribute of
+the `backend` provided to the certain `OAuth2Client`.
+
+```python
+from fastapi_oauth2.router import router as oauth2_router
+
+app.include_router(oauth2_router)
+```
+
+## Security
+
+FastAPI's `OAuth2`, `OAuth2PasswordBearer` and `OAuth2AuthorizationCodeBearer` security models are supported, but in
+case your application uses cookies for storing the authentication tokens, you can use the same named security models
+from the `fastapi_oauth2.security` module.
+
+## Examples
+
+Working examples of all the above-described topics can be found in
+the [examples](https://github.com/pysnippet/fastapi-oauth2/tree/master/examples) and
+the [tests](https://github.com/pysnippet/fastapi-oauth2/tree/master/tests) directories of the repository. Also, feel
+free to open an [issue](https://github.com/pysnippet/fastapi-oauth2/issues/new/choose) or
+a [discussion](https://github.com/pysnippet/fastapi-oauth2/discussions/new/choose) if you have any questions not covered
+by the documentation.
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 0000000..e462a7f
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "vitepress-docs",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "vitepress dev",
+ "build": "vitepress build",
+ "serve": "vitepress serve"
+ },
+ "dependencies": {
+ "vitepress": "^1.0.0-rc.4",
+ "vue": "^3.3.2"
+ }
+}
\ No newline at end of file
diff --git a/docs/public/index.css b/docs/public/index.css
new file mode 100644
index 0000000..6ed8f70
--- /dev/null
+++ b/docs/public/index.css
@@ -0,0 +1,11 @@
+:root {
+ --vp-c-brand: rgb(0, 148, 134);
+ --vp-c-brand-light: var(--vp-c-brand);
+ --vp-c-brand-lighter: var(--vp-c-brand);
+ --vp-c-brand-lightest: var(--vp-c-brand);
+ --vp-c-brand-dark: var(--vp-c-brand);
+ --vp-c-brand-darker: rgba(0, 148, 134, 0.8);
+
+ --vp-button-brand-bg: var(--vp-c-brand);
+ --vp-button-brand-active-bg: var(--vp-button-brand-bg);
+}
diff --git a/docs/public/logo.png b/docs/public/logo.png
new file mode 100644
index 0000000..ae24249
Binary files /dev/null and b/docs/public/logo.png differ
diff --git a/examples/demonstration/config.py b/examples/demonstration/config.py
index be64b0f..9877102 100644
--- a/examples/demonstration/config.py
+++ b/examples/demonstration/config.py
@@ -23,7 +23,7 @@
scope=["user:email"],
claims=Claims(
picture="avatar_url",
- identity=lambda user: "%s:%s" % (user.get("provider"), user.get("id")),
+ identity=lambda user: f"{user.provider}:{user.id}",
),
),
OAuth2Client(
@@ -32,7 +32,7 @@
client_secret=os.getenv("OAUTH2_GOOGLE_CLIENT_SECRET"),
scope=["openid", "profile", "email"],
claims=Claims(
- identity=lambda user: "%s:%s" % (user.get("provider"), user.get("sub")),
+ identity=lambda user: f"{user.provider}:{user.sub}",
),
),
]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index a388b0a..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-fastapi>=0.68.1
-httpx>=0.23.0
-oauthlib>=3.2.2
-python-jose>=3.3.0
-social-auth-core>=4.4.2
-starlette>=0.19.1
diff --git a/setup.cfg b/setup.cfg
index 81b6a1d..f8a7875 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,11 +3,11 @@ name = fastapi-oauth2
version = attr: fastapi_oauth2.__version__
author = Artyom Vancyan
author_email = artyom@pysnippet.org
-description = Easy to setup OAuth2 authentication with support for several auth providers.
+description = Easy to integrate OAuth2 authentication with support for several identity providers.
long_description = file: README.md
long_description_content_type = text/markdown
project_urls =
- Documentation=https://github.com/pysnippet/fastapi-oauth2/
+ Documentation=https://docs.pysnippet.org/fastapi-oauth2/
Source Code=https://github.com/pysnippet/fastapi-oauth2/
keywords =
python
@@ -27,7 +27,7 @@ license_files = LICENSE
platforms = unix, linux, osx, win32
classifiers =
Operating System :: OS Independent
- Development Status :: 3 - Alpha
+ Development Status :: 4 - Beta
Framework :: FastAPI
Programming Language :: Python
Programming Language :: Python :: 3
diff --git a/src/fastapi_oauth2/__init__.py b/src/fastapi_oauth2/__init__.py
index 5186ae4..b63a7ea 100644
--- a/src/fastapi_oauth2/__init__.py
+++ b/src/fastapi_oauth2/__init__.py
@@ -1 +1 @@
-__version__ = "1.0.0-alpha.2"
+__version__ = "1.0.0-beta"
diff --git a/src/fastapi_oauth2/middleware.py b/src/fastapi_oauth2/middleware.py
index 5dd5eb1..58dc564 100644
--- a/src/fastapi_oauth2/middleware.py
+++ b/src/fastapi_oauth2/middleware.py
@@ -115,6 +115,8 @@ def __getprop__(self, item, default="") -> Any:
return item(self)
return self.get(item, default)
+ __getattr__ = __getprop__
+
class OAuth2Backend(AuthenticationBackend):
"""Authentication backend for AuthenticationMiddleware."""
diff --git a/tests/conftest.py b/tests/conftest.py
index b96e6c5..ac6f86b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -54,9 +54,13 @@ def user(request: Request, _: str = Depends(oauth2)):
@app_router.get("/auth")
def auth(request: Request):
access_token = request.auth.jwt_create({
- "name": "test",
- "sub": "test",
- "id": "test",
+ "id": 54321,
+ "followers": 80,
+ "sub": "1234567890",
+ "name": "John Doe",
+ "provider": "github",
+ "emails": ["john.doe@test.py"],
+ "image": "https://example.com/john.doe.png",
})
response = Response()
response.set_cookie(
diff --git a/tests/test_claims.py b/tests/test_claims.py
new file mode 100644
index 0000000..f607d22
--- /dev/null
+++ b/tests/test_claims.py
@@ -0,0 +1,52 @@
+import pytest
+from fastapi import APIRouter
+from fastapi import Request
+from httpx import AsyncClient
+
+from fastapi_oauth2.claims import Claims
+
+
+@pytest.mark.anyio
+async def test_permanent_claims_mapping(get_app):
+ app = get_app()
+ router = APIRouter()
+
+ @router.get("/test_claims")
+ def test_claims(request: Request):
+ user = request.user.use_claims(Claims()) # use default claims mapping
+ assert user.display_name == "John Doe"
+ assert user.identity == "1234567890"
+ assert user.picture == ""
+ assert user.email == ""
+
+ app.include_router(router)
+
+ async with AsyncClient(app=app, base_url="http://test") as client:
+ await client.get("/auth") # Simulate login
+ await client.get("/test_claims")
+
+
+@pytest.mark.anyio
+async def test_custom_claims_mapping(get_app):
+ app = get_app()
+ router = APIRouter()
+
+ @router.get("/test_claims")
+ def test_claims(request: Request):
+ user = request.user.use_claims(Claims(
+ picture="image",
+ email=lambda u: u.emails[0],
+ identity=lambda u: f"{u.provider}:{u.sub}",
+ is_popular=lambda u: u.followers > 100,
+ )) # use custom claims mapping
+ assert user.display_name == "John Doe"
+ assert user.identity == "github:1234567890"
+ assert user.picture == "https://example.com/john.doe.png"
+ assert user.email == "john.doe@test.py"
+ assert not user.is_popular
+
+ app.include_router(router)
+
+ async with AsyncClient(app=app, base_url="http://test") as client:
+ await client.get("/auth") # Simulate login
+ await client.get("/test_claims")