Add docs for OAuth association router

This commit is contained in:
François Voron
2022-06-20 17:19:19 +02:00
parent f4338ca3df
commit b999ec9967
3 changed files with 114 additions and 6 deletions

View File

@ -65,7 +65,7 @@ The advantage of MongoDB is that you can easily embed sub-objects in a single do
It's worth to note that `OAuthAccount` is **not a Beanie document** but a Pydantic model that we'll embed inside the `User` document, through the `oauth_accounts` array. It's worth to note that `OAuthAccount` is **not a Beanie document** but a Pydantic model that we'll embed inside the `User` document, through the `oauth_accounts` array.
### Generate a router ### Generate routers
Once you have a `FastAPIUsers` instance, you can make it generate a single OAuth router for a given client **and** authentication backend. Once you have a `FastAPIUsers` instance, you can make it generate a single OAuth router for a given client **and** authentication backend.
@ -77,6 +77,50 @@ app.include_router(
) )
``` ```
!!! tip
If you have several OAuth clients and/or several authentication backends, you'll need to create a router for each pair you want to support.
#### Existing account association
If a user with the same e-mail address already exists, an HTTP 400 error will be raised by default.
You can however choose to automatically link this OAuth account to the existing user account by setting the `associate_by_email` flag:
```py
app.include_router(
fastapi_users.get_oauth_router(
google_oauth_client,
auth_backend,
"SECRET",
associate_by_email=True,
),
prefix="/auth/google",
tags=["auth"],
)
```
Bear in mind though that it can lead to security breaches if the OAuth provider does not validate e-mail addresses. How?
* Let's say your app support an OAuth provider, *Merlinbook*, which does not validate e-mail addresses.
* Imagine a user registers to your app with the e-mail address `lancelot@camelot.bt`.
* Now, a malicious user creates an account on *Merlinbook* with the same e-mail address. Without e-mail validation, the malicious user can use this account without limitation.
* The malicious user authenticates using *Merlinbook* OAuth on your app, which automatically associates to the existing `lancelot@camelot.bt`.
* Now, the malicious user has full access to the user account on your app 😞
#### Association router for authenticated users
We also provide a router to associate an already authenticated user with an OAuth account. After this association, the user will be able to authenticate with this OAuth provider.
```py
app.include_router(
fastapi_users.get_oauth_associate_router(google_oauth_client, UserRead, "SECRET"),
prefix="/auth/associate/google",
tags=["auth"],
)
```
Notice that, just like for the [Users router](./routers/users.md), you have to pass the `UserRead` Pydantic schema.
### Full example ### Full example
!!! warning !!! warning

View File

@ -23,6 +23,7 @@ flowchart TB
subgraph ROUTERS[Routers] subgraph ROUTERS[Routers]
AUTH[[get_auth_router]] AUTH[[get_auth_router]]
OAUTH[[get_oauth_router]] OAUTH[[get_oauth_router]]
OAUTH_ASSOCIATE[[get_oauth_associate_router]]
REGISTER[[get_register_router]] REGISTER[[get_register_router]]
VERIFY[[get_verify_router]] VERIFY[[get_verify_router]]
RESET[[get_reset_password_router]] RESET[[get_reset_password_router]]

View File

@ -214,9 +214,6 @@ Return the authorization URL for the OAuth service where you should redirect you
} }
``` ```
!!! fail "`422 Validation Error`"
Invalid parameters - e.g. unknown authentication backend.
### `GET /callback` ### `GET /callback`
Handle the OAuth callback. Handle the OAuth callback.
@ -232,8 +229,8 @@ Depending on the situation, several things can happen:
* OAuth account is updated in database with fresh access token. * OAuth account is updated in database with fresh access token.
* The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md). * The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md).
* The OAuth account doesn't exist in database but a user with the same email address exists: * The OAuth account doesn't exist in database but a user with the same email address exists:
* OAuth account is linked to the user. * By default, an HTTP 400 error is raised.
* The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md). * If [the `associate_by_email` flag is set to `True`](../configuration/oauth.md#existing-account-association) on the router declaration, OAuth account is linked to the user. The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md).
* The OAuth account doesn't exist in database and no user with the email address exists: * The OAuth account doesn't exist in database and no user with the email address exists:
* A new user is created and linked to the OAuth account. * A new user is created and linked to the OAuth account.
* The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md). * The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md).
@ -241,6 +238,15 @@ Depending on the situation, several things can happen:
!!! fail "`400 Bad Request`" !!! fail "`400 Bad Request`"
Invalid token. Invalid token.
!!! fail "`400 Bad Request`"
Another user with the same e-mail address already exists.
```json
{
"detail": "OAUTH_USER_ALREADY_EXISTS"
}
```
!!! fail "`400 Bad Request`" !!! fail "`400 Bad Request`"
User is inactive. User is inactive.
@ -250,6 +256,63 @@ Depending on the situation, several things can happen:
} }
``` ```
## OAuth association router
Each OAuth association router you define will expose the two following routes.
### `GET /authorize`
Return the authorization URL for the OAuth service where you should redirect your user.
!!! abstract "Query parameters"
* `scopes`: Optional list of scopes to ask for. Expected format: `scopes=a&scopes=b`.
!!! fail "`401 Unauthorized`"
Missing token or inactive user.
!!! success "`200 OK`"
```json
{
"authorization_url": "https://www.tintagel.bt/oauth/authorize?client_id=CLIENT_ID&scopes=a+b&redirect_uri=https://www.camelot.bt/oauth/callback"
}
```
### `GET /callback`
Handle the OAuth callback and add the OAuth account to the current authenticated active user.
!!! abstract "Query parameters"
* `code`: OAuth callback code.
* `state`: State token.
* `error`: OAuth error.
!!! fail "`401 Unauthorized`"
Missing token or inactive user.
!!! fail "`400 Bad Request`"
Invalid token.
!!! success "`200 OK`"
```json
{
"id": "57cbb51a-ab71-4009-8802-3f54b4f2e23",
"email": "king.arthur@tintagel.bt",
"is_active": true,
"is_superuser": false,
"oauth_accounts": [
{
"id": "6c98caf5-9bc5-4c4f-8a45-a0ae0c40cd77",
"oauth_name": "TINTAGEL",
"access_token": "ACCESS_TOKEN",
"expires_at": "1641040620",
"account_id": "king_arthur_tintagel",
"account_email": "king.arthur@tintagel.bt"
}
]
}
```
## Users router ## Users router
### `GET /me` ### `GET /me`