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

[Issue 2648] Add signout endpoint #2997

Merged
merged 13 commits into from
Nov 22, 2024
34 changes: 34 additions & 0 deletions api/openapi.generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ paths:
summary: User Token
security:
- ApiKeyAuth: []
/v1/users/token/logout:
post:
parameters: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UserTokenLogoutResponse'
description: Successful response
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Authentication error
tags:
- User v1
summary: User Token Logout
security:
- ApiJwtAuth: []
/v1/opportunities/search:
post:
parameters: []
Expand Down Expand Up @@ -645,6 +666,19 @@ components:
type: integer
description: The HTTP status code
example: 200
UserTokenLogoutResponse:
type: object
properties:
message:
type: string
description: The message to return
example: Success
data:
example: null
status_code:
type: integer
description: The HTTP status code
example: 200
FundingInstrumentFilterV1:
type: object
properties:
Expand Down
29 changes: 29 additions & 0 deletions api/src/api/users/user_routes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import logging

from src.adapters import db
from src.adapters.db import flask_db
from src.api import response
from src.api.route_utils import raise_flask_error
from src.api.users import user_schemas
from src.api.users.user_blueprint import user_blueprint
from src.api.users.user_schemas import UserTokenLogoutResponseSchema
from src.auth.api_jwt_auth import api_jwt_auth
from src.auth.api_key_auth import api_key_auth
from src.db.models.user_models import UserTokenSession

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -34,3 +39,27 @@ def user_token(x_oauth_login_gov: dict) -> response.ApiResponse:
logger.info(message)

raise_flask_error(400, message)


@user_blueprint.post("/token/logout")
@user_blueprint.output(UserTokenLogoutResponseSchema)
@user_blueprint.doc(responses=[200, 401])
@user_blueprint.auth_required(api_jwt_auth)
@flask_db.with_db_session()
def user_token_logout(db_session: db.Session) -> response.ApiResponse:
logger.info("POST /v1/users/token/logout")

user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore
with db_session.begin():
user_token_session.is_valid = False
db_session.add(user_token_session)

logger.info(
"Logged out a user",
extra={
"user_token_session.token_id": str(user_token_session.token_id),
"user_token_session.user_id": str(user_token_session.user_id),
},
)

return response.ApiResponse(message="Success")
5 changes: 5 additions & 0 deletions api/src/api/users/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ class UserTokenSchema(Schema):

class UserTokenResponseSchema(AbstractResponseSchema):
data = fields.Nested(UserTokenSchema)


class UserTokenLogoutResponseSchema(AbstractResponseSchema):
# No data returned
data = fields.MixinField(metadata={"example": None})
33 changes: 33 additions & 0 deletions api/tests/src/api/users/test_user_route_token.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from src.auth.api_jwt_auth import create_jwt_for_user
from tests.src.db.models.factories import UserFactory

##################
# POST /token
##################
Expand Down Expand Up @@ -25,3 +28,33 @@ def test_post_user_route_token_400(client, api_auth_token):
resp = client.post("v1/users/token", headers={"X-Auth": api_auth_token})
assert resp.status_code == 400
assert resp.get_json()["message"] == "Missing X-OAuth-login-gov header"


def test_post_user_route_token_logout_200(
enable_factory_create, client, db_session, api_auth_token
):
user = UserFactory.create()
token, user_token_session = create_jwt_for_user(user, db_session)
db_session.commit()

resp = client.post("v1/users/token/logout", headers={"X-SGG-Token": token})

db_session.refresh(user_token_session)

assert resp.status_code == 200
assert not user_token_session.is_valid


def test_post_user_route_token_logout_invalid(
enable_factory_create, client, db_session, api_auth_token
):
user = UserFactory.create()

token, session = create_jwt_for_user(user, db_session)
session.is_valid = False
db_session.commit()

resp = client.post("v1/users/token/logout", headers={"X-SGG-Token": token})

assert resp.status_code == 401
assert resp.get_json()["message"] == "Token is no longer valid"