From 9ff0411e9e873187cf5dc2f5f0c88ad2aef884ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 22 Dec 2023 21:18:02 +0100 Subject: [PATCH] tests: extracted the prompt tests in a dedicated file --- canaille/oidc/endpoints.py | 17 ++-- canaille/oidc/well_known.py | 1 + tests/oidc/test_authorization_code_flow.py | 68 -------------- tests/oidc/test_authorization_prompt.py | 100 +++++++++++++++++++++ 4 files changed, 111 insertions(+), 75 deletions(-) create mode 100644 tests/oidc/test_authorization_prompt.py diff --git a/canaille/oidc/endpoints.py b/canaille/oidc/endpoints.py index b402c954..2796cac8 100644 --- a/canaille/oidc/endpoints.py +++ b/canaille/oidc/endpoints.py @@ -55,9 +55,8 @@ def authorize(): abort(400, "Invalid client.") user = current_user() - scopes = client.get_allowed_scope(request.args.get("scope", "").split(" ")).split( - " " - ) + requested_scopes = request.args.get("scope", "").split(" ") + allowed_scopes = client.get_allowed_scope(requested_scopes).split(" ") # LOGIN @@ -69,7 +68,9 @@ def authorize(): return redirect(url_for("core.auth.login")) if not user.can_use_oidc: - abort(403, "User does not have the permission to achieve OIDC authentication.") + abort( + 403, "The user does not have the permission to achieve OIDC authentication." + ) # CONSENT @@ -82,7 +83,9 @@ def authorize(): if request.method == "GET": if ( (client.preconsent and (not consent or not consent.revoked)) - or (consent and all(scope in set(consent.scope) for scope in scopes)) + or ( + consent and all(scope in set(consent.scope) for scope in allowed_scopes) + ) and not consent.revoked ): return authorization.create_authorization_response(grant_user=user) @@ -125,14 +128,14 @@ def authorize(): if consent.revoked: consent.restore() consent.scope = client.get_allowed_scope( - list(set(scopes + consents[0].scope)) + list(set(allowed_scopes + consents[0].scope)) ).split(" ") else: consent = models.Consent( consent_id=str(uuid.uuid4()), client=client, subject=user, - scope=scopes, + scope=allowed_scopes, issue_date=datetime.datetime.now(datetime.timezone.utc), ) consent.save() diff --git a/canaille/oidc/well_known.py b/canaille/oidc/well_known.py index 972be97a..72704341 100644 --- a/canaille/oidc/well_known.py +++ b/canaille/oidc/well_known.py @@ -76,6 +76,7 @@ def openid_configuration(): ], "subject_types_supported": ["pairwise", "public"], "id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"], + "prompt_values_supported": ["none"], } diff --git a/tests/oidc/test_authorization_code_flow.py b/tests/oidc/test_authorization_code_flow.py index b9b968ee..8af21eb5 100644 --- a/tests/oidc/test_authorization_code_flow.py +++ b/tests/oidc/test_authorization_code_flow.py @@ -1,4 +1,3 @@ -import uuid from urllib.parse import parse_qs from urllib.parse import urlsplit @@ -610,73 +609,6 @@ def test_authorization_code_flow_but_user_cannot_use_oidc( res = res.follow(status=403) -def test_prompt_none(testclient, logged_user, client): - consent = models.Consent( - consent_id=str(uuid.uuid4()), - client=client, - subject=logged_user, - scope=["openid", "profile"], - ) - consent.save() - - res = testclient.get( - "/oauth/authorize", - params=dict( - response_type="code", - client_id=client.client_id, - scope="openid profile", - nonce="somenonce", - prompt="none", - ), - status=302, - ) - assert res.location.startswith(client.redirect_uris[0]) - params = parse_qs(urlsplit(res.location).query) - assert "code" in params - - consent.delete() - - -def test_prompt_not_logged(testclient, user, client): - consent = models.Consent( - consent_id=str(uuid.uuid4()), - client=client, - subject=user, - scope=["openid", "profile"], - ) - consent.save() - - res = testclient.get( - "/oauth/authorize", - params=dict( - response_type="code", - client_id=client.client_id, - scope="openid profile", - nonce="somenonce", - prompt="none", - ), - status=200, - ) - assert "login_required" == res.json.get("error") - - consent.delete() - - -def test_prompt_no_consent(testclient, logged_user, client): - res = testclient.get( - "/oauth/authorize", - params=dict( - response_type="code", - client_id=client.client_id, - scope="openid profile", - nonce="somenonce", - prompt="none", - ), - status=200, - ) - assert "consent_required" == res.json.get("error") - - def test_nonce_required_in_oidc_requests(testclient, logged_user, client): res = testclient.get( "/oauth/authorize", diff --git a/tests/oidc/test_authorization_prompt.py b/tests/oidc/test_authorization_prompt.py new file mode 100644 index 00000000..ac337843 --- /dev/null +++ b/tests/oidc/test_authorization_prompt.py @@ -0,0 +1,100 @@ +""" +Tests the behavior of Canaille depending on the OIDC 'prompt' parameter. +https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint +""" +import uuid +from urllib.parse import parse_qs +from urllib.parse import urlsplit + +from canaille.app import models + + +def test_prompt_none(testclient, logged_user, client): + """ + Nominal case with prompt=none + """ + consent = models.Consent( + consent_id=str(uuid.uuid4()), + client=client, + subject=logged_user, + scope=["openid", "profile"], + ) + consent.save() + + res = testclient.get( + "/oauth/authorize", + params=dict( + response_type="code", + client_id=client.client_id, + scope="openid profile", + nonce="somenonce", + prompt="none", + ), + status=302, + ) + assert res.location.startswith(client.redirect_uris[0]) + params = parse_qs(urlsplit(res.location).query) + assert "code" in params + + consent.delete() + + +def test_prompt_not_logged(testclient, user, client): + """ + prompt=none should return a login_required error when no + user is logged in. + + login_required + The Authorization Server requires End-User authentication. + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request + cannot be completed without displaying a user interface for End-User + authentication. + """ + consent = models.Consent( + consent_id=str(uuid.uuid4()), + client=client, + subject=user, + scope=["openid", "profile"], + ) + consent.save() + + res = testclient.get( + "/oauth/authorize", + params=dict( + response_type="code", + client_id=client.client_id, + scope="openid profile", + nonce="somenonce", + prompt="none", + ), + status=200, + ) + assert "login_required" == res.json.get("error") + + consent.delete() + + +def test_prompt_no_consent(testclient, logged_user, client): + """ + prompt=none should return a consent_required error when user + are logged in but have not granted their consent. + + consent_required + The Authorization Server requires End-User consent. This error MAY be + returned when the prompt parameter value in the Authentication Request + is none, but the Authentication Request cannot be completed without + displaying a user interface for End-User consent. + """ + res = testclient.get( + "/oauth/authorize", + params=dict( + response_type="code", + client_id=client.client_id, + scope="openid profile", + nonce="somenonce", + prompt="none", + ), + status=200, + ) + assert "consent_required" == res.json.get("error")