mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-08-15 03:04:27 +08:00
234 lines
6.9 KiB
Markdown
234 lines
6.9 KiB
Markdown
# OAuth2
|
|
|
|
FastAPI Users provides an optional OAuth2 authentication support. It relies on [HTTPX OAuth library](https://frankie567.github.io/httpx-oauth/), which is a pure-async implementation of OAuth2.
|
|
|
|
## Installation
|
|
|
|
You should install the library with the optional dependencies for OAuth:
|
|
|
|
```sh
|
|
pip install 'fastapi-users[sqlalchemy,oauth]'
|
|
```
|
|
|
|
```sh
|
|
pip install 'fastapi-users[beanie,oauth]'
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Instantiate an OAuth2 client
|
|
|
|
You first need to get an HTTPX OAuth client instance. [Read the documentation](https://frankie567.github.io/httpx-oauth/oauth2/) for more information.
|
|
|
|
```py
|
|
from httpx_oauth.clients.google import GoogleOAuth2
|
|
|
|
google_oauth_client = GoogleOAuth2("CLIENT_ID", "CLIENT_SECRET")
|
|
```
|
|
|
|
### Setup the database adapter
|
|
|
|
#### SQLAlchemy
|
|
|
|
You'll need to define the SQLAlchemy model for storing OAuth accounts. We provide a base one for this:
|
|
|
|
```py hl_lines="5 19-20 24-26 43-44"
|
|
--8<-- "docs/src/db_sqlalchemy_oauth.py"
|
|
```
|
|
|
|
Notice that we also manually added a `relationship` on `User` so that SQLAlchemy can properly retrieve the OAuth accounts of the user.
|
|
|
|
Besides, when instantiating the database adapter, we need pass this SQLAlchemy model as third argument.
|
|
|
|
!!! tip "Primary key is defined as UUID"
|
|
By default, we use UUID as a primary key ID for your user. If you want to use another type, like an auto-incremented integer, you can use `SQLAlchemyBaseOAuthAccountTable` as base class and define your own `id` and `user_id` column.
|
|
|
|
```py
|
|
class OAuthAccount(SQLAlchemyBaseOAuthAccountTable[int], Base):
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
@declared_attr
|
|
def user_id(cls) -> Mapped[int]:
|
|
return mapped_column(Integer, ForeignKey("user.id", ondelete="cascade"), nullable=False)
|
|
|
|
```
|
|
|
|
Notice that `SQLAlchemyBaseOAuthAccountTable` expects a generic type to define the actual type of ID you use.
|
|
|
|
#### Beanie
|
|
|
|
The advantage of MongoDB is that you can easily embed sub-objects in a single document. That's why the configuration for Beanie is quite simple. All we need to do is to define another class to structure an OAuth account object.
|
|
|
|
```py hl_lines="5 15-16 20"
|
|
--8<-- "docs/src/db_beanie_oauth.py"
|
|
```
|
|
|
|
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 routers
|
|
|
|
Once you have a `FastAPIUsers` instance, you can make it generate a single OAuth router for a given client **and** authentication backend.
|
|
|
|
```py
|
|
app.include_router(
|
|
fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"),
|
|
prefix="/auth/google",
|
|
tags=["auth"],
|
|
)
|
|
```
|
|
|
|
!!! 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.
|
|
|
|
#### Set `is_verified` to `True` by default
|
|
|
|
!!! tip "This section is only useful if you set up email verification"
|
|
You can read more about this feature [here](./routers/verify.md).
|
|
|
|
When a new user registers with an OAuth provider, the `is_verified` flag is set to `False`, which requires the user to verify its email address.
|
|
|
|
You can choose to trust the email address given by the OAuth provider and set the `is_verified` flag to `True` after registration. You can do this by setting the `is_verified_by_default` argument:
|
|
|
|
```py
|
|
app.include_router(
|
|
fastapi_users.get_oauth_router(
|
|
google_oauth_client,
|
|
auth_backend,
|
|
"SECRET",
|
|
is_verified_by_default=True,
|
|
),
|
|
prefix="/auth/google",
|
|
tags=["auth"],
|
|
)
|
|
```
|
|
|
|
!!! danger "Make sure you can trust the OAuth provider"
|
|
Make sure the OAuth provider you're using **does verify** the email address before enabling this flag.
|
|
|
|
### Full example
|
|
|
|
!!! warning
|
|
Notice that **SECRET** should be changed to a strong passphrase.
|
|
Insecure passwords may give attackers full access to your database.
|
|
|
|
#### SQLAlchemy
|
|
|
|
[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/sqlalchemy-oauth)
|
|
|
|
=== "requirements.txt"
|
|
|
|
```
|
|
--8<-- "examples/sqlalchemy-oauth/requirements.txt"
|
|
```
|
|
|
|
=== "main.py"
|
|
|
|
```py
|
|
--8<-- "examples/sqlalchemy-oauth/main.py"
|
|
```
|
|
|
|
=== "app/app.py"
|
|
|
|
```py
|
|
--8<-- "examples/sqlalchemy-oauth/app/app.py"
|
|
```
|
|
|
|
=== "app/db.py"
|
|
|
|
```py
|
|
--8<-- "examples/sqlalchemy-oauth/app/db.py"
|
|
```
|
|
|
|
=== "app/schemas.py"
|
|
|
|
```py
|
|
--8<-- "examples/sqlalchemy-oauth/app/schemas.py"
|
|
```
|
|
|
|
=== "app/users.py"
|
|
|
|
```py
|
|
--8<-- "examples/sqlalchemy-oauth/app/users.py"
|
|
```
|
|
|
|
#### Beanie
|
|
|
|
[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/beanie-oauth)
|
|
|
|
=== "requirements.txt"
|
|
|
|
```
|
|
--8<-- "examples/beanie-oauth/requirements.txt"
|
|
```
|
|
|
|
=== "main.py"
|
|
|
|
```py
|
|
--8<-- "examples/beanie-oauth/main.py"
|
|
```
|
|
|
|
=== "app/app.py"
|
|
|
|
```py
|
|
--8<-- "examples/beanie-oauth/app/app.py"
|
|
```
|
|
|
|
=== "app/db.py"
|
|
|
|
```py
|
|
--8<-- "examples/beanie-oauth/app/db.py"
|
|
```
|
|
|
|
=== "app/schemas.py"
|
|
|
|
```py
|
|
--8<-- "examples/beanie-oauth/app/schemas.py"
|
|
```
|
|
|
|
=== "app/users.py"
|
|
|
|
```py
|
|
--8<-- "examples/beanie-oauth/app/users.py"
|
|
```
|