Skip to content
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

Add refresh token implementation #1367

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fbbe394
feat: add abstract base classes for refresh token usage
Ae-Mc Mar 9, 2024
e86aede
feat: add refresh bearer
Ae-Mc Mar 9, 2024
42d1357
feat: add database refresh strategy
Ae-Mc Mar 9, 2024
c3dab8f
fix: fix BearerTransportRefresh get_login_response method
Ae-Mc Mar 9, 2024
0f8b98b
fix: fix some lint problems
Ae-Mc Mar 9, 2024
42362d8
refactor: rework class hierarchy so no lint errors occured
Ae-Mc Mar 12, 2024
8109750
fix: fix access_token argument type in BaseAccessTokenDatabase
Ae-Mc Mar 12, 2024
af43d4d
fix(transport/bearer): abstract method get_openapi_logout_responses_s…
Ae-Mc Mar 12, 2024
503e538
fix: fix access_token field name in create dict for refresh token
Ae-Mc Mar 13, 2024
3983ff8
fix(router): fix refresh route doesn't return anything
Ae-Mc Mar 13, 2024
866f33e
Fix utcnow deprecation warning (#1369)
MatthewScholefield Mar 11, 2024
b07e857
docs: add MatthewScholefield as a contributor for code (#1370)
allcontributors[bot] Mar 11, 2024
9a748c7
Upgrade and apply Ruff linting
frankie567 Mar 11, 2024
657b568
Replace passlib in favor of pwdlib
frankie567 Mar 11, 2024
06de5a5
Enable 3.12 support
frankie567 Mar 11, 2024
5ae64b4
Upgrade pytest-asyncio usage
frankie567 Mar 11, 2024
a51232e
Bump python-multipart
frankie567 Mar 11, 2024
e48af6d
Bump version 12.1.3 → 13.0.0
frankie567 Mar 11, 2024
64b1b4b
feat: add abstract base classes for refresh token usage
Ae-Mc Mar 9, 2024
4de46ba
fix: fix union usage
Ae-Mc Mar 13, 2024
0299fc4
fix: export BearerTrasportRefresh and StrategyRefresh from authentica…
Ae-Mc Mar 13, 2024
fd65fea
Update strategy.py
Chiggy-Playz Mar 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/5875019?v=4",
"profile": "http://matthewscholefield.github.io",
"contributions": [
"bug"
"bug",
"code"
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python_version: [3.8, 3.9, '3.10', '3.11']
python_version: [3.8, 3.9, '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand All @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python_version: [3.8, 3.9, '3.10', '3.11']
python_version: [3.8, 3.9, '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="http://francoisvoron.com"><img src="https://avatars.githubusercontent.com/u/1144727?v=4?s=100" width="100px;" alt="François Voron"/><br /><sub><b>François Voron</b></sub></a><br /><a href="#maintenance-frankie567" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/paolodina"><img src="https://avatars.githubusercontent.com/u/1157401?v=4?s=100" width="100px;" alt="Paolo Dina"/><br /><sub><b>Paolo Dina</b></sub></a><br /><a href="#financial-paolodina" title="Financial">💵</a> <a href="https://github.com/fastapi-users/fastapi-users/commits?author=paolodina" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://freelancehunt.com/freelancer/slado122.html"><img src="https://avatars.githubusercontent.com/u/46085159?v=4?s=100" width="100px;" alt="Dmytro Ohorodnik"/><br /><sub><b>Dmytro Ohorodnik</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/issues?q=author%3Aslado122" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://matthewscholefield.github.io"><img src="https://avatars.githubusercontent.com/u/5875019?v=4?s=100" width="100px;" alt="Matthew D. Scholefield"/><br /><sub><b>Matthew D. Scholefield</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/issues?q=author%3AMatthewScholefield" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://matthewscholefield.github.io"><img src="https://avatars.githubusercontent.com/u/5875019?v=4?s=100" width="100px;" alt="Matthew D. Scholefield"/><br /><sub><b>Matthew D. Scholefield</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/issues?q=author%3AMatthewScholefield" title="Bug reports">🐛</a> <a href="https://github.com/fastapi-users/fastapi-users/commits?author=MatthewScholefield" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/roywes"><img src="https://avatars.githubusercontent.com/u/3861579?v=4?s=100" width="100px;" alt="roywes"/><br /><sub><b>roywes</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/issues?q=author%3Aroywes" title="Bug reports">🐛</a> <a href="https://github.com/fastapi-users/fastapi-users/commits?author=roywes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://devwriters.com"><img src="https://avatars.githubusercontent.com/u/10217535?v=4?s=100" width="100px;" alt="Satwik Kansal"/><br /><sub><b>Satwik Kansal</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/commits?author=satwikkansal" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eddsalkield"><img src="https://avatars.githubusercontent.com/u/30939717?v=4?s=100" width="100px;" alt="Edd Salkield"/><br /><sub><b>Edd Salkield</b></sub></a><br /><a href="https://github.com/fastapi-users/fastapi-users/commits?author=eddsalkield" title="Code">💻</a> <a href="https://github.com/fastapi-users/fastapi-users/commits?author=eddsalkield" title="Documentation">📖</a></td>
Expand Down
24 changes: 12 additions & 12 deletions docs/configuration/password-hash.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# Password hash

By default, FastAPI Users will use the [BCrypt algorithm](https://en.wikipedia.org/wiki/Bcrypt) to **hash and salt** passwords before storing them in the database.
By default, FastAPI Users will use the [Argon2 algorithm](https://en.wikipedia.org/wiki/Argon2) to **hash and salt** passwords before storing them in the database, with backwards-compatibility with [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt).

The implementation is provided by [Passlib](https://passlib.readthedocs.io/en/stable/index.html), a battle-tested Python library for password hashing.
The implementation is provided by [pwdlib](https://github.com/frankie567/pwdlib), a modern password hashing wrapper.

## Customize `CryptContext`
## Customize `PasswordHash`

If you need to support other hashing algorithms, you can customize the [`CryptContext` object of Passlib](https://passlib.readthedocs.io/en/stable/lib/passlib.context.html#the-cryptcontext-class).
If you need to tune the algorithms used or their settings, you can customize the [`PasswordHash` object of pwdlib](https://frankie567.github.io/pwdlib/reference/pwdlib/#pwdlib.PasswordHash).

For this, you'll need to instantiate the `PasswordHelper` class and pass it your `CryptContext`. The example below shows you how you can create a `CryptContext` to add support for the Argon2 algorithm while deprecating BCrypt.
For this, you'll need to instantiate the `PasswordHelper` class and pass it your `PasswordHash`. The example below shows you how you can create a `PasswordHash` to only support the Argon2 algorithm.

```py
from fastapi_users.password import PasswordHelper
from passlib.context import CryptContext
from pwdlib import PasswordHash, exceptions
from pwdlib.hashers.argon2 import Argon2Hasher

context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto")
password_helper = PasswordHelper(context)
password_hash = PasswordHash((
Argon2Hasher(),
))
password_helper = PasswordHelper(password_hash)
```

Finally, pass the `password_helper` variable while instantiating your `UserManager`:
Expand All @@ -32,12 +35,9 @@ async def get_user_manager(user_db=Depends(get_user_db)):

If it is, we take the opportunity of having the password in plain-text at hand (since the user just logged in!) to hash it with a better algorithm and update it in database.

!!! warning "Dependencies for alternative algorithms are not included by default"
FastAPI Users won't install required dependencies to make other algorithms like Argon2 work. It's up to you to install them.

## Full customization

If you don't wish to use Passlib at all – **which we don't recommend unless you're absolutely sure of what you're doing** — you can implement your own `PasswordHelper` class as long as it implements the `PasswordHelperProtocol` and its methods.
If you don't wish to use `pwdlib` at all – **which we don't recommend unless you're absolutely sure of what you're doing** — you can implement your own `PasswordHelper` class as long as it implements the `PasswordHelperProtocol` and its methods.

```py
from typing import Tuple
Expand Down
2 changes: 1 addition & 1 deletion fastapi_users/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Ready-to-use and customizable users management for FastAPI."""

__version__ = "12.1.3"
__version__ = "13.0.0"

from fastapi_users import models, schemas # noqa: F401
from fastapi_users.exceptions import InvalidID, InvalidPasswordException
Expand Down
13 changes: 11 additions & 2 deletions fastapi_users/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from fastapi_users.authentication.authenticator import Authenticator
from fastapi_users.authentication.backend import AuthenticationBackend
from fastapi_users.authentication.strategy import JWTStrategy, Strategy
from fastapi_users.authentication.backend import (
AuthenticationBackend,
AuthenticationBackendRefresh,
BaseAuthenticationBackend,
)
from fastapi_users.authentication.strategy import JWTStrategy, Strategy, StrategyRefresh

try:
from fastapi_users.authentication.strategy import RedisStrategy
Expand All @@ -9,17 +13,22 @@

from fastapi_users.authentication.transport import (
BearerTransport,
BearerTransportRefresh,
CookieTransport,
Transport,
)

__all__ = [
"Authenticator",
"AuthenticationBackend",
"AuthenticationBackendRefresh",
"BaseAuthenticationBackend",
"BearerTransport",
"BearerTransportRefresh",
"CookieTransport",
"JWTStrategy",
"RedisStrategy",
"Strategy",
"StrategyRefresh",
"Transport",
]
18 changes: 9 additions & 9 deletions fastapi_users/authentication/authenticator.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import re
from inspect import Parameter, Signature
from typing import Callable, List, Optional, Sequence, Tuple, cast
from typing import Any, Callable, List, Optional, Sequence, Tuple, cast

from fastapi import Depends, HTTPException, status
from makefun import with_signature

from fastapi_users import models
from fastapi_users.authentication.backend import AuthenticationBackend
from fastapi_users.authentication.strategy import Strategy
from fastapi_users.authentication.backend import BaseAuthenticationBackend, TokenType
from fastapi_users.authentication.strategy.base import BaseStrategy
from fastapi_users.manager import BaseUserManager, UserManagerDependency
from fastapi_users.types import DependencyCallable

Expand All @@ -31,7 +31,7 @@ class DuplicateBackendNamesError(Exception):
pass


EnabledBackendsDependency = DependencyCallable[Sequence[AuthenticationBackend]]
EnabledBackendsDependency = DependencyCallable[Sequence[BaseAuthenticationBackend]]


class Authenticator:
Expand All @@ -46,11 +46,11 @@ class Authenticator:
:param get_user_manager: User manager dependency callable.
"""

backends: Sequence[AuthenticationBackend]
backends: Sequence[BaseAuthenticationBackend]

def __init__(
self,
backends: Sequence[AuthenticationBackend],
backends: Sequence[BaseAuthenticationBackend[models.UP, models.ID, TokenType]],
get_user_manager: UserManagerDependency[models.UP, models.ID],
):
self.backends = backends
Expand Down Expand Up @@ -154,16 +154,16 @@ async def _authenticate(
verified: bool = False,
superuser: bool = False,
**kwargs,
) -> Tuple[Optional[models.UP], Optional[str]]:
) -> Tuple[Optional[models.UP], Optional[Any]]:
user: Optional[models.UP] = None
token: Optional[str] = None
enabled_backends: Sequence[AuthenticationBackend] = kwargs.get(
enabled_backends: Sequence[BaseAuthenticationBackend] = kwargs.get(
"enabled_backends", self.backends
)
for backend in self.backends:
if backend in enabled_backends:
token = kwargs[name_to_variable_name(backend.name)]
strategy: Strategy[models.UP, models.ID] = kwargs[
strategy: BaseStrategy[models.UP, models.ID, str, Any] = kwargs[
name_to_strategy_variable_name(backend.name)
]
if token is not None:
Expand Down
69 changes: 60 additions & 9 deletions fastapi_users/authentication/backend.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
from typing import Generic
from typing import Generic, Optional, TypeVar

from fastapi import Response, status

from fastapi_users import models
from fastapi_users.authentication.models import AccessRefreshToken
from fastapi_users.authentication.strategy import (
Strategy,
BaseStrategy,
StrategyDestroyNotSupportedError,
StrategyRefresh,
)
from fastapi_users.authentication.transport import (
Transport,
BaseTransport,
TransportLogoutNotSupportedError,
TransportRefresh,
)
from fastapi_users.manager import BaseUserManager
from fastapi_users.types import DependencyCallable

TokenType = TypeVar("TokenType")

class AuthenticationBackend(Generic[models.UP, models.ID]):

class BaseAuthenticationBackend(Generic[models.UP, models.ID, TokenType]):
"""
Combination of an authentication transport and strategy.

Expand All @@ -27,26 +33,33 @@ class AuthenticationBackend(Generic[models.UP, models.ID]):
"""

name: str
transport: Transport
transport: BaseTransport[TokenType]

def __init__(
self,
name: str,
transport: Transport,
get_strategy: DependencyCallable[Strategy[models.UP, models.ID]],
transport: BaseTransport[TokenType],
get_strategy: DependencyCallable[
BaseStrategy[models.UP, models.ID, str, TokenType]
],
):
self.name = name
self.transport = transport
self.get_strategy = get_strategy

async def login(
self, strategy: Strategy[models.UP, models.ID], user: models.UP
self,
strategy: BaseStrategy[models.UP, models.ID, str, TokenType],
user: models.UP,
) -> Response:
token = await strategy.write_token(user)
return await self.transport.get_login_response(token)

async def logout(
self, strategy: Strategy[models.UP, models.ID], user: models.UP, token: str
self,
strategy: BaseStrategy[models.UP, models.ID, str, TokenType],
user: models.UP,
token: str,
) -> Response:
try:
await strategy.destroy_token(token, user)
Expand All @@ -59,3 +72,41 @@ async def logout(
response = Response(status_code=status.HTTP_204_NO_CONTENT)

return response


class AuthenticationBackend(
BaseAuthenticationBackend[models.UP, models.ID, str], Generic[models.UP, models.ID]
):
pass


class AuthenticationBackendRefresh(
BaseAuthenticationBackend[models.UP, models.ID, AccessRefreshToken],
Generic[models.UP, models.ID],
):
transport: TransportRefresh

def __init__(
self,
name: str,
transport: TransportRefresh,
get_strategy: DependencyCallable[StrategyRefresh[models.UP, models.ID]],
):
super().__init__(name, transport, get_strategy)

async def refresh(
self,
strategy: StrategyRefresh[models.UP, models.ID],
user_manager: BaseUserManager[models.UP, models.ID],
refresh_token: str,
) -> Optional[Response]:
user = await strategy.read_token_by_refresh(
refresh_token=refresh_token, user_manager=user_manager
)
if user is not None:
await strategy.destroy_token_by_refresh(
user=user, refresh_token=refresh_token
)
token = await strategy.write_token(user)
return await self.transport.get_login_response(token)
return None
6 changes: 6 additions & 0 deletions fastapi_users/authentication/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Tuple, TypeVar

TokenIdentityType = TypeVar("TokenIdentityType", contravariant=True)
TokenType = TypeVar("TokenType", covariant=True)

AccessRefreshToken = Tuple[str, str] # First is access second is refresh
10 changes: 10 additions & 0 deletions fastapi_users/authentication/strategy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from fastapi_users.authentication.strategy.base import (
BaseStrategy,
Strategy,
StrategyDestroyNotSupportedError,
StrategyRefresh,
)
from fastapi_users.authentication.strategy.db import (
AP,
APE,
AccessRefreshTokenDatabase,
AccessRefreshTokenProtocol,
AccessTokenDatabase,
AccessTokenProtocol,
DatabaseStrategy,
Expand All @@ -17,11 +22,16 @@

__all__ = [
"AP",
"APE",
"AccessRefreshTokenDatabase",
"AccessRefreshTokenProtocol",
"AccessTokenDatabase",
"AccessTokenProtocol",
"BaseStrategy",
"DatabaseStrategy",
"JWTStrategy",
"Strategy",
"StrategyDestroyNotSupportedError",
"StrategyRefresh",
"RedisStrategy",
]