From 96e244fd6c8846b2b5e66f59687d5cd0c773427a Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Wed, 15 May 2024 22:14:37 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(saml)=20use=20eduPersonPr?= =?UTF-8?q?incipalName=20rather=20than=20NameID=20for=20sub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NameID returned by the SAML server is actually transient so we can't use it as a sub. We'll use eduPersonPrincipalName both in sub and uid. --- Makefile | 8 ++- docker-compose.yml | 1 + src/satosa/internal_attributes.yaml | 5 +- .../microservices/primary_identifier.yaml | 22 ++++++ src/satosa/proxy_conf.yaml | 1 + src/satosa/tests/test_e2e.py | 67 ++++++++++++------- 6 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 src/satosa/plugins/microservices/primary_identifier.yaml diff --git a/Makefile b/Makefile index f37baec..935aeb8 100644 --- a/Makefile +++ b/Makefile @@ -131,7 +131,13 @@ oidc-test: ## open OIDC test client in browser @$(MAKE) down @$(MAKE) run python -mwebbrowser -n https://oidc-test-client.traefik.me -.PHONY: oidc-test +.phony: oidc-test + +e2e-test: ## automated end-to-end test in browser + @$(MAKE) down + @$(MAKE) run + TEST_E2E=1 pytest +.phony: e2e-test env.d/development/common: cp -n env.d/development/common.dist env.d/development/common diff --git a/docker-compose.yml b/docker-compose.yml index f7ef2ca..8356a03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: DOCKER_USER: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000} image: oidc2fer:development + pull_policy: never environment: PYLINTHOME: /app/.pylint.d BASE_URL: https://satosa.traefik.me diff --git a/src/satosa/internal_attributes.yaml b/src/satosa/internal_attributes.yaml index 30c22ff..51fa00f 100644 --- a/src/satosa/internal_attributes.yaml +++ b/src/satosa/internal_attributes.yaml @@ -22,8 +22,9 @@ attributes: acr: openid: - acr + eppn: + saml: + - eduPersonPrincipalName uid: openid: - uid - saml: - - eduPersonPrincipalName diff --git a/src/satosa/plugins/microservices/primary_identifier.yaml b/src/satosa/plugins/microservices/primary_identifier.yaml new file mode 100644 index 0000000..6616798 --- /dev/null +++ b/src/satosa/plugins/microservices/primary_identifier.yaml @@ -0,0 +1,22 @@ +module: satosa.micro_services.primary_identifier.PrimaryIdentifier +name: PrimaryIdentifier +config: + # The ordered identifier candidates are searched in order + # to find a candidate primary identifier. The search ends + # when the first candidate is found. The identifier or attribute + # names are the internal SATOSA names for the attributes as + # defined in internal_attributes.yaml. + ordered_identifier_candidates: + - attribute_names: [eppn] + + # The internal SATOSA attribute into which to place the primary + # identifier value once found from the above configured ordered + # candidates. + primary_identifier: uid + + # Whether or not to clear the input attributes after setting the + # primary identifier value. + clear_input_attributes: no + + # Whether to replace subject_id with the constructed primary identifier + replace_subject_id: yes diff --git a/src/satosa/proxy_conf.yaml b/src/satosa/proxy_conf.yaml index f992e2a..cf4ca86 100644 --- a/src/satosa/proxy_conf.yaml +++ b/src/satosa/proxy_conf.yaml @@ -12,6 +12,7 @@ FRONTEND_MODULES: - plugins/frontends/openid_connect_frontend.yaml - plugins/frontends/ping_frontend.yaml MICRO_SERVICES: + - plugins/microservices/primary_identifier.yaml - plugins/microservices/static_attributes.yaml - plugins/microservices/attribute_processor.yaml LOGGING: diff --git a/src/satosa/tests/test_e2e.py b/src/satosa/tests/test_e2e.py index 2f4867f..9e4656b 100644 --- a/src/satosa/tests/test_e2e.py +++ b/src/satosa/tests/test_e2e.py @@ -2,7 +2,7 @@ import json import os import pytest -from playwright.sync_api import Page, expect +from playwright.sync_api import Browser, BrowserContext, Page, expect @pytest.fixture(scope="session") @@ -22,17 +22,16 @@ def renater_wayf(page): page.get_by_role("button", name="Sélection").click() -@pytest.mark.skipif( - "TEST_E2E" not in os.environ, reason="Depends on app running locally" -) -def test_oidc_to_renater(page: Page): - page.goto("https://oidc-test-client.traefik.me") - renater_wayf(page) - renater_test_idp(page) +def oidc_to_renater(context: BrowserContext): + with context.new_page() as page: + page.goto("https://oidc-test-client.traefik.me") + renater_wayf(page) + renater_test_idp(page) + + expect(page.locator("pre")).to_contain_text('"usual_name":"Dupont"') + text = page.inner_text("pre") + result = json.loads(text) - expect(page.locator("pre")).to_contain_text('"usual_name":"Dupont"') - text = page.inner_text("pre") - result = json.loads(text) id_token = result["access_token_response"]["id_token"] assert {"acr": "eidas1"}.items() <= id_token.items() userinfo = result["userinfo"] @@ -42,6 +41,17 @@ def test_oidc_to_renater(page: Page): "uid": "etudiant1@test-renater.fr", "usual_name": "Dupont", }.items() <= userinfo.items() + return id_token + + +@pytest.mark.skipif( + "TEST_E2E" not in os.environ, reason="Depends on app running locally" +) +def test_oidc_to_renater(browser: Browser): + id_token1 = oidc_to_renater(browser.new_context()) + id_token2 = oidc_to_renater(browser.new_context()) + + assert id_token1["sub"] == id_token2["sub"] def agent_connect_login(page: Page): @@ -51,20 +61,29 @@ def agent_connect_login(page: Page): page.get_by_test_id("interaction-connection-button").click() +def agent_connect_to_renater(context: BrowserContext): + with context.new_page() as page: + agent_connect_login(page) + renater_wayf(page) + renater_test_idp(page) + + expect(page.locator("body")).to_contain_text("jean.dupont@formation.renater.fr") + text = page.inner_text("#json") + result = json.loads(text) + assert { + "email": "jean.dupont@formation.renater.fr", + "given_name": "Jean", + "uid": "etudiant1@test-renater.fr", + "usual_name": "Dupont", + }.items() <= result.items() + return result + + @pytest.mark.skipif( "TEST_E2E_AC" not in os.environ, reason="Depends on staging deployment" ) -def test_agent_connect_to_renater(page: Page): - agent_connect_login(page) - renater_wayf(page) - renater_test_idp(page) +def test_agent_connect_to_renater(browser: Browser): + result1 = agent_connect_to_renater(browser.new_context()) + result2 = agent_connect_to_renater(browser.new_context()) - expect(page.locator("body")).to_contain_text("jean.dupont@formation.renater.fr") - text = page.inner_text("#json") - result = json.loads(text) - assert { - "email": "jean.dupont@formation.renater.fr", - "given_name": "Jean", - "uid": "etudiant1@test-renater.fr", - "usual_name": "Dupont", - }.items() <= result.items() + assert result1["sub"] == result2["sub"]