Skip to content

Commit

Permalink
Implement PKCE support and API compatibility (GH-21)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtyomVancyan authored Sep 8, 2023
2 parents 1eb98e0 + 7f8643e commit f3ae78f
Show file tree
Hide file tree
Showing 25 changed files with 425 additions and 227 deletions.
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ hero:
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.
details: Dive into our OSS initiative, which not only grants complete access to the source code but also welcomes your contributions.
- 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.
- icon: ⚙️
title: Configurable Workflows
details: Customize authentication processes to align perfectly with your application's specific needs, ensuring flexibility and precision.
---
1 change: 1 addition & 0 deletions docs/integration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Python's `dict` type with the same structure as these two classes.

The `OAuth2Config` class is used to define the middleware configuration, and it has the following attributes:

- `enable_ssr` - Whether enable server-side rendering or not. Defaults to `True`.
- `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).
Expand Down
4 changes: 2 additions & 2 deletions docs/integration/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ 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 `/oauth2/{provider}/authorize` 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
Expand Down
22 changes: 9 additions & 13 deletions docs/references/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,24 @@ the [documentation](https://python-social-auth.readthedocs.io/en/latest/backends

## SSR & REST APIs

::: tip Ticket #19

This upcoming feature is under development and will be available in the next release. You can track the progress in
the [#19](https://github.com/pysnippet/fastapi-oauth2/issues/19) issue.

:::
It is compatible with both SSR and REST APIs. It means you can integrate it into your FastAPI templates and REST APIs.
By default, the `enable_ssr` parameter of the primary [configuration](/integration/configuration#oauth2config) is set
to `True`, which means that the application uses server-side rendering using Jinja2 templates and saves the access token
in the cookies. If you want to use it in your REST APIs, you should set the `enable_ssr` parameter to `False` and save
the access token on the client side.

## CSRF protection

CSRF protection is enabled by default which means when the user opens the `/oauth2/{provider}/auth` endpoint it
CSRF protection is enabled by default which means when the user opens the `/oauth2/{provider}/authorize` endpoint it
redirects to the authorization endpoint of the IDP with an autogenerated `state` parameter and saves it in the session
storage. After authorization, when the `/oauth2/{provider}/token` callback endpoint gets called with the
provided `state`, the `oauthlib` validates it and then redirects to the `redirect_uri`.

## PKCE support

::: tip Ticket #18

PKCE support is under development and will be available in the next release. You can track the progress in
the [#18](https://github.com/pysnippet/fastapi-oauth2/issues/18) issue.

:::
PKCE can be enabled by providing the `code_challenge` and `code_challenge_method` parameters to
the `/oauth2/{provider}/authorize` endpoint. Then, after the authorization passes, the `code_verifier` should be
provided to the `/oauth2/{provider}/token` endpoint to complete the authentication process.

<style>
.tip {
Expand Down
22 changes: 9 additions & 13 deletions docs/references/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ generated the client ID and secret to configure your `OAuth2Middleware` with at
3. Set the `redirect_uri` of your application that you have also configured in the IDP.
4. Add the middleware and include the router to your application as shown in the [integration](/integration/integration)
section.
5. Open the `/oauth2/{provider}/auth` endpoint on your browser and test the authentication flow. Check out
5. Open the `/oauth2/{provider}/authorize` endpoint on your browser and test the authentication flow. Check out
the [router](/integration/integration#router) for the `{provider}` variable.

Once the authentication is successful, the user will be redirected to the `redirect_uri` and the `request.user` will
Expand Down Expand Up @@ -90,8 +90,6 @@ Claims(
)
```

::: info NOTE

Not all IDPs provide the `first_name` and the `last_name` attributes already joined as in the example above, or
the email in a list. So you are given the flexibility using transformer function to map the attributes as you want.

Expand All @@ -104,14 +102,20 @@ flowchart LR
Transform --> IDPUserData
```

:::

## User provisioning

User provisioning refers to the process of creating, updating, and deleting user accounts within the OAuth2 IDP and
synchronizing that information with your FastAPI application's database. There are two approaches to user provisioning
and both require the user claims to be mapped properly for creating a new user or updating an existing one.

::: info NOTE

In both scenarios, it is recommended to use the `identity` attribute for uniquely identifying the user from the
database. So if the application uses or plans to use multiple IDPs, make sure to include the `provider` attribute when
calculating the `identity` attribute.

:::

### Automatic provisioning

After successful authentication, you can automatically create a user in your application's database using the
Expand All @@ -125,14 +129,6 @@ approach is useful when there missing mandatory attributes in `request.user` for
database. You need to define a route for provisioning and provide it as `redirect_uri`, so
the [user context](/integration/integration#user-context) will be available for usage.

::: info NOTE

In both scenarios, it is recommended to use the `identity` attribute for uniquely identifying the user from the
database. So if the application uses or plans to use multiple IDPs, make sure to include the `provider` attribute when
calculating the `identity` attribute.

:::

<style>
.info, .details {
border: 0;
Expand Down
6 changes: 4 additions & 2 deletions examples/demonstration/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
## Demonstration

This sample application is made to demonstrate the use of
the [**fastapi-oauth2**](https://github.com/pysnippet/fastapi-oauth2) package.
This sample application demonstrates the use of the [**fastapi-oauth2**](https://github.com/pysnippet/fastapi-oauth2)
package and covers many topics from the [documentation](https://docs.pysnippet.org/fastapi-oauth2/). It is mainly
designed to help developers integrate and configure the package in their own applications. However, it can also be used
as a template for a new application or testing purposes.

## Installation

Expand Down
6 changes: 4 additions & 2 deletions examples/demonstration/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
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
from router_api import router_api
from router_ssr import router_ssr

Base.metadata.create_all(bind=engine)

Expand All @@ -36,7 +37,8 @@ async def on_auth(auth: Auth, user: User):


app = FastAPI()
app.include_router(app_router)
app.include_router(router_api)
app.include_router(router_ssr)
app.include_router(oauth2_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
app.add_middleware(OAuth2Middleware, config=oauth2_config, callback=on_auth)
60 changes: 0 additions & 60 deletions examples/demonstration/router.py

This file was deleted.

32 changes: 32 additions & 0 deletions examples/demonstration/router_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from fastapi import APIRouter
from fastapi import Request
from fastapi.templating import Jinja2Templates
from starlette.responses import RedirectResponse

from fastapi_oauth2.security import OAuth2

oauth2 = OAuth2()
router_api = APIRouter()
templates = Jinja2Templates(directory="templates")


@router_api.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": "[email protected]",
"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
35 changes: 35 additions & 0 deletions examples/demonstration/router_ssr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json

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

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

oauth2 = OAuth2()
router_ssr = APIRouter()
templates = Jinja2Templates(directory="templates")


@router_ssr.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse("index.html", {
"json": json,
"request": request,
})


@router_ssr.get("/users", response_class=HTMLResponse)
async def users(request: Request, db: Session = Depends(get_db), _: str = Depends(oauth2)):
return templates.TemplateResponse("users.html", {
"json": json,
"request": request,
"users": [
dict([(k, v) for k, v in user.__dict__.items() if not k.startswith("_")]) for user in db.query(User).all()
],
})
55 changes: 55 additions & 0 deletions examples/demonstration/templates/base.html

Large diffs are not rendered by default.

62 changes: 7 additions & 55 deletions examples/demonstration/templates/index.html

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions examples/demonstration/templates/users.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "base.html" %}

{% block content %}
<p>This is the current list of all users. See <a style="color: #009486;" href="/">JWT</a> content.</p>
<div style="padding: 8px 16px; background: #161618; border-radius: 6px;">
<pre style="max-width: 500px; white-space: pre-wrap;">{{ json.dumps(users, indent=4) }}</pre>
</div>
{% endblock %}
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-beta"
__version__ = "1.0.0-beta.2"
3 changes: 3 additions & 0 deletions src/fastapi_oauth2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class OAuth2Config:
"""Configuration class of the authentication middleware."""

enable_ssr: bool
allow_http: bool
jwt_secret: str
jwt_expires: int
Expand All @@ -17,6 +18,7 @@ class OAuth2Config:
def __init__(
self,
*,
enable_ssr: bool = True,
allow_http: bool = False,
jwt_secret: str = "",
jwt_expires: Union[int, str] = 900,
Expand All @@ -25,6 +27,7 @@ def __init__(
) -> None:
if allow_http:
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
self.enable_ssr = enable_ssr
self.allow_http = allow_http
self.jwt_secret = jwt_secret
self.jwt_expires = int(jwt_expires)
Expand Down
Loading

0 comments on commit f3ae78f

Please sign in to comment.