Skip to content

Commit

Permalink
Implement logout_url parameter for CognitoOAuth
Browse files Browse the repository at this point in the history
- Add optional logout_url parameter, if set it will create an endpoint that handles logouts
- Extend end-to-end tests to test logout behavior
  • Loading branch information
MauriceBrg committed Apr 3, 2024
1 parent 9a952ba commit c754526
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 1 deletion.
47 changes: 46 additions & 1 deletion dash_cognito_auth/cognito_oauth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from urllib.parse import quote

from oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError
from dash import Dash
from flask import (
redirect,
request,
url_for,
Response,
session,
make_response,
)
from .cognito import make_cognito_blueprint, cognito

Expand All @@ -16,7 +20,14 @@ class CognitoOAuth(Auth):
Wraps a Dash App and adds Cognito based OAuth2 authentication.
"""

def __init__(self, app: Dash, domain: str, region=None, additional_scopes=None):
def __init__(
self,
app: Dash,
domain: str,
region=None,
additional_scopes=None,
logout_url: str = None,
):
"""
Wrap a Dash App with Cognito authentication.
Expand All @@ -43,6 +54,12 @@ def __init__(self, app: Dash, domain: str, region=None, additional_scopes=None):
AWS region of the User Pool. Mandatory if domain is NOT a custom domain, by default None
additional_scopes : Additional OAuth Scopes to request, optional
By default openid, email, and profile are requested - default value: None
logout_url : str, optional
Add a URL to the app that logs out the user when accessed via HTTP GET.
The URL automatically respects path prefixes, i.e. if your app is hosted
at example.com/some/prefix and you set logout_url to "logout", the actual
URL will be example.com/some/prefix/logout. By default, no logout URL is
added and you will have to create your own.
"""
super().__init__(app)

Expand All @@ -62,6 +79,34 @@ def __init__(self, app: Dash, domain: str, region=None, additional_scopes=None):

app.server.register_blueprint(cognito_bp, url_prefix=f"{dash_base_path}/login")

if logout_url is not None:
logout_url = (
dash_base_path.removesuffix("/") + "/" + logout_url.removeprefix("/")
)

cognito_hostname = (
f"{domain}.auth.{region}.amazoncognito.com"
if region is not None
else domain
)

@app.server.route(logout_url)
def handle_logout():

post_logout_redirect = (
request.host_url.removesuffix("/") + dash_base_path
)
cognito_logout_url = (
f"https://{cognito_hostname}/logout?"
+ f"client_id={cognito_bp.client_id}&logout_uri={quote(post_logout_redirect)}"
)

response = make_response(redirect(cognito_logout_url))

# Invalidate the session cookie
response.set_cookie("session", "empty", max_age=-3600)
return response

def is_authorized(self):
if not cognito.authorized:
# send to cognito login
Expand Down
14 changes: 14 additions & 0 deletions tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def end_to_end_app() -> CognitoOAuth:
dash_app,
domain=os.environ["COGNITO_DOMAIN"],
region=os.environ["COGNITO_REGION"],
logout_url="logout",
)
auth.app.server.config["COGNITO_OAUTH_CLIENT_ID"] = os.environ[
"COGNITO_OAUTH_CLIENT_ID"
Expand All @@ -70,6 +71,8 @@ def test_end_to_end(end_to_end_app: CognitoOAuth):
- Follow the redirect to the authorization endpoint
- Follow the redirect to the app home page (logged in)
- Check the /session-info endpoint to verify the correct user is logged in
- Call the /logout endpoint to end the current session
- Check a call to the homepage redirects us to the local cognito endpoint
"""

# Arrange
Expand Down Expand Up @@ -121,3 +124,14 @@ def test_end_to_end(end_to_end_app: CognitoOAuth):
# Verify that the logged in users' email matches the one from the env
session_info_response = client.get("/session-info")
assert session_info_response.json["email"] == os.environ["COGNITO_EMAIL"]

# Log out
logout_response = client.get("/logout")
assert logout_response.status_code == HTTPStatus.FOUND
assert "/logout" in logout_response.location

# Since we're not longer logged in, we should be redirected to the local
# Cognito endpoint.
homepage_response = client.get("/")
assert homepage_response.status_code == HTTPStatus.FOUND
assert homepage_response.location == "/login/cognito"
14 changes: 14 additions & 0 deletions tests/test_end_to_end_with_path_prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def end_to_end_app_with_prefix() -> CognitoOAuth:
dash_app,
domain=os.environ["COGNITO_DOMAIN"],
region=os.environ["COGNITO_REGION"],
logout_url="logout",
)
auth.app.server.config["COGNITO_OAUTH_CLIENT_ID"] = os.environ[
"COGNITO_OAUTH_CLIENT_ID"
Expand Down Expand Up @@ -77,6 +78,8 @@ def test_end_to_end_with_path_prefix(end_to_end_app_with_prefix: CognitoOAuth):
- Follow the redirect to the authorization endpoint
- Follow the redirect to the app home page (logged in)
- Check the /session-info endpoint to verify the correct user is logged in
- Call the /some/prefix/logout endpoint to end the current session
- Check a call to the homepage redirects us to the local cognito endpoint
"""

# Arrange
Expand Down Expand Up @@ -129,3 +132,14 @@ def test_end_to_end_with_path_prefix(end_to_end_app_with_prefix: CognitoOAuth):
# Verify that the logged in users' email matches the one from the env
session_info_response = client.get("/some/prefix/session-info")
assert session_info_response.json["email"] == os.environ["COGNITO_EMAIL"]

# Log out
logout_response = client.get("/some/prefix/logout")
assert logout_response.status_code == HTTPStatus.FOUND
assert "/logout" in logout_response.location

# Since we're not longer logged in, we should be redirected to the local
# Cognito endpoint.
homepage_response = client.get("/some/prefix/")
assert homepage_response.status_code == HTTPStatus.FOUND
assert homepage_response.location == "/some/prefix/login/cognito"

0 comments on commit c754526

Please sign in to comment.