diff --git a/CHANGES.rst b/CHANGES.rst index a86e5811..8e7de95b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ Fixed ***** - Correctly set up Client audience during OIDC dynamic registration. +- ``post_logout_redirect_uris`` was ignored during OIDC dynamic registration. [0.0.40] - 2023-12-22 ===================== diff --git a/canaille/oidc/endpoints.py b/canaille/oidc/endpoints.py index e09dc1a1..6c1738ca 100644 --- a/canaille/oidc/endpoints.py +++ b/canaille/oidc/endpoints.py @@ -3,6 +3,7 @@ import uuid from authlib.integrations.flask_oauth2 import current_token from authlib.jose import jwt +from authlib.jose.errors import JoseError from authlib.oauth2 import OAuth2Error from canaille import csrf from canaille.app import models @@ -273,9 +274,18 @@ def end_session(): return render_template("logout.html", form=form, client=client, menu=False) if data.get("id_token_hint"): - id_token = jwt.decode( - data["id_token_hint"], current_app.config["OIDC"]["JWT"]["PUBLIC_KEY"] - ) + try: + id_token = jwt.decode( + data["id_token_hint"], current_app.config["OIDC"]["JWT"]["PUBLIC_KEY"] + ) + except JoseError as exc: + return jsonify( + { + "status": "error", + "message": str(exc), + } + ) + if not id_token["iss"] == get_issuer(): return jsonify( { diff --git a/canaille/oidc/oauth.py b/canaille/oidc/oauth.py index 1a104224..248dc8b6 100644 --- a/canaille/oidc/oauth.py +++ b/canaille/oidc/oauth.py @@ -398,6 +398,10 @@ class ClientRegistrationEndpoint(ClientManagementMixin, _ClientRegistrationEndpo def save_client(self, client_info, client_metadata, request): client = models.Client( + # this won't be needed when OIDC RP Initiated Logout is + # directly implemented in authlib: + # https://gitlab.com/yaal/canaille/-/issues/157 + post_logout_redirect_uris=request.data.get("post_logout_redirect_uris"), **self.client_convert_data(**client_info, **client_metadata) ) client.audience = [client] diff --git a/demo/client/__init__.py b/demo/client/__init__.py index b1345892..937b1b51 100644 --- a/demo/client/__init__.py +++ b/demo/client/__init__.py @@ -13,23 +13,10 @@ from flask import session from flask import url_for -def create_app(): - app = Flask(__name__) - app.config.from_envvar("CONFIG") - app.static_folder = "../../canaille/static" +oauth = OAuth() - oauth = OAuth() - oauth.init_app(app) - oauth.register( - name="canaille", - client_id=app.config["OAUTH_CLIENT_ID"], - client_secret=app.config["OAUTH_CLIENT_SECRET"], - server_metadata_url=get_well_known_url( - app.config["OAUTH_AUTH_SERVER"], external=True - ), - client_kwargs={"scope": "openid profile email phone address groups"}, - ) +def setup_routes(app): @app.route("/") @app.route("/tos") @app.route("/policy") @@ -40,10 +27,12 @@ def create_app(): @app.route("/login") def login(): - return oauth.canaille.authorize_redirect(url_for("authorize", _external=True)) + return oauth.canaille.authorize_redirect( + url_for("login_callback", _external=True) + ) - @app.route("/authorize") - def authorize(): + @app.route("/login_callback") + def login_callback(): try: token = oauth.canaille.authorize_access_token() session["user"] = token.get("userinfo") @@ -56,13 +45,6 @@ def create_app(): @app.route("/logout") def logout(): - try: - del session["user"] - except KeyError: - pass - - flash("You have been successfully logged out", "success") - oauth.canaille.load_server_metadata() end_session_endpoint = oauth.canaille.server_metadata.get( "end_session_endpoint" @@ -71,10 +53,41 @@ def create_app(): end_session_endpoint, client_id=current_app.config["OAUTH_CLIENT_ID"], id_token_hint=session["id_token"], - post_logout_redirect_uri=url_for("index", _external=True), + post_logout_redirect_uri=url_for("logout_callback", _external=True), ) return redirect(end_session_url) + @app.route("/logout_callback") + def logout_callback(): + try: + del session["user"] + except KeyError: + pass + + flash("You have been successfully logged out", "success") + return redirect(url_for("index")) + + +def setup_oauth(app): + oauth.init_app(app) + oauth.register( + name="canaille", + client_id=app.config["OAUTH_CLIENT_ID"], + client_secret=app.config["OAUTH_CLIENT_SECRET"], + server_metadata_url=get_well_known_url( + app.config["OAUTH_AUTH_SERVER"], external=True + ), + client_kwargs={"scope": "openid profile email phone address groups"}, + ) + + +def create_app(): + app = Flask(__name__) + app.config.from_envvar("CONFIG") + app.static_folder = "../../canaille/static" + + setup_routes(app) + setup_oauth(app) return app diff --git a/demo/conf/client1.cfg b/demo/conf/client1.cfg index 076b78a6..bd788d46 100644 --- a/demo/conf/client1.cfg +++ b/demo/conf/client1.cfg @@ -1,6 +1,7 @@ SECRET_KEY="46bf9fb5-88d5-489b-9312-899588377ff0" NAME = "Client 1" SESSION_COOKIE_NAME="client1-session" +SERVER_NAME="localhost:5001" OAUTH_CLIENT_ID="1JGkkzCbeHpGtlqgI5EENByf" OAUTH_CLIENT_SECRET="2xYPSReTQRmGG1yppMVZQ0ASXwFejPyirvuPbKhNa6TmKC5x" diff --git a/demo/conf/client2.cfg b/demo/conf/client2.cfg index 8bc45fe3..22836182 100644 --- a/demo/conf/client2.cfg +++ b/demo/conf/client2.cfg @@ -1,6 +1,7 @@ SECRET_KEY="8e953ecc-13be-497b-806f-c65faa1e328f" NAME = "Client 2" SESSION_COOKIE_NAME="client2-session" +SERVER_NAME="localhost:5002" OAUTH_CLIENT_ID="gn4yFN7GDykL7QP8v8gS9YfV" OAUTH_CLIENT_SECRET="ouFJE5WpICt6hxTyf8icXPeeklMektMY4gV0Rmf3aY60VElA" diff --git a/demo/demoapp.py b/demo/demoapp.py index f57d6542..97921c11 100644 --- a/demo/demoapp.py +++ b/demo/demoapp.py @@ -107,8 +107,8 @@ def populate(app): client_name="Client1", contacts=["admin@mydomain.tld"], client_uri="http://localhost:5001", - redirect_uris=["http://localhost:5001/authorize"], - post_logout_redirect_uris=["http://localhost:5001/"], + redirect_uris=["http://localhost:5001/login_callback"], + post_logout_redirect_uris=["http://localhost:5001/logout_callback"], tos_uri="http://localhost:5001/tos", policy_uri="http://localhost:5001/policy", grant_types=["authorization_code", "refresh_token"], @@ -126,8 +126,8 @@ def populate(app): client_name="Client2", contacts=["admin@mydomain.tld"], client_uri="http://localhost:5002", - redirect_uris=["http://localhost:5002/authorize"], - post_logout_redirect_uris=["http://localhost:5002/"], + redirect_uris=["http://localhost:5002/login_callback"], + post_logout_redirect_uris=["http://localhost:5002/logout_callback"], tos_uri="http://localhost:5002/tos", policy_uri="http://localhost:5002/policy", grant_types=["authorization_code", "refresh_token"], diff --git a/tests/oidc/test_dynamic_client_registration.py b/tests/oidc/test_dynamic_client_registration.py index 944cd113..ab1b3361 100644 --- a/tests/oidc/test_dynamic_client_registration.py +++ b/tests/oidc/test_dynamic_client_registration.py @@ -19,6 +19,9 @@ def test_client_registration_with_authentication_static_token( "https://client.example.org/callback", "https://client.example.org/callback2", ], + "post_logout_redirect_uris": [ + "https://client.example.org/logout_callback", + ], "client_name": "My Example Client", "token_endpoint_auth_method": "client_secret_basic", "logo_uri": "https://client.example.org/logo.webp", @@ -53,6 +56,9 @@ def test_client_registration_with_authentication_static_token( "https://client.example.org/callback", "https://client.example.org/callback2", ] + assert client.post_logout_redirect_uris == [ + "https://client.example.org/logout_callback", + ] assert client.token_endpoint_auth_method == "client_secret_basic" assert client.logo_uri == "https://client.example.org/logo.webp" assert client.jwks_uri == "https://client.example.org/my_public_keys.jwks" diff --git a/tests/oidc/test_end_session.py b/tests/oidc/test_end_session.py index 8038d98e..fdab5aca 100644 --- a/tests/oidc/test_end_session.py +++ b/tests/oidc/test_end_session.py @@ -284,6 +284,22 @@ def test_client_hint_mismatch(testclient, backend, logged_user, client): } +def test_end_session_bad_id_token(testclient, backend, logged_user, client, id_token): + post_logout_redirect_url = "https://mydomain.tld/disconnected" + res = testclient.get( + "/oauth/end_session", + params={ + "id_token_hint": "invalid", + "logout_hint": logged_user.identifier, + "client_id": client.client_id, + "post_logout_redirect_uri": post_logout_redirect_url, + "state": "foobar", + }, + ) + + assert res.json == {"status": "error", "message": "Invalid input segments length: "} + + def test_bad_user_id_token_mismatch(testclient, backend, logged_user, client, admin): testclient.get(f"/profile/{logged_user.identifier}", status=200)