Skip to content

Implement support for PKCE #21

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 28 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
59b013c
GH-18: Pass all URL params to `oauthlib` including PKCE ones
ArtyomVancyan Aug 29, 2023
50d0ccd
Create a showcase page for users list
ArtyomVancyan Aug 29, 2023
1fab83d
Segregate the routers into two types
ArtyomVancyan Aug 29, 2023
fd5f19e
Change the README description of the demo app
ArtyomVancyan Aug 30, 2023
517d677
Implement a sample IDP for CSRF and PKCE tests
ArtyomVancyan Aug 30, 2023
8af36da
Convert the Flask IDP to FastAPI
ArtyomVancyan Aug 31, 2023
b885a43
Implement an IDP simulator for tests
ArtyomVancyan Sep 1, 2023
a328b93
Remove `httpx` as it already defined in setup.cfg
ArtyomVancyan Sep 1, 2023
6a515cf
Give option to provide `httpx_client_args` for testing purposes
ArtyomVancyan Sep 1, 2023
9143f34
Configure a client for `TestOAuth2` and connect IDP
ArtyomVancyan Sep 1, 2023
c6272a0
Initiate OAuth2 tests
ArtyomVancyan Sep 1, 2023
181bf82
Implement verification for the PKCE flow
ArtyomVancyan Sep 2, 2023
0be90fb
GH-18: Override default params with the provided ones
ArtyomVancyan Sep 2, 2023
aed3fae
Segregate the redirects from the results
ArtyomVancyan Sep 3, 2023
a438ad4
Add a new attribute for SSR mode
ArtyomVancyan Sep 3, 2023
e88d130
Refactor the `/auth` to `/authorize`
ArtyomVancyan Sep 3, 2023
d9a2393
Extend the usability of `get_app` fixture
ArtyomVancyan Sep 3, 2023
0ffac70
GH-18: Add tests for the PKCE workflow
ArtyomVancyan Sep 3, 2023
92467f0
Code coverage: group protected attributes together
ArtyomVancyan Sep 4, 2023
c794260
Code coverage: use direct assignments instead of setters
ArtyomVancyan Sep 4, 2023
72fe36a
Define user's permanent attributes in `__slots__`
ArtyomVancyan Sep 4, 2023
bc073ef
Fix the function naming convention
ArtyomVancyan Sep 6, 2023
7b5457f
GH-18: Write up the "PKCE support" section
ArtyomVancyan Sep 6, 2023
c774f04
GH-19: Document the compatibility with REST APIs
ArtyomVancyan Sep 7, 2023
a0f2b59
Update the new config parameter
ArtyomVancyan Sep 7, 2023
ec7bf74
Rearrange subsections and improve the doc view
ArtyomVancyan Sep 7, 2023
d70b564
Add a feature into the landing list
ArtyomVancyan Sep 8, 2023
7f8643e
Update the version to `beta.2`
ArtyomVancyan Sep 8, 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
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