mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2026-03-13 07:49:55 +08:00
Add get_enabled_backends advanced feature
This commit is contained in:
@@ -13,6 +13,7 @@ Return a dependency callable to retrieve currently authenticated user, passing t
|
||||
* `active`: If `True`, throw `401 Unauthorized` if the authenticated user is inactive. Defaults to `False`.
|
||||
* `verified`: If `True`, throw `403 Forbidden` if the authenticated user is not verified. Defaults to `False`.
|
||||
* `superuser`: If `True`, throw `403 Forbidden` if the authenticated user is not a superuser. Defaults to `False`.
|
||||
* `get_enabled_backends`: Optional dependency callable returning a list of enabled authentication backends. Useful if you want to dynamically enable some authentication backends based on external logic, like a configuration in database. By default, all specified authentication backends are enabled. *Please not however that every backends will appear in the OpenAPI documentation, as FastAPI resolves it statically.*
|
||||
|
||||
!!! tip "Create it once and reuse it"
|
||||
This function is a **factory**, a function returning another function 🤯
|
||||
@@ -63,6 +64,41 @@ def protected_route(user: User = Depends(current_superuser)):
|
||||
return f"Hello, {user.email}"
|
||||
```
|
||||
|
||||
### Dynamically enable authentication backends
|
||||
|
||||
!!! warning
|
||||
This is an advanced feature for cases where you have several authentication backends that are enabled conditionally. In most cases, you won't need this option.
|
||||
|
||||
```py
|
||||
from fastapi import Request
|
||||
from fastapi_users.authentication import CookieAuthentication, JWTAuthentication
|
||||
|
||||
SECRET = "SECRET"
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
cookie_authentication = CookieAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
|
||||
async def get_enabled_backends(request: Request):
|
||||
"""Return the enabled dependencies following custom logic."""
|
||||
if request.url.path == "/protected-route-only-jwt":
|
||||
return [jwt_authentication]
|
||||
else:
|
||||
return [cookie_authentication, jwt_authentication]
|
||||
|
||||
|
||||
current_active_user = fastapi_users.current_user(active=True, get_enabled_backends=get_enabled_backends)
|
||||
|
||||
|
||||
@app.get("/protected-route")
|
||||
def protected_route(user: User = Depends(current_active_user)):
|
||||
return f"Hello, {user.email}. You are authenticated with a cookie or a JWT."
|
||||
|
||||
|
||||
@app.get("/protected-route-only-jwt")
|
||||
def protected_route(user: User = Depends(current_active_user)):
|
||||
return f"Hello, {user.email}. You are authenticated with a JWT."
|
||||
```
|
||||
|
||||
## In a path operation
|
||||
|
||||
If you don't need the user in the route logic, you can use this syntax:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
from inspect import Parameter, Signature
|
||||
from typing import Optional, Sequence
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from makefun import with_signature
|
||||
@@ -26,6 +26,9 @@ class DuplicateBackendNamesError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
EnabledBackendsDependency = Callable[..., Sequence[BaseAuthentication]]
|
||||
|
||||
|
||||
class Authenticator:
|
||||
"""
|
||||
Provides dependency callables to retrieve authenticated user.
|
||||
@@ -54,6 +57,7 @@ class Authenticator:
|
||||
active: bool = False,
|
||||
verified: bool = False,
|
||||
superuser: bool = False,
|
||||
get_enabled_backends: Optional[EnabledBackendsDependency] = None,
|
||||
):
|
||||
"""
|
||||
Return a dependency callable to retrieve currently authenticated user.
|
||||
@@ -68,6 +72,13 @@ class Authenticator:
|
||||
the authenticated user is not verified. Defaults to `False`.
|
||||
:param superuser: If `True`, throw `403 Forbidden` if
|
||||
the authenticated user is not a superuser. Defaults to `False`.
|
||||
:param get_enabled_backends: Optional dependency callable returning
|
||||
a list of enabled authentication backends.
|
||||
Useful if you want to dynamically enable some authentication backends
|
||||
based on external logic, like a configuration in database.
|
||||
By default, all specified authentication backends are enabled.
|
||||
Please not however that every backends will appear in the OpenAPI documentation,
|
||||
as FastAPI resolves it statically.
|
||||
"""
|
||||
# Here comes some blood magic 🧙♂️
|
||||
# Thank to "makefun", we are able to generate callable
|
||||
@@ -88,6 +99,14 @@ class Authenticator:
|
||||
default=Depends(self.get_user_manager),
|
||||
)
|
||||
]
|
||||
if get_enabled_backends is not None:
|
||||
parameters += [
|
||||
Parameter(
|
||||
name="enabled_backends",
|
||||
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=Depends(get_enabled_backends),
|
||||
)
|
||||
]
|
||||
signature = Signature(parameters)
|
||||
except ValueError:
|
||||
raise DuplicateBackendNamesError()
|
||||
@@ -116,12 +135,16 @@ class Authenticator:
|
||||
**kwargs
|
||||
) -> Optional[models.UD]:
|
||||
user: Optional[models.UD] = None
|
||||
enabled_backends: Sequence[BaseAuthentication] = kwargs.get(
|
||||
"enabled_backends", self.backends
|
||||
)
|
||||
for backend in self.backends:
|
||||
token: str = kwargs[name_to_variable_name(backend.name)]
|
||||
if token:
|
||||
user = await backend(token, user_manager)
|
||||
if user:
|
||||
break
|
||||
if backend in enabled_backends:
|
||||
token: str = kwargs[name_to_variable_name(backend.name)]
|
||||
if token:
|
||||
user = await backend(token, user_manager)
|
||||
if user:
|
||||
break
|
||||
|
||||
status_code = status.HTTP_401_UNAUTHORIZED
|
||||
if user:
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import asyncio
|
||||
from typing import Any, AsyncGenerator, Callable, Generic, List, Optional, Type, Union
|
||||
from typing import Any, AsyncGenerator, Callable, Generic, Optional, Type, Union
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from asgi_lifespan import LifespanManager
|
||||
from fastapi import Depends, FastAPI, Response
|
||||
from fastapi import FastAPI, Response
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from httpx_oauth.oauth2 import OAuth2
|
||||
from pydantic import UUID4, SecretStr
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.jwt import SecretType
|
||||
from fastapi_users.manager import (
|
||||
@@ -476,39 +476,6 @@ def get_test_client():
|
||||
return _get_test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
def get_test_auth_client(get_user_manager, get_test_client):
|
||||
async def _get_test_auth_client(
|
||||
backends: List[BaseAuthentication],
|
||||
) -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
app = FastAPI()
|
||||
authenticator = Authenticator(backends, get_user_manager)
|
||||
|
||||
@app.get("/test-current-user")
|
||||
def test_current_user(user: UserDB = Depends(authenticator.current_user())):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-active-user")
|
||||
def test_current_active_user(
|
||||
user: UserDB = Depends(authenticator.current_user(active=True)),
|
||||
):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-superuser")
|
||||
def test_current_superuser(
|
||||
user: UserDB = Depends(
|
||||
authenticator.current_user(active=True, superuser=True)
|
||||
),
|
||||
):
|
||||
return user
|
||||
|
||||
async for client in get_test_client(app):
|
||||
yield client
|
||||
|
||||
return _get_test_auth_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_client() -> OAuth2:
|
||||
CLIENT_ID = "CLIENT_ID"
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from typing import Generic, Optional
|
||||
from typing import AsyncGenerator, Callable, Generic, List, Optional, Sequence
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import Request, status
|
||||
from fastapi import Depends, FastAPI, Request, status
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import BaseAuthentication, DuplicateBackendNamesError
|
||||
from fastapi_users.authentication import (
|
||||
Authenticator,
|
||||
BaseAuthentication,
|
||||
DuplicateBackendNamesError,
|
||||
)
|
||||
from fastapi_users.manager import BaseUserManager
|
||||
from tests.conftest import UserDB
|
||||
|
||||
|
||||
class MockSecurityScheme(SecurityBase):
|
||||
@@ -45,6 +51,54 @@ class BackendUser(
|
||||
return self.user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
def get_test_auth_client(get_user_manager, get_test_client):
|
||||
async def _get_test_auth_client(
|
||||
backends: List[BaseAuthentication],
|
||||
get_enabled_backends: Optional[
|
||||
Callable[..., Sequence[BaseAuthentication]]
|
||||
] = None,
|
||||
) -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
app = FastAPI()
|
||||
authenticator = Authenticator(backends, get_user_manager)
|
||||
|
||||
@app.get("/test-current-user")
|
||||
def test_current_user(
|
||||
user: UserDB = Depends(
|
||||
authenticator.current_user(get_enabled_backends=get_enabled_backends)
|
||||
),
|
||||
):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-active-user")
|
||||
def test_current_active_user(
|
||||
user: UserDB = Depends(
|
||||
authenticator.current_user(
|
||||
active=True, get_enabled_backends=get_enabled_backends
|
||||
)
|
||||
),
|
||||
):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-superuser")
|
||||
def test_current_superuser(
|
||||
user: UserDB = Depends(
|
||||
authenticator.current_user(
|
||||
active=True,
|
||||
superuser=True,
|
||||
get_enabled_backends=get_enabled_backends,
|
||||
)
|
||||
),
|
||||
):
|
||||
return user
|
||||
|
||||
async for client in get_test_client(app):
|
||||
yield client
|
||||
|
||||
return _get_test_auth_client
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator(get_test_auth_client, user):
|
||||
@@ -63,6 +117,22 @@ async def test_authenticator_none(get_test_auth_client):
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator_none_enabled(get_test_auth_client, user):
|
||||
backend_none = BackendNone()
|
||||
backend_user = BackendUser(user)
|
||||
|
||||
async def get_enabled_backends():
|
||||
return [backend_none]
|
||||
|
||||
async for client in get_test_auth_client(
|
||||
[backend_none, backend_user], get_enabled_backends
|
||||
):
|
||||
response = await client.get("/test-current-user")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticators_with_same_name(get_test_auth_client):
|
||||
|
||||
Reference in New Issue
Block a user