mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-02 21:24:34 +08:00
Implement logout route
This commit is contained in:
@ -47,6 +47,14 @@ This method will return a response with a valid `set-cookie` header upon success
|
|||||||
|
|
||||||
> Check documentation about [login route](../../usage/routes.md#post-loginname).
|
> Check documentation about [login route](../../usage/routes.md#post-loginname).
|
||||||
|
|
||||||
|
## Logout
|
||||||
|
|
||||||
|
This method will remove the authentication cookie:
|
||||||
|
|
||||||
|
!!! success "`200 OK`"
|
||||||
|
|
||||||
|
> Check documentation about [logout route](../../usage/routes.md#post-logoutname).
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
This method expects that you provide a valid cookie in the headers.
|
This method expects that you provide a valid cookie in the headers.
|
||||||
|
@ -10,6 +10,8 @@ When checking authentication, each method is run one after the other. The first
|
|||||||
|
|
||||||
Each defined method will generate a [`/login/{name}`](../../usage/routes.md#post-loginname) route where `name` is defined on the authentication method object.
|
Each defined method will generate a [`/login/{name}`](../../usage/routes.md#post-loginname) route where `name` is defined on the authentication method object.
|
||||||
|
|
||||||
|
Each defined method will generate a [`/logout/{name}`](../../usage/routes.md#post-logoutname) route where `name` is defined on the authentication method object.
|
||||||
|
|
||||||
## Provided methods
|
## Provided methods
|
||||||
|
|
||||||
* [JWT authentication](jwt.md)
|
* [JWT authentication](jwt.md)
|
||||||
|
@ -41,6 +41,14 @@ This method will return a JWT token upon successful login:
|
|||||||
|
|
||||||
> Check documentation about [login route](../../usage/routes.md#post-loginname).
|
> Check documentation about [login route](../../usage/routes.md#post-loginname).
|
||||||
|
|
||||||
|
## Logout
|
||||||
|
|
||||||
|
This method is not applicable to this backend and won't do anything.
|
||||||
|
|
||||||
|
!!! success "`202 Accepted`"
|
||||||
|
|
||||||
|
> Check documentation about [logout route](../../usage/routes.md#post-logoutname).
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
This method expects that you provide a `Bearer` authentication with a valid JWT.
|
This method expects that you provide a `Bearer` authentication with a valid JWT.
|
||||||
|
77
docs/src/full_sqlalchemy.py
Normal file
77
docs/src/full_sqlalchemy.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi_users import FastAPIUsers, models
|
||||||
|
from fastapi_users.authentication import JWTAuthentication
|
||||||
|
from fastapi_users.db import (
|
||||||
|
SQLAlchemyBaseUserTable,
|
||||||
|
SQLAlchemyUserDatabase,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./test.db"
|
||||||
|
SECRET = "SECRET"
|
||||||
|
|
||||||
|
|
||||||
|
class User(models.BaseUser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(User, models.BaseUserCreate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(User, models.BaseUserUpdate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserDB(User, models.BaseUserDB):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL)
|
||||||
|
Base: DeclarativeMeta = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class UserTable(Base, SQLAlchemyBaseUserTable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
engine = sqlalchemy.create_engine(
|
||||||
|
DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
users = UserTable.__table__
|
||||||
|
user_db = SQLAlchemyUserDatabase(UserDB, database, users)
|
||||||
|
|
||||||
|
|
||||||
|
auth_backends = [
|
||||||
|
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||||
|
]
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
fastapi_users = FastAPIUsers(
|
||||||
|
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||||
|
)
|
||||||
|
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
|
||||||
|
@fastapi_users.on_after_register()
|
||||||
|
def on_after_register(user: User):
|
||||||
|
print(f"User {user.id} has registered.")
|
||||||
|
|
||||||
|
|
||||||
|
@fastapi_users.on_after_forgot_password()
|
||||||
|
def on_after_forgot_password(user: User, token: str):
|
||||||
|
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
await database.connect()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
await database.disconnect()
|
55
docs/src/full_tortoise.py
Normal file
55
docs/src/full_tortoise.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi_users import FastAPIUsers, models
|
||||||
|
from fastapi_users.authentication import JWTAuthentication
|
||||||
|
from fastapi_users.db import (
|
||||||
|
TortoiseBaseUserModel,
|
||||||
|
TortoiseUserDatabase,
|
||||||
|
)
|
||||||
|
from tortoise.contrib.starlette import register_tortoise
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite://./test.db"
|
||||||
|
SECRET = "SECRET"
|
||||||
|
|
||||||
|
|
||||||
|
class User(models.BaseUser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(User, models.BaseUserCreate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(User, models.BaseUserUpdate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserDB(User, models.BaseUserDB):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserModel(TortoiseBaseUserModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
user_db = TortoiseUserDatabase(UserDB, UserModel)
|
||||||
|
app = FastAPI()
|
||||||
|
register_tortoise(app, db_url=DATABASE_URL, modules={"models": ["test"]})
|
||||||
|
|
||||||
|
auth_backends = [
|
||||||
|
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||||
|
]
|
||||||
|
|
||||||
|
fastapi_users = FastAPIUsers(
|
||||||
|
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||||
|
)
|
||||||
|
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
|
||||||
|
@fastapi_users.on_after_register()
|
||||||
|
def on_after_register(user: User):
|
||||||
|
print(f"User {user.id} has registered.")
|
||||||
|
|
||||||
|
|
||||||
|
@fastapi_users.on_after_forgot_password()
|
||||||
|
def on_after_forgot_password(user: User, token: str):
|
||||||
|
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
@ -57,6 +57,19 @@ Login a user against the method named `name`. Check the corresponding [authentic
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `POST /logout/{name}`
|
||||||
|
|
||||||
|
Logout the authenticated user against the method named `name`. Check the corresponding [authentication method](../configuration/authentication/index.md) to view the success response.
|
||||||
|
|
||||||
|
!!! fail "`401 Unauthorized`"
|
||||||
|
Missing token or inactive user.
|
||||||
|
|
||||||
|
!!! success "`200 OK`"
|
||||||
|
The logout process was successful.
|
||||||
|
|
||||||
|
!!! success "`202 Accepted`"
|
||||||
|
The logout process is not applicable for this authentication backend (e.g. JWT).
|
||||||
|
|
||||||
### `POST /forgot-password`
|
### `POST /forgot-password`
|
||||||
|
|
||||||
Request a reset password procedure. Will generate a temporary token and call the `on_after_forgot_password` [event handlers](../configuration/router.md#event-handlers) if the user exists.
|
Request a reset password procedure. Will generate a temporary token and call the `on_after_forgot_password` [event handlers](../configuration/router.md#event-handlers) if the user exists.
|
||||||
|
@ -28,3 +28,6 @@ class BaseAuthentication:
|
|||||||
|
|
||||||
async def get_login_response(self, user: BaseUserDB, response: Response) -> Any:
|
async def get_login_response(self, user: BaseUserDB, response: Response) -> Any:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def get_logout_response(self, user: BaseUserDB, response: Response) -> Any:
|
||||||
|
raise NotImplementedError()
|
||||||
|
@ -67,5 +67,10 @@ class CookieAuthentication(JWTAuthentication):
|
|||||||
# so that FastAPI can terminate it properly
|
# so that FastAPI can terminate it properly
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def get_logout_response(self, user: BaseUserDB, response: Response) -> Any:
|
||||||
|
response.delete_cookie(
|
||||||
|
self.cookie_name, path=self.cookie_path, domain=self.cookie_domain
|
||||||
|
)
|
||||||
|
|
||||||
async def _retrieve_token(self, request: Request) -> Optional[str]:
|
async def _retrieve_token(self, request: Request) -> Optional[str]:
|
||||||
return await self.api_key_cookie.__call__(request)
|
return await self.api_key_cookie.__call__(request)
|
||||||
|
@ -36,6 +36,21 @@ def _add_login_route(
|
|||||||
return await auth_backend.get_login_response(user, response)
|
return await auth_backend.get_login_response(user, response)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_logout_route(
|
||||||
|
router: EventHandlersRouter,
|
||||||
|
authenticator: Authenticator,
|
||||||
|
auth_backend: BaseAuthentication,
|
||||||
|
):
|
||||||
|
@router.post(f"/logout/{auth_backend.name}")
|
||||||
|
async def logout(
|
||||||
|
response: Response, user=Depends(authenticator.get_current_active_user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return await auth_backend.get_logout_response(user, response)
|
||||||
|
except NotImplementedError:
|
||||||
|
response.status_code = status.HTTP_202_ACCEPTED
|
||||||
|
|
||||||
|
|
||||||
def get_user_router(
|
def get_user_router(
|
||||||
user_db: BaseUserDatabase[models.BaseUserDB],
|
user_db: BaseUserDatabase[models.BaseUserDB],
|
||||||
user_model: Type[models.BaseUser],
|
user_model: Type[models.BaseUser],
|
||||||
@ -71,6 +86,7 @@ def get_user_router(
|
|||||||
|
|
||||||
for auth_backend in authenticator.backends:
|
for auth_backend in authenticator.backends:
|
||||||
_add_login_route(router, user_db, auth_backend)
|
_add_login_route(router, user_db, auth_backend)
|
||||||
|
_add_logout_route(router, authenticator, auth_backend)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
|
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
|
||||||
|
@ -25,3 +25,10 @@ class TestAuthenticate:
|
|||||||
async def test_get_login_response(base_authentication, user):
|
async def test_get_login_response(base_authentication, user):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
await base_authentication.get_login_response(user, Response())
|
await base_authentication.get_login_response(user, Response())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.authentication
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_logout_response(base_authentication, user):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
await base_authentication.get_logout_response(user, Response())
|
||||||
|
@ -132,3 +132,21 @@ async def test_get_login_response(
|
|||||||
cookie_value, SECRET, audience="fastapi-users:auth", algorithms=[JWT_ALGORITHM]
|
cookie_value, SECRET, audience="fastapi-users:auth", algorithms=[JWT_ALGORITHM]
|
||||||
)
|
)
|
||||||
assert decoded["user_id"] == user.id
|
assert decoded["user_id"] == user.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.authentication
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_logout_response(user):
|
||||||
|
response = Response()
|
||||||
|
logout_response = await cookie_authentication.get_logout_response(user, response)
|
||||||
|
|
||||||
|
# We shouldn't return directly the response
|
||||||
|
# so that FastAPI can terminate it properly
|
||||||
|
assert logout_response is None
|
||||||
|
|
||||||
|
cookies = [header for header in response.raw_headers if header[0] == b"set-cookie"]
|
||||||
|
assert len(cookies) == 1
|
||||||
|
|
||||||
|
cookie = cookies[0][1].decode("latin-1")
|
||||||
|
|
||||||
|
assert f"Max-Age=0" in cookie
|
||||||
|
@ -78,3 +78,10 @@ async def test_get_login_response(jwt_authentication, user):
|
|||||||
token, SECRET, audience="fastapi-users:auth", algorithms=[JWT_ALGORITHM]
|
token, SECRET, audience="fastapi-users:auth", algorithms=[JWT_ALGORITHM]
|
||||||
)
|
)
|
||||||
assert decoded["user_id"] == user.id
|
assert decoded["user_id"] == user.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.authentication
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_logout_response(jwt_authentication, user):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
await jwt_authentication.get_logout_response(user, Response())
|
||||||
|
@ -179,6 +179,25 @@ class TestLogin:
|
|||||||
assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.router
|
||||||
|
@pytest.mark.parametrize("path", ["/logout/mock", "/logout/mock-bis"])
|
||||||
|
class TestLogout:
|
||||||
|
def test_missing_token(self, path, test_app_client: TestClient):
|
||||||
|
response = test_app_client.post(path)
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
def test_unimplemented_logout(
|
||||||
|
self, mocker, path, test_app_client: TestClient, user: UserDB
|
||||||
|
):
|
||||||
|
get_logout_response_spy = mocker.spy(MockAuthentication, "get_logout_response")
|
||||||
|
response = test_app_client.post(
|
||||||
|
path, headers={"Authorization": f"Bearer {user.id}"}
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||||
|
|
||||||
|
get_logout_response_spy.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.router
|
@pytest.mark.router
|
||||||
class TestForgotPassword:
|
class TestForgotPassword:
|
||||||
def test_empty_body(self, test_app_client: TestClient, event_handler):
|
def test_empty_body(self, test_app_client: TestClient, event_handler):
|
||||||
|
Reference in New Issue
Block a user