From 82e469472af77d70c1f06f7e0e39e3afd808fee3 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Wed, 4 Sep 2024 18:19:28 -0400 Subject: [PATCH 01/12] Added new config property --- pom.xml | 2 +- ramls/schemas/SamlConfig.json | 5 +++++ ramls/schemas/SamlConfigRequest.json | 5 +++++ .../java/org/folio/config/model/SamlConfiguration.java | 7 +++++++ src/main/java/org/folio/rest/impl/SamlAPI.java | 6 ++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f5b3ef84..0c6df64f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.8.1 + 2.8.2-SNAPSHOT mod-login-saml diff --git a/ramls/schemas/SamlConfig.json b/ramls/schemas/SamlConfig.json index 8f6f4508..3b9262e6 100644 --- a/ramls/schemas/SamlConfig.json +++ b/ramls/schemas/SamlConfig.json @@ -43,6 +43,11 @@ "description": "Where the IDP should call back after login is successful. Either callback or callback-with-expiry. Defaults to callback-with-expiry if not present.", "type": "string", "required": false + }, + "useSecureTokens": { + "type": "boolean", + "description": "When present, and true, and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", + "required": false } } } diff --git a/ramls/schemas/SamlConfigRequest.json b/ramls/schemas/SamlConfigRequest.json index 9000fb27..e121f362 100644 --- a/ramls/schemas/SamlConfigRequest.json +++ b/ramls/schemas/SamlConfigRequest.json @@ -43,6 +43,11 @@ "description": "Where the IDP should call back after login is successful. Either callback or callback-with-expiry. Defaults to callback-with-expiry if not present.", "type": "string", "required": false + }, + "useSecureTokens": { + "type": "boolean", + "description": "When present, and true, and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", + "required": false } } } diff --git a/src/main/java/org/folio/config/model/SamlConfiguration.java b/src/main/java/org/folio/config/model/SamlConfiguration.java index ff820786..127581a9 100644 --- a/src/main/java/org/folio/config/model/SamlConfiguration.java +++ b/src/main/java/org/folio/config/model/SamlConfiguration.java @@ -22,6 +22,7 @@ public class SamlConfiguration { public static final String METADATA_INVALIDATED_CODE = "metadata.invalidated"; public static final String OKAPI_URL= "okapi.url"; public static final String SAML_CALLBACK = "saml.callback"; + public static final String SAML_USE_SECURE_TOKENS = "saml.useSecureTokens"; @JsonProperty(IDP_URL_CODE) private String idpUrl; @@ -45,6 +46,8 @@ public class SamlConfiguration { private String okapiUrl; @JsonProperty(SAML_CALLBACK) private String callback; + @JsonProperty(SAML_USE_SECURE_TOKENS) + private String useSecureTokens; public String getIdpUrl() { @@ -131,4 +134,8 @@ public void setIdpMetadata(String idpMetadata) { public void setCallback(String callback) { this.callback = callback; } + + public String getUseSecureTokens() { return useSecureTokens; } + + public void setUseSecureTokens(String useSecureTokens) { this.useSecureTokens = useSecureTokens; } } diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 3b8087f3..00394296 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -469,6 +469,11 @@ public void putSamlConfiguration(SamlConfigRequest updatedConfig, RoutingContext ConfigEntryUtil.valueChanged(config.getCallback(), updatedConfig.getCallback(), callback -> updateEntries.put(SamlConfiguration.SAML_CALLBACK, callback)); + // TODO Need to convert the SamlConfigRequest boolean to a string "true" or "false" or null. Must be one of those. + ConfigEntryUtil.valueChanged(config.getUseSecureTokens(), null, useSecureTokens -> + updateEntries.put(SamlConfiguration.SAML_USE_SECURE_TOKENS, useSecureTokens)); + + // TODO This returns a 404 if updatedEntries has something added by the code above rather than by the SamlConfigRequest return storeConfigEntries(rc, parsedHeaders, updateEntries, vertxContext); }) .onFailure(cause -> { @@ -608,6 +613,7 @@ private SamlConfig configToDto(SamlConfiguration config) { .withSamlAttribute(config.getSamlAttribute()) .withUserProperty(config.getUserProperty()) .withCallback(config.getCallback()) + .withUseSecureTokens(Boolean.valueOf(config.getUseSecureTokens())) .withMetadataInvalidated(Boolean.valueOf(config.getMetadataInvalidated())); try { URI uri = URI.create(config.getOkapiUrl()); From f20ee67b297dd8a5185c78c449c09dd37cc8dfd7 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Thu, 5 Sep 2024 14:09:56 -0400 Subject: [PATCH 02/12] Adds all the config combinations --- ramls/schemas/SamlConfigRequest.json | 2 +- .../java/org/folio/rest/impl/SamlAPI.java | 6 +- .../java/org/folio/util/ConfigEntryUtil.java | 13 +- .../java/org/folio/rest/impl/SamlAPITest.java | 50 +++- src/test/resources/mock_content.json | 2 +- src/test/resources/mock_content_legacy.json | 2 +- .../resources/mock_content_secure_tokens.json | 239 ++++++++++++++++++ .../resources/mock_content_with_metadata.json | 5 +- .../mock_content_with_metadata_legacy.json | 5 +- src/test/resources/mock_idptest_post.json | 5 +- .../resources/mock_idptest_post_legacy.json | 2 +- .../mock_idptest_post_secure_tokens.json | 113 +++++++++ src/test/resources/mock_idptest_redirect.json | 2 +- .../mock_idptest_redirect_legacy.json | 2 +- .../mock_idptest_redirect_secure_tokens.json | 113 +++++++++ 15 files changed, 544 insertions(+), 17 deletions(-) create mode 100644 src/test/resources/mock_content_secure_tokens.json create mode 100644 src/test/resources/mock_idptest_post_secure_tokens.json create mode 100644 src/test/resources/mock_idptest_redirect_secure_tokens.json diff --git a/ramls/schemas/SamlConfigRequest.json b/ramls/schemas/SamlConfigRequest.json index e121f362..1913261a 100644 --- a/ramls/schemas/SamlConfigRequest.json +++ b/ramls/schemas/SamlConfigRequest.json @@ -46,7 +46,7 @@ }, "useSecureTokens": { "type": "boolean", - "description": "When present, and true, and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", + "description": "When present, and 'true', and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", "required": false } } diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 00394296..d77861bb 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -469,11 +469,9 @@ public void putSamlConfiguration(SamlConfigRequest updatedConfig, RoutingContext ConfigEntryUtil.valueChanged(config.getCallback(), updatedConfig.getCallback(), callback -> updateEntries.put(SamlConfiguration.SAML_CALLBACK, callback)); - // TODO Need to convert the SamlConfigRequest boolean to a string "true" or "false" or null. Must be one of those. - ConfigEntryUtil.valueChanged(config.getUseSecureTokens(), null, useSecureTokens -> + ConfigEntryUtil.valueChanged(config.getUseSecureTokens(), updatedConfig.getUseSecureTokens(), useSecureTokens -> updateEntries.put(SamlConfiguration.SAML_USE_SECURE_TOKENS, useSecureTokens)); - // TODO This returns a 404 if updatedEntries has something added by the code above rather than by the SamlConfigRequest return storeConfigEntries(rc, parsedHeaders, updateEntries, vertxContext); }) .onFailure(cause -> { @@ -486,6 +484,8 @@ public void putSamlConfiguration(SamlConfigRequest updatedConfig, RoutingContext }); } + + private Future storeConfigEntries(RoutingContext rc, OkapiHeaders parsedHeaders, Map updateEntries, Context vertxContext) { diff --git a/src/main/java/org/folio/util/ConfigEntryUtil.java b/src/main/java/org/folio/util/ConfigEntryUtil.java index 1144b80c..7bfeeae4 100644 --- a/src/main/java/org/folio/util/ConfigEntryUtil.java +++ b/src/main/java/org/folio/util/ConfigEntryUtil.java @@ -33,7 +33,18 @@ public static void valueChanged(String oldValue, String newValue, Consumer onChanged) { + Objects.requireNonNull(onChanged); + + String newValueString = (newValue == null) ? null : newValue.toString(); + + if (valueChanged(oldValue, newValueString)) { + onChanged.accept(newValueString); + } + } } diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 567ec40b..ae6c0364 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -899,7 +899,7 @@ public void getConfigurationEndpoint() { } @Test - public void getConfigurationEndpointLegacy() { + public void getConfigurationEndpoint_Legacy() { mock.setMockContent("mock_content_legacy.json"); given() .header(TENANT_HEADER) @@ -916,6 +916,25 @@ public void getConfigurationEndpointLegacy() { .body("metadataInvalidated", equalTo(Boolean.FALSE)); } + @Test + public void getConfigurationEndpointUseSecureTokens() { + mock.setMockContent("mock_content_secure_tokens.json"); + given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .get("/saml/configuration") + .then() + .statusCode(200) + .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")) + .body("idpUrl", equalTo("https://idp.ssocircle.com")) + .body("samlBinding", equalTo("POST")) + .body("callback", equalTo("callback")) + .body("useSecureTokens", equalTo(Boolean.TRUE)) + .body("metadataInvalidated", equalTo(Boolean.FALSE)); + } + @Test public void putConfigurationEndpoint(TestContext context) { SamlConfigRequest samlConfigRequest = new SamlConfigRequest() @@ -993,6 +1012,35 @@ public void putConfiguration_Legacy() { .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")); } + @Test + public void putConfigurationUseSecureTokens() { + mock.setMockContent("mock_content_secure_tokens.json"); + + SamlConfigRequest samlConfigRequest = new SamlConfigRequest() + .withIdpUrl(URI.create("http://localhost:" + IDP_MOCK_PORT + "/xml")) + .withSamlAttribute("UserID") + .withSamlBinding(SamlConfigRequest.SamlBinding.POST) + .withUserProperty("externalSystemId") + .withOkapiUrl(URI.create("http://localhost:9130")) + .withCallback("callback") + .withUseSecureTokens(true); + + String jsonString = Json.encode(samlConfigRequest); + + // PUT + given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonString) + .put("/saml/configuration") + .then() + .statusCode(200) + .body("callback", equalTo("callback")) + .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")); + } + @Test public void putConfiguration() { mock.setMockContent("mock_content.json"); diff --git a/src/test/resources/mock_content.json b/src/test/resources/mock_content.json index 11163429..d8f65810 100644 --- a/src/test/resources/mock_content.json +++ b/src/test/resources/mock_content.json @@ -56,7 +56,7 @@ "value": "http://localhost:9130" } ], - "totalRecords": 6 + "totalRecords": 7 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_content_legacy.json b/src/test/resources/mock_content_legacy.json index 4541fdf1..7ad10c3f 100644 --- a/src/test/resources/mock_content_legacy.json +++ b/src/test/resources/mock_content_legacy.json @@ -63,7 +63,7 @@ "value": "callback" } ], - "totalRecords": 6 + "totalRecords": 9 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_content_secure_tokens.json b/src/test/resources/mock_content_secure_tokens.json new file mode 100644 index 00000000..94abcba5 --- /dev/null +++ b/src/test/resources/mock_content_secure_tokens.json @@ -0,0 +1,239 @@ +{ + "mocks": [ + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%29", + "method": "get", + "status": 200, + "receivedData": { + "configs": [ + { + "id": "60eead4f-de97-437c-9cb7-09966ce50e49", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "idp.url", + "value": "https://idp.ssocircle.com" + }, + { + "id": "022d8342-fa51-44d1-8b2b-27da36e11f07", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.file", + "value": "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" + }, + { + "id": "6dc15218-ed83-49e0-85ab-bb891e3f42c9", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.password", + "value": "iOzPffanq1xj" + }, + { + "id": "b5662280-81cc-462e-bb84-726e47cb58e4", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.privatekey.password", + "value": "iOzPffanq1xj" + }, + { + "id": "2dd0d26d-3be4-4e80-a631-f7bda5311719", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.binding", + "value": "POST" + }, + { + "id": "717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "metadata.invalidated", + "value": "false" + }, + { + "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "okapi.url", + "value": "http://localhost:9130" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" + }, + { + "id": "dcc452cf-f726-423b-91ec-ec89e36f9e98", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.useSecureTokens", + "value": "true" + } + ], + "totalRecords": 9 + }, + "receivedPath": "", + "sendData": {} + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20saml.attribute%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 0, + "configs": [] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20idp.metadata%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "configs": [ + { + "id": "60eead4f-de97-437c-9cb7-09966ce50e48", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "idp.metadata", + "value": "\n\n \n \n \n \n \nMIIEYzCCAkugAwIBAgIDIAZmMA0GCSqGSIb3DQEBCwUAMC4xCzAJBgNVBAYTAkRF\nMRIwEAYDVQQKDAlTU09DaXJjbGUxCzAJBgNVBAMMAkNBMB4XDTE2MDgwMzE1MDMy\nM1oXDTI2MDMwNDE1MDMyM1owPTELMAkGA1UEBhMCREUxEjAQBgNVBAoTCVNTT0Np\ncmNsZTEaMBgGA1UEAxMRaWRwLnNzb2NpcmNsZS5jb20wggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQCAwWJyOYhYmWZF2TJvm1VyZccs3ZJ0TsNcoazr2pTW\ncY8WTRbIV9d06zYjngvWibyiylewGXcYONB106ZNUdNgrmFd5194Wsyx6bPvnjZE\nERny9LOfuwQaqDYeKhI6c+veXApnOfsY26u9Lqb9sga9JnCkUGRaoVrAVM3yfghv\n/Cg/QEg+I6SVES75tKdcLDTt/FwmAYDEBV8l52bcMDNF+JWtAuetI9/dWCBe9VTC\nasAr2Fxw1ZYTAiqGI9sW4kWS2ApedbqsgH3qqMlPA7tg9iKy8Yw/deEn0qQIx8Gl\nVnQFpDgzG9k+jwBoebAYfGvMcO/BDXD2pbWTN+DvbURlAgMBAAGjezB5MAkGA1Ud\nEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmlj\nYXRlMB0GA1UdDgQWBBQhAmCewE7aonAvyJfjImCRZDtccTAfBgNVHSMEGDAWgBTA\n1nEA+0za6ppLItkOX5yEp8cQaTANBgkqhkiG9w0BAQsFAAOCAgEAAhC5/WsF9ztJ\nHgo+x9KV9bqVS0MmsgpG26yOAqFYwOSPmUuYmJmHgmKGjKrj1fdCINtzcBHFFBC1\nmaGJ33lMk2bM2THx22/O93f4RFnFab7t23jRFcF0amQUOsDvltfJw7XCal8JdgPU\ng6TNC4Fy9XYv0OAHc3oDp3vl1Yj8/1qBg6Rc39kehmD5v8SKYmpE7yFKxDF1ol9D\nKDG/LvClSvnuVP0b4BWdBAA9aJSFtdNGgEvpEUqGkJ1osLVqCMvSYsUtHmapaX3h\niM9RbX38jsSgsl44Rar5Ioc7KXOOZFGfEKyyUqucYpjWCOXJELAVAzp7XTvA2q55\nu31hO0w8Yx4uEQKlmxDuZmxpMz4EWARyjHSAuDKEW1RJvUr6+5uA9qeOKxLiKN1j\no6eWAcl6Wr9MreXR9kFpS6kHllfdVSrJES4ST0uh1Jp4EYgmiyMmFCbUpKXifpsN\nWCLDenE3hllF0+q3wIdu+4P82RIM71n7qVgnDnK29wnLhHDat9rkC62CIbonpkVY\nmnReX0jze+7twRanJOMCJ+lFg16BDvBcG8u0n/wIDkHHitBI7bU1k6c6DydLQ+69\nh8SCo6sO9YuD+/3xAGKad4ImZ6vTwlB4zDCpu6YgQWocWRXE+VkOb+RBfvP755PU\naLfL63AFVlpOnEpIio5++UjNJRuPuAA=\n \n \n \n \n \n \n \n \nMIIEYzCCAkugAwIBAgIDIAZmMA0GCSqGSIb3DQEBCwUAMC4xCzAJBgNVBAYTAkRF\nMRIwEAYDVQQKDAlTU09DaXJjbGUxCzAJBgNVBAMMAkNBMB4XDTE2MDgwMzE1MDMy\nM1oXDTI2MDMwNDE1MDMyM1owPTELMAkGA1UEBhMCREUxEjAQBgNVBAoTCVNTT0Np\ncmNsZTEaMBgGA1UEAxMRaWRwLnNzb2NpcmNsZS5jb20wggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQCAwWJyOYhYmWZF2TJvm1VyZccs3ZJ0TsNcoazr2pTW\ncY8WTRbIV9d06zYjngvWibyiylewGXcYONB106ZNUdNgrmFd5194Wsyx6bPvnjZE\nERny9LOfuwQaqDYeKhI6c+veXApnOfsY26u9Lqb9sga9JnCkUGRaoVrAVM3yfghv\n/Cg/QEg+I6SVES75tKdcLDTt/FwmAYDEBV8l52bcMDNF+JWtAuetI9/dWCBe9VTC\nasAr2Fxw1ZYTAiqGI9sW4kWS2ApedbqsgH3qqMlPA7tg9iKy8Yw/deEn0qQIx8Gl\nVnQFpDgzG9k+jwBoebAYfGvMcO/BDXD2pbWTN+DvbURlAgMBAAGjezB5MAkGA1Ud\nEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmlj\nYXRlMB0GA1UdDgQWBBQhAmCewE7aonAvyJfjImCRZDtccTAfBgNVHSMEGDAWgBTA\n1nEA+0za6ppLItkOX5yEp8cQaTANBgkqhkiG9w0BAQsFAAOCAgEAAhC5/WsF9ztJ\nHgo+x9KV9bqVS0MmsgpG26yOAqFYwOSPmUuYmJmHgmKGjKrj1fdCINtzcBHFFBC1\nmaGJ33lMk2bM2THx22/O93f4RFnFab7t23jRFcF0amQUOsDvltfJw7XCal8JdgPU\ng6TNC4Fy9XYv0OAHc3oDp3vl1Yj8/1qBg6Rc39kehmD5v8SKYmpE7yFKxDF1ol9D\nKDG/LvClSvnuVP0b4BWdBAA9aJSFtdNGgEvpEUqGkJ1osLVqCMvSYsUtHmapaX3h\niM9RbX38jsSgsl44Rar5Ioc7KXOOZFGfEKyyUqucYpjWCOXJELAVAzp7XTvA2q55\nu31hO0w8Yx4uEQKlmxDuZmxpMz4EWARyjHSAuDKEW1RJvUr6+5uA9qeOKxLiKN1j\no6eWAcl6Wr9MreXR9kFpS6kHllfdVSrJES4ST0uh1Jp4EYgmiyMmFCbUpKXifpsN\nWCLDenE3hllF0+q3wIdu+4P82RIM71n7qVgnDnK29wnLhHDat9rkC62CIbonpkVY\nmnReX0jze+7twRanJOMCJ+lFg16BDvBcG8u0n/wIDkHHitBI7bU1k6c6DydLQ+69\nh8SCo6sO9YuD+/3xAGKad4ImZ6vTwlB4zDCpu6YgQWocWRXE+VkOb+RBfvP755PU\naLfL63AFVlpOnEpIio5++UjNJRuPuAA=\n \n \n \n \n 128\n\n \n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos\n \n \n \n \n \n\n" + } + ] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20idp.url%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "configs": [ + { + "id": "60eead4f-de97-437c-9cb7-09966ce50e49", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "idp.url", + "value": "https://idp.ssocircle.com" + } + ] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20saml.callback%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "configs": [ + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback-b" + } + ] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20saml.useSecureTokens%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "configs": [ + { + "id": "dcc452cf-f726-423b-91ec-ec89e36f9e98", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.useSecureTokens", + "value": "false" + } + ] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20metadata.invalidated%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "configs": [ + { + "id": "717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "metadata.invalidated", + "value": "false" + } + ] + } + }, + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%20AND%20code%3D%3D%20user.property%29", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 0, + "configs": [] + } + }, + { + "url": "/configurations/entries", + "method": "post", + "status": 201 + }, + { + "url": "/configurations/entries/60eead4f-de97-437c-9cb7-09966ce50e49", + "method": "put", + "status": 204 + }, + { + "url": "/configurations/entries/60eead4f-de97-437c-9cb7-09966ce50e48", + "method": "put", + "status": 204 + }, + { + "url": "/configurations/entries/717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "method": "put", + "status": 204 + }, + { + "url": "/configurations/entries/81816efc-63b1-11ee-8c99-0242ac120002", + "method": "put", + "status": 204 + }, + { + "url": "/users?query=externalSystemId%3D%3D%22saml-user-id%22", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "users": [ + { + "id": "saml-user", + "username": "samluser", + "active": true + } + ] + } + }, + { + "url": "/token", + "method": "post", + "status": 201, + "receivedData": { + "token": "saml-token" + } + }, + { + "url": "/user-tenants?externalSystemId=saml-user-id", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 0, + "userTenants": [] + } + } + ] +} diff --git a/src/test/resources/mock_content_with_metadata.json b/src/test/resources/mock_content_with_metadata.json index 94ccf75d..76ecd01b 100644 --- a/src/test/resources/mock_content_with_metadata.json +++ b/src/test/resources/mock_content_with_metadata.json @@ -47,7 +47,8 @@ "configName": "saml", "code": "metadata.invalidated", "value": "false" - },{ + }, + { "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", "module": "LOGIN-SAML", "configName": "saml", @@ -56,7 +57,7 @@ } ], - "totalRecords": 6 + "totalRecords": 7 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_content_with_metadata_legacy.json b/src/test/resources/mock_content_with_metadata_legacy.json index 51a0bdcf..6a1f6ea3 100644 --- a/src/test/resources/mock_content_with_metadata_legacy.json +++ b/src/test/resources/mock_content_with_metadata_legacy.json @@ -47,7 +47,8 @@ "configName": "saml", "code": "metadata.invalidated", "value": "false" - },{ + }, + { "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", "module": "LOGIN-SAML", "configName": "saml", @@ -56,7 +57,7 @@ } ], - "totalRecords": 6 + "totalRecords": 7 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_idptest_post.json b/src/test/resources/mock_idptest_post.json index 03c345b9..05d97286 100644 --- a/src/test/resources/mock_idptest_post.json +++ b/src/test/resources/mock_idptest_post.json @@ -47,7 +47,8 @@ "configName": "saml", "code": "metadata.invalidated", "value": "false" - },{ + }, + { "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", "module": "LOGIN-SAML", "configName": "saml", @@ -55,7 +56,7 @@ "value": "http://localhost:9230" } ], - "totalRecords": 6 + "totalRecords": 7 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_idptest_post_legacy.json b/src/test/resources/mock_idptest_post_legacy.json index a4958781..3122c626 100644 --- a/src/test/resources/mock_idptest_post_legacy.json +++ b/src/test/resources/mock_idptest_post_legacy.json @@ -63,7 +63,7 @@ "value": "callback" } ], - "totalRecords": 6 + "totalRecords": 9 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_idptest_post_secure_tokens.json b/src/test/resources/mock_idptest_post_secure_tokens.json new file mode 100644 index 00000000..09b7073d --- /dev/null +++ b/src/test/resources/mock_idptest_post_secure_tokens.json @@ -0,0 +1,113 @@ +{ + "mocks": [ + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%29", + "method": "get", + "status": 200, + "receivedData": { + "configs": [ + { + "id": "60eead4f-de97-437c-9cb7-09966ce50e49", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "idp.url", + "value": "http://localhost:8888/simplesaml/saml2/idp/metadata.php" + }, + { + "id": "022d8342-fa51-44d1-8b2b-27da36e11f07", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.file", + "value": "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" + }, + { + "id": "6dc15218-ed83-49e0-85ab-bb891e3f42c9", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.password", + "value": "iOzPffanq1xj" + }, + { + "id": "b5662280-81cc-462e-bb84-726e47cb58e4", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.privatekey.password", + "value": "iOzPffanq1xj" + }, + { + "id": "2dd0d26d-3be4-4e80-a631-f7bda5311719", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.binding", + "value": "POST" + }, + { + "id": "717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "metadata.invalidated", + "value": "false" + }, + { + "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "okapi.url", + "value": "http://localhost:9230" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" + }, + { + "id": "dcc452cf-f726-423b-91ec-ec89e36f9e98", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.useSecureTokens", + "value": "true" + } + ], + "totalRecords": 9 + }, + "receivedPath": "", + "sendData": {} + }, + { + "url": "/users?query=externalSystemId%3D%3D%22saml-user-id%22", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "users": [ + { + "id": "saml-user", + "username": "samluser", + "active": true + } + ] + }, + "receivedPath": "", + "sendData": {} + }, + { + "url": "/token", + "method": "post", + "status": 201, + "receivedData": { + "token": "saml-token" + } + }, + { + "url": "/user-tenants?externalSystemId=saml-user-id", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 0, + "userTenants": [] + } + } + ] +} diff --git a/src/test/resources/mock_idptest_redirect.json b/src/test/resources/mock_idptest_redirect.json index 26812246..537077fd 100644 --- a/src/test/resources/mock_idptest_redirect.json +++ b/src/test/resources/mock_idptest_redirect.json @@ -56,7 +56,7 @@ "value": "http://localhost:9230" } ], - "totalRecords": 6 + "totalRecords": 7 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_idptest_redirect_legacy.json b/src/test/resources/mock_idptest_redirect_legacy.json index b00e4eda..41f9f19d 100644 --- a/src/test/resources/mock_idptest_redirect_legacy.json +++ b/src/test/resources/mock_idptest_redirect_legacy.json @@ -63,7 +63,7 @@ "value": "callback" } ], - "totalRecords": 6 + "totalRecords": 9 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_idptest_redirect_secure_tokens.json b/src/test/resources/mock_idptest_redirect_secure_tokens.json new file mode 100644 index 00000000..2c210992 --- /dev/null +++ b/src/test/resources/mock_idptest_redirect_secure_tokens.json @@ -0,0 +1,113 @@ +{ + "mocks": [ + { + "url": "/configurations/entries?query=%28module%3D%3DLOGIN-SAML%20AND%20configName%3D%3Dsaml%29", + "method": "get", + "status": 200, + "receivedData": { + "configs": [ + { + "id": "60eead4f-de97-437c-9cb7-09966ce50e49", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "idp.url", + "value": "http://localhost:8888/simplesaml/saml2/idp/metadata.php" + }, + { + "id": "022d8342-fa51-44d1-8b2b-27da36e11f07", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.file", + "value": "/u3+7QAAAAIAAAABAAAAAQAYc2FtbDJjbGllbnRjb25maWd1cmF0aW9uAAABXgmqUasAAAUCMIIE/jAOBgorBgEEASoCEQEBBQAEggTqAM9d1z/8cEsfS4brWTi+sTcK6/YoGpduJKVnDnFIYlNthuZ6curH714Mj3a79ZFziqbv6EGU/uOXXP7cD8S2oSEzhnkbubIokyJPNo14v9hsvapnP8oz04HDAEFO0+v1RLeoe/08VKdUqEtrdg7W6r3HIjNHfbSHZEpTCfWnyovL+NPVI8lYz38EGksp/SPWJFdOcAmMgN0a2zDI9R9WgQJ2GUYwyy3XzlKgbIj3Xe+lpo1B3nq4+WQ7UUqWBnfdW5t2a6Ld6R2dU7kly9qR906UYP+2wNsk66/ME97hCriyn3JWdhs+fjpts5ReV/Q5X7O5SMjmfYQOfgRzsjD4+Eh9Bqptg9dUvZ+7nJbqu2k+2RFEfPZdBkgQ0QsnpkkCiu9whKqKj5uInhBlNGaAokP39mKOdvYZ1l1VVoIdVY1XLkPJ0BRDrIm4MLW6B4/+cIsL9MS0/bFsgSd8UURfP9fGONzmBbwUapRu2/zBCgKM2PbV82n+TKZT1x/RXN05ARb63xIC2HFTq3LImRWf6V081M9YhMaIQLhA6IPbFnzNNwRO0i1WzHF0FJXVQjLXx1//i8k/nxeUEIIh3TtHlEpTtlmt+GDFFVgXCOt+S78VZyh7FgRmlx4V/5w7KlvzljbqS0hq2DkAGPWPCTu0PyeZcssSTqUTNYmgbwbsHhYzubOFh0pgEuN8hK3dsQf1YGBWfehspsbicwmuvSAMyBcdXd497NRmzIJU4GjCvBj+vmnpCkgkVg2SXK6aFo2SbYXUrRjDzqql3ElQ7/jlIWqnoU7J653TqN6O5uo8ZQGi2BWhldCyC/JKa0LastcRrFNjEeGk19+FRVYprKcDtFGwn4TWQtwqf9BDbXDKBtTQyIoNVFoGUd9dxPryOmUet3Ipxgqy6yoGQwlRvJ4EEyTETQRLx/foGT7JMAAOdNnSWsmyFvPkXLfzHZTSVrtOBHXj83svt5929NgVMx/HJa2aaCdkCZGs/VpRHDzJX35yyr5pzCGau0pVXRZetqKW1Yp+rfWnhwtvgnDZ+kOYLfoFSFPYtKqfMtHZih8byIW/vRLEanNY54l1tG3l0STF+VkH4QudLtZTm8mhbQJDWGOOT76VF+Yf1u1J00spDyl2HLgHHZRV3z7UfLW9kQLnkpFPmiduM8JIBx9z8cfSSuSru9TtrNmmXbXWTgFv5nzsRqZ3czWQyURujOqQJXwgKJTDNNfOnVgY7WZ+GIWAas/JBnS+V3HFCvC63rQxZohj6d+zQF5FDVbc2rceZ3ihGurTnbbl4Ebflgw9XACPipa4CqqbUEuhuNwzgr2/h/l236PBcMW4Y1PPAEZ77x45KYjFmod83mSt9Ibxz/QgZiUkd1ZkaspJCnd/bcSkAtiptso3hyW2jt1W4ftLFdzshU1t872aW7yr+FqgDzynE1Wh9DVTd02Fu5fH2g7qDLwlp5aXRXQclVi3Y7iYSiL4KZ7fJ7bEV4ZD4XyzO9CI8owZc5/HxYcW3BmiZhONMppy7gAc+LHJgPT4GgbqD5BEll1qoxMkqC0WQCbb4pX65ZcnBfzsd5tMhvqEK3Sea3WZk4dGCeegVMzs4ziT7ybGhixM+4f1kpwaTnL0keOWHa6IQm/rLetkZATx4U+gPqgqVw9JVhdgwsn+QAdiC9YPSgWsokmhIIODhQAAAAEABVguNTA5AAACpTCCAqEwggGJoAMCAQICAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJcnNhc3MtbWFjMB4XDTE3MDgyMjExMTgzMFoXDTE4MDgyMjExMTgzMFowFDESMBAGA1UEAwwJcnNhc3MtbWFjMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAymMQp4w6fwYDxofV05joravMyR60aNWYemzWJbb720KuqpH46sXsEe+bWyeC2/HRylduKRRhtZsIQAfsLiYRWq+jG55bt4tizmP8WLZo0/niuxvBVuaV2lUYxay22JdgM0EfUoAdQ925AjDzyRZXslFTJAxCxtuZZ6gdvzJ4DPQyV+q7/m2n3cPlQhMxGezVrQ3ymJkJwIeqBcljBazXAg2OzsCiE5cd98SgYMNglxv1mtATXABlIn1MMVxObmQJ7jMk3+C1m4Kk6YmSPZFWJLMuhUHDVfR0N0pUG08rx6rsA7h2GzZOFoKyzMWeY8HWK/fxTpnyvHrvl4fPcX7lswIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCT6o0PuH4ZpU4fP4ahSHCi0vob7F+bEy/NDIbH5rz79z7rxFjk2A4fmXoSsl56DCSuQT685FeL8D3yZdCJNrKdCw5Vp3Rv4xX1GOn0DNq9n8qMKRGAWgUKQAekHp/+8EBG5YSICDsslDPrDQTiPnVO8LXN8Rdr4zUFf+Kfpfg1XX4sDIZ0b67jOmJQ/h+s4oiuFcgbCr26DwtVO3SOJoYI4V84HYROaP7KGffDoLMIV2JpfodsbHMvzSrcNYC2jEFzym/RdhMK5RPsd/4P5eYyY0vye6WQzBnKmK7cmTjYtAtaWsJz+jfppvIHM0Tk1DyP34qyM2YYngl49bKkEC1hocbn4gFApIceJxZDg9mQ0EGlUZ0=" + }, + { + "id": "6dc15218-ed83-49e0-85ab-bb891e3f42c9", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.password", + "value": "iOzPffanq1xj" + }, + { + "id": "b5662280-81cc-462e-bb84-726e47cb58e4", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "keystore.privatekey.password", + "value": "iOzPffanq1xj" + }, + { + "id": "2dd0d26d-3be4-4e80-a631-f7bda5311719", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.binding", + "value": "REDIRECT" + }, + { + "id": "717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "metadata.invalidated", + "value": "false" + }, + { + "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "okapi.url", + "value": "http://localhost:9230" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" + }, + { + "id": "dcc452cf-f726-423b-91ec-ec89e36f9e98", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.useSecureTokens", + "value": "true" + } + ], + "totalRecords": 9 + }, + "receivedPath": "", + "sendData": {} + }, + { + "url": "/users?query=externalSystemId%3D%3D%22saml-user-id%22", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 1, + "users": [ + { + "id": "saml-user", + "username": "samluser", + "active": true + } + ] + }, + "receivedPath": "", + "sendData": {} + }, + { + "url": "/token", + "method": "post", + "status": 201, + "receivedData": { + "token": "saml-token" + } + }, + { + "url": "/user-tenants?externalSystemId=saml-user-id", + "method": "get", + "status": 200, + "receivedData": { + "totalRecords": 0, + "userTenants": [] + } + } + ] +} From 6b7a2335d097cd47f1dee0af9c5d90b0b2195eac Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Thu, 5 Sep 2024 18:48:59 -0400 Subject: [PATCH 03/12] Has new logic for the config --- .../java/org/folio/rest/impl/SamlAPI.java | 22 ++++++++++++++----- .../java/org/folio/rest/impl/SamlAPITest.java | 17 +------------- .../mock_content_with_metadata_legacy.json | 8 ++++++- .../resources/mock_multiple_user_tenant.json | 10 +++++++-- src/test/resources/mock_nouser.json | 11 ++++++++-- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index d77861bb..1a1f506b 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -11,6 +11,7 @@ import java.time.Instant; import java.util.*; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -199,17 +200,17 @@ private String getRelayState(RoutingContext routingContext, String body) { @Override public void postSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext, TOKEN_SIGN_ENDPOINT_LEGACY); + doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext); } @Override public void postSamlCallbackWithExpiry(String body, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext, TOKEN_SIGN_ENDPOINT); + doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext); } private void doPostSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext, String tokenSignEndpoint) { + Handler> asyncResultHandler, Context vertxContext) { registerFakeSession(routingContext); @@ -251,8 +252,9 @@ private void doPostSamlCallback(String body, RoutingContext routingContext, Map< JsonObject payload = new JsonObject().put("payload", new JsonObject().put("sub", userObject.getString(USERNAME)).put("user_id", userId)); + var tokenSignEndpoint = getTokenSignEndpoint(configuration); return fetchToken(webClient, payload, parsedHeaders, tokenSignEndpoint).map(jsonResponse -> { - if (isLegacyResponse(tokenSignEndpoint)) { + if (isLegacyResponse(configuration)) { return redirectResponseLegacy(jsonResponse, stripesBaseUrl, originalUrl); } else { return redirectResponse(jsonResponse, stripesBaseUrl, originalUrl); @@ -281,8 +283,16 @@ private PostSamlCallbackResponse failCallbackResponse(Throwable cause, RoutingCo return response; } - private boolean isLegacyResponse(String endpoint) { - return endpoint.equals(TOKEN_SIGN_ENDPOINT_LEGACY); + private boolean isLegacyResponse(SamlConfiguration configuration) { + return "callback".equals(configuration.getCallback()) && (configuration.getUseSecureTokens() == null + || "false".equals(configuration.getUseSecureTokens())); + } + + private String getTokenSignEndpoint(SamlConfiguration configuration) { + if (isLegacyResponse(configuration)) { + return TOKEN_SIGN_ENDPOINT_LEGACY; + } + return TOKEN_SIGN_ENDPOINT; } private Future fetchToken(WebClient client, JsonObject payload, OkapiHeaders parsedHeaders, String endpoint) { diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index ae6c0364..44ffc0d2 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -575,7 +575,7 @@ public void callbackEndpointTests_Legacy() { .body("bindingMethod", equalTo("POST")) .statusCode(200) .extract(); - +// String cookie = resp.cookie(SamlAPI.RELAY_STATE); String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); @@ -671,21 +671,6 @@ public void callbackEndpointTests_Legacy() { .then() .statusCode(403) .body(is("Inactive user account!")); - - mock.setMockContent("mock_tokenresponse.json"); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback") - .then() - .statusCode(302) - .header("Location", containsString(PercentCodec.encodeAsString(testPath))) - .header("x-okapi-token", "saml-token") - .cookie("ssoToken", "saml-token"); } @Test diff --git a/src/test/resources/mock_content_with_metadata_legacy.json b/src/test/resources/mock_content_with_metadata_legacy.json index 6a1f6ea3..deaa19b2 100644 --- a/src/test/resources/mock_content_with_metadata_legacy.json +++ b/src/test/resources/mock_content_with_metadata_legacy.json @@ -54,8 +54,14 @@ "configName": "saml", "code": "okapi.url", "value": "http://localhost:9130" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" } - ], "totalRecords": 7 }, diff --git a/src/test/resources/mock_multiple_user_tenant.json b/src/test/resources/mock_multiple_user_tenant.json index cf916cb6..ca39c38f 100644 --- a/src/test/resources/mock_multiple_user_tenant.json +++ b/src/test/resources/mock_multiple_user_tenant.json @@ -61,10 +61,16 @@ "configName": "saml", "code": "user.property", "value": "username" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" } - ], - "totalRecords": 6 + "totalRecords": 8 }, "receivedPath": "", "sendData": {} diff --git a/src/test/resources/mock_nouser.json b/src/test/resources/mock_nouser.json index 1befbbea..a965817f 100644 --- a/src/test/resources/mock_nouser.json +++ b/src/test/resources/mock_nouser.json @@ -47,14 +47,21 @@ "configName": "saml", "code": "metadata.invalidated", "value": "false" - },{ + }, + { "id": "cb20fa86-affb-4488-8b37-2e8c597fff66", "module": "LOGIN-SAML", "configName": "saml", "code": "okapi.url", "value": "http://localhost:9130" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" } - ], "totalRecords": 6 }, From a1d26cf0738c8119dda663e13f4bea764485fcaa Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Fri, 6 Sep 2024 14:46:41 -0400 Subject: [PATCH 04/12] Has test cases for new useSecureTokens config --- .../java/org/folio/rest/impl/IdpTest.java | 5 +- .../java/org/folio/rest/impl/SamlAPITest.java | 131 ++++-------------- .../java/org/folio/util/SamlTestHelper.java | 4 +- .../resources/mock_content_secure_tokens.json | 8 +- 4 files changed, 41 insertions(+), 107 deletions(-) diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index afd3090d..287751df 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -154,7 +154,7 @@ private void post0() { assertThat(matcher.find(), is(true)); SamlTestHelper.testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER); + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, SamlAPITest.CALLBACK_WITH_EXPIRY_URL); } @Test @@ -210,7 +210,8 @@ private void redirect0() { assertThat(matcher.find(), is(true)); SamlTestHelper.testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER); + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, + SamlAPITest.CALLBACK_WITH_EXPIRY_URL); } private void setIdpBinding(String binding) { diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 44ffc0d2..85443035 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -82,11 +82,13 @@ public class SamlAPITest { private static final Header ACCESS_CONTROL_REQUEST_METHOD_HEADER = new Header( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD.toString(), "POST"); private static final String STRIPES_URL = "http://localhost:3000"; - public static final int PORT = 8081; public static final int IDP_MOCK_PORT = NetworkUtils.nextFreePort(); + public static final String CALLBACK_URL = "/saml/callback"; + public static final String CALLBACK_WITH_EXPIRY_URL = "/saml/callback-with-expiry"; private static final int JSON_MOCK_PORT = NetworkUtils.nextFreePort(); private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", "http://localhost:" + JSON_MOCK_PORT); + public MockJson mock; private static Vertx mockVertx = Vertx.vertx(); @@ -594,6 +596,10 @@ public void callbackEndpointTests_Legacy() { .header("x-okapi-token", "saml-token") .cookie("ssoToken", "saml-token"); + testCallbackErrorCases(CALLBACK_URL, relayState, cookie); + } + + private void testCallbackErrorCases(String callbackUrl, String relayState, String cookie) { log.info("=== Test - POST /saml/callback - failure (wrong cookie) ==="); given() .header(TENANT_HEADER) @@ -602,7 +608,7 @@ public void callbackEndpointTests_Legacy() { .cookie(SamlAPI.RELAY_STATE, "bad" + cookie) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post(callbackUrl) .then() .statusCode(403) .body(is("CSRF attempt detected")); @@ -615,7 +621,7 @@ public void callbackEndpointTests_Legacy() { .cookie(SamlAPI.RELAY_STATE, cookie) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState.replace("localhost", "^")) - .post("/saml/callback") + .post(callbackUrl) .then() .statusCode(400) .body(containsString("Invalid relay state url")); @@ -627,7 +633,7 @@ public void callbackEndpointTests_Legacy() { .header(OKAPI_URL_HEADER) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post(callbackUrl) .then() .statusCode(403) .body(is("CSRF attempt detected")); @@ -641,7 +647,7 @@ public void callbackEndpointTests_Legacy() { .cookie(SamlAPI.RELAY_STATE, cookie) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post( callbackUrl) .then() .statusCode(500) .body(is("Response status code 404 is not equal to 200")); @@ -654,7 +660,7 @@ public void callbackEndpointTests_Legacy() { .cookie(SamlAPI.RELAY_STATE, cookie) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post(callbackUrl) .then() .statusCode(400) .body(is("No user found by externalSystemId == saml-user-id")); @@ -667,7 +673,7 @@ public void callbackEndpointTests_Legacy() { .cookie(SamlAPI.RELAY_STATE, cookie) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post(callbackUrl) .then() .statusCode(403) .body(is("Inactive user account!")); @@ -719,9 +725,21 @@ public void callbackForConsortium() { @Test public void callbackEndpointTests() { + // Default. No configuration needed. /saml/callback-with-expiry returns RTR tokens. + testCallback(CALLBACK_WITH_EXPIRY_URL); + } + + @Test + public void callbackEndpointTestsUseSecureTokens() { + // Configuration needed. /saml/callback returns RTR tokens allowing existing metadata to be used. + mock.setMockContent("mock_content_secure_tokens.json"); + testCallback(CALLBACK_URL); + } + + private void testCallback(String callbackUrl) { final String testPath = "/test/path"; - log.info("=== Setup - POST /saml/login - need relayState and cookie ==="); + log.info("=== Setup - POST /saml/login RTR - need relayState and cookie ==="); ExtractableResponse resp = given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -741,105 +759,16 @@ public void callbackEndpointTests() { String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); String samlResponse = "saml-response"; - log.info("=== Test - POST /saml/callback-with-expiry - success ==="); + log.info("=== Test - POST /saml/callback RTR - success ==="); SamlTestHelper.testCookieResponse(detailedCookie, relayState, testPath, CookieSameSite.LAX.toString(), - samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER); + samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); CookieSameSiteConfig.set(Map.of("LOGIN_COOKIE_SAMESITE", CookieSameSite.NONE.toString())); SamlTestHelper.testCookieResponse(detailedCookie, relayState, testPath, CookieSameSite.NONE.toString(), - samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER); + samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); CookieSameSiteConfig.set(Map.of()); - log.info("=== Test - POST /saml/callback-with-expiry - failure (wrong cookie) ==="); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, "bad" + cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(403) - .body(is("CSRF attempt detected")); - - log.info("=== Test - POST /saml/callback/callback-with-expiry - failure (wrong relay) ==="); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState.replace("localhost", "^")) - .post("/saml/callback-with-expiry") - .then() - .statusCode(400) - .body(containsString("Invalid relay state url")); - - log.info("=== Test - POST /saml/callback-with-expiry - failure (no cookie) ==="); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(403) - .body(is("CSRF attempt detected")); - - // not found .. - mock.setMockContent("mock_400.json"); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(500) - .body(is("Response status code 404 is not equal to 200")); - - mock.setMockContent("mock_nouser.json"); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(400) - .body(is("No user found by externalSystemId == saml-user-id")); - - mock.setMockContent("mock_inactiveuser.json"); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(403) - .body(is("Inactive user account!")); - - mock.setMockContent("mock_tokenresponse.json"); - given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .cookie(SamlAPI.RELAY_STATE, cookie) - .formParam("SAMLResponse", "saml-response") - .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") - .then() - .statusCode(302) - .header("Location", containsString(PercentCodec.encodeAsString(testPath))); + testCallbackErrorCases(callbackUrl, relayState, cookie); } void postSamlLogin(int expectedStatus) { diff --git a/src/test/java/org/folio/util/SamlTestHelper.java b/src/test/java/org/folio/util/SamlTestHelper.java index c46c394b..c2194e09 100644 --- a/src/test/java/org/folio/util/SamlTestHelper.java +++ b/src/test/java/org/folio/util/SamlTestHelper.java @@ -13,7 +13,7 @@ public class SamlTestHelper { public static void testCookieResponse(Cookie cookie, String relayState, String testPath, String sameSite, String samlResponse, Header tenantHeader, Header tokenHeader, - Header okapiUrlHeader) { + Header okapiUrlHeader, String callbackUrl) { RestAssured.given() .header(tenantHeader) .header(tokenHeader) @@ -21,7 +21,7 @@ public static void testCookieResponse(Cookie cookie, String relayState, String t .cookie(cookie) .formParam("SAMLResponse", samlResponse) .formParam("RelayState", relayState) - .post("/saml/callback-with-expiry") + .post(callbackUrl) .then() .statusCode(302) .cookie("folioRefreshToken", RestAssuredMatchers.detailedCookie() diff --git a/src/test/resources/mock_content_secure_tokens.json b/src/test/resources/mock_content_secure_tokens.json index 94abcba5..ce55ee46 100644 --- a/src/test/resources/mock_content_secure_tokens.json +++ b/src/test/resources/mock_content_secure_tokens.json @@ -219,11 +219,15 @@ } }, { - "url": "/token", + "url": "/token/sign", "method": "post", "status": 201, "receivedData": { - "token": "saml-token" + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z", + "tenantId": "diku" } }, { From fd9b523e41488c08d9c1eb7df086f38635d22274 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Fri, 6 Sep 2024 17:03:46 -0400 Subject: [PATCH 05/12] Has idp tests, but don't like the duplication; will fix --- .../java/org/folio/rest/impl/IdpTest.java | 19 +- .../org/folio/rest/impl/IdpTestCallback.java | 245 ++++++++++++++++++ .../mock_idptest_post_secure_tokens.json | 8 +- .../mock_idptest_redirect_secure_tokens.json | 8 +- 4 files changed, 267 insertions(+), 13 deletions(-) create mode 100644 src/test/java/org/folio/rest/impl/IdpTestCallback.java diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index 287751df..0117b104 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -112,16 +112,16 @@ public void after() { } @Test - public void post() { + public void postCallbackWithExpiry() { setIdpBinding("POST"); setOkapi("mock_idptest_post.json"); for (int i = 0; i < 2; i++) { - post0(); + post0(SamlAPITest.CALLBACK_WITH_EXPIRY_URL); } } - private void post0() { + private void post0(String callbackUrl) { ExtractableResponse resp = given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -154,20 +154,20 @@ private void post0() { assertThat(matcher.find(), is(true)); SamlTestHelper.testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, SamlAPITest.CALLBACK_WITH_EXPIRY_URL); + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); } @Test - public void redirect() { + public void redirectCallbackWithExpiry() { setIdpBinding("Redirect"); setOkapi("mock_idptest_redirect.json"); for (int i = 0; i < 2; i++) { - redirect0(); + redirect0(SamlAPITest.CALLBACK_WITH_EXPIRY_URL); } } - private void redirect0() { + private void redirect0(String callbackUrl) { ExtractableResponse resp = given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -195,7 +195,8 @@ private void redirect0() { String [] relayState = parameters[1].split("=", 2); location = location.substring(0, location.indexOf("?")); - String body = given() + String body = + given() .param(samlRequest[0], samlRequest[1]) .param(relayState[0], relayState[1]) .when() @@ -211,7 +212,7 @@ private void redirect0() { SamlTestHelper.testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, - SamlAPITest.CALLBACK_WITH_EXPIRY_URL); + callbackUrl); } private void setIdpBinding(String binding) { diff --git a/src/test/java/org/folio/rest/impl/IdpTestCallback.java b/src/test/java/org/folio/rest/impl/IdpTestCallback.java new file mode 100644 index 00000000..1966dc6e --- /dev/null +++ b/src/test/java/org/folio/rest/impl/IdpTestCallback.java @@ -0,0 +1,245 @@ +package org.folio.rest.impl; + +import io.restassured.RestAssured; +import io.restassured.http.Cookie; +import io.restassured.http.Header; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import io.vertx.core.http.CookieSameSite; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.folio.config.SamlConfigHolder; +import org.folio.rest.RestVerticle; +import org.folio.util.MockJson; +import org.folio.util.SamlTestHelper; +import org.folio.util.StringUtil; +import org.junit.*; +import org.junit.runner.RunWith; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.util.regex.Pattern; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. + */ +@RunWith(VertxUnitRunner.class) +public class IdpTestCallback { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); + private static final boolean DEBUG = false; + private static final ImageFromDockerfile simplesamlphp = + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + + private static final String TENANT = "diku"; + private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); + private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); + private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); + private static final String STRIPES_URL = "http://localhost:3000"; + + private static final int MODULE_PORT = 9231; + private static final String MODULE_URL = "http://localhost:" + MODULE_PORT; + private static final int OKAPI_PORT = 9230; + private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; + + private static final String TEST_PATH = "/test/path"; + + private static int IDP_PORT; + private static String IDP_BASE_URL; + private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); + private static MockJson OKAPI; + + private static Vertx VERTX; + + @ClassRule + public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); + + @BeforeClass + public static void setupOnce(TestContext context) throws Exception { + RestAssured.port = MODULE_PORT; + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + VERTX = Vertx.vertx(); + + if (DEBUG) { + IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); + } + IDP_PORT = IDP.getFirstMappedPort(); + IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; + String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); + exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", + "/var/www/simplesamlphp/config/config.php"); + exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + + DeploymentOptions moduleOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", MODULE_PORT) + .put("mock", true)); // to use SAML2ClientMock + + OKAPI = new MockJson(); + DeploymentOptions okapiOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); + + VERTX.deployVerticle(new RestVerticle(), moduleOptions) + .compose(x -> VERTX.deployVerticle(OKAPI, okapiOptions)) + .onComplete(context.asyncAssertSuccess()); + } + + @AfterClass + public static void tearDownOnce(TestContext context) { + VERTX.close() + .onComplete(context.asyncAssertSuccess()); + } + + @After + public void after() { + SamlConfigHolder.getInstance().removeClient(TENANT); + } + + @Test + public void postCallback() { + setIdpBinding("POST"); + setOkapi("mock_idptest_post_secure_tokens.json"); + + for (int i = 0; i < 2; i++) { + post0(SamlAPITest.CALLBACK_URL); + } + } + + private void post0(String callbackUrl) { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) + .post("/saml/login") + .then() + .statusCode(200) + .body("bindingMethod", is("POST")) + .extract(); + + String location = resp.body().jsonPath().getString("location"); + String samlRequest = resp.body().jsonPath().getString("samlRequest"); + String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); + Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); + assertThat(cookie.getValue(), is(relayState)); + + String body = given() + .formParams("RelayState", relayState) + .formParams("SAMLRequest", samlRequest) + .post(location) + .then() + .statusCode(200) + .body(containsString("
")) + .extract().asString(); + + var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); + assertThat(matcher.find(), is(true)); + + SamlTestHelper.testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); + } + + @Test + public void redirectCallback() { + setIdpBinding("Redirect"); + setOkapi("mock_idptest_redirect_secure_tokens.json"); + + for (int i = 0; i < 2; i++) { + redirect0(SamlAPITest.CALLBACK_URL); + } + } + + private void redirect0(String callbackUrl) { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) + .when() + .post("/saml/login") + .then() + .statusCode(200) + .body("bindingMethod", is("GET")) + .body("location", containsString("/simplesaml/saml2/idp/SSOService.php?")) + .extract(); + + Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); + String location = resp.body().jsonPath().getString("location"); + URL url; + try { + url = new URL(location); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + String [] parameters = StringUtil.urlDecode(url.getQuery()).split("&", 2); + String [] samlRequest = parameters[0].split("=", 2); + String [] relayState = parameters[1].split("=", 2); + location = location.substring(0, location.indexOf("?")); + + String body = + given() + .param(samlRequest[0], samlRequest[1]) + .param(relayState[0], relayState[1]) + .when() + .get(location) + .then() + .statusCode(200) + .body(containsString(" method=\"post\" "), + containsString("action=\"" + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback\">")) + .extract().asString(); + + var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); + assertThat(matcher.find(), is(true)); + + SamlTestHelper.testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, + callbackUrl); + } + + private void setIdpBinding(String binding) { + // append entry at end, last entry wins + exec("sed", "-i", + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + + private static void exec(String... command) { + try { + var result = IDP.execInContainer(command); + if (result.getExitCode() > 0) { + System.out.println(result.getStdout()); + System.err.println(result.getStderr()); + throw new RuntimeException("failure in IDP.execInContainer"); + } + } catch (UnsupportedOperationException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void setOkapi(String resource) { + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); + } + + private String jsonEncode(String key, String value) { + return new JsonObject().put(key, value).encode(); + } +} diff --git a/src/test/resources/mock_idptest_post_secure_tokens.json b/src/test/resources/mock_idptest_post_secure_tokens.json index 09b7073d..c3f3c119 100644 --- a/src/test/resources/mock_idptest_post_secure_tokens.json +++ b/src/test/resources/mock_idptest_post_secure_tokens.json @@ -93,11 +93,15 @@ "sendData": {} }, { - "url": "/token", + "url": "/token/sign", "method": "post", "status": 201, "receivedData": { - "token": "saml-token" + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z", + "tenantId": "diku" } }, { diff --git a/src/test/resources/mock_idptest_redirect_secure_tokens.json b/src/test/resources/mock_idptest_redirect_secure_tokens.json index 2c210992..a61b9e25 100644 --- a/src/test/resources/mock_idptest_redirect_secure_tokens.json +++ b/src/test/resources/mock_idptest_redirect_secure_tokens.json @@ -93,11 +93,15 @@ "sendData": {} }, { - "url": "/token", + "url": "/token/sign", "method": "post", "status": 201, "receivedData": { - "token": "saml-token" + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z", + "tenantId": "diku" } }, { From 89a507ee553788d743a0ee076aea3ef1341e3f64 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Mon, 9 Sep 2024 15:29:43 -0400 Subject: [PATCH 06/12] IdP tests are now more DRY --- pom.xml | 2 +- .../org/folio/rest/impl/IdpCallbackTest.java | 75 ++++++ .../org/folio/rest/impl/IdpLegacyTest.java | 55 +--- .../java/org/folio/rest/impl/IdpTest.java | 203 ++------------- .../org/folio/rest/impl/IdpTestCallback.java | 245 ------------------ .../testutil/SimpleSamlPhpContainer.java | 77 ++++++ .../java/org/folio/util/SamlTestHelper.java | 136 +++++++++- 7 files changed, 314 insertions(+), 479 deletions(-) create mode 100644 src/test/java/org/folio/rest/impl/IdpCallbackTest.java delete mode 100644 src/test/java/org/folio/rest/impl/IdpTestCallback.java create mode 100644 src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java diff --git a/pom.xml b/pom.xml index 0c6df64f..82236e26 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl diff --git a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java new file mode 100644 index 00000000..93df9ade --- /dev/null +++ b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java @@ -0,0 +1,75 @@ +package org.folio.rest.impl; + +import io.restassured.RestAssured; +import io.vertx.core.Vertx; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.folio.config.SamlConfigHolder; +import org.folio.testutil.SimpleSamlPhpContainer; +import org.folio.util.SamlTestHelper; +import org.junit.*; +import org.junit.runner.RunWith; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +/** + * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. + */ +@RunWith(VertxUnitRunner.class) +public class IdpCallbackTest { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); + private static final boolean DEBUG = false; + private static final String TENANT = SamlTestHelper.TENANT; + private static final int MODULE_PORT = SamlTestHelper.MODULE_PORT; + private static final String OKAPI_URL = SamlTestHelper.OKAPI_URL; + private static final String CALLBACK = "callback"; + private static Vertx VERTX; + + @ClassRule + public static final SimpleSamlPhpContainer IDP = new SimpleSamlPhpContainer<>(OKAPI_URL, CALLBACK); + + @BeforeClass + public static void setupOnce(TestContext context) { + RestAssured.port = MODULE_PORT; + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + VERTX = Vertx.vertx(); + + if (DEBUG) { + IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); + } + IDP.init(); + + SamlTestHelper.deployVerticle(VERTX, context); + } + + @AfterClass + public static void tearDownOnce(TestContext context) { + VERTX.close() + .onComplete(context.asyncAssertSuccess()); + } + + @After + public void after() { + SamlConfigHolder.getInstance().removeClient(TENANT); + } + + @Test + public void postCallback() { + IDP.setPostBinding(); + SamlTestHelper.setOkapi("mock_idptest_post_secure_tokens.json", IDP); + + for (int i = 0; i < 2; i++) { + SamlTestHelper.testPost(CALLBACK); + } + } + + @Test + public void redirectCallback() { + IDP.setRedirectBinding(); + SamlTestHelper.setOkapi("mock_idptest_redirect_secure_tokens.json", IDP); + + for (int i = 0; i < 2; i++) { + SamlTestHelper.testRedirect(CALLBACK); + } + } +} diff --git a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java index 4b99ce5d..ad653365 100644 --- a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java +++ b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java @@ -16,12 +16,11 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.nio.file.Path; import java.util.regex.Pattern; import org.folio.config.SamlConfigHolder; +import org.folio.testutil.SimpleSamlPhpContainer; import org.folio.rest.RestVerticle; import org.folio.util.MockJson; import org.folio.util.StringUtil; @@ -32,9 +31,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.images.builder.ImageFromDockerfile; /** * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. @@ -43,35 +40,26 @@ public class IdpLegacyTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpLegacyTest.class); private static final boolean DEBUG = false; - private static final ImageFromDockerfile simplesamlphp = - new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); - private static final String TENANT = "diku"; private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); private static final String STRIPES_URL = "http://localhost:3000"; - private static final int MODULE_PORT = 9231; private static final String MODULE_URL = "http://localhost:" + MODULE_PORT; private static final int OKAPI_PORT = 9230; private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; - private static int IDP_PORT; - private static String IDP_BASE_URL; private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); private static MockJson OKAPI; private static Vertx VERTX; @ClassRule - public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); + public static final SimpleSamlPhpContainer IDP = + new SimpleSamlPhpContainer<>(OKAPI_URL, "callback"); @BeforeClass - public static void setupOnce(TestContext context) throws Exception { + public static void setupOnce(TestContext context) { RestAssured.port = MODULE_PORT; RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); VERTX = Vertx.vertx(); @@ -79,13 +67,8 @@ public static void setupOnce(TestContext context) throws Exception { if (DEBUG) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } - IDP_PORT = IDP.getFirstMappedPort(); - IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; - String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); - exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); - exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + + IDP.init(); DeploymentOptions moduleOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", MODULE_PORT) @@ -113,7 +96,7 @@ public void after() { @Test public void post() { - setIdpBinding("POST"); + IDP.setPostBinding();; setOkapi("mock_idptest_post_legacy.json"); for (int i = 0; i < 2; i++) { @@ -169,7 +152,7 @@ private void post0() { @Test public void redirect() { - setIdpBinding("Redirect"); + IDP.setRedirectBinding(); setOkapi("mock_idptest_redirect_legacy.json"); for (int i = 0; i < 2; i++) { @@ -235,28 +218,8 @@ private void redirect0() { header("Location", startsWith("http://localhost:3000/sso-landing?ssoToken=saml-token")); } - private void setIdpBinding(String binding) { - // append entry at end, last entry wins - exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - } - - private static void exec(String... command) { - try { - var result = IDP.execInContainer(command); - if (result.getExitCode() > 0) { - System.out.println(result.getStdout()); - System.err.println(result.getStderr()); - throw new RuntimeException("failure in IDP.execInContainer"); - } - } catch (UnsupportedOperationException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - private void setOkapi(String resource) { - OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP.getBaseUrl())); } private String jsonEncode(String key, String value) { diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index 0117b104..ad9d325c 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -1,37 +1,16 @@ package org.folio.rest.impl; import io.restassured.RestAssured; -import io.restassured.http.Cookie; -import io.restassured.http.Header; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; -import io.vertx.core.DeploymentOptions; import io.vertx.core.Vertx; -import io.vertx.core.http.CookieSameSite; -import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.folio.config.SamlConfigHolder; -import org.folio.rest.RestVerticle; -import org.folio.util.MockJson; +import org.folio.testutil.SimpleSamlPhpContainer; import org.folio.util.SamlTestHelper; -import org.folio.util.StringUtil; import org.junit.*; import org.junit.runner.RunWith; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.images.builder.ImageFromDockerfile; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Path; -import java.util.regex.Pattern; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; /** * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. @@ -40,38 +19,17 @@ public class IdpTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); private static final boolean DEBUG = false; - private static final ImageFromDockerfile simplesamlphp = - new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); - - private static final String TENANT = "diku"; - private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); - private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); - private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); - private static final String STRIPES_URL = "http://localhost:3000"; - - private static final int MODULE_PORT = 9231; - private static final String MODULE_URL = "http://localhost:" + MODULE_PORT; - private static final int OKAPI_PORT = 9230; - private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; - - private static final String TEST_PATH = "/test/path"; - - private static int IDP_PORT; - private static String IDP_BASE_URL; - private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); - private static MockJson OKAPI; - + private static final String TENANT = SamlTestHelper.TENANT; + private static final int MODULE_PORT = SamlTestHelper.MODULE_PORT; + private static final String OKAPI_URL = SamlTestHelper.OKAPI_URL; + private static final String CALLBACK_WITH_EXPIRY = "callback-with-expiry"; private static Vertx VERTX; @ClassRule - public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry"); + public static final SimpleSamlPhpContainer IDP = new SimpleSamlPhpContainer<>(OKAPI_URL, CALLBACK_WITH_EXPIRY); @BeforeClass - public static void setupOnce(TestContext context) throws Exception { + public static void setupOnce(TestContext context) { RestAssured.port = MODULE_PORT; RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); VERTX = Vertx.vertx(); @@ -79,25 +37,10 @@ public static void setupOnce(TestContext context) throws Exception { if (DEBUG) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } - IDP_PORT = IDP.getFirstMappedPort(); - IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; - String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); - exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); - exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - - DeploymentOptions moduleOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", MODULE_PORT) - .put("mock", true)); // to use SAML2ClientMock - OKAPI = new MockJson(); - DeploymentOptions okapiOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); + IDP.init(); - VERTX.deployVerticle(new RestVerticle(), moduleOptions) - .compose(x -> VERTX.deployVerticle(OKAPI, okapiOptions)) - .onComplete(context.asyncAssertSuccess()); + SamlTestHelper.deployVerticle(VERTX, context); } @AfterClass @@ -112,134 +55,22 @@ public void after() { } @Test - public void postCallbackWithExpiry() { - setIdpBinding("POST"); - setOkapi("mock_idptest_post.json"); + public void post() { + IDP.setPostBinding(); + SamlTestHelper.setOkapi("mock_idptest_post.json", IDP); for (int i = 0; i < 2; i++) { - post0(SamlAPITest.CALLBACK_WITH_EXPIRY_URL); + SamlTestHelper.testPost(CALLBACK_WITH_EXPIRY); } } - private void post0(String callbackUrl) { - ExtractableResponse resp = given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .header(JSON_CONTENT_TYPE_HEADER) - .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) - .post("/saml/login") - .then() - .statusCode(200) - .body("bindingMethod", is("POST")) - .extract(); - - String location = resp.body().jsonPath().getString("location"); - String samlRequest = resp.body().jsonPath().getString("samlRequest"); - String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); - Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); - assertThat(cookie.getValue(), is(relayState)); - - String body = given() - .formParams("RelayState", relayState) - .formParams("SAMLRequest", samlRequest) - .post(location) - .then() - .statusCode(200) - .body(containsString("")) - .extract().asString(); - - var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); - assertThat(matcher.find(), is(true)); - - SamlTestHelper.testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); - } - @Test - public void redirectCallbackWithExpiry() { - setIdpBinding("Redirect"); - setOkapi("mock_idptest_redirect.json"); + public void redirect() { + IDP.setRedirectBinding(); + SamlTestHelper.setOkapi("mock_idptest_redirect.json", IDP); for (int i = 0; i < 2; i++) { - redirect0(SamlAPITest.CALLBACK_WITH_EXPIRY_URL); + SamlTestHelper.testRedirect(CALLBACK_WITH_EXPIRY); } } - - private void redirect0(String callbackUrl) { - ExtractableResponse resp = given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .header(JSON_CONTENT_TYPE_HEADER) - .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) - .when() - .post("/saml/login") - .then() - .statusCode(200) - .body("bindingMethod", is("GET")) - .body("location", containsString("/simplesaml/saml2/idp/SSOService.php?")) - .extract(); - - Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); - String location = resp.body().jsonPath().getString("location"); - URL url; - try { - url = new URL(location); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - String [] parameters = StringUtil.urlDecode(url.getQuery()).split("&", 2); - String [] samlRequest = parameters[0].split("=", 2); - String [] relayState = parameters[1].split("=", 2); - location = location.substring(0, location.indexOf("?")); - - String body = - given() - .param(samlRequest[0], samlRequest[1]) - .param(relayState[0], relayState[1]) - .when() - .get(location) - .then() - .statusCode(200) - .body(containsString(" method=\"post\" "), - containsString("action=\"" + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry\">")) - .extract().asString(); - - var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); - assertThat(matcher.find(), is(true)); - - SamlTestHelper.testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, - callbackUrl); - } - - private void setIdpBinding(String binding) { - // append entry at end, last entry wins - exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - } - - private static void exec(String... command) { - try { - var result = IDP.execInContainer(command); - if (result.getExitCode() > 0) { - System.out.println(result.getStdout()); - System.err.println(result.getStderr()); - throw new RuntimeException("failure in IDP.execInContainer"); - } - } catch (UnsupportedOperationException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private void setOkapi(String resource) { - OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); - } - - private String jsonEncode(String key, String value) { - return new JsonObject().put(key, value).encode(); - } } diff --git a/src/test/java/org/folio/rest/impl/IdpTestCallback.java b/src/test/java/org/folio/rest/impl/IdpTestCallback.java deleted file mode 100644 index 1966dc6e..00000000 --- a/src/test/java/org/folio/rest/impl/IdpTestCallback.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.folio.rest.impl; - -import io.restassured.RestAssured; -import io.restassured.http.Cookie; -import io.restassured.http.Header; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Vertx; -import io.vertx.core.http.CookieSameSite; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.unit.TestContext; -import io.vertx.ext.unit.junit.VertxUnitRunner; -import org.folio.config.SamlConfigHolder; -import org.folio.rest.RestVerticle; -import org.folio.util.MockJson; -import org.folio.util.SamlTestHelper; -import org.folio.util.StringUtil; -import org.junit.*; -import org.junit.runner.RunWith; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.images.builder.ImageFromDockerfile; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Path; -import java.util.regex.Pattern; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - -/** - * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. - */ -@RunWith(VertxUnitRunner.class) -public class IdpTestCallback { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); - private static final boolean DEBUG = false; - private static final ImageFromDockerfile simplesamlphp = - new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); - - private static final String TENANT = "diku"; - private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); - private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); - private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); - private static final String STRIPES_URL = "http://localhost:3000"; - - private static final int MODULE_PORT = 9231; - private static final String MODULE_URL = "http://localhost:" + MODULE_PORT; - private static final int OKAPI_PORT = 9230; - private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; - - private static final String TEST_PATH = "/test/path"; - - private static int IDP_PORT; - private static String IDP_BASE_URL; - private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); - private static MockJson OKAPI; - - private static Vertx VERTX; - - @ClassRule - public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); - - @BeforeClass - public static void setupOnce(TestContext context) throws Exception { - RestAssured.port = MODULE_PORT; - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); - VERTX = Vertx.vertx(); - - if (DEBUG) { - IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); - } - IDP_PORT = IDP.getFirstMappedPort(); - IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; - String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); - exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); - exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - - DeploymentOptions moduleOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", MODULE_PORT) - .put("mock", true)); // to use SAML2ClientMock - - OKAPI = new MockJson(); - DeploymentOptions okapiOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); - - VERTX.deployVerticle(new RestVerticle(), moduleOptions) - .compose(x -> VERTX.deployVerticle(OKAPI, okapiOptions)) - .onComplete(context.asyncAssertSuccess()); - } - - @AfterClass - public static void tearDownOnce(TestContext context) { - VERTX.close() - .onComplete(context.asyncAssertSuccess()); - } - - @After - public void after() { - SamlConfigHolder.getInstance().removeClient(TENANT); - } - - @Test - public void postCallback() { - setIdpBinding("POST"); - setOkapi("mock_idptest_post_secure_tokens.json"); - - for (int i = 0; i < 2; i++) { - post0(SamlAPITest.CALLBACK_URL); - } - } - - private void post0(String callbackUrl) { - ExtractableResponse resp = given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .header(JSON_CONTENT_TYPE_HEADER) - .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) - .post("/saml/login") - .then() - .statusCode(200) - .body("bindingMethod", is("POST")) - .extract(); - - String location = resp.body().jsonPath().getString("location"); - String samlRequest = resp.body().jsonPath().getString("samlRequest"); - String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); - Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); - assertThat(cookie.getValue(), is(relayState)); - - String body = given() - .formParams("RelayState", relayState) - .formParams("SAMLRequest", samlRequest) - .post(location) - .then() - .statusCode(200) - .body(containsString("")) - .extract().asString(); - - var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); - assertThat(matcher.find(), is(true)); - - SamlTestHelper.testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); - } - - @Test - public void redirectCallback() { - setIdpBinding("Redirect"); - setOkapi("mock_idptest_redirect_secure_tokens.json"); - - for (int i = 0; i < 2; i++) { - redirect0(SamlAPITest.CALLBACK_URL); - } - } - - private void redirect0(String callbackUrl) { - ExtractableResponse resp = given() - .header(TENANT_HEADER) - .header(TOKEN_HEADER) - .header(OKAPI_URL_HEADER) - .header(JSON_CONTENT_TYPE_HEADER) - .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) - .when() - .post("/saml/login") - .then() - .statusCode(200) - .body("bindingMethod", is("GET")) - .body("location", containsString("/simplesaml/saml2/idp/SSOService.php?")) - .extract(); - - Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); - String location = resp.body().jsonPath().getString("location"); - URL url; - try { - url = new URL(location); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - String [] parameters = StringUtil.urlDecode(url.getQuery()).split("&", 2); - String [] samlRequest = parameters[0].split("=", 2); - String [] relayState = parameters[1].split("=", 2); - location = location.substring(0, location.indexOf("?")); - - String body = - given() - .param(samlRequest[0], samlRequest[1]) - .param(relayState[0], relayState[1]) - .when() - .get(location) - .then() - .statusCode(200) - .body(containsString(" method=\"post\" "), - containsString("action=\"" + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback\">")) - .extract().asString(); - - var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); - assertThat(matcher.find(), is(true)); - - SamlTestHelper.testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), - matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, - callbackUrl); - } - - private void setIdpBinding(String binding) { - // append entry at end, last entry wins - exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - } - - private static void exec(String... command) { - try { - var result = IDP.execInContainer(command); - if (result.getExitCode() > 0) { - System.out.println(result.getStdout()); - System.err.println(result.getStderr()); - throw new RuntimeException("failure in IDP.execInContainer"); - } - } catch (UnsupportedOperationException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private void setOkapi(String resource) { - OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); - } - - private String jsonEncode(String key, String value) { - return new JsonObject().put(key, value).encode(); - } -} diff --git a/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java b/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java new file mode 100644 index 00000000..a8b331a0 --- /dev/null +++ b/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java @@ -0,0 +1,77 @@ +package org.folio.testutil; + +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class SimpleSamlPhpContainer> + extends GenericContainer { + + private static final boolean DEBUG = false; + private static final Logger logger = LoggerFactory.getLogger(SimpleSamlPhpContainer.class); + + private String baseUrl; + + /** + * @param callback either "callback" or "callback-with-expiry" + */ + public SimpleSamlPhpContainer(String okapiUrl, String callback) { + super(DockerImageName.parse("kenchan0130/simplesamlphp:1.19.9")); + var authsources = MountableFile.forClasspathResource("simplesamlphp/authsources.php"); + withCopyToContainer(authsources, "/var/www/simplesamlphp/config/authsources.php"); + withExposedPorts(Integer.valueOf(8080)); + var callbackUrl = okapiUrl + "/_/invoke/tenant/diku/saml/" + callback; + withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", callbackUrl); + withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", callbackUrl); + } + + public void init() { + if (DEBUG) { + followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); + } + // kenchan0130/simplesamlphp doesn't support https + baseUrl = "http://" + getHost() + ":" + getFirstMappedPort() + "/simplesaml/"; + String baseurlpath = baseUrl.replace("/", "\\/"); + exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", + "/var/www/simplesamlphp/config/config.php"); + exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + + public String getBaseUrl() { + return baseUrl; + } + + public void exec(String... command) { + try { + var result = execInContainer(command); + if (result.getExitCode() > 0) { + System.out.println(result.getStdout()); + System.err.println(result.getStderr()); + throw new RuntimeException("failure in execInContainer"); + } + } catch (UnsupportedOperationException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void setPostBinding() { + setIdpBinding("POST"); + } + + public void setRedirectBinding() { + setIdpBinding("Redirect"); + } + + private void setIdpBinding(String binding) { + // append entry at end, last entry wins + exec("sed", "-i", + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + +} diff --git a/src/test/java/org/folio/util/SamlTestHelper.java b/src/test/java/org/folio/util/SamlTestHelper.java index c2194e09..a5764243 100644 --- a/src/test/java/org/folio/util/SamlTestHelper.java +++ b/src/test/java/org/folio/util/SamlTestHelper.java @@ -4,12 +4,142 @@ import io.restassured.http.Cookie; import io.restassured.http.Header; import io.restassured.matcher.RestAssuredMatchers; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import io.vertx.core.http.CookieSameSite; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.TestContext; +import org.folio.rest.RestVerticle; import org.folio.rest.impl.SamlAPI; +import org.folio.testutil.SimpleSamlPhpContainer; -import static org.hamcrest.Matchers.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Pattern; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.both; public class SamlTestHelper { + public static final int OKAPI_PORT = 9230; + public static final int MODULE_PORT = 9231; + public static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; + public static final String TENANT = "diku"; + private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); + private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); + private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); + private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); + private static final String TEST_PATH = "/test/path"; + private static final String STRIPES_URL = "http://localhost:3000"; + private static MockJson OKAPI; + + public static void deployVerticle(Vertx vertx, TestContext context) { + DeploymentOptions moduleOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", MODULE_PORT) + .put("mock", true)); // to use SAML2ClientMock + + OKAPI = new MockJson(); + DeploymentOptions okapiOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); + + vertx.deployVerticle(new RestVerticle(), moduleOptions) + .compose(x -> vertx.deployVerticle(OKAPI, okapiOptions)) + .onComplete(context.asyncAssertSuccess()); + } + + public static void setOkapi(String resource, SimpleSamlPhpContainer idp) { + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", idp.getBaseUrl())); + } + + public static void testPost(String callback) { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) + .post("/saml/login") + .then() + .statusCode(200) + .body("bindingMethod", is("POST")) + .extract(); + + String location = resp.body().jsonPath().getString("location"); + String samlRequest = resp.body().jsonPath().getString("samlRequest"); + String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); + Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); + assertThat(cookie.getValue(), is(relayState)); + + String body = given() + .formParams("RelayState", relayState) + .formParams("SAMLRequest", samlRequest) + .post(location) + .then() + .statusCode(200) + .body(containsString("", OKAPI_URL, callback))) + .extract().asString(); + + var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); + assertThat(matcher.find(), is(true)); + + testCookieResponse(cookie, relayState, TEST_PATH, CookieSameSite.LAX.toString(), + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, "/saml/" + callback); + } + + public static void testRedirect(String callback) { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL + TEST_PATH)) + .when() + .post("/saml/login") + .then() + .statusCode(200) + .body("bindingMethod", is("GET")) + .body("location", containsString("/simplesaml/saml2/idp/SSOService.php?")) + .extract(); + + Cookie cookie = resp.detailedCookie(SamlAPI.RELAY_STATE); + String location = resp.body().jsonPath().getString("location"); + URL url; + try { + url = new URL(location); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + String [] parameters = StringUtil.urlDecode(url.getQuery()).split("&", 2); + String [] samlRequest = parameters[0].split("=", 2); + String [] relayState = parameters[1].split("=", 2); + location = location.substring(0, location.indexOf("?")); + + String body = + given() + .param(samlRequest[0], samlRequest[1]) + .param(relayState[0], relayState[1]) + .when() + .get(location) + .then() + .statusCode(200) + .body(containsString(" method=\"post\" "), + containsString(String.format("action=\"%s/_/invoke/tenant/diku/saml/%s\">", OKAPI_URL, callback))) + .extract().asString(); + + var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); + assertThat(matcher.find(), is(true)); + + testCookieResponse(cookie, relayState[1], TEST_PATH, CookieSameSite.LAX.toString(), + matcher.group(1), TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, + "/saml/" + callback); + } public static void testCookieResponse(Cookie cookie, String relayState, String testPath, String sameSite, String samlResponse, Header tenantHeader, Header tokenHeader, @@ -45,4 +175,8 @@ public static void testCookieResponse(Cookie cookie, String relayState, String t .header("Location", both(containsString(PercentCodec.encodeAsString("2050-10-05T20:19:33Z"))) .and(containsString(PercentCodec.encodeAsString("2050-10-05T20:19:33Z")))); } + + private static String jsonEncode(String key, String value) { + return new JsonObject().put(key, value).encode(); + } } From 2c42c9deeb37eab27ef237d4fb6222961067e42b Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Mon, 9 Sep 2024 17:41:33 -0400 Subject: [PATCH 07/12] Missed check for useSecureTokens in test --- src/test/java/org/folio/rest/impl/SamlAPITest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 85443035..00b61366 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -952,6 +952,7 @@ public void putConfigurationUseSecureTokens() { .then() .statusCode(200) .body("callback", equalTo("callback")) + .body("useSecureTokens", equalTo(true)) .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")); } From 8f9fdb8e476c294a6385c424ff3972681b7ec9b7 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Tue, 10 Sep 2024 11:28:23 -0400 Subject: [PATCH 08/12] Remove deprecation note on raml. Also changes suggested by Julian: - Remove code dup from ConfigEntryUtil - Fix up code formatting - Simplification of expression to detect legacy response --- ramls/saml-login.raml | 2 +- .../java/org/folio/config/model/SamlConfiguration.java | 8 ++++++-- src/main/java/org/folio/rest/impl/SamlAPI.java | 3 +-- src/main/java/org/folio/util/ConfigEntryUtil.java | 4 +--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ramls/saml-login.raml b/ramls/saml-login.raml index 3f2c2bf0..0310fda2 100644 --- a/ramls/saml-login.raml +++ b/ramls/saml-login.raml @@ -59,7 +59,7 @@ types: example: "Bad request" /callback: post: - description: Redirect browser to sso-landing page with generated token. Deprecated. + description: Redirect browser to sso-landing page with generated token. body: application/octet-stream: type: string diff --git a/src/main/java/org/folio/config/model/SamlConfiguration.java b/src/main/java/org/folio/config/model/SamlConfiguration.java index 127581a9..613bcf0a 100644 --- a/src/main/java/org/folio/config/model/SamlConfiguration.java +++ b/src/main/java/org/folio/config/model/SamlConfiguration.java @@ -135,7 +135,11 @@ public void setCallback(String callback) { this.callback = callback; } - public String getUseSecureTokens() { return useSecureTokens; } + public String getUseSecureTokens() { + return useSecureTokens; + } - public void setUseSecureTokens(String useSecureTokens) { this.useSecureTokens = useSecureTokens; } + public void setUseSecureTokens(String useSecureTokens) { + this.useSecureTokens = useSecureTokens; + } } diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 1a1f506b..a6f23d24 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -284,8 +284,7 @@ private PostSamlCallbackResponse failCallbackResponse(Throwable cause, RoutingCo } private boolean isLegacyResponse(SamlConfiguration configuration) { - return "callback".equals(configuration.getCallback()) && (configuration.getUseSecureTokens() == null - || "false".equals(configuration.getUseSecureTokens())); + return "callback".equals(configuration.getCallback()) && ! "true".equals(configuration.getUseSecureTokens()); } private String getTokenSignEndpoint(SamlConfiguration configuration) { diff --git a/src/main/java/org/folio/util/ConfigEntryUtil.java b/src/main/java/org/folio/util/ConfigEntryUtil.java index 7bfeeae4..141e0867 100644 --- a/src/main/java/org/folio/util/ConfigEntryUtil.java +++ b/src/main/java/org/folio/util/ConfigEntryUtil.java @@ -43,8 +43,6 @@ public static void valueChanged(String oldValue, Boolean newValue, Consumer Date: Tue, 10 Sep 2024 14:36:14 -0400 Subject: [PATCH 09/12] Removing SimpleSamlPhpContainer --- .../org/folio/rest/impl/IdpCallbackTest.java | 84 ++++++++++++++++--- .../org/folio/rest/impl/IdpLegacyTest.java | 55 ++++++++++-- .../java/org/folio/rest/impl/IdpTest.java | 83 +++++++++++++++--- .../testutil/SimpleSamlPhpContainer.java | 77 ----------------- .../java/org/folio/util/SamlTestHelper.java | 25 ------ 5 files changed, 192 insertions(+), 132 deletions(-) delete mode 100644 src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java diff --git a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java index 93df9ade..62bc3748 100644 --- a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java +++ b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java @@ -1,16 +1,24 @@ package org.folio.rest.impl; import io.restassured.RestAssured; +import io.vertx.core.DeploymentOptions; import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.folio.config.SamlConfigHolder; -import org.folio.testutil.SimpleSamlPhpContainer; +import org.folio.rest.RestVerticle; +import org.folio.util.MockJson; import org.folio.util.SamlTestHelper; import org.junit.*; import org.junit.runner.RunWith; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.IOException; +import java.nio.file.Path; /** * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. @@ -19,14 +27,28 @@ public class IdpCallbackTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); private static final boolean DEBUG = false; - private static final String TENANT = SamlTestHelper.TENANT; - private static final int MODULE_PORT = SamlTestHelper.MODULE_PORT; - private static final String OKAPI_URL = SamlTestHelper.OKAPI_URL; + private static final ImageFromDockerfile simplesamlphp = + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + + private static final String TENANT = "diku"; + + private static final int MODULE_PORT = 9231; + private static final int OKAPI_PORT = 9230; + private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; private static final String CALLBACK = "callback"; + + private static int IDP_PORT; + private static String IDP_BASE_URL; + private static MockJson OKAPI; + private static Vertx VERTX; @ClassRule - public static final SimpleSamlPhpContainer IDP = new SimpleSamlPhpContainer<>(OKAPI_URL, CALLBACK); + public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); @BeforeClass public static void setupOnce(TestContext context) { @@ -37,9 +59,25 @@ public static void setupOnce(TestContext context) { if (DEBUG) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } - IDP.init(); + IDP_PORT = IDP.getFirstMappedPort(); + IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; + String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); + exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", + "/var/www/simplesamlphp/config/config.php"); + exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + + DeploymentOptions moduleOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", MODULE_PORT) + .put("mock", true)); // to use SAML2ClientMock - SamlTestHelper.deployVerticle(VERTX, context); + OKAPI = new MockJson(); + DeploymentOptions okapiOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); + + VERTX.deployVerticle(new RestVerticle(), moduleOptions) + .compose(x -> VERTX.deployVerticle(OKAPI, okapiOptions)) + .onComplete(context.asyncAssertSuccess()); } @AfterClass @@ -55,8 +93,8 @@ public void after() { @Test public void postCallback() { - IDP.setPostBinding(); - SamlTestHelper.setOkapi("mock_idptest_post_secure_tokens.json", IDP); + setIdpBinding("POST"); + setOkapi("mock_idptest_post_secure_tokens.json"); for (int i = 0; i < 2; i++) { SamlTestHelper.testPost(CALLBACK); @@ -65,11 +103,35 @@ public void postCallback() { @Test public void redirectCallback() { - IDP.setRedirectBinding(); - SamlTestHelper.setOkapi("mock_idptest_redirect_secure_tokens.json", IDP); + setIdpBinding("Redirect"); + setOkapi("mock_idptest_redirect_secure_tokens.json"); for (int i = 0; i < 2; i++) { SamlTestHelper.testRedirect(CALLBACK); } } + + private void setIdpBinding(String binding) { + // append entry at end, last entry wins + exec("sed", "-i", + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + + private static void exec(String... command) { + try { + var result = IDP.execInContainer(command); + if (result.getExitCode() > 0) { + System.out.println(result.getStdout()); + System.err.println(result.getStderr()); + throw new RuntimeException("failure in IDP.execInContainer"); + } + } catch (UnsupportedOperationException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void setOkapi(String resource) { + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); + } } diff --git a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java index ad653365..1b9b5aef 100644 --- a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java +++ b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java @@ -16,11 +16,12 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Path; import java.util.regex.Pattern; import org.folio.config.SamlConfigHolder; -import org.folio.testutil.SimpleSamlPhpContainer; import org.folio.rest.RestVerticle; import org.folio.util.MockJson; import org.folio.util.StringUtil; @@ -31,7 +32,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; /** * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. @@ -40,23 +43,32 @@ public class IdpLegacyTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpLegacyTest.class); private static final boolean DEBUG = false; + private static final ImageFromDockerfile simplesamlphp = + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + private static final String TENANT = "diku"; private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); private static final Header TOKEN_HEADER = new Header("X-Okapi-Token", "mytoken"); private static final Header JSON_CONTENT_TYPE_HEADER = new Header("Content-Type", "application/json"); private static final String STRIPES_URL = "http://localhost:3000"; + private static final int MODULE_PORT = 9231; private static final String MODULE_URL = "http://localhost:" + MODULE_PORT; private static final int OKAPI_PORT = 9230; private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; + private static int IDP_PORT; + private static String IDP_BASE_URL; private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); private static MockJson OKAPI; private static Vertx VERTX; @ClassRule - public static final SimpleSamlPhpContainer IDP = - new SimpleSamlPhpContainer<>(OKAPI_URL, "callback"); + public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); @BeforeClass public static void setupOnce(TestContext context) { @@ -68,7 +80,14 @@ public static void setupOnce(TestContext context) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } - IDP.init(); + IDP_PORT = IDP.getFirstMappedPort(); + IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; + String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); + exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", + "/var/www/simplesamlphp/config/config.php"); + exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + DeploymentOptions moduleOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", MODULE_PORT) @@ -96,7 +115,7 @@ public void after() { @Test public void post() { - IDP.setPostBinding();; + setIdpBinding("POST"); setOkapi("mock_idptest_post_legacy.json"); for (int i = 0; i < 2; i++) { @@ -152,7 +171,7 @@ private void post0() { @Test public void redirect() { - IDP.setRedirectBinding(); + setIdpBinding("Redirect"); setOkapi("mock_idptest_redirect_legacy.json"); for (int i = 0; i < 2; i++) { @@ -218,11 +237,31 @@ private void redirect0() { header("Location", startsWith("http://localhost:3000/sso-landing?ssoToken=saml-token")); } + private void setIdpBinding(String binding) { + // append entry at end, last entry wins + exec("sed", "-i", + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + + private static void exec(String... command) { + try { + var result = IDP.execInContainer(command); + if (result.getExitCode() > 0) { + System.out.println(result.getStdout()); + System.err.println(result.getStderr()); + throw new RuntimeException("failure in IDP.execInContainer"); + } + } catch (UnsupportedOperationException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + private void setOkapi(String resource) { - OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP.getBaseUrl())); + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); } - private String jsonEncode(String key, String value) { + private static String jsonEncode(String key, String value) { return new JsonObject().put(key, value).encode(); } } diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index ad9d325c..36848bd3 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -1,16 +1,24 @@ package org.folio.rest.impl; import io.restassured.RestAssured; +import io.vertx.core.DeploymentOptions; import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.folio.config.SamlConfigHolder; -import org.folio.testutil.SimpleSamlPhpContainer; +import org.folio.rest.RestVerticle; +import org.folio.util.MockJson; import org.folio.util.SamlTestHelper; import org.junit.*; import org.junit.runner.RunWith; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.IOException; +import java.nio.file.Path; /** * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. @@ -19,14 +27,28 @@ public class IdpTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); private static final boolean DEBUG = false; - private static final String TENANT = SamlTestHelper.TENANT; - private static final int MODULE_PORT = SamlTestHelper.MODULE_PORT; - private static final String OKAPI_URL = SamlTestHelper.OKAPI_URL; + private static final ImageFromDockerfile simplesamlphp = + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + + private static final String TENANT = "diku"; + + private static final int MODULE_PORT = 9231; + private static final int OKAPI_PORT = 9230; + private static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; private static final String CALLBACK_WITH_EXPIRY = "callback-with-expiry"; + + private static int IDP_PORT; + private static String IDP_BASE_URL; + private static MockJson OKAPI; + private static Vertx VERTX; @ClassRule - public static final SimpleSamlPhpContainer IDP = new SimpleSamlPhpContainer<>(OKAPI_URL, CALLBACK_WITH_EXPIRY); + public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry"); @BeforeClass public static void setupOnce(TestContext context) { @@ -37,10 +59,25 @@ public static void setupOnce(TestContext context) { if (DEBUG) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } + IDP_PORT = IDP.getFirstMappedPort(); + IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; + String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); + exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", + "/var/www/simplesamlphp/config/config.php"); + exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + + DeploymentOptions moduleOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", MODULE_PORT) + .put("mock", true)); // to use SAML2ClientMock - IDP.init(); + OKAPI = new MockJson(); + DeploymentOptions okapiOptions = new DeploymentOptions() + .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); - SamlTestHelper.deployVerticle(VERTX, context); + VERTX.deployVerticle(new RestVerticle(), moduleOptions) + .compose(x -> VERTX.deployVerticle(OKAPI, okapiOptions)) + .onComplete(context.asyncAssertSuccess()); } @AfterClass @@ -56,8 +93,8 @@ public void after() { @Test public void post() { - IDP.setPostBinding(); - SamlTestHelper.setOkapi("mock_idptest_post.json", IDP); + setIdpBinding("POST"); + setOkapi("mock_idptest_post.json"); for (int i = 0; i < 2; i++) { SamlTestHelper.testPost(CALLBACK_WITH_EXPIRY); @@ -66,11 +103,35 @@ public void post() { @Test public void redirect() { - IDP.setRedirectBinding(); - SamlTestHelper.setOkapi("mock_idptest_redirect.json", IDP); + setIdpBinding("Redirect"); + setOkapi("mock_idptest_redirect.json"); for (int i = 0; i < 2; i++) { SamlTestHelper.testRedirect(CALLBACK_WITH_EXPIRY); } } + + private void setIdpBinding(String binding) { + // append entry at end, last entry wins + exec("sed", "-i", + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + } + + private static void exec(String... command) { + try { + var result = IDP.execInContainer(command); + if (result.getExitCode() > 0) { + System.out.println(result.getStdout()); + System.err.println(result.getStderr()); + throw new RuntimeException("failure in IDP.execInContainer"); + } + } catch (UnsupportedOperationException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void setOkapi(String resource) { + OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); + } } diff --git a/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java b/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java deleted file mode 100644 index a8b331a0..00000000 --- a/src/test/java/org/folio/testutil/SimpleSamlPhpContainer.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.folio.testutil; - -import java.io.IOException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; - -public class SimpleSamlPhpContainer> - extends GenericContainer { - - private static final boolean DEBUG = false; - private static final Logger logger = LoggerFactory.getLogger(SimpleSamlPhpContainer.class); - - private String baseUrl; - - /** - * @param callback either "callback" or "callback-with-expiry" - */ - public SimpleSamlPhpContainer(String okapiUrl, String callback) { - super(DockerImageName.parse("kenchan0130/simplesamlphp:1.19.9")); - var authsources = MountableFile.forClasspathResource("simplesamlphp/authsources.php"); - withCopyToContainer(authsources, "/var/www/simplesamlphp/config/authsources.php"); - withExposedPorts(Integer.valueOf(8080)); - var callbackUrl = okapiUrl + "/_/invoke/tenant/diku/saml/" + callback; - withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", callbackUrl); - withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", callbackUrl); - } - - public void init() { - if (DEBUG) { - followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); - } - // kenchan0130/simplesamlphp doesn't support https - baseUrl = "http://" + getHost() + ":" + getFirstMappedPort() + "/simplesaml/"; - String baseurlpath = baseUrl.replace("/", "\\/"); - exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); - exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - } - - public String getBaseUrl() { - return baseUrl; - } - - public void exec(String... command) { - try { - var result = execInContainer(command); - if (result.getExitCode() > 0) { - System.out.println(result.getStdout()); - System.err.println(result.getStderr()); - throw new RuntimeException("failure in execInContainer"); - } - } catch (UnsupportedOperationException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - public void setPostBinding() { - setIdpBinding("POST"); - } - - public void setRedirectBinding() { - setIdpBinding("Redirect"); - } - - private void setIdpBinding(String binding) { - // append entry at end, last entry wins - exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - } - -} diff --git a/src/test/java/org/folio/util/SamlTestHelper.java b/src/test/java/org/folio/util/SamlTestHelper.java index a5764243..4cc3c10b 100644 --- a/src/test/java/org/folio/util/SamlTestHelper.java +++ b/src/test/java/org/folio/util/SamlTestHelper.java @@ -6,14 +6,9 @@ import io.restassured.matcher.RestAssuredMatchers; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Vertx; import io.vertx.core.http.CookieSameSite; import io.vertx.core.json.JsonObject; -import io.vertx.ext.unit.TestContext; -import org.folio.rest.RestVerticle; import org.folio.rest.impl.SamlAPI; -import org.folio.testutil.SimpleSamlPhpContainer; import java.net.MalformedURLException; import java.net.URL; @@ -28,7 +23,6 @@ public class SamlTestHelper { public static final int OKAPI_PORT = 9230; - public static final int MODULE_PORT = 9231; public static final String OKAPI_URL = "http://localhost:" + OKAPI_PORT; public static final String TENANT = "diku"; private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); @@ -37,25 +31,6 @@ public class SamlTestHelper { private static final Header OKAPI_URL_HEADER = new Header("X-Okapi-Url", OKAPI_URL); private static final String TEST_PATH = "/test/path"; private static final String STRIPES_URL = "http://localhost:3000"; - private static MockJson OKAPI; - - public static void deployVerticle(Vertx vertx, TestContext context) { - DeploymentOptions moduleOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", MODULE_PORT) - .put("mock", true)); // to use SAML2ClientMock - - OKAPI = new MockJson(); - DeploymentOptions okapiOptions = new DeploymentOptions() - .setConfig(new JsonObject().put("http.port", OKAPI_PORT)); - - vertx.deployVerticle(new RestVerticle(), moduleOptions) - .compose(x -> vertx.deployVerticle(OKAPI, okapiOptions)) - .onComplete(context.asyncAssertSuccess()); - } - - public static void setOkapi(String resource, SimpleSamlPhpContainer idp) { - OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", idp.getBaseUrl())); - } public static void testPost(String callback) { ExtractableResponse resp = given() From 345a61b26fbd0e446e80cdb14eef49d1aff3a032 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Tue, 10 Sep 2024 15:14:00 -0400 Subject: [PATCH 10/12] Clean up --- pom.xml | 2 +- .../java/org/folio/rest/impl/SamlAPI.java | 1 - .../org/folio/rest/impl/IdpCallbackTest.java | 14 ++++++------- .../org/folio/rest/impl/IdpLegacyTest.java | 20 +++++++++---------- .../java/org/folio/rest/impl/IdpTest.java | 14 ++++++------- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 82236e26..91b79568 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.8.2-SNAPSHOT + 2.9.0-SNAPSHOT mod-login-saml diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index a6f23d24..9135dff9 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -11,7 +11,6 @@ import java.time.Instant; import java.util.*; -import javax.ws.rs.core.Configuration; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; diff --git a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java index 62bc3748..fa865893 100644 --- a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java +++ b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java @@ -45,10 +45,10 @@ public class IdpCallbackTest { @ClassRule public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); @BeforeClass public static void setupOnce(TestContext context) { @@ -63,13 +63,13 @@ public static void setupOnce(TestContext context) { IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); + "/var/www/simplesamlphp/config/config.php"); exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); DeploymentOptions moduleOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", MODULE_PORT) - .put("mock", true)); // to use SAML2ClientMock + .put("mock", true)); // to use SAML2ClientMock OKAPI = new MockJson(); DeploymentOptions okapiOptions = new DeploymentOptions() diff --git a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java index 1b9b5aef..8a76dd49 100644 --- a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java +++ b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java @@ -65,10 +65,10 @@ public class IdpLegacyTest { @ClassRule public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback"); @BeforeClass public static void setupOnce(TestContext context) { @@ -79,15 +79,13 @@ public static void setupOnce(TestContext context) { if (DEBUG) { IDP.followOutput(new Slf4jLogConsumer(logger).withSeparateOutputStreams()); } - IDP_PORT = IDP.getFirstMappedPort(); IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); + "/var/www/simplesamlphp/config/config.php"); exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); - + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); DeploymentOptions moduleOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", MODULE_PORT) @@ -240,8 +238,8 @@ private void redirect0() { private void setIdpBinding(String binding) { // append entry at end, last entry wins exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); } private static void exec(String... command) { @@ -261,7 +259,7 @@ private void setOkapi(String resource) { OKAPI.setMockContent(resource, s -> s.replace("http://localhost:8888/simplesaml/", IDP_BASE_URL)); } - private static String jsonEncode(String key, String value) { + private String jsonEncode(String key, String value) { return new JsonObject().put(key, value).encode(); } } diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index 36848bd3..49794939 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -45,10 +45,10 @@ public class IdpTest { @ClassRule public static final GenericContainer IDP = new GenericContainer<>(simplesamlphp) - .withExposedPorts(8080) - .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry") - .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", - OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry"); + .withExposedPorts(8080) + .withEnv("SIMPLESAMLPHP_SP_ENTITY_ID", OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry"); @BeforeClass public static void setupOnce(TestContext context) { @@ -63,13 +63,13 @@ public static void setupOnce(TestContext context) { IDP_BASE_URL = "http://" + IDP.getHost() + ":" + IDP_PORT + "/simplesaml/"; String baseurlpath = IDP_BASE_URL.replace("/", "\\/"); exec("sed", "-i", "s/'baseurlpath' =>.*/'baseurlpath' => '" + baseurlpath + "',/", - "/var/www/simplesamlphp/config/config.php"); + "/var/www/simplesamlphp/config/config.php"); exec("sed", "-i", "s/'auth' =>.*/'auth' => 'example-static',/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); DeploymentOptions moduleOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", MODULE_PORT) - .put("mock", true)); // to use SAML2ClientMock + .put("mock", true)); // to use SAML2ClientMock OKAPI = new MockJson(); DeploymentOptions okapiOptions = new DeploymentOptions() From 697b49d8454a31fe0b8bdfa8e8e6cf749f106c43 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Tue, 10 Sep 2024 15:28:26 -0400 Subject: [PATCH 11/12] More clean up --- src/test/java/org/folio/rest/impl/IdpCallbackTest.java | 2 +- src/test/java/org/folio/rest/impl/IdpLegacyTest.java | 2 +- src/test/java/org/folio/rest/impl/IdpTest.java | 4 ++-- src/test/java/org/folio/rest/impl/SamlAPITest.java | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java index fa865893..7aa6bea8 100644 --- a/src/test/java/org/folio/rest/impl/IdpCallbackTest.java +++ b/src/test/java/org/folio/rest/impl/IdpCallbackTest.java @@ -28,7 +28,7 @@ public class IdpCallbackTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); private static final boolean DEBUG = false; private static final ImageFromDockerfile simplesamlphp = - new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); private static final String TENANT = "diku"; diff --git a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java index 8a76dd49..43a411a6 100644 --- a/src/test/java/org/folio/rest/impl/IdpLegacyTest.java +++ b/src/test/java/org/folio/rest/impl/IdpLegacyTest.java @@ -44,7 +44,7 @@ public class IdpLegacyTest { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpLegacyTest.class); private static final boolean DEBUG = false; private static final ImageFromDockerfile simplesamlphp = - new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); + new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); private static final String TENANT = "diku"; private static final Header TENANT_HEADER = new Header("X-Okapi-Tenant", TENANT); diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java index 49794939..5ff6c7ec 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -114,8 +114,8 @@ public void redirect() { private void setIdpBinding(String binding) { // append entry at end, last entry wins exec("sed", "-i", - "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", - "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); + "s/];/'SingleSignOnServiceBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-" + binding + "',\\n];/", + "/var/www/simplesamlphp/metadata/saml20-idp-hosted.php"); } private static void exec(String... command) { diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 00b61366..997034dd 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -577,7 +577,7 @@ public void callbackEndpointTests_Legacy() { .body("bindingMethod", equalTo("POST")) .statusCode(200) .extract(); -// + String cookie = resp.cookie(SamlAPI.RELAY_STATE); String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); @@ -761,11 +761,11 @@ private void testCallback(String callbackUrl) { log.info("=== Test - POST /saml/callback RTR - success ==="); SamlTestHelper.testCookieResponse(detailedCookie, relayState, testPath, CookieSameSite.LAX.toString(), - samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); + samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); CookieSameSiteConfig.set(Map.of("LOGIN_COOKIE_SAMESITE", CookieSameSite.NONE.toString())); SamlTestHelper.testCookieResponse(detailedCookie, relayState, testPath, CookieSameSite.NONE.toString(), - samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); + samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl); CookieSameSiteConfig.set(Map.of()); testCallbackErrorCases(callbackUrl, relayState, cookie); From 5b0e7404b1071ae2f7e8b88c7361d0256cb34dc1 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Wed, 11 Sep 2024 12:10:18 -0400 Subject: [PATCH 12/12] Resolves Julian's latest comments --- pom.xml | 2 +- ramls/schemas/SamlConfig.json | 2 +- ramls/schemas/SamlConfigRequest.json | 2 +- src/main/java/org/folio/util/ConfigEntryUtil.java | 4 +--- src/test/java/org/folio/rest/impl/SamlAPITest.java | 8 ++++---- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 91b79568..82236e26 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.9.0-SNAPSHOT + 2.8.2-SNAPSHOT mod-login-saml diff --git a/ramls/schemas/SamlConfig.json b/ramls/schemas/SamlConfig.json index 3b9262e6..51ea8c83 100644 --- a/ramls/schemas/SamlConfig.json +++ b/ramls/schemas/SamlConfig.json @@ -46,7 +46,7 @@ }, "useSecureTokens": { "type": "boolean", - "description": "When present, and true, and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", + "description": "When present, and true, and when callback is configured with the value 'callback', enables the refresh token payload on the /callback endpoint.", "required": false } } diff --git a/ramls/schemas/SamlConfigRequest.json b/ramls/schemas/SamlConfigRequest.json index 1913261a..5aceaea3 100644 --- a/ramls/schemas/SamlConfigRequest.json +++ b/ramls/schemas/SamlConfigRequest.json @@ -46,7 +46,7 @@ }, "useSecureTokens": { "type": "boolean", - "description": "When present, and 'true', and when callback is configured with the value 'callback', enables the refresh token payload on the callback endpoint.", + "description": "When present, and 'true', and when callback is configured with the value 'callback', enables the refresh token payload on the /callback endpoint.", "required": false } } diff --git a/src/main/java/org/folio/util/ConfigEntryUtil.java b/src/main/java/org/folio/util/ConfigEntryUtil.java index 141e0867..b49edcbd 100644 --- a/src/main/java/org/folio/util/ConfigEntryUtil.java +++ b/src/main/java/org/folio/util/ConfigEntryUtil.java @@ -39,10 +39,8 @@ public static void valueChanged(String oldValue, String newValue, Consumer onChanged) { - Objects.requireNonNull(onChanged); - String newValueString = (newValue == null) ? null : newValue.toString(); - valueChanged(oldValue, newValueString); + valueChanged(oldValue, newValueString, onChanged); } } diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 997034dd..dff39903 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -600,7 +600,7 @@ public void callbackEndpointTests_Legacy() { } private void testCallbackErrorCases(String callbackUrl, String relayState, String cookie) { - log.info("=== Test - POST /saml/callback - failure (wrong cookie) ==="); + log.info("=== Test - POST /saml/{} - failure (wrong cookie) ===", callbackUrl); given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -613,7 +613,7 @@ private void testCallbackErrorCases(String callbackUrl, String relayState, Strin .statusCode(403) .body(is("CSRF attempt detected")); - log.info("=== Test - POST /saml/callback - failure (wrong relay) ==="); + log.info("=== Test - POST /saml/{} - failure (wrong relay) ===", callbackUrl); given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -626,7 +626,7 @@ private void testCallbackErrorCases(String callbackUrl, String relayState, Strin .statusCode(400) .body(containsString("Invalid relay state url")); - log.info("=== Test - POST /saml/callback - failure (no cookie) ==="); + log.info("=== Test - POST /saml/{} - failure (no cookie) ===", callbackUrl); given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -759,7 +759,7 @@ private void testCallback(String callbackUrl) { String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); String samlResponse = "saml-response"; - log.info("=== Test - POST /saml/callback RTR - success ==="); + log.info("=== Test - POST /saml/{} RTR - success ===", callbackUrl); SamlTestHelper.testCookieResponse(detailedCookie, relayState, testPath, CookieSameSite.LAX.toString(), samlResponse, TENANT_HEADER, TOKEN_HEADER, OKAPI_URL_HEADER, callbackUrl);