Skip to content

Commit f3ae78f

Browse files
Implement PKCE support and API compatibility (GH-21)
2 parents 1eb98e0 + 7f8643e commit f3ae78f

25 files changed

+425
-227
lines changed

docs/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ hero:
2323
features:
2424
- icon: 🛠️
2525
title: Free and open source
26-
details: Enjoy the freedom of our OSS project, giving you full access to its source code and allowing you to contribute to its development.
26+
details: Dive into our OSS initiative, which not only grants complete access to the source code but also welcomes your contributions.
2727
- icon: 🧩
2828
title: Easy to integrate
2929
details: Incorporate FastAPI OAuth2 into your existing projects with its straightforward integration process, saving you time.
3030
- icon:
3131
title: Compatible with FastAPI 0.68.1+
3232
details: The package is fully compatible with FastAPI v0.68.1 and above, ensuring smooth operation and integration with your application.
33+
- icon: ⚙️
34+
title: Configurable Workflows
35+
details: Customize authentication processes to align perfectly with your application's specific needs, ensuring flexibility and precision.
3336
---

docs/integration/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Python's `dict` type with the same structure as these two classes.
88

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

11+
- `enable_ssr` - Whether enable server-side rendering or not. Defaults to `True`.
1112
- `allow_http` - Whether allow HTTP requests or not. Defaults to `False`.
1213
- `jwt_secret` - Secret used to sign the JWT tokens. Defaults to an empty string.
1314
- `jwt_expires` - JWT lifetime in seconds. Defaults to 900 (15 minutes).

docs/integration/integration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ choices, this kind of solution gives developers freedom.
5050
## Router
5151

5252
Router defines the endpoints that are used for the authentication and logout. The authentication is done by
53-
the `/oauth2/{provider}/auth` endpoint and the logout is done by the `/oauth2/logout` endpoint. The `{provider}` is the
54-
name of the provider that is going to be used for the authentication and coincides with the `name` attribute of
53+
the `/oauth2/{provider}/authorize` endpoint and the logout is done by the `/oauth2/logout` endpoint. The `{provider}` is
54+
the name of the provider that is going to be used for the authentication and coincides with the `name` attribute of
5555
the `backend` provided to the certain `OAuth2Client`.
5656

5757
```python

docs/references/index.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,24 @@ the [documentation](https://python-social-auth.readthedocs.io/en/latest/backends
1313

1414
## SSR & REST APIs
1515

16-
::: tip Ticket #19
17-
18-
This upcoming feature is under development and will be available in the next release. You can track the progress in
19-
the [#19](https://github.com/pysnippet/fastapi-oauth2/issues/19) issue.
20-
21-
:::
16+
It is compatible with both SSR and REST APIs. It means you can integrate it into your FastAPI templates and REST APIs.
17+
By default, the `enable_ssr` parameter of the primary [configuration](/integration/configuration#oauth2config) is set
18+
to `True`, which means that the application uses server-side rendering using Jinja2 templates and saves the access token
19+
in the cookies. If you want to use it in your REST APIs, you should set the `enable_ssr` parameter to `False` and save
20+
the access token on the client side.
2221

2322
## CSRF protection
2423

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

3029
## PKCE support
3130

32-
::: tip Ticket #18
33-
34-
PKCE support is under development and will be available in the next release. You can track the progress in
35-
the [#18](https://github.com/pysnippet/fastapi-oauth2/issues/18) issue.
36-
37-
:::
31+
PKCE can be enabled by providing the `code_challenge` and `code_challenge_method` parameters to
32+
the `/oauth2/{provider}/authorize` endpoint. Then, after the authorization passes, the `code_verifier` should be
33+
provided to the `/oauth2/{provider}/token` endpoint to complete the authentication process.
3834

3935
<style>
4036
.tip {

docs/references/tutorials.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ generated the client ID and secret to configure your `OAuth2Middleware` with at
2222
3. Set the `redirect_uri` of your application that you have also configured in the IDP.
2323
4. Add the middleware and include the router to your application as shown in the [integration](/integration/integration)
2424
section.
25-
5. Open the `/oauth2/{provider}/auth` endpoint on your browser and test the authentication flow. Check out
25+
5. Open the `/oauth2/{provider}/authorize` endpoint on your browser and test the authentication flow. Check out
2626
the [router](/integration/integration#router) for the `{provider}` variable.
2727

2828
Once the authentication is successful, the user will be redirected to the `redirect_uri` and the `request.user` will
@@ -90,8 +90,6 @@ Claims(
9090
)
9191
```
9292

93-
::: info NOTE
94-
9593
Not all IDPs provide the `first_name` and the `last_name` attributes already joined as in the example above, or
9694
the email in a list. So you are given the flexibility using transformer function to map the attributes as you want.
9795

@@ -104,14 +102,20 @@ flowchart LR
104102
Transform --> IDPUserData
105103
```
106104

107-
:::
108-
109105
## User provisioning
110106

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

111+
::: info NOTE
112+
113+
In both scenarios, it is recommended to use the `identity` attribute for uniquely identifying the user from the
114+
database. So if the application uses or plans to use multiple IDPs, make sure to include the `provider` attribute when
115+
calculating the `identity` attribute.
116+
117+
:::
118+
115119
### Automatic provisioning
116120

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

128-
::: info NOTE
129-
130-
In both scenarios, it is recommended to use the `identity` attribute for uniquely identifying the user from the
131-
database. So if the application uses or plans to use multiple IDPs, make sure to include the `provider` attribute when
132-
calculating the `identity` attribute.
133-
134-
:::
135-
136132
<style>
137133
.info, .details {
138134
border: 0;

examples/demonstration/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## Demonstration
22

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

68
## Installation
79

examples/demonstration/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from fastapi_oauth2.middleware import User
1313
from fastapi_oauth2.router import router as oauth2_router
1414
from models import User as UserModel
15-
from router import router as app_router
15+
from router_api import router_api
16+
from router_ssr import router_ssr
1617

1718
Base.metadata.create_all(bind=engine)
1819

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

3738

3839
app = FastAPI()
39-
app.include_router(app_router)
40+
app.include_router(router_api)
41+
app.include_router(router_ssr)
4042
app.include_router(oauth2_router)
4143
app.mount("/static", StaticFiles(directory="static"), name="static")
4244
app.add_middleware(OAuth2Middleware, config=oauth2_config, callback=on_auth)

examples/demonstration/router.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

examples/demonstration/router_api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import APIRouter
2+
from fastapi import Request
3+
from fastapi.templating import Jinja2Templates
4+
from starlette.responses import RedirectResponse
5+
6+
from fastapi_oauth2.security import OAuth2
7+
8+
oauth2 = OAuth2()
9+
router_api = APIRouter()
10+
templates = Jinja2Templates(directory="templates")
11+
12+
13+
@router_api.get("/auth")
14+
def sim_auth(request: Request):
15+
access_token = request.auth.jwt_create({
16+
"id": 1,
17+
"identity": "demo:1",
18+
"image": None,
19+
"display_name": "John Doe",
20+
"email": "john.doe@auth.sim",
21+
"username": "JohnDoe",
22+
"exp": 3689609839,
23+
})
24+
response = RedirectResponse("/")
25+
response.set_cookie(
26+
"Authorization",
27+
value=f"Bearer {access_token}",
28+
max_age=request.auth.expires,
29+
expires=request.auth.expires,
30+
httponly=request.auth.http,
31+
)
32+
return response

examples/demonstration/router_ssr.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import json
2+
3+
from fastapi import APIRouter
4+
from fastapi import Depends
5+
from fastapi import Request
6+
from fastapi.responses import HTMLResponse
7+
from fastapi.templating import Jinja2Templates
8+
from sqlalchemy.orm import Session
9+
10+
from database import get_db
11+
from fastapi_oauth2.security import OAuth2
12+
from models import User
13+
14+
oauth2 = OAuth2()
15+
router_ssr = APIRouter()
16+
templates = Jinja2Templates(directory="templates")
17+
18+
19+
@router_ssr.get("/", response_class=HTMLResponse)
20+
async def root(request: Request):
21+
return templates.TemplateResponse("index.html", {
22+
"json": json,
23+
"request": request,
24+
})
25+
26+
27+
@router_ssr.get("/users", response_class=HTMLResponse)
28+
async def users(request: Request, db: Session = Depends(get_db), _: str = Depends(oauth2)):
29+
return templates.TemplateResponse("users.html", {
30+
"json": json,
31+
"request": request,
32+
"users": [
33+
dict([(k, v) for k, v in user.__dict__.items() if not k.startswith("_")]) for user in db.query(User).all()
34+
],
35+
})

examples/demonstration/templates/base.html

Lines changed: 55 additions & 0 deletions
Large diffs are not rendered by default.

examples/demonstration/templates/index.html

Lines changed: 7 additions & 55 deletions
Large diffs are not rendered by default.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
<p>This is the current list of all users. See <a style="color: #009486;" href="/">JWT</a> content.</p>
5+
<div style="padding: 8px 16px; background: #161618; border-radius: 6px;">
6+
<pre style="max-width: 500px; white-space: pre-wrap;">{{ json.dumps(users, indent=4) }}</pre>
7+
</div>
8+
{% endblock %}

src/fastapi_oauth2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0-beta"
1+
__version__ = "1.0.0-beta.2"

src/fastapi_oauth2/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class OAuth2Config:
99
"""Configuration class of the authentication middleware."""
1010

11+
enable_ssr: bool
1112
allow_http: bool
1213
jwt_secret: str
1314
jwt_expires: int
@@ -17,6 +18,7 @@ class OAuth2Config:
1718
def __init__(
1819
self,
1920
*,
21+
enable_ssr: bool = True,
2022
allow_http: bool = False,
2123
jwt_secret: str = "",
2224
jwt_expires: Union[int, str] = 900,
@@ -25,6 +27,7 @@ def __init__(
2527
) -> None:
2628
if allow_http:
2729
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
30+
self.enable_ssr = enable_ssr
2831
self.allow_http = allow_http
2932
self.jwt_secret = jwt_secret
3033
self.jwt_expires = int(jwt_expires)

0 commit comments

Comments
 (0)