Skip to content

Make access token accessible from auth context #20

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 9 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 11 additions & 2 deletions docs/.vitepress/config.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/integration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,5 @@ Claims(
identity=lambda user: f"{user.provider}:{user.id}",
)
```

Check out the [tutorial](/references/tutorials#claims-mapping) on claims mapping for a clearer understanding.
15 changes: 3 additions & 12 deletions docs/integration/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ 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.
succeeds. This can be used for [user provisioning](/references/tutorials#user-provisioning). 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

Expand All @@ -65,12 +65,3 @@ app.include_router(oauth2_router)
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.
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"serve": "vitepress serve"
},
"dependencies": {
"mermaid": "^10.3.1",
"vitepress": "^1.0.0-rc.4",
"vitepress-plugin-mermaid": "^2.0.14",
"vue": "^3.3.2"
}
}
43 changes: 43 additions & 0 deletions docs/references/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Features

Below is the incomplete list of features that are supported by the library. Other features and samples can be found in
the [tutorials](/references/tutorials) section.

## Several providers

The library leverages the [social-core](https://github.com/python-social-auth/social-core)
authentication [backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends), which
means it supports all the providers that are supported by it. However, if the provider you are interested in does not
exist in the list, you can add one by following
the [documentation](https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html).

## 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.

:::

## CSRF protection

CSRF protection is enabled by default which means when the user opens the `/oauth2/{provider}/auth` 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.

:::

<style>
.tip {
border: 0;
}
</style>
140 changes: 140 additions & 0 deletions docs/references/tutorials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
outline: deep
---

# Tutorials

This documentation section covers samples and tutorials on important topics of using the library. Look at
the [examples](https://github.com/pysnippet/fastapi-oauth2/tree/master/examples)
and [tests](https://github.com/pysnippet/fastapi-oauth2/tree/master/tests) directories of the repository for other
use-case implementations. 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 your question is not covered by
the documentation.

## Authentication

By following the [integration](/integration/integration) docs, for the basic user authentication, you must already have
generated the client ID and secret to configure your `OAuth2Middleware` with at least one client configuration.

1. Go to the developer console or settings of your OAuth2 identity provider and generate new client credentials.
2. Provide the [client configuration](/integration/configuration#oauth2client) with the obtained client ID and secret
into the clients of the middleware's config.
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
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
contain the user information obtained from the IDP.

## Access token

When the user is authenticated, the `request.user` will contain the user information obtained from the IDP and
the `request.auth` will contain the authentication related information including the access token issued by the IDP. It
can be used to perform authorized requests to the IDP's API endpoints. Just make sure the token is issued with the
scopes required for the API endpoint.

::: details `request.auth.provider.access_token`

```mermaid
flowchart TB
subgraph level2["request (Starlette's Request object)"]
direction TB
subgraph level1["auth (Starlette's extended Auth Credentials)"]
direction TB
subgraph level0["provider (OAuth2 provider with client's credentials)"]
direction TB
token["access_token (Access token for the specified scopes)"]
end
end
end
style level2 fill:#00948680,color:#f6f6f7,stroke:#3c3c43;
style level1 fill:#2b75a080,color:#f6f6f7,stroke:#3c3c43;
style level0 fill:#5c837480,color:#f6f6f7,stroke:#3c3c43;
style token fill:#44506980,color:#f6f6f7,stroke:#3c3c43;
```

:::

## Claims mapping

The `Claims` class includes permanent attributes like `display_name`, `identity`, `picture`, and `email`. It also allows
for custom attributes. Each attribute can either be a string or a callable function that takes user data and returns a
string. Suppose the user data obtained from IDP looks like follows, and you need to map the corresponding attributes for
the user provisioning and other stuff.

```json
{
"id": 54321,
"sub": "1234567890",
"name": "John Doe",
"provider": "github",
"emails": [
"[email protected]"
],
"avatar_url": "https://example.com/john.doe.png"
}
```

It looks easy for the `picture` and `display_name` attributes, but how to map `email` from `emails` or create a
unique `identity` attribute. Well, that is where the callable functions come in handy. You can use the `lambda` function
to map the attributes as follows.

```python
Claims(
picture="image",
display_name="avatar_url",
email=lambda u: u.emails[0],
identity=lambda u: f"{u.provider}:{u.sub}",
)
```

::: 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.

```mermaid
flowchart LR
IDPUserData("display_name string")
FastAPIUserData("first_name string\nlast_name string")
Transform[["transform into desired format"]]
FastAPIUserData --> Transform
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.

### Automatic provisioning

After successful authentication, you can automatically create a user in your application's database using the
information obtained from the IDP. The user creation or update can be handled at the `callback` function of the
[middleware](/integration/integration#oauth2middleware) as it is called when authentication succeeds.

### Manual provisioning

After successful authentication, redirect the user to a registration form where they can complete their profile. This
approach is useful when there missing mandatory attributes in `request.user` for creating a user in your application's
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;
}
</style>
15 changes: 7 additions & 8 deletions src/fastapi_oauth2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,19 @@ def __init__(self, client: OAuth2Client) -> None:
self.backend = client.backend(OAuth2Strategy())
self.authorization_endpoint = client.backend.AUTHORIZATION_URL
self.token_endpoint = client.backend.ACCESS_TOKEN_URL
self._oauth_client = WebApplicationClient(self.client_id)

@property
def oauth_client(self) -> WebApplicationClient:
if self._oauth_client is None:
self._oauth_client = WebApplicationClient(self.client_id)
return self._oauth_client
def access_token(self) -> str:
return self._oauth_client.access_token

def get_redirect_uri(self, request: Request) -> str:
return urljoin(str(request.base_url), "/oauth2/%s/token" % self.provider)

async def login_redirect(self, request: Request) -> RedirectResponse:
redirect_uri = self.get_redirect_uri(request)
state = "".join([random.choice(string.ascii_letters) for _ in range(32)])
return RedirectResponse(str(self.oauth_client.prepare_request_uri(
return RedirectResponse(str(self._oauth_client.prepare_request_uri(
self.authorization_endpoint, redirect_uri=redirect_uri, state=state, scope=self.scope
)), 303)

Expand All @@ -95,7 +94,7 @@ async def token_redirect(self, request: Request) -> RedirectResponse:
current_url = re.sub(r"^https?", scheme, str(url))
redirect_uri = self.get_redirect_uri(request)

token_url, headers, content = self.oauth_client.prepare_token_request(
token_url, headers, content = self._oauth_client.prepare_token_request(
self.token_endpoint,
redirect_url=redirect_uri,
authorization_response=current_url,
Expand All @@ -111,8 +110,8 @@ async def token_redirect(self, request: Request) -> RedirectResponse:
async with httpx.AsyncClient() as session:
response = await session.post(token_url, headers=headers, content=content, auth=auth)
try:
token = self.oauth_client.parse_request_body_response(json.dumps(response.json()))
token_data = self.standardize(self.backend.user_data(token.get("access_token")))
self._oauth_client.parse_request_body_response(json.dumps(response.json()))
token_data = self.standardize(self.backend.user_data(self.access_token))
access_token = request.auth.jwt_create(token_data)
except (CustomOAuth2Error, Exception) as e:
raise OAuth2LoginError(400, str(e))
Expand Down
13 changes: 2 additions & 11 deletions src/fastapi_oauth2/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,9 @@ class Auth(AuthCredentials):
expires: int
algorithm: str
scopes: List[str]
provider: OAuth2Core = None
clients: Dict[str, OAuth2Core] = {}

_provider: OAuth2Core = None

@property
def provider(self) -> Union[OAuth2Core, None]:
return self._provider

@provider.setter
def provider(self, identifier) -> None:
self._provider = self.clients.get(identifier)

@classmethod
def set_http(cls, http: bool) -> None:
cls.http = http
Expand Down Expand Up @@ -146,7 +137,7 @@ async def authenticate(self, request: Request) -> Optional[Tuple[Auth, User]]:

user = User(Auth.jwt_decode(param))
auth = Auth(user.pop("scope", []))
auth.provider = user.get("provider")
auth.provider = auth.clients.get(user.get("provider"))
claims = auth.provider.claims if auth.provider else {}

# Call the callback function on authentication
Expand Down