From 684c461e54b09dd169b0cb5b645a23415e70e555 Mon Sep 17 00:00:00 2001 From: Maarten de Waard Date: Thu, 12 May 2022 11:49:15 +0200 Subject: [PATCH] implement logout endpoint to be called by Hydra on logout --- .gitignore | 1 + web/login/login.py | 94 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 5570a53..fbc8c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ *.swp .envrc .direnv +run_app.local.sh diff --git a/web/login/login.py b/web/login/login.py index 7874f8e..d0c01d2 100644 --- a/web/login/login.py +++ b/web/login/login.py @@ -6,9 +6,10 @@ the user entries in the database(s)""" import urllib.parse import urllib.request +import ast + import hydra_client import ory_kratos_client -import ast from ory_kratos_client.api import v0alpha2_api as kratos_api from flask import abort, redirect, render_template, request, current_app @@ -75,7 +76,8 @@ def settings(): def error(): """Show error messages from Kratos - Implements user-facing errors as described in https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors + Implements user-facing errors as described in + https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors :param id: error ID as given by Kratos :return: redirect or settings page @@ -87,7 +89,9 @@ def error(): # Get Self-Service Errors api_response = KRATOS_ADMIN.get_self_service_error(error_id) except ory_kratos_client.ApiException as ex: - print("Exception when calling V0alpha2Api->get_self_service_error: %s\n" % ex) + current_app.logger.error( + "Exception when calling V0alpha2Api->get_self_service_error: %s\n", + ex) return render_template("error.html", error_message=api_response) @@ -229,11 +233,11 @@ def consent(): current_app.logger.error(f"Info: Found kratos_id {kratos_id}") current_app.logger.error(f"Info: Found app_id {app_id}") - except Exception as error: + except Exception as ex: current_app.logger.error( - f"Error: Unable to extract information from consent request" + "Error: Unable to extract information from consent request" ) - current_app.logger.error(f"Error: {error}") + current_app.logger.error(f"Error: {ex}") current_app.logger.error(f"Client: {consent_request.client}") current_app.logger.error(f"Subject: {consent_request.subject}") abort(501, description="Internal error occured") @@ -301,11 +305,8 @@ def get_auth(): :return: Profile or False if not logged in """ - try: - cookie = request.cookies.get("ory_kratos_session") - cookie = "ory_kratos_session=" + cookie - except TypeError: - current_app.logger.info("User not logged in or cookie corrupted") + cookie = get_kratos_cookie() + if not cookie: return False # Given a cookie, check if it is valid and get the profile @@ -315,9 +316,76 @@ def get_auth(): # Get all traits from ID return api_response.identity - except ory_kratos_client.ApiException as error: + except ory_kratos_client.ApiException as ex: current_app.logger.error( - f"Exception when calling V0alpha2Api->to_session(): {error}\n" + f"Exception when calling V0alpha2Api->to_session(): {ex}\n" ) return False + +def get_kratos_cookie(): + """Retrieves the Kratos cookie from the session. + + Returns False if the cookie does not exist or is corrupted. + """ + try: + cookie = request.cookies.get("ory_kratos_session") + cookie = "ory_kratos_session=" + cookie + except TypeError: + current_app.logger.info("User not logged in or cookie corrupted") + cookie = False + return cookie + + +@web.route("/logout", methods=["GET"]) +def logout(): + """Handles the Hydra OpenID Connect Logout flow as well as the Kratos + logout flow + + Steps: + + 1. Hydra's /oauth2/sessions/logout endpoint is called by an application + 2. Hydra calls this endpoint with a `logout_challenge` get parameter + 3. We retrieve the logout request using the challenge + 4. We retrieve the Kratos cookie from the browser + 5. We generate a Kratos logout URL + 6. We accept the Hydra logout request + 7. We redirect to the Kratos logout URL + + Args: + logout_challenge (string): Reference to a Hydra logout challenge object + + Returns: + Redirect to the url that is provided by the LogoutRequest object. + """ + challenge = request.args.get("logout_challenge") + current_app.logger.info("Logout request: challenge=%s", challenge) + if not challenge: + abort(403) + try: + logout_request = HYDRA.logout_request(challenge) + except hydra_client.exceptions.NotFound: + current_app.logger.error("Logout request with challenge '%s' not found", challenge) + abort(404, "Hydra session invalid or not found") + except hydra_client.exceptions.HTTPError: + current_app.logger.error( + "Conflict. Logout request with challenge '%s' has been used already.", + challenge) + abort(503) + + kratos_cookie = get_kratos_cookie() + if not kratos_cookie: + abort(404, "Kratos session invalid or not found") + try: + # Create a Logout URL for Browsers + kratos_api_response = \ + KRATOS_ADMIN.create_self_service_logout_flow_url_for_browsers( + cookie=kratos_cookie) + current_app.logger.info(kratos_api_response) + except ory_kratos_client.ApiException as ex: + current_app.logger.error("Exception when calling" + " V0alpha2Api->create_self_service_logout_flow_url_for_browsers: %s\n", + ex) + hydra_return = logout_request.accept(subject=logout_request.subject) + current_app.logger.info("Hydra info: %s", hydra_return) + return redirect(kratos_api_response.logout_url)