From ee1158d7caf3a8aef274034cb00545337696b114 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Tue, 7 Mar 2023 12:19:46 +0100 Subject: [PATCH 01/40] MODLOGSAML-161: Use TLSv1.2, not SSL, in SSLContext.getInstance Encryption that is older than TLSv1.2 is considered insecure. Task: Use TLSv1.2, not SSL, in SSLContext.getInstance method in ApiInitializer.java. JDK and OpenSSL already have disabled older encryption. This change is an additional safeguard, but it is mainly used to make static code checkers like Snyk quiet. --- src/main/java/org/folio/rest/impl/ApiInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/rest/impl/ApiInitializer.java b/src/main/java/org/folio/rest/impl/ApiInitializer.java index d53fe35c..4029e9d1 100644 --- a/src/main/java/org/folio/rest/impl/ApiInitializer.java +++ b/src/main/java/org/folio/rest/impl/ApiInitializer.java @@ -64,7 +64,7 @@ public void checkServerTrusted( // Install the all-trusting trust manager try { - SSLContext sc = SSLContext.getInstance("SSL"); + SSLContext sc = SSLContext.getInstance("TLSv1.2"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); } catch (GeneralSecurityException e) { From ac070c8ef438454e1ab861a9b7bd6b897f99d22b Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Sat, 1 Apr 2023 16:51:14 +0200 Subject: [PATCH 02/40] NEWS for 2.6.1 --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 32b07a26..4ba2f63a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## 2.6.1 - 2023-04-01 + + * [MODLOGSAML-159](https://issues.folio.org/browse/MODLOGSAML-159) OpenSSL 3.0.8 fixing 8 vulns + * [MODLOGSAML-161](https://issues.folio.org/browse/MODLOGSAML-161) Use TLSv1.2, not SSL, in SSLContext.getInstance + ## 2.6.0 - 2023-02-16 * [MODLOGSAML-157](https://issues.folio.org/browse/MODLOGSAML-157) Upgrade dependences: pac4j 5.7.0, RMB 35.0.6, Vert.x 4.3.8 From 6ebe6742c33cb8e98bc7fa726b865d1eddf0c20e Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Sat, 1 Apr 2023 16:52:49 +0200 Subject: [PATCH 03/40] [maven-release-plugin] prepare release v2.6.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fc2c2a5c..04f94b69 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.7.0-SNAPSHOT + 2.6.1 mod-login-saml @@ -196,7 +196,7 @@ https://github.com/folio-org/mod-login-saml scm:git:git://github.com:folio-org/mod-login-saml.git scm:git:git@github.com:folio-org/mod-login-saml.git - HEAD + v2.6.1 From fe750d153e44cb23eb2cf332e6aa7bc5784870dd Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Sat, 1 Apr 2023 16:52:49 +0200 Subject: [PATCH 04/40] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 04f94b69..fc2c2a5c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.6.1 + 2.7.0-SNAPSHOT mod-login-saml @@ -196,7 +196,7 @@ https://github.com/folio-org/mod-login-saml scm:git:git://github.com:folio-org/mod-login-saml.git scm:git:git@github.com:folio-org/mod-login-saml.git - v2.6.1 + HEAD From 5d6e295ff9deaea26323b7abad0f7aacb778ab67 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Thu, 6 Apr 2023 23:11:01 +0200 Subject: [PATCH 05/40] MODLOGSAML-165: json-smart 2.4.10 Upgrade json-smart from 2.4.8 to 2.4.10 fixing Denial of Service (DoS) on deeply nested JSON array or object: https://nvd.nist.gov/vuln/detail/CVE-2023-1370 --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index fc2c2a5c..613dfd12 100644 --- a/pom.xml +++ b/pom.xml @@ -128,6 +128,16 @@ + + + net.minidev + json-smart + 2.4.10 + + io.rest-assured rest-assured From 33c9e0abbc1c9661e2b7490b5c7a89dd5dfbe059 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Tue, 18 Apr 2023 23:44:22 +0200 Subject: [PATCH 06/40] MODLOGSAML-165: json-smart 2.4.10 Enforce upgrade by exclusion --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 613dfd12..d9de6117 100644 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,13 @@ org.pac4j vertx-pac4j ${vertx-pac4j.version} + + + + net.minidev + json-smart + + From 7b16ce38d3c8841a2e3cd0a9f6cd221b393435e3 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Fri, 21 Apr 2023 12:31:17 +0200 Subject: [PATCH 07/40] MODLOGSAML-166: xmlsec 2.3.3 https://issues.folio.org/browse/MODLOGSAML-166 Upgrade xmlsec from 2.3.0 to 2.3.3. --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index d9de6117..247e10cb 100644 --- a/pom.xml +++ b/pom.xml @@ -145,6 +145,16 @@ 2.4.10 + + + org.apache.santuario + xmlsec + 2.3.3 + + io.rest-assured rest-assured From 9135530d0244368b2119460a2a94ce6a495ac948 Mon Sep 17 00:00:00 2001 From: David Crossley Date: Wed, 26 Apr 2023 18:28:09 +1000 Subject: [PATCH 08/40] Use API-related Workflows FOLIO-3678 --- .github/workflows/api-doc.yml | 91 +++++++++++++++++++++++++++ .github/workflows/api-lint.yml | 65 +++++++++++++++++++ .github/workflows/api-schema-lint.yml | 46 ++++++++++++++ Jenkinsfile | 5 -- 4 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/api-doc.yml create mode 100644 .github/workflows/api-lint.yml create mode 100644 .github/workflows/api-schema-lint.yml diff --git a/.github/workflows/api-doc.yml b/.github/workflows/api-doc.yml new file mode 100644 index 00000000..a308c9a8 --- /dev/null +++ b/.github/workflows/api-doc.yml @@ -0,0 +1,91 @@ +name: api-doc + +# https://dev.folio.org/guides/api-doc/ + +# API_TYPES: string: The space-separated list of types to consider. +# One or more of 'RAML OAS'. +# e.g. 'OAS' +# +# API_DIRECTORIES: string: The space-separated list of directories to search +# for API description files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' + +env: + API_TYPES: 'RAML' + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + OUTPUT_DIR: 'folio-api-docs' + AWS_S3_BUCKET: 'foliodocs' + AWS_S3_FOLDER: 'api' + AWS_S3_REGION: 'us-east-1' + AWS_S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + AWS_S3_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} + +on: + push: + branches: [ main, master ] + paths: + - 'ramls/**' + tags: '[vV][0-9]+.[0-9]+.[0-9]+*' + +jobs: + api-doc: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.REF }} + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-doc \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Obtain version if release tag + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + version=$(echo ${GITHUB_REF#refs/tags/[vV]} | awk -F'.' '{ printf("%d.%d", $1, $2) }') + echo "VERSION_MAJ_MIN=${version}" >> $GITHUB_ENV + - name: Set some vars + run: | + echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + - name: Report some info + run: | + echo "REPO_NAME=${{ env.REPO_NAME }}" + - name: Do api-doc + run: | + if test -n "${{ env.VERSION_MAJ_MIN }}"; then + echo "Docs for release version ${{ env.VERSION_MAJ_MIN }}" + option_release=$(echo "--version ${{ env.VERSION_MAJ_MIN }}") + else + option_release="" + fi + python3 folio-tools/api-doc/api_doc.py \ + --loglevel info \ + --types ${{ env.API_TYPES }} \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} \ + --output ${{ env.OUTPUT_DIR }} $option_release + - name: Show generated files + working-directory: ${{ env.OUTPUT_DIR }} + run: ls -R + - name: Publish to AWS S3 + uses: sai-sharan/aws-s3-sync-action@v0.1.0 + with: + access_key: ${{ env.AWS_S3_ACCESS_KEY_ID }} + secret_access_key: ${{ env.AWS_S3_ACCESS_KEY }} + region: ${{ env.AWS_S3_REGION }} + source: ${{ env.OUTPUT_DIR }} + destination_bucket: ${{ env.AWS_S3_BUCKET }} + destination_prefix: ${{ env.AWS_S3_FOLDER }} + delete: false + quiet: false + diff --git a/.github/workflows/api-lint.yml b/.github/workflows/api-lint.yml new file mode 100644 index 00000000..de292ded --- /dev/null +++ b/.github/workflows/api-lint.yml @@ -0,0 +1,65 @@ +name: api-lint + +# https://dev.folio.org/guides/api-lint/ + +# API_TYPES: string: The space-separated list of types to consider. +# One or more of 'RAML OAS'. +# e.g. 'OAS' +# +# API_DIRECTORIES: string: The space-separated list of directories to search +# for API description files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' +# +# API_WARNINGS: boolean: Whether to cause Warnings to be displayed, +# and to fail the workflow. +# e.g. false + +env: + API_TYPES: 'RAML' + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + API_WARNINGS: false + +on: + push: + paths: + - 'ramls/**' + pull_request: + paths: + - 'ramls/**' + +jobs: + api-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-lint \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Configure default options + run: | + echo "OPTION_WARNINGS=''" >> $GITHUB_ENV + - name: Configure option warnings + if: ${{ env.API_WARNINGS == 'true' }} + run: | + echo "OPTION_WARNINGS=--warnings" >> $GITHUB_ENV + - name: Do api-lint + run: | + python3 folio-tools/api-lint/api_lint.py \ + --loglevel info \ + --types ${{ env.API_TYPES }} \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} \ + ${{ env.OPTION_WARNINGS }} diff --git a/.github/workflows/api-schema-lint.yml b/.github/workflows/api-schema-lint.yml new file mode 100644 index 00000000..7a08b5ac --- /dev/null +++ b/.github/workflows/api-schema-lint.yml @@ -0,0 +1,46 @@ +name: api-schema-lint + +# https://dev.folio.org/guides/describe-schema/ + +# API_DIRECTORIES: string: The space-separated list of directories to search +# for JSON Schema files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' + +env: + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + +on: + push: + paths: + - 'ramls/**' + pull_request: + paths: + - 'ramls/**' + +jobs: + api-schema-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-schema-lint \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Do api-schema-lint + run: | + python3 folio-tools/api-schema-lint/api_schema_lint.py \ + --loglevel info \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} diff --git a/Jenkinsfile b/Jenkinsfile index 7dfef044..0adb0174 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,11 +6,6 @@ buildMvn { doKubeDeploy = true buildNode = 'jenkins-agent-java11' - doApiLint = true - doApiDoc = true - apiTypes = 'RAML' - apiDirectories = 'ramls' - doDocker = { buildJavaDocker { publishMaster = 'yes' From e87839323a76f28b27ba7a98fdfb698834098c0f Mon Sep 17 00:00:00 2001 From: David Crossley Date: Wed, 26 Apr 2023 18:29:45 +1000 Subject: [PATCH 09/40] Verify Workflow api related change FOLIO-3678 --- ramls/saml-login.raml | 1 + 1 file changed, 1 insertion(+) diff --git a/ramls/saml-login.raml b/ramls/saml-login.raml index bd614898..4c3e8772 100644 --- a/ramls/saml-login.raml +++ b/ramls/saml-login.raml @@ -179,3 +179,4 @@ types: body: text/plain: example: "Internal server error" + From b2c6a5dfda2791c5dfc6480b1dfca2e2ec5abfa9 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Thu, 1 Jun 2023 15:00:09 +0200 Subject: [PATCH 10/40] NEWS for 2.6.2 --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 4ba2f63a..c28f0039 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## 2.6.2 - 2023-06-01 + + * [MODLOGSAML-166](https://issues.folio.org/browse/MODLOGSAML-166) xmlsec 2.3.3, woodstox-core 6.5.0 fixing DoS (CVE-2022-40152) + * [MODLOGSAML-165](https://issues.folio.org/browse/MODLOGSAML-165) json-smart 2.4.10 fixing DoS (CVE-2023-1370) + ## 2.6.1 - 2023-04-01 * [MODLOGSAML-159](https://issues.folio.org/browse/MODLOGSAML-159) OpenSSL 3.0.8 fixing 8 vulns From 007c47208830896beb278a2cc2d96a309c65cf1d Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Thu, 1 Jun 2023 15:01:54 +0200 Subject: [PATCH 11/40] [maven-release-plugin] prepare release v2.6.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 247e10cb..9fff3b8f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.7.0-SNAPSHOT + 2.6.2 mod-login-saml @@ -223,7 +223,7 @@ https://github.com/folio-org/mod-login-saml scm:git:git://github.com:folio-org/mod-login-saml.git scm:git:git@github.com:folio-org/mod-login-saml.git - HEAD + v2.6.2 From 82705366756a7b59c15292be217badfb06e00fac Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Thu, 1 Jun 2023 15:01:54 +0200 Subject: [PATCH 12/40] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9fff3b8f..247e10cb 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.folio mod-login-saml jar - 2.6.2 + 2.7.0-SNAPSHOT mod-login-saml @@ -223,7 +223,7 @@ https://github.com/folio-org/mod-login-saml scm:git:git://github.com:folio-org/mod-login-saml.git scm:git:git@github.com:folio-org/mod-login-saml.git - v2.6.2 + HEAD From 2fe2cfc709904a75324c634d2e69413c06c91d47 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Mon, 19 Jun 2023 11:02:02 +0200 Subject: [PATCH 13/40] MODLOGSAML-169: Update to Java 17 https://issues.folio.org/browse/MODLOGSAML-169 https://wiki.folio.org/display/TC/JDK+17+and+Java+17 https://wiki.folio.org/display/TC/DR-000034+-+Java+17+Support --- Dockerfile | 2 +- Jenkinsfile | 4 ++-- pom.xml | 13 +++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8397d7cd..a84b0228 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM folioci/alpine-jre-openjdk11:latest +FROM folioci/alpine-jre-openjdk17:latest # Install latest patch versions of packages: https://pythonspeed.com/articles/security-updates-in-docker/ USER root diff --git a/Jenkinsfile b/Jenkinsfile index 0adb0174..37d03250 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,13 +4,13 @@ buildMvn { publishModDescriptor = 'yes' mvnDeploy = 'yes' doKubeDeploy = true - buildNode = 'jenkins-agent-java11' + buildNode = 'jenkins-agent-java17' doDocker = { buildJavaDocker { publishMaster = 'yes' healthChk = 'yes' - healthChkCmd = 'curl -sS --fail -o /dev/null http://localhost:8081/apidocs/ || exit 1' + healthChkCmd = 'wget --no-verbose --tries=1 --spider http://localhost:8081/admin/health || exit 1' } } } diff --git a/pom.xml b/pom.xml index 247e10cb..9fd560c3 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ /saml/callback,/saml/regenerate,/saml/login,/saml/check,/saml/configuration + 1.9.19 5.7.0 6.0.1 @@ -232,7 +233,7 @@ maven-compiler-plugin 3.8.1 - 11 + 17 UTF-8 @@ -260,13 +261,13 @@ - com.nickwongdev + dev.aspectj aspectj-maven-plugin - 1.12.6 + 1.13.1 true false - 1.8 + 17 **/impl/*.java **/*.aj @@ -292,12 +293,12 @@ org.aspectj aspectjrt - 1.9.7 + ${aspectj.version} org.aspectj aspectjtools - 1.9.7 + ${aspectj.version} From 554f48e29272e850923f6a8394238bc55618a170 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Wed, 6 Sep 2023 05:04:24 +0200 Subject: [PATCH 14/40] Explain pac4j authentication and authorization mechanisms --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c48c7d39..11ffc8af 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,17 @@ Refer to the user documentation [Guide](GUIDE.md). For upgrading see [NEWS](NEWS.md) or [Releases](https://github.com/folio-org/mod-login-saml/releases). -This module is based on the [https://www.pac4j.org/](PAC4J) library, more -authentication methods supported by PAC4J can be added to this module if -needed. +This module is based on the [https://www.pac4j.org/](PAC4J) library +and supports SAML Single Sign On (SSO) including federations like +[https://edugain.org/](eduGAIN). + +More mechanisms supported by PAC4J can be added to this module if needed: + +Authentication mechanisms: OAuth (Facebook, Twitter, Google...) - CAS - +OpenID Connect - HTTP - Google App Engine - LDAP - SQL - JWT - MongoDB - +CouchDB - IP address - Kerberos (SPNEGO) - REST API. + +Authorization mechanisms: Roles/permissions. Other [modules](https://dev.folio.org/source-code/#server-side) are described, with further FOLIO Developer documentation at From dbf0363670d4e5880615f720531a9318bf9a2ca5 Mon Sep 17 00:00:00 2001 From: steveellis Date: Tue, 26 Sep 2023 18:06:40 -0400 Subject: [PATCH 15/40] Added methods to fetch token from legacy and new endpoint --- .../java/org/folio/rest/impl/SamlAPI.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index c02748ba..e417b7a3 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -14,11 +14,7 @@ import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; @@ -105,6 +101,11 @@ public ForbiddenException(String message) { } } + public static class FetchTokenException extends RuntimeException { + public FetchTokenException(String message) { + super(message); + } + } /** * Check that client can be loaded, SAML-Login button can be displayed. @@ -256,6 +257,7 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map fetchToken(WebClient client, JsonObject payload, String tenant, + String okapiURL, String requestToken) { + return + fetchToken(client, payload, tenant, okapiURL, requestToken, "/token/sign", "accessToken") + .compose(token -> token != null ? Future.succeededFuture(token) : + fetchToken(client, payload, tenant, okapiURL, requestToken, "/token", "token")); + } + + private Future fetchToken(WebClient client, JsonObject payload, String tenant, + String okapiURL, String requestToken, String endpoint, String key) { + try { + var request = client.postAbs(okapiURL + endpoint); + request.putHeader(XOkapiHeaders.TENANT, tenant).putHeader(XOkapiHeaders.TOKEN, requestToken); + return request.sendJson(new JsonObject().put("payload", payload)).map(response -> { + if (response.statusCode() == 404) { + throw new FetchTokenException("Token endpoint is not available: " + endpoint); + } + if (response.statusCode() != 200 && response.statusCode() != 201) { + throw new FetchTokenException("Got response unexpected " + response.statusCode() + " fetching token"); + } + return response.bodyAsJsonObject().getString(key); + }); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + /** * Get the user id from the first samlAttribute of userProfile. * From 08f0eea182c579f8003c8e25da25ee114492c23b Mon Sep 17 00:00:00 2001 From: steveellis Date: Wed, 4 Oct 2023 15:50:39 -0400 Subject: [PATCH 16/40] Removing support of authtoken 1.0 --- .gitignore | 1 + GUIDE.md | 2 +- descriptors/ModuleDescriptor-template.json | 6 +- .../java/org/folio/rest/impl/SamlAPI.java | 154 +++++++++++++----- src/test/resources/after_regenerate.json | 9 +- src/test/resources/mock_content.json | 9 +- .../resources/mock_content_no_keystore.json | 9 +- .../resources/mock_content_with_metadata.json | 9 +- src/test/resources/mock_idptest_post.json | 9 +- src/test/resources/mock_idptest_redirect.json | 9 +- 10 files changed, 138 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index cfe1a101..2feda3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ bin/ *.jks .classpath .project +saml-signing-cert.* diff --git a/GUIDE.md b/GUIDE.md index df767a90..f7d643fd 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -21,7 +21,7 @@ Examples: ### SP metadata -An XML file that describes the Service Point's configuration like successful login callback URL, and the encryption keys. +An XML file that describes the Service Provider's configuration like successful login callback URL, and the encryption keys. ### SAML binding diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b2debd0e..98792268 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -123,7 +123,11 @@ "requires": [ { "id": "authtoken", - "version": "1.0 2.0" + "version": "2.0" + }, + { + "id": "authtoken2", + "version": "1.0" }, { "id": "users", diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index e417b7a3..6a3feada 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -14,8 +14,10 @@ import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.*; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -25,7 +27,9 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.Cookie; +import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.Json; @@ -34,6 +38,7 @@ import io.vertx.ext.auth.PRNG; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.Session; +import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.client.predicate.ResponsePredicate; import io.vertx.ext.web.impl.Utils; @@ -88,6 +93,19 @@ public class SamlAPI implements Saml { private static final Logger log = LogManager.getLogger(SamlAPI.class); public static final String CSRF_TOKEN = "csrfToken"; public static final String RELAY_STATE = "relayState"; + private static final String TOKEN_SIGN_ENDPOINT_LEGACY = "/token"; + private static final String TOKEN_SIGN_ENDPOINT = "/token/sign"; + public static final String SET_COOKIE = "Set-Cookie"; + public static final String COOKIE_SAME_SITE_LAX = "Lax"; + public static final String COOKIE_SAME_SITE_NONE = "None"; + public static final String REFRESH_TOKEN = "refreshToken"; + public static final String ACCESS_TOKEN = "accessToken"; + public static final String FOLIO_ACCESS_TOKEN = "folioAccessToken"; + public static final String FOLIO_REFRESH_TOKEN = "folioRefreshToken"; + public static final String REFRESH_TOKEN_EXPIRATION = "refreshTokenExpiration"; + public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration"; + public static final String COOKIE_SAME_SITE = "login.cookie.samesite"; + public static final String COOKIE_SAME_SITE_ENV = "LOGIN_COOKIE_SAMESITE"; public static class UserErrorException extends RuntimeException { public UserErrorException(String message) { @@ -258,25 +276,9 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map { - String candidateAuthToken; - if (tokenResponse.statusCode() == 200) { - candidateAuthToken = tokenResponse.getHeader(XOkapiHeaders.TOKEN); - } else if (tokenResponse.statusCode() == 201) { //mod-authtoken v2.x returns 201, with token in JSON response body - try { - candidateAuthToken = tokenResponse.bodyAsJsonObject().getString("token"); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - } else { - throw new RuntimeException("POST /token returned " + tokenResponse.statusCode()); - } - final String authToken = candidateAuthToken; + return fetchToken(webClient, payload, parsedHeaders.getTenant(), parsedHeaders.getUrl(), parsedHeaders.getToken(), TOKEN_SIGN_ENDPOINT_LEGACY).map(jsonResponse -> { + + String authToken = jsonResponse.getString("token"); final String location = UriBuilder.fromUri(stripesBaseUrl) .path("sso-landing") @@ -309,31 +311,101 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map fetchToken(WebClient client, JsonObject payload, String tenant, - String okapiURL, String requestToken) { - return - fetchToken(client, payload, tenant, okapiURL, requestToken, "/token/sign", "accessToken") - .compose(token -> token != null ? Future.succeededFuture(token) : - fetchToken(client, payload, tenant, okapiURL, requestToken, "/token", "token")); + private Future fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { + HttpRequest request = client.postAbs(okapiURL + endpoint); + + request + .putHeader(XOkapiHeaders.TENANT, tenant) + .putHeader(XOkapiHeaders.TOKEN, requestToken) + .putHeader(XOkapiHeaders.URL, okapiURL); + + return request.sendJson(payload).map(response -> { + if (response.statusCode() != 200 && response.statusCode() != 201) { + throw new FetchTokenException("Got response " + response.statusCode() + " fetching token"); + } + + return response.bodyAsJsonObject(); + }); } - private Future fetchToken(WebClient client, JsonObject payload, String tenant, - String okapiURL, String requestToken, String endpoint, String key) { - try { - var request = client.postAbs(okapiURL + endpoint); - request.putHeader(XOkapiHeaders.TENANT, tenant).putHeader(XOkapiHeaders.TOKEN, requestToken); - return request.sendJson(new JsonObject().put("payload", payload)).map(response -> { - if (response.statusCode() == 404) { - throw new FetchTokenException("Token endpoint is not available: " + endpoint); - } - if (response.statusCode() != 200 && response.statusCode() != 201) { - throw new FetchTokenException("Got response unexpected " + response.statusCode() + " fetching token"); - } - return response.bodyAsJsonObject().getString(key); - }); - } catch (Exception e) { - return Future.failedFuture(e); + private Response tokenResponse(JsonObject tokens) { + String accessToken = tokens.getString(ACCESS_TOKEN); + String refreshToken = tokens.getString(REFRESH_TOKEN); + String accessTokenExpiration = tokens.getString(ACCESS_TOKEN_EXPIRATION); + String refreshTokenExpiration = tokens.getString(REFRESH_TOKEN_EXPIRATION); + // Use the ResponseBuilder rather than RMB-generated code. We need to do this because + // RMB generated-code does not allow multiple headers with the same key -- which is what we need + // here. + var body = new JsonObject() + .put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration) + .put(REFRESH_TOKEN_EXPIRATION, refreshTokenExpiration) + .toString(); + return Response.status(201) + .header(SET_COOKIE, accessTokenCookie(accessToken, accessTokenExpiration)) + .header(SET_COOKIE, refreshTokenCookie(refreshToken, refreshTokenExpiration)) + .type(MediaType.APPLICATION_JSON) + .entity(body) + .build(); + } + + private String refreshTokenCookie(String refreshToken, String refreshTokenExpiration) { + // The refresh token expiration is the time after which the token will be considered expired. + var exp = Instant.parse(refreshTokenExpiration).getEpochSecond(); + var ttlSeconds = exp - Instant.now().getEpochSecond(); + + // RFC 6265 mandates that MaxAge is >= 1: https://datatracker.ietf.org/doc/html/rfc6265#page-9 + if (ttlSeconds < 1) { + throw new FetchTokenException("MaxAge of cookie is < 1. This is not permitted."); + } + + var rtCookie = Cookie.cookie(FOLIO_REFRESH_TOKEN, refreshToken) + .setMaxAge(ttlSeconds) + .setSecure(true) + .setPath("/authn") + .setHttpOnly(true) + .setSameSite(getSameSiteAttribute()) + .setDomain(null) + .encode(); + + log.debug("refreshToken cookie: {}", rtCookie); + + return rtCookie; + } + + private String accessTokenCookie(String accessToken, String accessTokenExpiration) { + // The refresh token expiration is the time after which the token will be considered expired. + var exp = Instant.parse(accessTokenExpiration).getEpochSecond(); + var ttlSeconds = exp - Instant.now().getEpochSecond(); + + // RFC 6265 mandates that MaxAge is >= 1: https://datatracker.ietf.org/doc/html/rfc6265#page-9 + if (ttlSeconds < 1) { + throw new FetchTokenException("MaxAge of cookie is < 1. This is not permitted."); + } + + var atCookie = Cookie.cookie(FOLIO_ACCESS_TOKEN, accessToken) + .setMaxAge(ttlSeconds) + .setSecure(true) + .setPath("/") + .setHttpOnly(true) + .setSameSite(getSameSiteAttribute()) + .encode(); + + log.debug("accessToken cookie: {}", atCookie); + + return atCookie; + } + + private CookieSameSite getSameSiteAttribute() { + return isSameSiteLax() ? CookieSameSite.LAX : CookieSameSite.NONE; + } + + private boolean isSameSiteLax() { + if (System.getProperty(COOKIE_SAME_SITE) != null && + System.getProperty(COOKIE_SAME_SITE).equals(COOKIE_SAME_SITE_LAX)) { + return true; } + return System.getenv(COOKIE_SAME_SITE_ENV) != null && + System.getenv(COOKIE_SAME_SITE_ENV).equals(COOKIE_SAME_SITE_LAX); } /** diff --git a/src/test/resources/after_regenerate.json b/src/test/resources/after_regenerate.json index b11466d0..1be4aea7 100644 --- a/src/test/resources/after_regenerate.json +++ b/src/test/resources/after_regenerate.json @@ -147,12 +147,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } diff --git a/src/test/resources/mock_content.json b/src/test/resources/mock_content.json index a5a82aaf..53f1e74b 100644 --- a/src/test/resources/mock_content.json +++ b/src/test/resources/mock_content.json @@ -169,12 +169,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } diff --git a/src/test/resources/mock_content_no_keystore.json b/src/test/resources/mock_content_no_keystore.json index e11af243..e5752428 100644 --- a/src/test/resources/mock_content_no_keystore.json +++ b/src/test/resources/mock_content_no_keystore.json @@ -149,12 +149,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } diff --git a/src/test/resources/mock_content_with_metadata.json b/src/test/resources/mock_content_with_metadata.json index c2729a70..cfe4a0e0 100644 --- a/src/test/resources/mock_content_with_metadata.json +++ b/src/test/resources/mock_content_with_metadata.json @@ -153,12 +153,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } diff --git a/src/test/resources/mock_idptest_post.json b/src/test/resources/mock_idptest_post.json index 9243fd7f..36739c11 100644 --- a/src/test/resources/mock_idptest_post.json +++ b/src/test/resources/mock_idptest_post.json @@ -82,12 +82,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } diff --git a/src/test/resources/mock_idptest_redirect.json b/src/test/resources/mock_idptest_redirect.json index ac06e45b..890b8e30 100644 --- a/src/test/resources/mock_idptest_redirect.json +++ b/src/test/resources/mock_idptest_redirect.json @@ -82,12 +82,9 @@ "url": "/token", "method": "post", "status": 200, - "headers": [ - { - "name": "x-okapi-token", - "value": "saml-token" - } - ] + "receivedData": { + "token": "saml-token" + } } ] } From 9905143d9a57fea9d4517578e858b4043b0dcf33 Mon Sep 17 00:00:00 2001 From: steveellis Date: Wed, 4 Oct 2023 17:25:31 -0400 Subject: [PATCH 17/40] Has callback-with-expiry raml --- pom.xml | 2 +- ramls/saml-login.raml | 45 ++++++++++++++++++- .../java/org/folio/rest/impl/SamlAPI.java | 14 +++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 9fd560c3..b16cf053 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ UTF-8 35.0.6 - /saml/callback,/saml/regenerate,/saml/login,/saml/check,/saml/configuration + /saml/callback,/saml/callback-with-expiry,/saml/regenerate,/saml/login,/saml/check,/saml/configuration 1.9.19 diff --git a/ramls/saml-login.raml b/ramls/saml-login.raml index 4c3e8772..316030b2 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. + description: Redirect browser to sso-landing page with generated token. Deprecated. body: application/octet-stream: type: string @@ -102,6 +102,49 @@ types: body: text/plain: example: "Bad request" + /callback-with-expiry: + post: + description: Redirect browser to sso-landing page with expiring access and refresh tokens. + body: + application/octet-stream: + type: string + application/x-www-form-urlencoded: + type: string + responses: + 302: + description: "Generate JWT token and set cookie" + headers: + Location: + 400: + description: "Bad request" + body: + text/plain: + example: "Bad request" + 401: + description: "Unauthorized" + body: + text/plain: + example: "Unauthorized" + 403: + description: "Forbidden" + body: + text/plain: + example: "Forbidden" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + options: + description: "Preflight CORS for /saml/callback" + responses: + 204: + description: "Return with appropriate CORS headers" + 400: + description: "Bad request" + body: + text/plain: + example: "Bad request" /check: get: description: Decides if SSO login is configured properly, returns true or false diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 6a3feada..82d15df7 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -44,6 +44,7 @@ import io.vertx.ext.web.impl.Utils; import io.vertx.ext.web.sstore.impl.SharedDataSessionImpl; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.config.ConfigurationsClient; @@ -311,7 +312,12 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { + @Override + public void postSamlCallbackWithExpiry(String body, RoutingContext routingContext, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + } + + private Future fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { HttpRequest request = client.postAbs(okapiURL + endpoint); request @@ -684,6 +690,12 @@ public void optionsSamlCallback(RoutingContext routingContext, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + handleOptions(routingContext); + } + private void handleOptions(RoutingContext routingContext) { HttpServerRequest request = routingContext.request(); HttpServerResponse response = routingContext.response(); From faca48f13b33a06a1b039c7b101521c6816f9b9a Mon Sep 17 00:00:00 2001 From: steveellis Date: Thu, 5 Oct 2023 11:30:28 -0400 Subject: [PATCH 18/40] Now handles redirect response for legacy and non --- .../java/org/folio/rest/impl/SamlAPI.java | 106 ++++++++++-------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 82d15df7..6a72753d 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -1,23 +1,16 @@ package org.folio.rest.impl; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.vertx.core.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; -import static io.vertx.core.http.HttpHeaders.ORIGIN; -import static io.vertx.core.http.HttpHeaders.VARY; +import static io.vertx.core.http.HttpHeaders.*; import static org.pac4j.saml.state.SAML2StateGenerator.SAML_RELAY_STATE_ATTRIBUTE; import static org.folio.rest.impl.ApiInitializer.MAX_FORM_ATTRIBUTE_SIZE; import java.io.InputStream; import java.net.URI; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.*; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -44,7 +37,6 @@ import io.vertx.ext.web.impl.Utils; import io.vertx.ext.web.sstore.impl.SharedDataSessionImpl; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.NotImplementedException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.config.ConfigurationsClient; @@ -97,6 +89,7 @@ public class SamlAPI implements Saml { private static final String TOKEN_SIGN_ENDPOINT_LEGACY = "/token"; private static final String TOKEN_SIGN_ENDPOINT = "/token/sign"; public static final String SET_COOKIE = "Set-Cookie"; + public static final String LOCATION = "Location"; public static final String COOKIE_SAME_SITE_LAX = "Lax"; public static final String COOKIE_SAME_SITE_NONE = "None"; public static final String REFRESH_TOKEN = "refreshToken"; @@ -216,7 +209,11 @@ private String getRelayState(RoutingContext routingContext, String body) { @Override public void postSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { + Handler> asyncResultHandler, Context vertxContext) { + doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext, TOKEN_SIGN_ENDPOINT_LEGACY); + } + private void doPostSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext, String tokenSignEndpoint) { registerFakeSession(routingContext); @@ -226,11 +223,11 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map { - - String authToken = jsonResponse.getString("token"); - - final String location = UriBuilder.fromUri(stripesBaseUrl) - .path("sso-landing") - .queryParam("ssoToken", authToken) - .queryParam("fwd", originalUrl.getPath()) - .build() - .toString(); - - final String cookie = new NewCookie("ssoToken", authToken, "", originalUrl.getHost(), "", 3600, false).toString(); - return PostSamlCallbackResponse - .headersFor302().withSetCookie(cookie).withXOkapiToken(authToken).withLocation(location); - }); + return fetchToken(webClient, payload, parsedHeaders.getTenant(), parsedHeaders.getUrl(), + parsedHeaders.getToken(), tokenSignEndpoint).map(jsonResponse -> { + if (isLegacyResponse(tokenSignEndpoint)) { + return redirectResponseLegacy(jsonResponse, stripesBaseUrl, originalUrl); + } else { + return redirectResponse(jsonResponse, stripesBaseUrl, originalUrl); + } + }); }); }) - .onSuccess(headers -> - asyncResultHandler.handle(Future.succeededFuture(PostSamlCallbackResponse.respond302(headers))) + .onSuccess(response -> + asyncResultHandler.handle(Future.succeededFuture(response)) ) .onFailure(cause -> { PostSamlCallbackResponse response; @@ -315,9 +305,14 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + throw new UnsupportedOperationException(); + } + + private boolean isLegacyResponse(String endpoint) { + return endpoint.equals(TOKEN_SIGN_ENDPOINT_LEGACY); } - private Future fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { + private Future fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { HttpRequest request = client.postAbs(okapiURL + endpoint); request @@ -334,23 +329,40 @@ private Future fetchToken(WebClient client, JsonObject payload, Stri }); } - private Response tokenResponse(JsonObject tokens) { - String accessToken = tokens.getString(ACCESS_TOKEN); - String refreshToken = tokens.getString(REFRESH_TOKEN); - String accessTokenExpiration = tokens.getString(ACCESS_TOKEN_EXPIRATION); - String refreshTokenExpiration = tokens.getString(REFRESH_TOKEN_EXPIRATION); - // Use the ResponseBuilder rather than RMB-generated code. We need to do this because - // RMB generated-code does not allow multiple headers with the same key -- which is what we need - // here. - var body = new JsonObject() - .put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration) - .put(REFRESH_TOKEN_EXPIRATION, refreshTokenExpiration) + private Response redirectResponseLegacy(JsonObject jsonObject, URI stripesBaseUrl, URI originalUrl) { + String authToken = jsonObject.getString("token"); + + final String location = UriBuilder.fromUri(stripesBaseUrl) + .path("sso-landing") + .queryParam("ssoToken", authToken) + .queryParam("fwd", originalUrl.getPath()) + .build() + .toString(); + + final String cookie = new NewCookie("ssoToken", + authToken, "", originalUrl.getHost(), "", 3600, false).toString(); + var headers = PostSamlCallbackResponse.headersFor302().withSetCookie(cookie).withXOkapiToken(authToken).withLocation(location); + return PostSamlCallbackResponse.respond302(headers); + } + + private Response redirectResponse(JsonObject jsonObject, URI stripesBaseUrl, URI originalUrl) { + String accessToken = jsonObject.getString(ACCESS_TOKEN); + String refreshToken = jsonObject.getString(REFRESH_TOKEN); + String accessTokenExpiration = jsonObject.getString(ACCESS_TOKEN_EXPIRATION); + String refreshTokenExpiration = jsonObject.getString(REFRESH_TOKEN_EXPIRATION); + + final String location = UriBuilder.fromUri(stripesBaseUrl) + .path("sso-landing") + .queryParam("fwd", originalUrl.getPath()) + .queryParam(ACCESS_TOKEN, URLEncoder.encode(accessTokenExpiration, StandardCharsets.UTF_8)) + .queryParam(REFRESH_TOKEN, URLEncoder.encode(refreshTokenExpiration, StandardCharsets.UTF_8)) + .build() .toString(); - return Response.status(201) + + return Response.status(302) .header(SET_COOKIE, accessTokenCookie(accessToken, accessTokenExpiration)) .header(SET_COOKIE, refreshTokenCookie(refreshToken, refreshTokenExpiration)) - .type(MediaType.APPLICATION_JSON) - .entity(body) + .header(LOCATION, location) .build(); } From ab99fd2594c0624f14314ec23cad011dfd94cbef Mon Sep 17 00:00:00 2001 From: steveellis Date: Thu, 5 Oct 2023 13:29:41 -0400 Subject: [PATCH 19/40] Reduced cognitive complexity of method --- .../java/org/folio/rest/impl/SamlAPI.java | 135 ++++++++++-------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 6a72753d..c00aea8b 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -119,6 +119,12 @@ public FetchTokenException(String message) { } } + public static class UnsupportedUserPropertyException extends RuntimeException { + public UnsupportedUserPropertyException(String message) { + super(message); + } + } + /** * Check that client can be loaded, SAML-Login button can be displayed. */ @@ -212,6 +218,13 @@ public void postSamlCallback(String body, RoutingContext routingContext, Map> asyncResultHandler, Context vertxContext) { doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext, TOKEN_SIGN_ENDPOINT_LEGACY); } + + @Override + public void postSamlCallbackWithExpiry(String body, RoutingContext routingContext, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + doPostSamlCallback(body, routingContext, okapiHeaders, asyncResultHandler, vertxContext, TOKEN_SIGN_ENDPOINT); + } + private void doPostSamlCallback(String body, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext, String tokenSignEndpoint) { @@ -243,82 +256,87 @@ private void doPostSamlCallback(String body, RoutingContext routingContext, Map< .compose(samlClientComposite -> { final SAML2Client client = samlClientComposite.getClient(); final SamlConfiguration configuration = samlClientComposite.getConfiguration(); - final String userPropertyName = - configuration.getUserProperty() == null ? "externalSystemId" : configuration.getUserProperty(); - final SAML2Credentials credentials = (SAML2Credentials) client.getCredentials(webContext, sessionStore).get(); - final String samlAttributeValue = - getSamlAttributeValue(configuration.getSamlAttribute(), credentials.getUserProfile()); - final String usersCql = getCqlUserQuery(userPropertyName, samlAttributeValue); - final String userQuery = UriBuilder.fromPath("/users").queryParam("query", usersCql).build().toString(); - OkapiHeaders parsedHeaders = OkapiHelper.okapiHeaders(okapiHeaders); - WebClient webClient = WebClientFactory.getWebClient(vertxContext.owner()); - return webClient.getAbs(parsedHeaders.getUrl() + userQuery) - .putHeader(XOkapiHeaders.TOKEN, parsedHeaders.getToken()) - .putHeader(XOkapiHeaders.URL, parsedHeaders.getUrl()) - .putHeader(XOkapiHeaders.TENANT, parsedHeaders.getTenant()) - .expect(ResponsePredicate.SC_OK) - .expect(ResponsePredicate.JSON) - .send() - .compose(res -> { - JsonArray users = res.bodyAsJsonObject().getJsonArray("users"); - if (users.isEmpty()) { - String message = "No user found by " + userPropertyName + " == " + samlAttributeValue; - throw new UserErrorException(message); - } - final JsonObject userObject = users.getJsonObject(0); - String userId = userObject.getString("id"); - if (!userObject.getBoolean("active", false)) { - throw new ForbiddenException("Inactive user account!"); + + return getUsersResponse(webClient, configuration, webContext, client, sessionStore, parsedHeaders) + .compose(userObject -> { + String userId = userObject.getString("id"); + if (Boolean.FALSE.equals(userObject.getBoolean("active", false))) { + throw new ForbiddenException("Inactive user account!"); + } + JsonObject payload = new JsonObject().put("payload", + new JsonObject().put("sub", userObject.getString("username")).put("user_id", userId)); + + return fetchToken(webClient, payload, parsedHeaders, tokenSignEndpoint).map(jsonResponse -> { + if (isLegacyResponse(tokenSignEndpoint)) { + return redirectResponseLegacy(jsonResponse, stripesBaseUrl, originalUrl); + } else { + return redirectResponse(jsonResponse, stripesBaseUrl, originalUrl); } - JsonObject payload = new JsonObject().put("payload", new JsonObject().put("sub", userObject.getString("username")).put("user_id", userId)); - - return fetchToken(webClient, payload, parsedHeaders.getTenant(), parsedHeaders.getUrl(), - parsedHeaders.getToken(), tokenSignEndpoint).map(jsonResponse -> { - if (isLegacyResponse(tokenSignEndpoint)) { - return redirectResponseLegacy(jsonResponse, stripesBaseUrl, originalUrl); - } else { - return redirectResponse(jsonResponse, stripesBaseUrl, originalUrl); - } - }); }); + }); }) - .onSuccess(response -> - asyncResultHandler.handle(Future.succeededFuture(response)) - ) + .onSuccess(response -> asyncResultHandler.handle(Future.succeededFuture(response))) .onFailure(cause -> { - PostSamlCallbackResponse response; - if (cause instanceof ForbiddenException) { - response = PostSamlCallbackResponse.respond403WithTextPlain(cause.getMessage()); - } else if (cause instanceof UserErrorException) { - response = PostSamlCallbackResponse.respond400WithTextPlain(cause.getMessage()); - } else { - removeSaml2Client(routingContext); - response = PostSamlCallbackResponse.respond500WithTextPlain(cause.getMessage()); - } - log.error(cause.getMessage(), cause); + var response = failCallbackResponse(cause, routingContext); asyncResultHandler.handle(Future.succeededFuture(response)); }); } - @Override - public void postSamlCallbackWithExpiry(String body, RoutingContext routingContext, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - throw new UnsupportedOperationException(); + private Future getUsersResponse(WebClient webClient, SamlConfiguration configuration, + VertxWebContext webContext, SAML2Client client, SessionStore sessionStore, + OkapiHeaders parsedHeaders) { + final String userPropertyName = + configuration.getUserProperty() == null ? "externalSystemId" : configuration.getUserProperty(); + final SAML2Credentials credentials = (SAML2Credentials) client.getCredentials(webContext, sessionStore).get(); + final String samlAttributeValue = + getSamlAttributeValue(configuration.getSamlAttribute(), credentials.getUserProfile()); + final String usersCql = getCqlUserQuery(userPropertyName, samlAttributeValue); + final String userQuery = UriBuilder.fromPath("/users").queryParam("query", usersCql).build().toString(); + + return webClient.getAbs(parsedHeaders.getUrl() + userQuery) + .putHeader(XOkapiHeaders.TOKEN, parsedHeaders.getToken()) + .putHeader(XOkapiHeaders.URL, parsedHeaders.getUrl()) + .putHeader(XOkapiHeaders.TENANT, parsedHeaders.getTenant()) + .expect(ResponsePredicate.SC_OK) + .expect(ResponsePredicate.JSON) + .send() + .map(res -> { + JsonArray users = res.bodyAsJsonObject().getJsonArray("users"); + if (users.isEmpty()) { + String message = "No user found by " + userPropertyName + " == " + samlAttributeValue; + throw new UserErrorException(message); + } + return users.getJsonObject(0); + }); + } + + private PostSamlCallbackResponse failCallbackResponse(Throwable cause, RoutingContext routingContext) { + PostSamlCallbackResponse response; + if (cause instanceof ForbiddenException) { + response = PostSamlCallbackResponse.respond403WithTextPlain(cause.getMessage()); + } else if (cause instanceof UserErrorException) { + response = PostSamlCallbackResponse.respond400WithTextPlain(cause.getMessage()); + } else { + removeSaml2Client(routingContext); + response = PostSamlCallbackResponse.respond500WithTextPlain(cause.getMessage()); + } + log.error(cause.getMessage(), cause); + return response; } private boolean isLegacyResponse(String endpoint) { return endpoint.equals(TOKEN_SIGN_ENDPOINT_LEGACY); } - private Future fetchToken(WebClient client, JsonObject payload, String tenant, String okapiURL, String requestToken, String endpoint) { - HttpRequest request = client.postAbs(okapiURL + endpoint); + private Future fetchToken(WebClient client, JsonObject payload, OkapiHeaders parsedHeaders, String endpoint) { + HttpRequest request = client.postAbs(parsedHeaders.getUrl() + endpoint); request - .putHeader(XOkapiHeaders.TENANT, tenant) - .putHeader(XOkapiHeaders.TOKEN, requestToken) - .putHeader(XOkapiHeaders.URL, okapiURL); + .putHeader(XOkapiHeaders.TENANT, parsedHeaders.getTenant()) + .putHeader(XOkapiHeaders.TOKEN, parsedHeaders.getToken()) + .putHeader(XOkapiHeaders.URL, parsedHeaders.getUrl()); return request.sendJson(payload).map(response -> { if (response.statusCode() != 200 && response.statusCode() != 201) { @@ -359,6 +377,7 @@ private Response redirectResponse(JsonObject jsonObject, URI stripesBaseUrl, URI .build() .toString(); + // NOTE RMB doesn't support sending multiple headers with the same key so we make our own response. return Response.status(302) .header(SET_COOKIE, accessTokenCookie(accessToken, accessTokenExpiration)) .header(SET_COOKIE, refreshTokenCookie(refreshToken, refreshTokenExpiration)) From aebce2e72859bce077bc1d6599f2812dffd654a7 Mon Sep 17 00:00:00 2001 From: steveellis Date: Thu, 5 Oct 2023 16:51:04 -0400 Subject: [PATCH 20/40] Callback is now in config --- ramls/schemas/SamlConfig.json | 5 + ramls/schemas/SamlConfigRequest.json | 5 + .../org/folio/config/SamlClientLoader.java | 5 +- .../folio/config/model/SamlConfiguration.java | 11 +- .../java/org/folio/rest/impl/SamlAPI.java | 5 + .../java/org/folio/rest/impl/SamlAPITest.java | 48 ++++ src/test/resources/mock_content.json | 4 +- .../resources/mock_content_with_callback.json | 206 ++++++++++++++++++ 8 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 src/test/resources/mock_content_with_callback.json diff --git a/ramls/schemas/SamlConfig.json b/ramls/schemas/SamlConfig.json index c918b74a..8f6f4508 100644 --- a/ramls/schemas/SamlConfig.json +++ b/ramls/schemas/SamlConfig.json @@ -38,6 +38,11 @@ "type": "string", "format": "uri", "required": true + }, + "callback": { + "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 } } } diff --git a/ramls/schemas/SamlConfigRequest.json b/ramls/schemas/SamlConfigRequest.json index 097acc9f..9000fb27 100644 --- a/ramls/schemas/SamlConfigRequest.json +++ b/ramls/schemas/SamlConfigRequest.json @@ -38,6 +38,11 @@ "type": "string", "format": "uri", "required": true + }, + "callback": { + "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 } } } diff --git a/src/main/java/org/folio/config/SamlClientLoader.java b/src/main/java/org/folio/config/SamlClientLoader.java index f646c2d1..df8ec71e 100644 --- a/src/main/java/org/folio/config/SamlClientLoader.java +++ b/src/main/java/org/folio/config/SamlClientLoader.java @@ -59,6 +59,8 @@ public static Future loadFromConfiguration(RoutingContext r final Resource idpMetadata = samlConfiguration.getIdpMetadata() != null ? new ByteArrayResource(samlConfiguration.getIdpMetadata().getBytes()) : null; final String okapiUrl = samlConfiguration.getOkapiUrl(); + final String callback = samlConfiguration.getCallback(); + // TODO Check for null here and return login-with-expiry if needed for this. if (StringUtils.isBlank(idpUrl)) { return Future.failedFuture("There is no IdP configuration stored!"); @@ -113,7 +115,6 @@ public static Future loadFromConfiguration(RoutingContext r }); } - /** * Store KeyStore (as Base64 string), KeyStorePassword and PrivateKeyPassword in mod-configuration, * complete returned future with original file bytes. @@ -198,6 +199,4 @@ private static SAML2Client assembleSaml2Client(String okapiUrl, String tenantId, private static String buildCallbackUrl(String okapiUrl, String tenantId) { return okapiUrl + "/_/invoke/tenant/" + CommonHelper.urlEncode(tenantId) + CALLBACK_ENDPOINT; } - - } diff --git a/src/main/java/org/folio/config/model/SamlConfiguration.java b/src/main/java/org/folio/config/model/SamlConfiguration.java index 998e887e..ff820786 100644 --- a/src/main/java/org/folio/config/model/SamlConfiguration.java +++ b/src/main/java/org/folio/config/model/SamlConfiguration.java @@ -21,6 +21,7 @@ public class SamlConfiguration { public static final String USER_PROPERTY_CODE = "user.property"; 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"; @JsonProperty(IDP_URL_CODE) private String idpUrl; @@ -40,10 +41,10 @@ public class SamlConfiguration { private String idpMetadata; @JsonProperty(METADATA_INVALIDATED_CODE) private String metadataInvalidated = "true"; - - @JsonProperty(OKAPI_URL) private String okapiUrl; + @JsonProperty(SAML_CALLBACK) + private String callback; public String getIdpUrl() { @@ -124,4 +125,10 @@ public String getIdpMetadata() { public void setIdpMetadata(String idpMetadata) { this.idpMetadata = idpMetadata; } + + public String getCallback() { return callback; } + + public void setCallback(String callback) { + this.callback = callback; + } } diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index c00aea8b..487d8f6f 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -545,6 +545,10 @@ public void putSamlConfiguration(SamlConfigRequest updatedConfig, RoutingContext updateEntries.put(SamlConfiguration.OKAPI_URL, okapiUrl); updateEntries.put(SamlConfiguration.METADATA_INVALIDATED_CODE, "true"); }); + + ConfigEntryUtil.valueChanged(config.getCallback(), updatedConfig.getCallback(), callback -> + updateEntries.put(SamlConfiguration.SAML_CALLBACK, callback)); + return storeConfigEntries(rc, parsedHeaders, updateEntries, vertxContext); }) .onFailure(cause -> { @@ -683,6 +687,7 @@ private SamlConfig configToDto(SamlConfiguration config) { SamlConfig samlConfig = new SamlConfig() .withSamlAttribute(config.getSamlAttribute()) .withUserProperty(config.getUserProperty()) + .withCallback(config.getCallback()) .withMetadataInvalidated(Boolean.valueOf(config.getMetadataInvalidated())); try { URI uri = URI.create(config.getOkapiUrl()); diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index d24f976c..a4b3741f 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -500,6 +500,8 @@ public void regenerateEndpointTests() { @Test public void callbackEndpointTests() { + mock.setMockContent("mock_content_with_callback.json"); + final String testPath = "/test/path"; log.info("=== Setup - POST /saml/login - need relayState and cookie ==="); @@ -671,6 +673,25 @@ public void getConfigurationEndpoint() { .body("metadataInvalidated", equalTo(Boolean.FALSE)); } + @Test + public void getConfigurationEndpointWithCallback() { + mock.setMockContent("mock_content_with_callback.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("metadataInvalidated", equalTo(Boolean.FALSE)); + } + + @Test public void putConfigurationEndpoint(TestContext context) { SamlConfigRequest samlConfigRequest = new SamlConfigRequest() @@ -720,6 +741,33 @@ public void putConfigurationWithIdpMetadata(TestContext context) { .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")); } + @Test + public void putConfigurationWithCallback(TestContext context) { + mock.setMockContent("mock_content_with_callback.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"); + + 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(matchesJsonSchemaInClasspath("ramls/schemas/SamlConfig.json")); + } + private String readResourceToString(String idpMetadataFile) { try { return IOUtils.toString(Objects diff --git a/src/test/resources/mock_content.json b/src/test/resources/mock_content.json index 53f1e74b..0bd79984 100644 --- a/src/test/resources/mock_content.json +++ b/src/test/resources/mock_content.json @@ -47,14 +47,14 @@ "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" } - ], "totalRecords": 6 }, diff --git a/src/test/resources/mock_content_with_callback.json b/src/test/resources/mock_content_with_callback.json new file mode 100644 index 00000000..e5356bd0 --- /dev/null +++ b/src/test/resources/mock_content_with_callback.json @@ -0,0 +1,206 @@ +{ + "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" + } + ], + "totalRecords": 6 + }, + "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%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": 200, + "receivedData": { + "token": "saml-token" + } + } + ] +} From 24b87dc48e06addad61f66c36bdb6814a0dfb70f Mon Sep 17 00:00:00 2001 From: steveellis Date: Thu, 5 Oct 2023 18:01:34 -0400 Subject: [PATCH 21/40] SamlClientLoader now loads callback from configuration --- .../org/folio/config/SamlClientLoader.java | 39 +++-- .../folio/config/SamlClientLoaderTest.java | 2 +- .../java/org/folio/rest/impl/SamlAPITest.java | 142 +++++++++++++++++- src/test/resources/mock_content.json | 7 +- ...callback.json => mock_content_legacy.json} | 0 src/test/resources/mock_tokenresponse.json | 12 +- 6 files changed, 176 insertions(+), 26 deletions(-) rename src/test/resources/{mock_content_with_callback.json => mock_content_legacy.json} (100%) diff --git a/src/main/java/org/folio/config/SamlClientLoader.java b/src/main/java/org/folio/config/SamlClientLoader.java index df8ec71e..4c27c796 100644 --- a/src/main/java/org/folio/config/SamlClientLoader.java +++ b/src/main/java/org/folio/config/SamlClientLoader.java @@ -12,6 +12,7 @@ import org.folio.config.model.SAML2ClientMock; import org.folio.config.model.SamlClientComposite; import org.folio.config.model.SamlConfiguration; +import org.folio.rest.impl.SamlAPI; import org.folio.util.OkapiHelper; import org.folio.util.model.OkapiHeaders; import org.opensaml.saml.common.xml.SAMLConstants; @@ -36,7 +37,8 @@ */ public class SamlClientLoader { - public static final String CALLBACK_ENDPOINT = "/saml/callback"; + public static final String SAML = "/saml/"; + public static final String CALLBACK_WITH_EXPIRY = "callback-with-expiry"; private static final Logger log = LogManager.getLogger(SamlClientLoader.class); private SamlClientLoader() { @@ -57,10 +59,10 @@ public static Future loadFromConfiguration(RoutingContext r final String privateKeyPassword = samlConfiguration.getPrivateKeyPassword(); final String samlBinding = samlConfiguration.getSamlBinding(); final Resource idpMetadata = samlConfiguration.getIdpMetadata() != null ? - new ByteArrayResource(samlConfiguration.getIdpMetadata().getBytes()) : null; + new ByteArrayResource(samlConfiguration.getIdpMetadata().getBytes()) : null; final String okapiUrl = samlConfiguration.getOkapiUrl(); - final String callback = samlConfiguration.getCallback(); - // TODO Check for null here and return login-with-expiry if needed for this. + final String callback = samlConfiguration.getCallback() == null ? + CALLBACK_WITH_EXPIRY : samlConfiguration.getCallback(); if (StringUtils.isBlank(idpUrl)) { return Future.failedFuture("There is no IdP configuration stored!"); @@ -79,7 +81,7 @@ public static Future loadFromConfiguration(RoutingContext r final String keystoreFileName = "temp_" + randomFileName + ".jks"; SAML2Client saml2Client = configureSaml2Client(okapiUrl, tenantId, idpUrl, actualKeystorePassword, - actualPrivateKeyPassword, keystoreFileName, samlBinding, idpMetadata, vertxContext); + actualPrivateKeyPassword, keystoreFileName, samlBinding, idpMetadata, vertxContext, callback); return vertx.executeBlocking(blockingHandler -> { saml2Client.init(); @@ -91,7 +93,8 @@ public static Future loadFromConfiguration(RoutingContext r try { UrlResource idpUrlResource = new UrlResource(idpUrl); SAML2Client reinitedSaml2Client = configureSaml2Client(okapiUrl, tenantId, actualKeystorePassword, - actualPrivateKeyPassword, idpUrlResource, keystoreResource, samlBinding, idpMetadata, vertxContext); + actualPrivateKeyPassword, idpUrlResource, keystoreResource, samlBinding, idpMetadata, vertxContext, + callback); return new SamlClientComposite(reinitedSaml2Client, samlConfiguration); } catch (MalformedURLException e) { @@ -106,7 +109,7 @@ public static Future loadFromConfiguration(RoutingContext r try { UrlResource idpUrlResource = new UrlResource(idpUrl); SAML2Client saml2Client = configureSaml2Client(okapiUrl, tenantId, keystorePassword, privateKeyPassword, - idpUrlResource, keystoreResource, samlBinding, idpMetadata, vertxContext); + idpUrlResource, keystoreResource, samlBinding, idpMetadata, vertxContext, callback); return Future.succeededFuture(new SamlClientComposite(saml2Client, samlConfiguration)); } catch (MalformedURLException e) { @@ -146,8 +149,9 @@ private static Future storeKeystore(OkapiHeaders okapiHeaders, Vertx ver private static SAML2Client configureSaml2Client(String okapiUrl, String tenantId, String idpUrl, - String keystorePassword, String actualPrivateKeyPassword, String keystoreFileName, String samlBinding, Resource idpMetadata, - Context vertxContext) { + String keystorePassword, String actualPrivateKeyPassword, + String keystoreFileName, String samlBinding, Resource idpMetadata, + Context vertxContext, String callback) { final SAML2Configuration cfg = new SAML2Configuration(keystoreFileName, keystorePassword, @@ -158,10 +162,13 @@ private static SAML2Client configureSaml2Client(String okapiUrl, String tenantId } cfg.setMaximumAuthenticationLifetime(18000); - return assembleSaml2Client(okapiUrl, tenantId, cfg, samlBinding, vertxContext); + return assembleSaml2Client(okapiUrl, tenantId, cfg, samlBinding, vertxContext, callback); } - protected static SAML2Client configureSaml2Client(String okapiUrl, String tenantId, String keystorePassword, String privateKeyPassword, UrlResource idpUrlResource, ByteArrayResource keystoreResource, String samlBinding, Resource idpMetadata, Context vertxContext) { + protected static SAML2Client configureSaml2Client(String okapiUrl, String tenantId, String keystorePassword, + String privateKeyPassword, UrlResource idpUrlResource, + ByteArrayResource keystoreResource, String samlBinding, + Resource idpMetadata, Context vertxContext, String callback) { final SAML2Configuration byteArrayCfg = new SAML2Configuration(keystoreResource, keystorePassword, @@ -172,11 +179,11 @@ protected static SAML2Client configureSaml2Client(String okapiUrl, String tenant } byteArrayCfg.setMaximumAuthenticationLifetime(18000); - return assembleSaml2Client(okapiUrl, tenantId, byteArrayCfg, samlBinding, vertxContext); + return assembleSaml2Client(okapiUrl, tenantId, byteArrayCfg, samlBinding, vertxContext, callback); } private static SAML2Client assembleSaml2Client(String okapiUrl, String tenantId, SAML2Configuration cfg, - String samlBinding, Context vertxContext) { + String samlBinding, Context vertxContext, String callback) { if ("REDIRECT".equals(samlBinding)) { cfg.setAuthnRequestBindingType(SAMLConstants.SAML2_REDIRECT_BINDING_URI); @@ -188,7 +195,7 @@ private static SAML2Client assembleSaml2Client(String okapiUrl, String tenantId, Boolean mock = vertxContext.config().getBoolean("mock", false); SAML2Client saml2Client = Boolean.TRUE.equals(mock) ? new SAML2ClientMock(cfg) : new SAML2Client(cfg); saml2Client.setName(tenantId); - saml2Client.setCallbackUrl(buildCallbackUrl(okapiUrl, tenantId)); + saml2Client.setCallbackUrl(buildCallbackUrl(okapiUrl, tenantId, callback)); saml2Client.setRedirectionActionBuilder(new JsonReponseSaml2RedirectActionBuilder(saml2Client)); saml2Client.setStateGenerator(new SAML2StateGenerator(saml2Client)); @@ -196,7 +203,7 @@ private static SAML2Client assembleSaml2Client(String okapiUrl, String tenantId, return saml2Client; } - private static String buildCallbackUrl(String okapiUrl, String tenantId) { - return okapiUrl + "/_/invoke/tenant/" + CommonHelper.urlEncode(tenantId) + CALLBACK_ENDPOINT; + private static String buildCallbackUrl(String okapiUrl, String tenantId, String callback) { + return okapiUrl + "/_/invoke/tenant/" + CommonHelper.urlEncode(tenantId) + SAML + callback; } } diff --git a/src/test/java/org/folio/config/SamlClientLoaderTest.java b/src/test/java/org/folio/config/SamlClientLoaderTest.java index d0c5aa67..9e0369f5 100644 --- a/src/test/java/org/folio/config/SamlClientLoaderTest.java +++ b/src/test/java/org/folio/config/SamlClientLoaderTest.java @@ -27,7 +27,7 @@ public void configureSaml2ClientTest(TestContext context) throws MalformedURLExc Resource idpMetadata = new UrlResource("http://localhost:80"); SAML2Client saml2Client = SamlClientLoader .configureSaml2Client(okaiUrl, tenantId, keystorePassword, privateKeyPassword, idpUrlResource, - keystoreResource, samlBinding, idpMetadata, Vertx.vertx().getOrCreateContext()); + keystoreResource, samlBinding, idpMetadata, Vertx.vertx().getOrCreateContext(), "callback-with-expiry"); Assert.assertNotNull(saml2Client); } } diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index a4b3741f..5be76981 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -499,8 +499,8 @@ public void regenerateEndpointTests() { } @Test - public void callbackEndpointTests() { - mock.setMockContent("mock_content_with_callback.json"); + public void callbackEndpointTestsLegacy() { + mock.setMockContent("mock_content_legacy.json"); final String testPath = "/test/path"; @@ -632,6 +632,136 @@ public void callbackEndpointTests() { } + @Test + public void callbackEndpointTests() { + final String testPath = "/test/path"; + + // TODO Make these work with new endpoint and new response. + + log.info("=== Setup - POST /saml/login - need relayState and cookie ==="); + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body("{\"stripesUrl\":\"" + STRIPES_URL + testPath + "\"}") + .post("/saml/login") + .then() + .contentType(ContentType.JSON) + .body(matchesJsonSchemaInClasspath("ramls/schemas/SamlLogin.json")) + .body("bindingMethod", equalTo("POST")) + .statusCode(200) + .extract(); + + String cookie = resp.cookie(SamlAPI.RELAY_STATE); + String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); + + log.info("=== Test - POST /saml/callback-with-expiry - success ==="); + 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))); + + 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") + .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))); + } + + void postSamlLogin(int expectedStatus) { given() .header(TENANT_HEADER) @@ -674,8 +804,8 @@ public void getConfigurationEndpoint() { } @Test - public void getConfigurationEndpointWithCallback() { - mock.setMockContent("mock_content_with_callback.json"); + public void getConfigurationEndpointLegacy() { + mock.setMockContent("mock_content_legacy.json"); given() .header(TENANT_HEADER) .header(TOKEN_HEADER) @@ -742,8 +872,8 @@ public void putConfigurationWithIdpMetadata(TestContext context) { } @Test - public void putConfigurationWithCallback(TestContext context) { - mock.setMockContent("mock_content_with_callback.json"); + public void putConfigurationLegacy(TestContext context) { + mock.setMockContent("mock_content_legacy.json"); SamlConfigRequest samlConfigRequest = new SamlConfigRequest() .withIdpUrl(URI.create("http://localhost:" + IDP_MOCK_PORT + "/xml")) diff --git a/src/test/resources/mock_content.json b/src/test/resources/mock_content.json index 0bd79984..c88844b3 100644 --- a/src/test/resources/mock_content.json +++ b/src/test/resources/mock_content.json @@ -166,11 +166,14 @@ } }, { - "url": "/token", + "url": "/token/sign", "method": "post", "status": 200, "receivedData": { - "token": "saml-token" + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z" } } ] diff --git a/src/test/resources/mock_content_with_callback.json b/src/test/resources/mock_content_legacy.json similarity index 100% rename from src/test/resources/mock_content_with_callback.json rename to src/test/resources/mock_content_legacy.json diff --git a/src/test/resources/mock_tokenresponse.json b/src/test/resources/mock_tokenresponse.json index c21e3b10..d8e3cbbe 100644 --- a/src/test/resources/mock_tokenresponse.json +++ b/src/test/resources/mock_tokenresponse.json @@ -22,7 +22,17 @@ "receivedData": { "token": "saml-token" } + }, + { + "url": "/token/sign", + "method": "post", + "status": 200, + "receivedData": { + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z" + } } - ] } From d55645f4f1f9c45962babb29d7f6c6c1fb3ed57f Mon Sep 17 00:00:00 2001 From: steveellis Date: Mon, 9 Oct 2023 15:55:31 -0400 Subject: [PATCH 22/40] Legacy and non differentiated; cookies tested; readme --- README.md | 6 +- .../java/org/folio/rest/impl/SamlAPI.java | 6 +- .../java/org/folio/rest/impl/SamlAPITest.java | 115 +++++++++++-- src/test/resources/after_regenerate.json | 2 +- src/test/resources/mock_content.json | 2 +- src/test/resources/mock_content_legacy.json | 2 +- .../resources/mock_content_no_keystore.json | 2 +- .../resources/mock_content_with_metadata.json | 9 +- .../mock_content_with_metadata_legacy.json | 161 ++++++++++++++++++ src/test/resources/mock_idptest_post.json | 2 +- src/test/resources/mock_idptest_redirect.json | 2 +- src/test/resources/mock_tokenresponse.json | 2 +- 12 files changed, 286 insertions(+), 25 deletions(-) create mode 100644 src/test/resources/mock_content_with_metadata_legacy.json diff --git a/README.md b/README.md index 11ffc8af..39a24c49 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,15 @@ This module provides SAML2 SSO functionality for FOLIO. Endpoints are documented in [RAML file](ramls/saml-login.raml) -### Environment variables +### Environment variables and system properties `TRUST_ALL_CERTIFICATES`: if value is `true` then HTTPS certificates not checked. This is a security issue in production environment, use it for testing only! Default value is `false`. +`SAML_COOKIE_SAMESITE`: Set to `Lax` if domain for front end and backend are the same for a more secure cookie (default value `None` if this environment variable is not set). + +`Lax` should only be used if the backend and frontend hosts are the same, otherwise the browser will reject the cookies. There is a corresponding system property `saml.cookie.samesite`. + ### Sample users for samltest.id mod-users ships with three sample users that allow SSO login using diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 487d8f6f..52d5051d 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -98,8 +98,8 @@ public class SamlAPI implements Saml { public static final String FOLIO_REFRESH_TOKEN = "folioRefreshToken"; public static final String REFRESH_TOKEN_EXPIRATION = "refreshTokenExpiration"; public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration"; - public static final String COOKIE_SAME_SITE = "login.cookie.samesite"; - public static final String COOKIE_SAME_SITE_ENV = "LOGIN_COOKIE_SAMESITE"; + public static final String COOKIE_SAME_SITE = "saml.cookie.samesite"; + public static final String COOKIE_SAME_SITE_ENV = "SAML_COOKIE_SAMESITE"; public static class UserErrorException extends RuntimeException { public UserErrorException(String message) { @@ -339,7 +339,7 @@ private Future fetchToken(WebClient client, JsonObject payload, Okap .putHeader(XOkapiHeaders.URL, parsedHeaders.getUrl()); return request.sendJson(payload).map(response -> { - if (response.statusCode() != 200 && response.statusCode() != 201) { + if (response.statusCode() != 201) { throw new FetchTokenException("Got response " + response.statusCode() + " fetching token"); } diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 5be76981..8eca6fe2 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -4,10 +4,8 @@ import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; import static org.folio.util.Base64AwareXsdMatcher.matchesBase64XsdInClasspath; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.hasKey; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; @@ -19,6 +17,8 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; + +import io.restassured.matcher.RestAssuredMatchers; import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -104,6 +104,8 @@ public void printTestMethod() { @BeforeClass public static void setupOnce(TestContext context) { + System.clearProperty(SamlAPI.COOKIE_SAME_SITE); + DeploymentOptions mockOptions = new DeploymentOptions() .setConfig(new JsonObject().put("http.port", IDP_MOCK_PORT)) .setWorker(true); @@ -334,12 +336,12 @@ public void loginCorsTests() { } @Test - public void callbackIdpMetadataTest() { + public void callbackIdpMetadataTest_Legacy() { String origin = "http://localhost"; log.info("=== Test Callback with right metadata - POST /saml/callback - success ==="); - mock.setMockContent("mock_content_with_metadata.json"); + mock.setMockContent("mock_content_with_metadata_legacy.json"); given() .header(new Header(HttpHeaders.ORIGIN.toString(), origin)) @@ -355,9 +357,30 @@ public void callbackIdpMetadataTest() { } @Test - public void callbackIdpMetadataHttp2Test(TestContext context) { + public void callbackIdpMetadataTest() { + String origin = "http://localhost"; + + log.info("=== Test Callback with right metadata - POST /saml/callback-with-expiry - success ==="); + mock.setMockContent("mock_content_with_metadata.json"); + given() + .header(new Header(HttpHeaders.ORIGIN.toString(), origin)) + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .contentType(ContentType.URLENC) + .cookie(SamlAPI.RELAY_STATE, readResourceToString("relay_state.txt")) + .body(readResourceToString("saml_response.txt")) + .post("/saml/callback-with-expiry") + .then() + .statusCode(302); + } + + @Test + public void callbackIdpMetadataHttp2Test_Legacy(TestContext context) { + mock.setMockContent("mock_content_with_metadata_legacy.json"); + WebClient.create(vertx) .post(PORT, "localhost", "/saml/callback") .putHeader("X-Okapi-Token", TENANT) @@ -372,7 +395,24 @@ public void callbackIdpMetadataHttp2Test(TestContext context) { } @Test - public void callbackCorsTests() { + public void callbackIdpMetadataHttp2Test(TestContext context) { + mock.setMockContent("mock_content_with_metadata.json"); + + WebClient.create(vertx) + .post(PORT, "localhost", "/saml/callback-with-expiry") + .putHeader("X-Okapi-Token", TENANT) + .putHeader("X-Okapi-Tenant", TENANT) + .putHeader("X-Okapi-Url", "http://localhost:" + JSON_MOCK_PORT) + .putHeader("Content-Type", "application/x-www-form-urlencoded") + .putHeader("Cookie", SamlAPI.RELAY_STATE + "=" + readResourceToString("relay_state.txt").trim()) + .sendBuffer(Buffer.buffer(readResourceToString("saml_response.txt").trim())) + .onComplete(context.asyncAssertSuccess(response -> { + assertThat(response.statusMessage() + "\n" + response.bodyAsString(), response.statusCode(), is(302)); + })); + } + + @Test + public void callbackCorsTests_Legacy() { String origin = "http://localhost"; log.info("=== Test CORS preflight - OPTIONS /saml/callback - success ==="); @@ -390,6 +430,25 @@ public void callbackCorsTests() { .header(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS.toString(), equalTo("true")); } + @Test + public void callbackCorsTests() { + String origin = "http://localhost"; + + log.info("=== Test CORS preflight - OPTIONS /saml/callback - success ==="); + given() + .header(new Header(HttpHeaders.ORIGIN.toString(), origin)) + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(ACCESS_CONTROL_REQ_HEADERS_HEADER) + .header(ACCESS_CONTROL_REQUEST_METHOD_HEADER) + .options("/saml/callback-with-expiry") + .then() + .statusCode(204) + .header(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), equalTo(origin)) + .header(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS.toString(), equalTo("true")); + } + @Test public void getSamlAttributeValue() { UserProfile userProfile = new BasicUserProfile(); @@ -499,7 +558,7 @@ public void regenerateEndpointTests() { } @Test - public void callbackEndpointTestsLegacy() { + public void callbackEndpointTests_Legacy() { mock.setMockContent("mock_content_legacy.json"); final String testPath = "/test/path"; @@ -629,7 +688,6 @@ public void callbackEndpointTestsLegacy() { .header("Location", containsString(PercentCodec.encodeAsString(testPath))) .header("x-okapi-token", "saml-token") .cookie("ssoToken", "saml-token"); - } @Test @@ -669,6 +727,12 @@ public void callbackEndpointTests() { .statusCode(302) .header("Location", containsString(PercentCodec.encodeAsString(testPath))); + testCookieResponse(cookie, relayState, testPath, SamlAPI.COOKIE_SAME_SITE_NONE); + + System.setProperty(SamlAPI.COOKIE_SAME_SITE, SamlAPI.COOKIE_SAME_SITE_LAX); + testCookieResponse(cookie, relayState, testPath, SamlAPI.COOKIE_SAME_SITE_LAX); + System.clearProperty(SamlAPI.COOKIE_SAME_SITE); + log.info("=== Test - POST /saml/callback-with-expiry - failure (wrong cookie) ==="); given() .header(TENANT_HEADER) @@ -702,7 +766,7 @@ public void callbackEndpointTests() { .header(OKAPI_URL_HEADER) .formParam("SAMLResponse", "saml-response") .formParam("RelayState", relayState) - .post("/saml/callback") + .post("/saml/callback-with-expiry") .then() .statusCode(403) .body(is("CSRF attempt detected")); @@ -761,6 +825,35 @@ public void callbackEndpointTests() { .header("Location", containsString(PercentCodec.encodeAsString(testPath))); } + private void testCookieResponse(String cookie, String relayState, String testPath, String sameSite) { + RestAssured.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) + .cookie("folioRefreshToken", RestAssuredMatchers.detailedCookie() + .value("saml-refresh-token") + .path("/authn") // Refresh is restricted to this domain. + .httpOnly(true) + .secured(true) + .domain(is(nullValue())) // Not setting domain disables subdomains. + .sameSite(sameSite)) + .cookie("folioAccessToken", RestAssuredMatchers.detailedCookie() + .value("saml-access-token") + .path("/") // Path must be set in this way for it to mean "all paths". + .httpOnly(true) + .secured(true) + .domain(is(nullValue())) // Not setting domain disables subdomains. + .sameSite(sameSite)) + .header("Location", containsString(PercentCodec.encodeAsString(testPath))) + .header("Location", containsString("accessToken")) + .header("Location", containsString("refreshToken")); + } void postSamlLogin(int expectedStatus) { given() diff --git a/src/test/resources/after_regenerate.json b/src/test/resources/after_regenerate.json index 1be4aea7..740f7c71 100644 --- a/src/test/resources/after_regenerate.json +++ b/src/test/resources/after_regenerate.json @@ -146,7 +146,7 @@ { "url": "/token", "method": "post", - "status": 200, + "status": 201, "receivedData": { "token": "saml-token" } diff --git a/src/test/resources/mock_content.json b/src/test/resources/mock_content.json index c88844b3..0ad3fa5a 100644 --- a/src/test/resources/mock_content.json +++ b/src/test/resources/mock_content.json @@ -168,7 +168,7 @@ { "url": "/token/sign", "method": "post", - "status": 200, + "status": 201, "receivedData": { "accessToken": "saml-access-token", "refreshToken": "saml-refresh-token", diff --git a/src/test/resources/mock_content_legacy.json b/src/test/resources/mock_content_legacy.json index e5356bd0..be3f6b12 100644 --- a/src/test/resources/mock_content_legacy.json +++ b/src/test/resources/mock_content_legacy.json @@ -197,7 +197,7 @@ { "url": "/token", "method": "post", - "status": 200, + "status": 201, "receivedData": { "token": "saml-token" } diff --git a/src/test/resources/mock_content_no_keystore.json b/src/test/resources/mock_content_no_keystore.json index e5752428..ddeff2f0 100644 --- a/src/test/resources/mock_content_no_keystore.json +++ b/src/test/resources/mock_content_no_keystore.json @@ -148,7 +148,7 @@ { "url": "/token", "method": "post", - "status": 200, + "status": 201, "receivedData": { "token": "saml-token" } diff --git a/src/test/resources/mock_content_with_metadata.json b/src/test/resources/mock_content_with_metadata.json index cfe4a0e0..11e70037 100644 --- a/src/test/resources/mock_content_with_metadata.json +++ b/src/test/resources/mock_content_with_metadata.json @@ -150,11 +150,14 @@ } }, { - "url": "/token", + "url": "/token/sign", "method": "post", - "status": 200, + "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" } } ] diff --git a/src/test/resources/mock_content_with_metadata_legacy.json b/src/test/resources/mock_content_with_metadata_legacy.json new file mode 100644 index 00000000..c8ddabd7 --- /dev/null +++ b/src/test/resources/mock_content_with_metadata_legacy.json @@ -0,0 +1,161 @@ +{ + "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" + } + + ], + "totalRecords": 6 + }, + "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+AND+configName%3D%3Dsaml+AND+code%3D%3D+idp.metadata%29", + "method": "get", + "status": 200, + "value": "\n\n \n\n \n\n samltest.id\n\n\n \n SAMLtest IdP\n A free and basic IdP for testing SAML deployments\n https://samltest.id/saml/logo.png\n \n \n\n \n \n \n \nMIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw\nODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA\n8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd\n+nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K\nk2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD\nc4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e\n8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD\nVR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0\nLmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA\nA4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC\ncxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO\nL3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN\nJXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/\nDPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC\nvoQR2qr2xJBixsg+MIORKtmKHLfU\n \n \n \n\n \n \n \n \n \nMIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB\nCwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4\nMDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0\nThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE\njj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl\nbN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF\n/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n\nspXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G\nA1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz\ndC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF\nAAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn\n7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT\nTNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl\nD1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU\nZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu\n3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==\n \n \n \n\n \n \n \n \n \nMIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB\nCwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4\nMDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa\nMwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4\nsSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc\n4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs\n1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca\nuzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G\nA1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz\ndC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF\nAAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj\nWcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD\ndmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3\nsQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4\nJW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR\nzBDsMIEzRtQZm4GIoHJae4zmnCekkQ==\n \n \n \n\n \n\n\n\n \n\n\n \n \n \n\n\n \n \n \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%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/717bf1d1-a5a3-460f-a0de-29e6b70a0027", + "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" + } + } + ] +} diff --git a/src/test/resources/mock_idptest_post.json b/src/test/resources/mock_idptest_post.json index 36739c11..32ee3495 100644 --- a/src/test/resources/mock_idptest_post.json +++ b/src/test/resources/mock_idptest_post.json @@ -81,7 +81,7 @@ { "url": "/token", "method": "post", - "status": 200, + "status": 201, "receivedData": { "token": "saml-token" } diff --git a/src/test/resources/mock_idptest_redirect.json b/src/test/resources/mock_idptest_redirect.json index 890b8e30..75039d58 100644 --- a/src/test/resources/mock_idptest_redirect.json +++ b/src/test/resources/mock_idptest_redirect.json @@ -81,7 +81,7 @@ { "url": "/token", "method": "post", - "status": 200, + "status": 201, "receivedData": { "token": "saml-token" } diff --git a/src/test/resources/mock_tokenresponse.json b/src/test/resources/mock_tokenresponse.json index d8e3cbbe..e82c4eb2 100644 --- a/src/test/resources/mock_tokenresponse.json +++ b/src/test/resources/mock_tokenresponse.json @@ -26,7 +26,7 @@ { "url": "/token/sign", "method": "post", - "status": 200, + "status": 201, "receivedData": { "accessToken": "saml-access-token", "refreshToken": "saml-refresh-token", From 0383274db7760cbb9f62700917a5639570a2f5f3 Mon Sep 17 00:00:00 2001 From: steveellis Date: Mon, 9 Oct 2023 17:07:30 -0400 Subject: [PATCH 23/40] Should be ready --- README.md | 3 +- .../java/org/folio/rest/impl/SamlAPI.java | 4 +-- .../java/org/folio/rest/impl/SamlAPITest.java | 34 ++++++++++++++++--- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 39a24c49..81c081a0 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,7 @@ Endpoints are documented in [RAML file](ramls/saml-login.raml) ### Environment variables and system properties -`TRUST_ALL_CERTIFICATES`: if value is `true` then HTTPS certificates not checked. This is a security issue in -production environment, use it for testing only! Default value is `false`. +`TRUST_ALL_CERTIFICATES`: if value is `true` then HTTPS certificates not checked. This is a security issue in production environment, use it for testing only! Default value is `false`. `SAML_COOKIE_SAMESITE`: Set to `Lax` if domain for front end and backend are the same for a more secure cookie (default value `None` if this environment variable is not set). diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 52d5051d..4a5979a9 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -372,8 +372,8 @@ private Response redirectResponse(JsonObject jsonObject, URI stripesBaseUrl, URI final String location = UriBuilder.fromUri(stripesBaseUrl) .path("sso-landing") .queryParam("fwd", originalUrl.getPath()) - .queryParam(ACCESS_TOKEN, URLEncoder.encode(accessTokenExpiration, StandardCharsets.UTF_8)) - .queryParam(REFRESH_TOKEN, URLEncoder.encode(refreshTokenExpiration, StandardCharsets.UTF_8)) + .queryParam(ACCESS_TOKEN_EXPIRATION, URLEncoder.encode(accessTokenExpiration, StandardCharsets.UTF_8)) + .queryParam(REFRESH_TOKEN_EXPIRATION, URLEncoder.encode(refreshTokenExpiration, StandardCharsets.UTF_8)) .build() .toString(); diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 8eca6fe2..894ba51e 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -851,8 +851,8 @@ private void testCookieResponse(String cookie, String relayState, String testPat .domain(is(nullValue())) // Not setting domain disables subdomains. .sameSite(sameSite)) .header("Location", containsString(PercentCodec.encodeAsString(testPath))) - .header("Location", containsString("accessToken")) - .header("Location", containsString("refreshToken")); + .header("Location", containsString(SamlAPI.ACCESS_TOKEN_EXPIRATION)) + .header("Location", containsString(SamlAPI.REFRESH_TOKEN_EXPIRATION)); } void postSamlLogin(int expectedStatus) { @@ -965,7 +965,7 @@ public void putConfigurationWithIdpMetadata(TestContext context) { } @Test - public void putConfigurationLegacy(TestContext context) { + public void putConfiguration_Legacy() { mock.setMockContent("mock_content_legacy.json"); SamlConfigRequest samlConfigRequest = new SamlConfigRequest() @@ -978,6 +978,33 @@ public void putConfigurationLegacy(TestContext context) { 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"); + + 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")); + + String jsonString = Json.encode(samlConfigRequest); + // PUT given() .header(TENANT_HEADER) @@ -1028,7 +1055,6 @@ public void testWithConfiguration400(TestContext context) { .body(containsString("Cannot get configuration")); } - @Test public void regenerateEndpointNoIdP() { mock.setMockContent("mock_noidp.json"); From 2f8a98ee0a8cf6a0e26cbac443ee37ecad549926 Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Wed, 11 Oct 2023 10:59:37 -0400 Subject: [PATCH 24/40] Fixing IDPTest --- .../rest/impl/{IdpTest.java => IdpTestLegacy.java} | 8 ++++---- ...dptest_post.json => mock_idptest_post_legacy.json} | 8 +++++++- ...edirect.json => mock_idptest_redirect_legacy.json} | 11 +++++++++-- src/test/resources/simplesamlphp/Dockerfile | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) rename src/test/java/org/folio/rest/impl/{IdpTest.java => IdpTestLegacy.java} (98%) rename src/test/resources/{mock_idptest_post.json => mock_idptest_post_legacy.json} (95%) rename src/test/resources/{mock_idptest_redirect.json => mock_idptest_redirect_legacy.json} (95%) diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTestLegacy.java similarity index 98% rename from src/test/java/org/folio/rest/impl/IdpTest.java rename to src/test/java/org/folio/rest/impl/IdpTestLegacy.java index 7f7b89eb..0ed2336f 100644 --- a/src/test/java/org/folio/rest/impl/IdpTest.java +++ b/src/test/java/org/folio/rest/impl/IdpTestLegacy.java @@ -40,8 +40,8 @@ * Test against a real IDP: https://simplesamlphp.org/ running in a Docker container. */ @RunWith(VertxUnitRunner.class) -public class IdpTest { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTest.class); +public class IdpTestLegacy { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IdpTestLegacy.class); private static final boolean DEBUG = false; private static final ImageFromDockerfile simplesamlphp = new ImageFromDockerfile().withFileFromPath(".", Path.of("src/test/resources/simplesamlphp/")); @@ -114,7 +114,7 @@ public void after() { @Test public void post() { setIdpBinding("POST"); - setOkapi("mock_idptest_post.json"); + setOkapi("mock_idptest_post_legacy.json"); for (int i = 0; i < 2; i++) { post0(); @@ -170,7 +170,7 @@ private void post0() { @Test public void redirect() { setIdpBinding("Redirect"); - setOkapi("mock_idptest_redirect.json"); + setOkapi("mock_idptest_redirect_legacy.json"); for (int i = 0; i < 2; i++) { redirect0(); diff --git a/src/test/resources/mock_idptest_post.json b/src/test/resources/mock_idptest_post_legacy.json similarity index 95% rename from src/test/resources/mock_idptest_post.json rename to src/test/resources/mock_idptest_post_legacy.json index 32ee3495..3b0b563c 100644 --- a/src/test/resources/mock_idptest_post.json +++ b/src/test/resources/mock_idptest_post_legacy.json @@ -53,8 +53,14 @@ "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" } - ], "totalRecords": 6 }, diff --git a/src/test/resources/mock_idptest_redirect.json b/src/test/resources/mock_idptest_redirect_legacy.json similarity index 95% rename from src/test/resources/mock_idptest_redirect.json rename to src/test/resources/mock_idptest_redirect_legacy.json index 75039d58..c8e537f3 100644 --- a/src/test/resources/mock_idptest_redirect.json +++ b/src/test/resources/mock_idptest_redirect_legacy.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:9230" + }, + { + "id": "81816efc-63b1-11ee-8c99-0242ac120002", + "module": "LOGIN-SAML", + "configName": "saml", + "code": "saml.callback", + "value": "callback" } - ], "totalRecords": 6 }, diff --git a/src/test/resources/simplesamlphp/Dockerfile b/src/test/resources/simplesamlphp/Dockerfile index 2439a671..a1b0cafd 100644 --- a/src/test/resources/simplesamlphp/Dockerfile +++ b/src/test/resources/simplesamlphp/Dockerfile @@ -1,4 +1,4 @@ -FROM kenchan0130/simplesamlphp:1.19.3 +FROM kenchan0130/simplesamlphp:develop COPY server.key /var/www/simplesamlphp/cert/server.key COPY server.crt /var/www/simplesamlphp/cert/server.crt From 7c9f531c12b08871a0b2dd3748e4742a257540bf Mon Sep 17 00:00:00 2001 From: Steve Ellis Date: Wed, 11 Oct 2023 17:41:57 -0400 Subject: [PATCH 25/40] Has non-legacy idp test --- .../java/org/folio/rest/impl/SamlAPI.java | 8 +- .../java/org/folio/rest/impl/IdpTest.java | 259 ++++++++++++++++++ .../org/folio/rest/impl/IdpTestLegacy.java | 2 +- .../java/org/folio/rest/impl/SamlAPITest.java | 20 +- src/test/resources/mock_idptest_post.json | 92 +++++++ src/test/resources/mock_idptest_redirect.json | 93 +++++++ 6 files changed, 454 insertions(+), 20 deletions(-) create mode 100644 src/test/java/org/folio/rest/impl/IdpTest.java create mode 100644 src/test/resources/mock_idptest_post.json create mode 100644 src/test/resources/mock_idptest_redirect.json diff --git a/src/main/java/org/folio/rest/impl/SamlAPI.java b/src/main/java/org/folio/rest/impl/SamlAPI.java index 4a5979a9..513818c2 100644 --- a/src/main/java/org/folio/rest/impl/SamlAPI.java +++ b/src/main/java/org/folio/rest/impl/SamlAPI.java @@ -243,8 +243,8 @@ private void doPostSamlCallback(String body, RoutingContext routingContext, Map< "Invalid relay state url: " + relayState))); return; } - final URI originalUrl = relayStateUrl; - final URI stripesBaseUrl = UrlUtil.parseBaseUrl(originalUrl); + URI originalUrl = relayStateUrl; + URI stripesBaseUrl = UrlUtil.parseBaseUrl(originalUrl); Cookie relayStateCookie = routingContext.getCookie(RELAY_STATE); if (relayStateCookie == null || !relayState.contentEquals(relayStateCookie.getValue())) { @@ -372,8 +372,8 @@ private Response redirectResponse(JsonObject jsonObject, URI stripesBaseUrl, URI final String location = UriBuilder.fromUri(stripesBaseUrl) .path("sso-landing") .queryParam("fwd", originalUrl.getPath()) - .queryParam(ACCESS_TOKEN_EXPIRATION, URLEncoder.encode(accessTokenExpiration, StandardCharsets.UTF_8)) - .queryParam(REFRESH_TOKEN_EXPIRATION, URLEncoder.encode(refreshTokenExpiration, StandardCharsets.UTF_8)) + .queryParam(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration, StandardCharsets.UTF_8) + .queryParam(REFRESH_TOKEN_EXPIRATION, refreshTokenExpiration, StandardCharsets.UTF_8) .build() .toString(); diff --git a/src/test/java/org/folio/rest/impl/IdpTest.java b/src/test/java/org/folio/rest/impl/IdpTest.java new file mode 100644 index 00000000..2c40df89 --- /dev/null +++ b/src/test/java/org/folio/rest/impl/IdpTest.java @@ -0,0 +1,259 @@ +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.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.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 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 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-with-expiry") + .withEnv("SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE", + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback-with-expiry"); + + @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 post() { + setIdpBinding("POST"); + setOkapi("mock_idptest_post.json"); + + for (int i = 0; i < 2; i++) { + post0(); + } + } + + private void post0() { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL)) + .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)); + + given() + .header("X-Okapi-Url", OKAPI_URL) + .header("X-Okapi-Tenant", "diku") + .cookie(cookie) + .formParams("RelayState", relayState) + .formParams("SAMLResponse", matcher.group(1)) + .post(MODULE_URL + "/saml/callback-with-expiry") + .then() + .statusCode(302) + .header("Location", startsWith("http://localhost:3000/sso-landing")) + .header("Location", containsString(SamlAPI.ACCESS_TOKEN_EXPIRATION)) + .header("Location", containsString(SamlAPI.REFRESH_TOKEN_EXPIRATION)); + } + + @Test + public void redirect() { + setIdpBinding("Redirect"); + setOkapi("mock_idptest_redirect.json"); + + for (int i = 0; i < 2; i++) { + redirect0(); + } + } + + private void redirect0() { + ExtractableResponse resp = given() + .header(TENANT_HEADER) + .header(TOKEN_HEADER) + .header(OKAPI_URL_HEADER) + .header(JSON_CONTENT_TYPE_HEADER) + .body(jsonEncode("stripesUrl", STRIPES_URL)) + .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)); + + given() + .header("X-Okapi-Url", OKAPI_URL) + .header("X-Okapi-Tenant", "diku") + .cookie(cookie) + .params("RelayState", relayState[1]) + .params("SAMLResponse", matcher.group(1)) + .when() + .post(MODULE_URL + "/saml/callback-with-expiry") + .then() + .statusCode(302) + .header("Location", startsWith("http://localhost:3000/sso-landing")) + .header("Location", containsString(SamlAPI.ACCESS_TOKEN_EXPIRATION)) + .header("Location", containsString(SamlAPI.REFRESH_TOKEN_EXPIRATION)); + } + + 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/IdpTestLegacy.java b/src/test/java/org/folio/rest/impl/IdpTestLegacy.java index 0ed2336f..1a1d1c7b 100644 --- a/src/test/java/org/folio/rest/impl/IdpTestLegacy.java +++ b/src/test/java/org/folio/rest/impl/IdpTestLegacy.java @@ -148,7 +148,7 @@ private void post0() { then(). statusCode(200). body(containsString("")). + containsString("action=\"" + OKAPI_URL + "/_/invoke/tenant/diku/saml/callback\">")). extract().asString(); var matcher = Pattern.compile("name=\"SAMLResponse\" value=\"([^\"]+)").matcher(body); diff --git a/src/test/java/org/folio/rest/impl/SamlAPITest.java b/src/test/java/org/folio/rest/impl/SamlAPITest.java index 894ba51e..89060074 100644 --- a/src/test/java/org/folio/rest/impl/SamlAPITest.java +++ b/src/test/java/org/folio/rest/impl/SamlAPITest.java @@ -694,8 +694,6 @@ public void callbackEndpointTests_Legacy() { public void callbackEndpointTests() { final String testPath = "/test/path"; - // TODO Make these work with new endpoint and new response. - log.info("=== Setup - POST /saml/login - need relayState and cookie ==="); ExtractableResponse resp = given() .header(TENANT_HEADER) @@ -715,18 +713,6 @@ public void callbackEndpointTests() { String relayState = resp.body().jsonPath().getString(SamlAPI.RELAY_STATE); log.info("=== Test - POST /saml/callback-with-expiry - success ==="); - 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))); - testCookieResponse(cookie, relayState, testPath, SamlAPI.COOKIE_SAME_SITE_NONE); System.setProperty(SamlAPI.COOKIE_SAME_SITE, SamlAPI.COOKIE_SAME_SITE_LAX); @@ -852,7 +838,11 @@ private void testCookieResponse(String cookie, String relayState, String testPat .sameSite(sameSite)) .header("Location", containsString(PercentCodec.encodeAsString(testPath))) .header("Location", containsString(SamlAPI.ACCESS_TOKEN_EXPIRATION)) - .header("Location", containsString(SamlAPI.REFRESH_TOKEN_EXPIRATION)); + .header("Location", containsString(SamlAPI.REFRESH_TOKEN_EXPIRATION)) + .header("Location", both(containsString(PercentCodec.encodeAsString("2050-10-05T20:19:33Z"))) + .and(containsString(PercentCodec.encodeAsString("2050-10-05T20:19:33Z")))) + .header("Location", containsString("fwd")) + .header("Location", containsString(PercentCodec.encodeAsString("/test/path"))); } void postSamlLogin(int expectedStatus) { diff --git a/src/test/resources/mock_idptest_post.json b/src/test/resources/mock_idptest_post.json new file mode 100644 index 00000000..d34d9612 --- /dev/null +++ b/src/test/resources/mock_idptest_post.json @@ -0,0 +1,92 @@ +{ + "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": "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" + } + ], + "totalRecords": 6 + }, + "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/sign", + "method": "post", + "status": 201, + "receivedData": { + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z" + } + } + ] +} diff --git a/src/test/resources/mock_idptest_redirect.json b/src/test/resources/mock_idptest_redirect.json new file mode 100644 index 00000000..3238b583 --- /dev/null +++ b/src/test/resources/mock_idptest_redirect.json @@ -0,0 +1,93 @@ +{ + "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": "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" + } + ], + "totalRecords": 6 + }, + "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/sign", + "method": "post", + "status": 201, + "receivedData": { + "accessToken": "saml-access-token", + "refreshToken": "saml-refresh-token", + "accessTokenExpiration": "2050-10-05T20:19:33Z", + "refreshTokenExpiration": "2050-10-05T20:19:33Z" + } + } + ] +} From dfc3b6bf4abccec9b5b2e96830aa3bb19cb92a69 Mon Sep 17 00:00:00 2001 From: Julian Ladisch Date: Thu, 12 Oct 2023 10:32:37 +0200 Subject: [PATCH 26/40] MODLOGSAML-173: Upgrade dependencies for Poppy --- pom.xml | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 9fd560c3..af5c2a3a 100644 --- a/pom.xml +++ b/pom.xml @@ -37,12 +37,12 @@ UTF-8 - 35.0.6 + 35.1.0 /saml/callback,/saml/regenerate,/saml/login,/saml/check,/saml/configuration - 1.9.19 - 5.7.0 + 1.9.20.1 + 5.7.1 6.0.1 ${basedir}/ramls @@ -55,7 +55,7 @@ io.rest-assured rest-assured-bom - 5.2.0 + 5.3.2 pom import @@ -63,7 +63,7 @@ io.vertx vertx-stack-depchain - 4.3.8 + 4.4.5 pom import @@ -71,7 +71,7 @@ org.apache.logging.log4j log4j-bom - 2.19.0 + 2.20.0 pom import @@ -79,7 +79,7 @@ org.testcontainers testcontainers-bom - 1.17.6 + 1.19.1 pom import @@ -143,7 +143,7 @@ net.minidev json-smart - 2.4.10 + 2.4.11