diff --git a/.github/actions/archive-surefire-reports/action.yml b/.github/actions/archive-surefire-reports/action.yml index a9104e66bcef..5862825479a6 100644 --- a/.github/actions/archive-surefire-reports/action.yml +++ b/.github/actions/archive-surefire-reports/action.yml @@ -7,7 +7,7 @@ inputs: release-branches: description: 'List of all related release branches (in JSON format)' required: false - default: '["refs/heads/release/22.0"]' + default: '["refs/heads/release/22.0","refs/heads/release/24.0"]' keep-days: description: 'For how many days to store the particular artifact.' required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 3f1406802f78..000000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - open-pull-requests-limit: 999 - rebase-strategy: disabled - schedule: - interval: daily - time: "00:00" - timezone: Etc/GMT - labels: - - area/dependencies - - area/ci - - package-ecosystem: npm - directory: /themes/src/main/resources/theme/keycloak/common/resources - schedule: - interval: daily - time: "00:00" - timezone: Etc/GMT - open-pull-requests-limit: 999 - rebase-strategy: disabled - labels: - - area/dependencies - - team/ui - - package-ecosystem: npm - directory: js - open-pull-requests-limit: 999 - rebase-strategy: disabled - versioning-strategy: increase - schedule: - interval: daily - time: "00:00" - timezone: Etc/GMT - labels: - - area/dependencies - - team/ui diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 000000000000..df3d8ed7d120 --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,67 @@ +name: Docker CI + +on: + workflow_dispatch: + push: + branches: + - main + +env: + DEFAULT_JDK_VERSION: 17 + +concurrency: + # Only run once for latest commit per ref and cancel other (previous) runs. + group: docker-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and push docker image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: ${{ env.DEFAULT_JDK_VERSION }} + cache: 'maven' + + - name: Build Keycloak + run: | + mvn clean install -DskipTestsuite -DskipExamples -DskipTests + mvn -f quarkus/pom.xml clean install -DskipTests + + - name: Set up environment + run: cat release-details >> $GITHUB_ENV + + - name: Copy keycloak artifact + run: cp quarkus/dist/target/keycloak-${{env.VERSION}}.tar.gz quarkus/container/ + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: primesign-services + password: ${{ secrets.CR_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: quarkus/container + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + build-args: KEYCLOAK_DIST=keycloak-${{env.VERSION}}.tar.gz + tags: | + ghcr.io/primesign/keycloak:latest + ghcr.io/primesign/keycloak:${{env.VERSION}} + ghcr.io/primesign/keycloak:${{env.SHORT_VERSION}} + + - name: Remove keycloak artifacts before caching + if: steps.cache.outputs.cache-hit != 'true' + run: rm -rf ~/.m2/repository/org/keycloak \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 9e4bb7b6d5a6..39068b710a55 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - fb-* pull_request: workflow_dispatch: diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 38ae8315db57..b6294f1860d8 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -54,13 +54,13 @@ jobs: - name: Build Keycloak run: | ./mvnw clean install --errors -DskipTests -DskipTestsuite -DskipExamples -Pdistribution - mv ./quarkus/dist/target/keycloak-999.0.0-SNAPSHOT.tar.gz ./keycloak-999.0.0-SNAPSHOT.tar.gz + mv ./quarkus/dist/target/keycloak-24.0.5-PS-2.tar.gz ./keycloak-24.0.5-PS-2.tar.gz - name: Upload Keycloak dist uses: actions/upload-artifact@v3 with: name: keycloak - path: keycloak-999.0.0-SNAPSHOT.tar.gz + path: keycloak-24.0.5-PS-2.tar.gz admin-client: name: Admin Client @@ -214,8 +214,8 @@ jobs: - name: Start Keycloak server run: | - tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=transient-users &> ~/server.log & + tar xfvz keycloak-24.0.5-PS-2.tar.gz + keycloak-24.0.5-PS-2/bin/kc.sh start-dev --features=transient-users &> ~/server.log & env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin @@ -297,8 +297,8 @@ jobs: - name: Start Keycloak server run: | - tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,transient-users &> ~/server.log & + tar xfvz keycloak-24.0.5-PS-2.tar.gz + keycloak-24.0.5-PS-2/bin/kc.sh start-dev --features=admin-fine-grained-authz,transient-users &> ~/server.log & env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 5b3a17282bcd..92be95950bb8 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -31,7 +31,7 @@ jobs: BACKPORT_LABEL="backport/main" elif [[ "$GITHUB_BASE_REF" = release/* ]]; then MAJOR_MINOR="$(echo $GITHUB_BASE_REF | cut -d '/' -f 2)" - LAST_MICRO="$(gh api /repos/$GITHUB_REPOSITORY/tags --jq .[].name | sort -n -r | grep $MAJOR_MINOR | head -n 1 | cut -d '.' -f 3)" + LAST_MICRO="$(gh api /repos/$GITHUB_REPOSITORY/tags --jq .[].name | sort -V -r | grep $MAJOR_MINOR | head -n 1 | cut -d '.' -f 3)" NEXT_MICRO="$(($LAST_MICRO + 1))" LABEL="release/$MAJOR_MINOR.$NEXT_MICRO" BACKPORT_LABEL="backport/$MAJOR_MINOR" diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml index d8fa7efc98e9..3ad64e801530 100644 --- a/.github/workflows/operator-ci.yml +++ b/.github/workflows/operator-ci.yml @@ -10,8 +10,9 @@ on: env: MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" - MINIKUBE_VERSION: v1.31.2 - KUBERNETES_VERSION: v1.24.17 # OCP 4.11 + MINIKUBE_VERSION: v1.32.0 + KUBERNETES_VERSION: v1.27.10 # OCP 4.14 + MINIKUBE_MEMORY: 4096 # Without explicitly setting memory, minikube uses ~25% of available memory which might be too little on smaller GitHub runners for running the tests defaults: run: @@ -72,7 +73,7 @@ jobs: kubernetes version: ${{ env.KUBERNETES_VERSION }} github token: ${{ secrets.GITHUB_TOKEN }} driver: docker - start args: --addons=ingress + start args: --addons=ingress --memory=${{ env.MINIKUBE_MEMORY }} - name: Download keycloak distribution id: download-keycloak-dist @@ -116,7 +117,7 @@ jobs: kubernetes version: ${{ env.KUBERNETES_VERSION }} github token: ${{ secrets.GITHUB_TOKEN }} driver: docker - start args: --addons=ingress + start args: --addons=ingress --memory=${{ env.MINIKUBE_MEMORY }} - name: Download keycloak distribution id: download-keycloak-dist @@ -159,6 +160,7 @@ jobs: kubernetes version: ${{ env.KUBERNETES_VERSION }} github token: ${{ secrets.GITHUB_TOKEN }} driver: docker + start args: --memory=${{ env.MINIKUBE_MEMORY }} - name: Install OPM uses: redhat-actions/openshift-tools-installer@v1 diff --git a/.github/workflows/snyk-analysis.yml b/.github/workflows/snyk-analysis.yml index 369b67344c1c..3d484338805e 100644 --- a/.github/workflows/snyk-analysis.yml +++ b/.github/workflows/snyk-analysis.yml @@ -31,6 +31,7 @@ jobs: - name: Upload Quarkus scanner results to GitHub uses: github/codeql-action/upload-sarif@v3 + continue-on-error: true with: sarif_file: quarkus-report.sarif category: snyk-quarkus-report diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000000..1c692c8fbccc --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +--- +stages: + - build + +image: maven:3.6.3-openjdk-17 + + +variables: + MAVEN_OPTS: "-Dmaven.repo.local=${CI_PROJECT_DIR}/.m2/repository -Dhttps.protocols=TLSv1.2 -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true" + MAVEN_CLI_OPTS: "-f ${CI_PROJECT_DIR}/pom.xml -s ${CI_PROJECT_DIR}/.m2/settings.xml --batch-mode --errors --show-version -DskipTests -DskipExamples -DskipTestsuite -DinstallAtEnd=false -DdeployAtEnd=false" + +cache: + paths: + - .m2/repository + +build: + stage: build + only: + refs: + - main + except: + variables: + - $CI_COMMIT_MESSAGE =~ /\[maven-release-plugin\] prepare release/ + script: + - mvn $MAVEN_CLI_OPTS $MAVEN_PROJECT_OPTS deploy + +build-fb: + stage: build + only: + refs: + # feature branch + - /^fb-.*$/ + # bugfix branch + - /^fix-.*$/ + except: + variables: + - $CI_COMMIT_MESSAGE =~ /\[maven-release-plugin\] prepare release/ + script: + - mvn $MAVEN_CLI_OPTS $MAVEN_PROJECT_OPTS clean deploy diff --git a/.m2/settings.xml b/.m2/settings.xml new file mode 100644 index 000000000000..692d898c04b6 --- /dev/null +++ b/.m2/settings.xml @@ -0,0 +1,61 @@ + + + + + releases + ${env.MAVEN_REPO_USER} + ${env.MAVEN_REPO_PASS} + + + snapshots + ${env.MAVEN_REPO_USER} + ${env.MAVEN_REPO_PASS} + + + + + + + + false + + central + libs-releases + https://artifactory.intra.prime-sign.com/artifactory/libs-releases + + + + snapshots + libs-snapshots + https://artifactory.intra.prime-sign.com/artifactory/libs-snapshots + + + Redhat Repo + libs-snapshots + https://maven.repository.redhat.com/ga/ + + + + + + false + + central + plugins-releases + https://artifactory.intra.prime-sign.com/artifactory/plugins-releases + + + + snapshots + plugins-snapshots + https://artifactory.intra.prime-sign.com/artifactory/plugins-snapshots + + + artifactory + + + + artifactory + + \ No newline at end of file diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml index 2baddd35d190..67419ec02959 100755 --- a/adapters/oidc/adapter-core/pom.xml +++ b/adapters/oidc/adapter-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml index 985b81d485f2..87c15d322f30 100755 --- a/adapters/oidc/installed/pom.xml +++ b/adapters/oidc/installed/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jakarta-servlet-filter/pom.xml b/adapters/oidc/jakarta-servlet-filter/pom.xml index c2105f8e1e94..324cdcc02237 100755 --- a/adapters/oidc/jakarta-servlet-filter/pom.xml +++ b/adapters/oidc/jakarta-servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jaxrs-oauth-client/pom.xml b/adapters/oidc/jaxrs-oauth-client/pom.xml index ef98434d3561..e728e5c6649b 100755 --- a/adapters/oidc/jaxrs-oauth-client/pom.xml +++ b/adapters/oidc/jaxrs-oauth-client/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty-core/pom.xml b/adapters/oidc/jetty/jetty-core/pom.xml index f36d9780af3f..fbcb879e1465 100755 --- a/adapters/oidc/jetty/jetty-core/pom.xml +++ b/adapters/oidc/jetty/jetty-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty9.4/pom.xml b/adapters/oidc/jetty/jetty9.4/pom.xml index 3ddd0b2fa930..dfc9fc673a71 100644 --- a/adapters/oidc/jetty/jetty9.4/pom.xml +++ b/adapters/oidc/jetty/jetty9.4/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/pom.xml b/adapters/oidc/jetty/pom.xml index 30b8b7706bea..f9e9a7de8223 100755 --- a/adapters/oidc/jetty/pom.xml +++ b/adapters/oidc/jetty/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak Jetty Integration diff --git a/adapters/oidc/js/pom.xml b/adapters/oidc/js/pom.xml index e9bdb2780847..ef4b3172fcf3 100644 --- a/adapters/oidc/js/pom.xml +++ b/adapters/oidc/js/pom.xml @@ -5,7 +5,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml index f4e37273be18..af3d337e2b06 100755 --- a/adapters/oidc/pom.xml +++ b/adapters/oidc/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml Keycloak OIDC Client Adapter Modules diff --git a/adapters/oidc/servlet-filter/pom.xml b/adapters/oidc/servlet-filter/pom.xml index 1e26928aa9b0..724c531c1d5f 100755 --- a/adapters/oidc/servlet-filter/pom.xml +++ b/adapters/oidc/servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-boot-adapter-core/pom.xml b/adapters/oidc/spring-boot-adapter-core/pom.xml index ea1f9c6a6736..2814849b5911 100755 --- a/adapters/oidc/spring-boot-adapter-core/pom.xml +++ b/adapters/oidc/spring-boot-adapter-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-boot-container-bundle/pom.xml b/adapters/oidc/spring-boot-container-bundle/pom.xml index fd0be595f8d6..f653d5cb7eff 100644 --- a/adapters/oidc/spring-boot-container-bundle/pom.xml +++ b/adapters/oidc/spring-boot-container-bundle/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml spring-boot-container-bundle diff --git a/adapters/oidc/spring-boot2/pom.xml b/adapters/oidc/spring-boot2/pom.xml index 86115f6004fa..882646dff81a 100755 --- a/adapters/oidc/spring-boot2/pom.xml +++ b/adapters/oidc/spring-boot2/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml index c85b8180a240..829b9c07163e 100644 --- a/adapters/oidc/spring-security/pom.xml +++ b/adapters/oidc/spring-security/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/tomcat/pom.xml b/adapters/oidc/tomcat/pom.xml index e79897be4b29..36ee1fc83c5c 100755 --- a/adapters/oidc/tomcat/pom.xml +++ b/adapters/oidc/tomcat/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak Tomcat Integration diff --git a/adapters/oidc/tomcat/tomcat-core/pom.xml b/adapters/oidc/tomcat/tomcat-core/pom.xml index b7859c0d7d20..942924c80866 100755 --- a/adapters/oidc/tomcat/tomcat-core/pom.xml +++ b/adapters/oidc/tomcat/tomcat-core/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/adapters/oidc/tomcat/tomcat/pom.xml b/adapters/oidc/tomcat/tomcat/pom.xml index 2a5d96149038..f50fbd827216 100755 --- a/adapters/oidc/tomcat/tomcat/pom.xml +++ b/adapters/oidc/tomcat/tomcat/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/adapters/oidc/undertow/pom.xml b/adapters/oidc/undertow/pom.xml index 6d179ae84d86..e0eb4bb30d0e 100755 --- a/adapters/oidc/undertow/pom.xml +++ b/adapters/oidc/undertow/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/wildfly-elytron/pom.xml b/adapters/oidc/wildfly-elytron/pom.xml index 1d8f5c9b5b86..ebd68036e655 100755 --- a/adapters/oidc/wildfly-elytron/pom.xml +++ b/adapters/oidc/wildfly-elytron/pom.xml @@ -22,7 +22,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/wildfly/pom.xml b/adapters/oidc/wildfly/pom.xml index 173bf616d01a..26f479b309ea 100755 --- a/adapters/oidc/wildfly/pom.xml +++ b/adapters/oidc/wildfly/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak WildFly Integration diff --git a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml index 15f36da7fef0..ee3373482c04 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml +++ b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/adapters/pom.xml b/adapters/pom.xml index fac0534171c2..73b3af85f436 100755 --- a/adapters/pom.xml +++ b/adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Adapters diff --git a/adapters/saml/core-jakarta/pom.xml b/adapters/saml/core-jakarta/pom.xml index 2b4f82342130..e3bea09820db 100644 --- a/adapters/saml/core-jakarta/pom.xml +++ b/adapters/saml/core-jakarta/pom.xml @@ -6,7 +6,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml diff --git a/adapters/saml/core-public/pom.xml b/adapters/saml/core-public/pom.xml index a5e4e0aa3f9b..026a66757f63 100755 --- a/adapters/saml/core-public/pom.xml +++ b/adapters/saml/core-public/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml index d72de7b0ee37..b38bae3363af 100755 --- a/adapters/saml/core/pom.xml +++ b/adapters/saml/core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/jakarta-servlet-filter/pom.xml b/adapters/saml/jakarta-servlet-filter/pom.xml index 03b33d337b4c..67c2aed81c75 100755 --- a/adapters/saml/jakarta-servlet-filter/pom.xml +++ b/adapters/saml/jakarta-servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty-core/pom.xml b/adapters/saml/jetty/jetty-core/pom.xml index 7ad707523966..0e4702fc2907 100755 --- a/adapters/saml/jetty/jetty-core/pom.xml +++ b/adapters/saml/jetty/jetty-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty9.4/pom.xml b/adapters/saml/jetty/jetty9.4/pom.xml index 91e5589f0b4c..faec4e68cd1e 100644 --- a/adapters/saml/jetty/jetty9.4/pom.xml +++ b/adapters/saml/jetty/jetty9.4/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/pom.xml b/adapters/saml/jetty/pom.xml index dda79695de4a..ab37cebfdda5 100755 --- a/adapters/saml/jetty/pom.xml +++ b/adapters/saml/jetty/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak SAML Jetty Integration diff --git a/adapters/saml/pom.xml b/adapters/saml/pom.xml index 443ad2a468d0..851bba52ed9d 100755 --- a/adapters/saml/pom.xml +++ b/adapters/saml/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml Keycloak SAML Client Adapter Modules diff --git a/adapters/saml/servlet-filter/pom.xml b/adapters/saml/servlet-filter/pom.xml index 5fc027042952..0a595dd8b8a1 100755 --- a/adapters/saml/servlet-filter/pom.xml +++ b/adapters/saml/servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/pom.xml b/adapters/saml/tomcat/pom.xml index 70bd25315d01..e4402d9ae76d 100755 --- a/adapters/saml/tomcat/pom.xml +++ b/adapters/saml/tomcat/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak SAML Tomcat Integration diff --git a/adapters/saml/tomcat/tomcat-core/pom.xml b/adapters/saml/tomcat/tomcat-core/pom.xml index 9844e59f090d..f80827b4a6d5 100755 --- a/adapters/saml/tomcat/tomcat-core/pom.xml +++ b/adapters/saml/tomcat/tomcat-core/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/tomcat/pom.xml b/adapters/saml/tomcat/tomcat/pom.xml index 0dd64f673b34..d164e40cece4 100755 --- a/adapters/saml/tomcat/tomcat/pom.xml +++ b/adapters/saml/tomcat/tomcat/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/adapters/saml/undertow/pom.xml b/adapters/saml/undertow/pom.xml index b4d5480a78c1..31679ed27d06 100755 --- a/adapters/saml/undertow/pom.xml +++ b/adapters/saml/undertow/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly-elytron-jakarta/pom.xml b/adapters/saml/wildfly-elytron-jakarta/pom.xml index f8dc3b5d5d88..e5814f62e8c7 100755 --- a/adapters/saml/wildfly-elytron-jakarta/pom.xml +++ b/adapters/saml/wildfly-elytron-jakarta/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml index cc78c6e482b1..5877c99bfb9f 100755 --- a/adapters/saml/wildfly-elytron/pom.xml +++ b/adapters/saml/wildfly-elytron/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly/pom.xml b/adapters/saml/wildfly/pom.xml index 02f49d830727..1987d300a7bd 100755 --- a/adapters/saml/wildfly/pom.xml +++ b/adapters/saml/wildfly/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak SAML Wildfly Integration diff --git a/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml b/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml index 15e52a641515..15ed3ccb70d2 100755 --- a/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml +++ b/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/adapters/saml/wildfly/wildfly-subsystem/pom.xml b/adapters/saml/wildfly/wildfly-subsystem/pom.xml index 90b38b0f7462..f69fce4f6b5d 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/pom.xml +++ b/adapters/saml/wildfly/wildfly-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/adapters/spi/adapter-spi/pom.xml b/adapters/spi/adapter-spi/pom.xml index aab344d284be..29a57e26fefd 100755 --- a/adapters/spi/adapter-spi/pom.xml +++ b/adapters/spi/adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/jakarta-servlet-adapter-spi/pom.xml b/adapters/spi/jakarta-servlet-adapter-spi/pom.xml index a5d8d48e4729..4df81636acf3 100755 --- a/adapters/spi/jakarta-servlet-adapter-spi/pom.xml +++ b/adapters/spi/jakarta-servlet-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/jboss-adapter-core/pom.xml b/adapters/spi/jboss-adapter-core/pom.xml index a184ae5e0d8a..f3143b6cb633 100755 --- a/adapters/spi/jboss-adapter-core/pom.xml +++ b/adapters/spi/jboss-adapter-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/jetty-adapter-spi/pom.xml b/adapters/spi/jetty-adapter-spi/pom.xml index 17a0654fd008..e4297b8d53c6 100755 --- a/adapters/spi/jetty-adapter-spi/pom.xml +++ b/adapters/spi/jetty-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/pom.xml b/adapters/spi/pom.xml index 6bd05d9b412a..8fcbe9e7eef9 100755 --- a/adapters/spi/pom.xml +++ b/adapters/spi/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml Keycloak Client Adapter SPI Modules diff --git a/adapters/spi/servlet-adapter-spi/pom.xml b/adapters/spi/servlet-adapter-spi/pom.xml index 2d8f0ca1f6dd..145d666ab78c 100755 --- a/adapters/spi/servlet-adapter-spi/pom.xml +++ b/adapters/spi/servlet-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/tomcat-adapter-spi/pom.xml b/adapters/spi/tomcat-adapter-spi/pom.xml index 0588a9826061..eec4f8cadbc6 100755 --- a/adapters/spi/tomcat-adapter-spi/pom.xml +++ b/adapters/spi/tomcat-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/adapters/spi/undertow-adapter-spi/pom.xml b/adapters/spi/undertow-adapter-spi/pom.xml index 884b93c77400..30bdb2542233 100755 --- a/adapters/spi/undertow-adapter-spi/pom.xml +++ b/adapters/spi/undertow-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml 4.0.0 diff --git a/authz/client/pom.xml b/authz/client/pom.xml index d410042e7ec0..af27b451765d 100644 --- a/authz/client/pom.xml +++ b/authz/client/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-authz-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java index 3bf90f37457d..0a2887f0bbf1 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java @@ -86,6 +86,7 @@ public List getPermissions(final AuthorizationRequest request) throw if (request.getMetadata() == null) { metadata = new AuthorizationRequest.Metadata(); + request.setMetadata(metadata); } else { metadata = request.getMetadata(); } diff --git a/authz/policy-enforcer/pom.xml b/authz/policy-enforcer/pom.xml index 096833adf698..ebb8944ed276 100755 --- a/authz/policy-enforcer/pom.xml +++ b/authz/policy-enforcer/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-authz-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml index ec7c0f5ddc4d..e590e7dfb89a 100644 --- a/authz/policy/common/pom.xml +++ b/authz/policy/common/pom.xml @@ -25,7 +25,7 @@ org.keycloak keycloak-authz-provider-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml index 48091592faad..d060e984c42d 100644 --- a/authz/policy/pom.xml +++ b/authz/policy/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-authz-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/authz/pom.xml b/authz/pom.xml index 31224da50309..710dfa951375 100644 --- a/authz/pom.xml +++ b/authz/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/boms/adapter/pom.xml b/boms/adapter/pom.xml index 417c7107fde5..475bd72e80e5 100644 --- a/boms/adapter/pom.xml +++ b/boms/adapter/pom.xml @@ -22,7 +22,7 @@ org.keycloak.bom keycloak-bom-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak.bom diff --git a/boms/misc/pom.xml b/boms/misc/pom.xml index 7305aa3bb610..b64e141731c7 100644 --- a/boms/misc/pom.xml +++ b/boms/misc/pom.xml @@ -22,7 +22,7 @@ org.keycloak.bom keycloak-bom-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak.bom diff --git a/boms/pom.xml b/boms/pom.xml index c67416d7afa0..5d962cb073f1 100644 --- a/boms/pom.xml +++ b/boms/pom.xml @@ -27,7 +27,7 @@ org.keycloak.bom keycloak-bom-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 pom @@ -57,7 +57,20 @@ spi misc - + + + + releases + libs-releases-local + ${env.MAVEN_REPO_URL}/libs-releases-local + + + snapshots + libs-snapshots-local + ${env.MAVEN_REPO_URL}/libs-snapshots-local + + + diff --git a/boms/spi/pom.xml b/boms/spi/pom.xml index 339c18bb3033..6cd6dfa4f446 100644 --- a/boms/spi/pom.xml +++ b/boms/spi/pom.xml @@ -23,7 +23,7 @@ org.keycloak.bom keycloak-bom-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak.bom diff --git a/common/pom.xml b/common/pom.xml index 5f644cbc0af2..c2942e44d23d 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/common/src/main/java/org/keycloak/common/util/Encode.java b/common/src/main/java/org/keycloak/common/util/Encode.java index b17d61be5bf5..682d29fedaab 100755 --- a/common/src/main/java/org/keycloak/common/util/Encode.java +++ b/common/src/main/java/org/keycloak/common/util/Encode.java @@ -46,6 +46,7 @@ public class Encode private static final String[] matrixParameterEncoding = new String[128]; private static final String[] queryNameValueEncoding = new String[128]; private static final String[] queryStringEncoding = new String[128]; + private static final String[] userInfoStringEncoding = new String[128]; static { @@ -158,6 +159,44 @@ public class Encode } queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); } + + /* + * userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + for (int i = 0; i < 128; i++) + { + if (i >= 'a' && i <= 'z') continue; + if (i >= 'A' && i <= 'Z') continue; + if (i >= '0' && i <= '9') continue; + switch ((char) i) + { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + case ':': + continue; + case ' ': + userInfoStringEncoding[i] = "%20"; + continue; + } + userInfoStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); + } } /** @@ -177,6 +216,24 @@ public static String encodeQueryStringNotTemplateParameters(String value) { return encodeNonCodes(encodeFromArray(value, queryStringEncoding, false)); } + /** + * Keep encoded values "%..." and template parameters intact. + * @param value The user-info value to encode + * @return The user-info encoded + */ + public static String encodeUserInfo(String value) { + return encodeValue(value, userInfoStringEncoding); + } + + /** + * Keep encoded values "%..." but not the template parameters. + * @param value The user-info to encode + * @return The user-info encoded + */ + public static String encodeUserInfoNotTemplateParameters(String value) { + return encodeNonCodes(encodeFromArray(value, userInfoStringEncoding, false)); + } + /** * Keep encoded values "%...", matrix parameters, template parameters, and '/' characters intact. */ @@ -424,6 +481,30 @@ public static String encodeQueryParamSaveEncodings(String segment) return result; } + /** + * Encodes everything in user-info + * + * @param nameOrValue + * @return + */ + public static String encodeUserInfoAsIs(String nameOrValue) + { + return encodeFromArray(nameOrValue, userInfoStringEncoding, true); + } + + /** + * Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p" + * + * @param segment + * @return + */ + public static String encodeUserInfoSaveEncodings(String segment) + { + String result = encodeFromArray(segment, userInfoStringEncoding, false); + result = encodeNonCodes(result); + return result; + } + public static String encodeFragmentAsIs(String nameOrValue) { return encodeFromArray(nameOrValue, queryNameValueEncoding, true); diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java index 659c1d8c418c..5b27f988796c 100755 --- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java +++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java @@ -150,7 +150,7 @@ protected KeycloakUriBuilder parseHierarchicalUri(String uri, Matcher match, boo if (at > -1) { String user = host.substring(0, at); host = host.substring(at + 1); - this.userInfo = user; + replaceUserInfo(user, template); } Matcher hostPortMatch = hostPortPattern.matcher(host); if (hostPortMatch.matches()) { @@ -459,7 +459,7 @@ private String buildString(Map paramMap, boolean fromEncodedMap, bool } else if (userInfo != null || host != null || port != -1) { buffer.append("//"); if (userInfo != null) - replaceParameter(paramMap, fromEncodedMap, isTemplate, userInfo, buffer, encodeSlash).append("@"); + replaceUserInfoParameter(paramMap, fromEncodedMap, isTemplate, userInfo, buffer).append("@"); if (host != null) { if ("".equals(host)) throw new RuntimeException("empty host name"); replaceParameter(paramMap, fromEncodedMap, isTemplate, host, buffer, encodeSlash); @@ -571,6 +571,33 @@ protected StringBuffer replaceQueryStringParameter(Map paramMap, bool return buffer; } + protected StringBuffer replaceUserInfoParameter(Map paramMap, boolean fromEncodedMap, boolean isTemplate, String string, StringBuffer buffer) { + Matcher matcher = createUriParamMatcher(string); + while (matcher.find()) { + String param = matcher.group(1); + Object valObj = paramMap.get(param); + if (valObj == null && !isTemplate) { + throw new IllegalArgumentException("NULL value for template parameter: " + param); + } else if (valObj == null && isTemplate) { + matcher.appendReplacement(buffer, matcher.group()); + continue; + } + String value = valObj.toString(); + if (value != null) { + if (!fromEncodedMap) { + value = Encode.encodeUserInfoAsIs(value); + } else { + value = Encode.encodeUserInfoSaveEncodings(value); + } + matcher.appendReplacement(buffer, value); + } else { + throw new IllegalArgumentException("path param " + param + " has not been provided by the parameter map"); + } + } + matcher.appendTail(buffer); + return buffer; + } + /** * Return a unique order list of path params * @@ -742,6 +769,15 @@ public KeycloakUriBuilder replacePath(String path, boolean template) { return this; } + public KeycloakUriBuilder replaceUserInfo(String userInfo, boolean template) { + if (userInfo == null) { + this.userInfo = null; + return this; + } + this.userInfo = template? Encode.encodeUserInfo(userInfo) : Encode.encodeUserInfoNotTemplateParameters(userInfo); + return this; + } + public URI build(Object[] values, boolean encodeSlashInPath) throws IllegalArgumentException { if (values == null) throw new IllegalArgumentException("values param is null"); return buildFromValues(encodeSlashInPath, false, values); diff --git a/common/src/main/java/org/keycloak/common/util/Retry.java b/common/src/main/java/org/keycloak/common/util/Retry.java index 05894afb358b..d3225b9b9c6e 100644 --- a/common/src/main/java/org/keycloak/common/util/Retry.java +++ b/common/src/main/java/org/keycloak/common/util/Retry.java @@ -18,7 +18,7 @@ package org.keycloak.common.util; import java.time.Duration; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; /** * @author Stian Thorgersen @@ -125,8 +125,8 @@ public static int executeWithBackoff(AdvancedRunnable runnable, ThrowableCallbac } } - private static int computeBackoffInterval(int base, int iteration) { - return new Random().nextInt(computeIterationBase(base, iteration)); + public static int computeBackoffInterval(int base, int iteration) { + return ThreadLocalRandom.current().nextInt(computeIterationBase(base, iteration)); } private static int computeIterationBase(int base, int iteration) { diff --git a/common/src/test/java/org/keycloak/common/util/KeycloakUriBuilderTest.java b/common/src/test/java/org/keycloak/common/util/KeycloakUriBuilderTest.java index 5e418d73e5de..950b3db7c0ab 100644 --- a/common/src/test/java/org/keycloak/common/util/KeycloakUriBuilderTest.java +++ b/common/src/test/java/org/keycloak/common/util/KeycloakUriBuilderTest.java @@ -80,4 +80,16 @@ public void testTemplateAndNotTemplate() { Assert.assertEquals("https://localhost:8443/%7Bpath%7D?key=%7Bquery%7D#%7Bfragment%7D", KeycloakUriBuilder.fromUri( "https://localhost:8443/{path}?key={query}#{fragment}", false).buildAsString()); } + + @Test + public void testUserInfo() { + Assert.assertEquals("https://user-info@localhost:8443/path?key=query#fragment", KeycloakUriBuilder.fromUri( + "https://{userinfo}@localhost:8443/{path}?key={query}#{fragment}").buildAsString("user-info", "path", "query", "fragment")); + Assert.assertEquals("https://user%20info%40%2F@localhost:8443/path?key=query#fragment", KeycloakUriBuilder.fromUri( + "https://{userinfo}@localhost:8443/{path}?key={query}#{fragment}").buildAsString("user info@/", "path", "query", "fragment")); + Assert.assertEquals("https://user-info%E2%82%AC@localhost:8443", KeycloakUriBuilder.fromUri( + "https://user-info%E2%82%AC@localhost:8443", false).buildAsString()); + Assert.assertEquals("https://user-info%E2%82%AC@localhost:8443", KeycloakUriBuilder.fromUri( + "https://user-info€@localhost:8443", false).buildAsString()); + } } diff --git a/core/pom.xml b/core/pom.xml index c057066ccc49..94fdd884e1c5 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 41432d68e7ae..92d7d132734a 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -42,6 +42,10 @@ public class TokenUtil { public static final String TOKEN_TYPE_DPOP = "DPoP"; + // JWT Access Token types from https://datatracker.ietf.org/doc/html/rfc9068#section-2.1 + public static final String TOKEN_TYPE_JWT_ACCESS_TOKEN = "at+jwt"; + public static final String TOKEN_TYPE_JWT_ACCESS_TOKEN_PREFIXED = "application/" + TOKEN_TYPE_JWT_ACCESS_TOKEN; + public static final String TOKEN_TYPE_KEYCLOAK_ID = "Serialized-ID"; public static final String TOKEN_TYPE_ID = "ID"; diff --git a/crypto/default/pom.xml b/crypto/default/pom.xml index 2b0cf1fb234f..7a7b0dba2923 100644 --- a/crypto/default/pom.xml +++ b/crypto/default/pom.xml @@ -21,7 +21,7 @@ keycloak-crypto-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/crypto/elytron/pom.xml b/crypto/elytron/pom.xml index 51b7d6dbeac7..613b55832cf8 100644 --- a/crypto/elytron/pom.xml +++ b/crypto/elytron/pom.xml @@ -21,7 +21,7 @@ keycloak-crypto-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/crypto/fips1402/pom.xml b/crypto/fips1402/pom.xml index 7691a0f12880..2b58299384b1 100644 --- a/crypto/fips1402/pom.xml +++ b/crypto/fips1402/pom.xml @@ -21,7 +21,7 @@ keycloak-crypto-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/crypto/pom.xml b/crypto/pom.xml index de2f6d339653..a7a160c1ecdd 100644 --- a/crypto/pom.xml +++ b/crypto/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Crypto Parent diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 019146bf55c3..cacafe14aa99 100755 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index c25827c2a373..8be89560b57d 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -21,7 +21,7 @@ keycloak-dependencies-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml index 8749833cd6fd..bef65f64a2a5 100755 --- a/dependencies/server-min/pom.xml +++ b/dependencies/server-min/pom.xml @@ -21,7 +21,7 @@ keycloak-dependencies-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/distribution/adapters/pom.xml b/distribution/adapters/pom.xml index 2ae54a9b8a74..37eed0063c5d 100755 --- a/distribution/adapters/pom.xml +++ b/distribution/adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Adapters Distribution Parent diff --git a/distribution/adapters/tomcat-adapter-zip/pom.xml b/distribution/adapters/tomcat-adapter-zip/pom.xml index c6f4d90b26c6..2e0cef0f1a7b 100755 --- a/distribution/adapters/tomcat-adapter-zip/pom.xml +++ b/distribution/adapters/tomcat-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml diff --git a/distribution/adapters/wildfly-adapter/pom.xml b/distribution/adapters/wildfly-adapter/pom.xml index 96ad4d4de431..257f3a4dc7f6 100644 --- a/distribution/adapters/wildfly-adapter/pom.xml +++ b/distribution/adapters/wildfly-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-adapters-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 diff --git a/distribution/api-docs-dist/pom.xml b/distribution/api-docs-dist/pom.xml index dc67e12f110e..dabeb8e808b4 100755 --- a/distribution/api-docs-dist/pom.xml +++ b/distribution/api-docs-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-api-docs-dist diff --git a/distribution/downloads/pom.xml b/distribution/downloads/pom.xml index 3651b049e9b5..ca76405cc1ea 100755 --- a/distribution/downloads/pom.xml +++ b/distribution/downloads/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-dist-downloads diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml index cd9b1baf68a9..cd09fde7c0c1 100755 --- a/distribution/feature-packs/adapter-feature-pack/pom.xml +++ b/distribution/feature-packs/adapter-feature-pack/pom.xml @@ -19,7 +19,7 @@ org.keycloak feature-packs-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 diff --git a/distribution/feature-packs/pom.xml b/distribution/feature-packs/pom.xml index 4c66eecf261f..36d3faee9820 100644 --- a/distribution/feature-packs/pom.xml +++ b/distribution/feature-packs/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Feature Pack Builds diff --git a/distribution/galleon-feature-packs/pom.xml b/distribution/galleon-feature-packs/pom.xml index fc16e8511026..c40019ab338f 100644 --- a/distribution/galleon-feature-packs/pom.xml +++ b/distribution/galleon-feature-packs/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Galleon Feature Pack Builds diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack-layer-metadata-tests/pom.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack-layer-metadata-tests/pom.xml index d9d706e53e88..1295160e5da4 100644 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack-layer-metadata-tests/pom.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack-layer-metadata-tests/pom.xml @@ -19,7 +19,7 @@ org.keycloak galleon-feature-packs-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml index d46987e74272..f15b538894ce 100644 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml @@ -19,7 +19,7 @@ org.keycloak galleon-feature-packs-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml index 0440e67ff9f1..68538bb94398 100755 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml @@ -38,6 +38,7 @@ + diff --git a/distribution/licenses-common/pom.xml b/distribution/licenses-common/pom.xml index bf615a63ee6f..c5bb46f950e7 100644 --- a/distribution/licenses-common/pom.xml +++ b/distribution/licenses-common/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-distribution-licenses-common diff --git a/distribution/maven-plugins/licenses-processor/pom.xml b/distribution/maven-plugins/licenses-processor/pom.xml index 547d24d96374..1b96541bc449 100644 --- a/distribution/maven-plugins/licenses-processor/pom.xml +++ b/distribution/maven-plugins/licenses-processor/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-maven-plugins-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-distribution-licenses-maven-plugin diff --git a/distribution/maven-plugins/pom.xml b/distribution/maven-plugins/pom.xml index fe9b7db2e36c..9efefd1f6a75 100644 --- a/distribution/maven-plugins/pom.xml +++ b/distribution/maven-plugins/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-distribution-maven-plugins-parent diff --git a/distribution/pom.xml b/distribution/pom.xml index d5ba3d6a1533..94b9d3321a9d 100755 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/distribution/saml-adapters/pom.xml b/distribution/saml-adapters/pom.xml index 14e68e4f3511..1e10a15fa3d2 100755 --- a/distribution/saml-adapters/pom.xml +++ b/distribution/saml-adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 SAML Adapters Distribution Parent diff --git a/distribution/saml-adapters/tomcat-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat-adapter-zip/pom.xml index 3710b43611de..c71e1f63d495 100755 --- a/distribution/saml-adapters/tomcat-adapter-zip/pom.xml +++ b/distribution/saml-adapters/tomcat-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/pom.xml b/distribution/saml-adapters/wildfly-adapter/pom.xml index ab52d865dab5..d1c4f819bb9b 100755 --- a/distribution/saml-adapters/wildfly-adapter/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../pom.xml Keycloak Wildfly SAML Adapter diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/pom.xml index 794389bd1605..c2c8a601ef91 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml index d503885a0fb5..14fd05fda3d1 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml index 634ddcbee246..905c9db3b83b 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml index 68cc6b70703f..729af504edbf 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../../../pom.xml diff --git a/docs/documentation/aggregation/pom.xml b/docs/documentation/aggregation/pom.xml index af92b7cddec8..0be7314368db 100644 --- a/docs/documentation/aggregation/pom.xml +++ b/docs/documentation/aggregation/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/api_documentation/pom.xml b/docs/documentation/api_documentation/pom.xml index 580cefbb335e..f59ec28e7099 100644 --- a/docs/documentation/api_documentation/pom.xml +++ b/docs/documentation/api_documentation/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/authorization_services/pom.xml b/docs/documentation/authorization_services/pom.xml index 6e1042aa8410..14f5329ff4e0 100644 --- a/docs/documentation/authorization_services/pom.xml +++ b/docs/documentation/authorization_services/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/dist/pom.xml b/docs/documentation/dist/pom.xml index 7ae91bdb25a5..207b7964b0b3 100644 --- a/docs/documentation/dist/pom.xml +++ b/docs/documentation/dist/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/header-maven-plugin/pom.xml b/docs/documentation/header-maven-plugin/pom.xml index ff6ef9310a40..2b9ff178da19 100644 --- a/docs/documentation/header-maven-plugin/pom.xml +++ b/docs/documentation/header-maven-plugin/pom.xml @@ -5,12 +5,12 @@ documentation-parent org.keycloak.documentation - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak.documentation header-maven-plugin - 999.0.0-SNAPSHOT + 24.0.5-PS-2 maven-plugin github-maven-plugin diff --git a/docs/documentation/pom.xml b/docs/documentation/pom.xml index d228d63f1adb..22ccefa859d0 100644 --- a/docs/documentation/pom.xml +++ b/docs/documentation/pom.xml @@ -5,14 +5,14 @@ keycloak-docs-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Documentation Parent org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 pom diff --git a/docs/documentation/release_notes/images/new-account-console.png b/docs/documentation/release_notes/images/new-account-console.png deleted file mode 100644 index 5a0beda4063c..000000000000 Binary files a/docs/documentation/release_notes/images/new-account-console.png and /dev/null differ diff --git a/docs/documentation/release_notes/images/new-welcome-screen.png b/docs/documentation/release_notes/images/new-welcome-screen.png deleted file mode 100644 index fffe4563d3a4..000000000000 Binary files a/docs/documentation/release_notes/images/new-welcome-screen.png and /dev/null differ diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 5195032a73dd..32cbb0575ad1 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -13,6 +13,15 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 24.0.5 +include::topics/24_0_5.adoc[leveloffset=2] + +== {project_name_full} 24.0.4 +include::topics/24_0_4.adoc[leveloffset=2] + +== {project_name_full} 24.0.1 +include::topics/24_0_1.adoc[leveloffset=2] + == {project_name_full} 24.0.0 include::topics/24_0_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/pom.xml b/docs/documentation/release_notes/pom.xml index 57d62bebae50..5b7f09380f53 100644 --- a/docs/documentation/release_notes/pom.xml +++ b/docs/documentation/release_notes/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/release_notes/topics/23_0_0.adoc b/docs/documentation/release_notes/topics/23_0_0.adoc index 1de4a9688772..c2d5a1cc8a97 100644 --- a/docs/documentation/release_notes/topics/23_0_0.adoc +++ b/docs/documentation/release_notes/topics/23_0_0.adoc @@ -80,7 +80,7 @@ Deploying {project_name} to multiple independent sites is essential for some env This release adds preview-support for active-passive deployments for {project_name}. A lot of work has gone into testing and verifying a setup which can sustain load and recover from the failure scenarios. -To get started, use the https://www.keycloak.org/guides#high-availability[high-availability guide] which also includes a comprehensive blueprint to deploy a highly available {project_name} to a cloud environment. +To get started, use the link:{highavailabilityguide_link}[{highavailabilityguide_name}] which also includes a comprehensive blueprint to deploy a highly available {project_name} to a cloud environment. = Adapters diff --git a/docs/documentation/release_notes/topics/24_0_0.adoc b/docs/documentation/release_notes/topics/24_0_0.adoc index 23d7f61c7246..6772329a3574 100644 --- a/docs/documentation/release_notes/topics/24_0_0.adoc +++ b/docs/documentation/release_notes/topics/24_0_0.adoc @@ -96,9 +96,6 @@ endif::[] The 'welcome' page that appears at the first use of {project_name} is redesigned. It provides a better setup experience and conforms to the latest version of https://www.patternfly.org/[PatternFly]. The simplified page layout includes only a form to register the first administrative user. After completing the registration, the user is sent directly to the Admin Console. -.New welcome page with a simplified layout and registration form -image::images/new-welcome-screen.png[New welcome page with a simplified layout and registration form] - If you use a custom theme, you may need to update it to support the new welcome page. For details, see the link:{upgradingguide_link}[{upgradingguide_name}]. = New Account Console now the default @@ -107,9 +104,6 @@ We introduced version 3 of the Account Console in {project_name} 22 as a preview This new version has built-in support for the user profile feature, which allows administrators to configure which attributes are available to users in the Account Console, and lands a user directly on their personal account page after logging in. -.New Account Console with custom attributes -image::images/new-account-console.png[New Account Console with custom attributes] - If you are using or extending the customization features of this theme, you may need to perform additional migrations. For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. = Keycloak JS @@ -436,13 +430,14 @@ mappers would never be used. The supported options were updated to only include - `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` - `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` -= Different JVM memory settings when running in container += Different JVM memory settings when running in a container Instead of specifying hardcoded values for the initial and maximum heap size, {project_name} uses relative values to the total memory of a container. -The JVM options `-Xms`, and `-Xmx` were replaced by `-XX:InitialRAMPercentage`, and `-XX:MaxRAMPercentage`. +The JVM options `-Xms` and `-Xmx` were replaced by `-XX:InitialRAMPercentage` and `-XX:MaxRAMPercentage`. -For more details, see the -https://www.keycloak.org/server/containers[Running Keycloak in a container] guide. +WARNING: It can significantly impact memory consumption, so executing particular actions might be required. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. ifeval::[{project_community}==true] = GELF log handler has been deprecated @@ -450,4 +445,11 @@ ifeval::[{project_community}==true] With sunsetting of the https://github.com/mp911de/logstash-gelf[underlying library] providing integration with GELF, Keycloak will no longer support the GELF log handler out-of-the-box. This feature will be removed in a future release. If you require an external log management, consider using file log parsing. -endif::[] \ No newline at end of file +endif::[] + += Support for multi-site active-passive deployments + +Deploying {project_name} to multiple independent sites is essential for some environments to provide high availability and a speedy recovery from failures. +This release supports active-passive deployments for {project_name}. + +To get started, use the link:{highavailabilityguide_link}[{highavailabilityguide_name}] which also includes a comprehensive blueprint to deploy a highly available {project_name} to a cloud environment. diff --git a/docs/documentation/release_notes/topics/24_0_1.adoc b/docs/documentation/release_notes/topics/24_0_1.adoc new file mode 100644 index 000000000000..dd648e7eaad1 --- /dev/null +++ b/docs/documentation/release_notes/topics/24_0_1.adoc @@ -0,0 +1,21 @@ += Operator deploys nightly build instead of 24.0.0 + +Due to an issue in the release process when deploying Keycloak using the Operator it installed the `nightly` container +instead of `24.0.0`. + +As a quick fix to the issue, the `24.0.0` container was tagged with `nightly`, and the `nightly` releases was temporarily +disabled. + +If you installed or upgraded to `24.0.0` using the Operator before 5pm CET yesterday the database may have been updated +with the wrong versions. To check if you are affected connect to your database and run the following SQL command: + +``` +SELECT * from migration_model WHERE version = '999.0.0'; +``` + +If the above returns a matching row you will need to take some actions, otherwise database migrations will not run for +future releases. To resolve this run the following SQL command: + +``` +UPDATE migration_model SET version = '24.0.0' WHERE version = '999.0.0'; +``` \ No newline at end of file diff --git a/docs/documentation/release_notes/topics/24_0_4.adoc b/docs/documentation/release_notes/topics/24_0_4.adoc new file mode 100644 index 000000000000..33b329eec6bc --- /dev/null +++ b/docs/documentation/release_notes/topics/24_0_4.adoc @@ -0,0 +1,6 @@ += Partial update to user attributes when updating users through the Admin User API is no longer supported + +When updating user attributes through the Admin User API, you cannot execute partial updates when updating the +user attributes, including the root attributes like `username`, `email`, `firstName`, and `lastName`. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. \ No newline at end of file diff --git a/docs/documentation/release_notes/topics/24_0_5.adoc b/docs/documentation/release_notes/topics/24_0_5.adoc new file mode 100644 index 000000000000..9af7da2adc84 --- /dev/null +++ b/docs/documentation/release_notes/topics/24_0_5.adoc @@ -0,0 +1,5 @@ += Security issue with PAR clients using client_secret_post based authentication + +This release contains the fix of the important security issue affecting some OIDC confidential clients using PAR (Pushed authorization request). In case you use OIDC confidential clients together +with PAR and you use client authentication based on `client_id` and `client_secret` sent as parameters in the HTTP request body (method `client_secret_post` specified in the OIDC specification), it is +highly encouraged to rotate the client secrets of your clients after upgrading to this version. diff --git a/docs/documentation/securing_apps/pom.xml b/docs/documentation/securing_apps/pom.xml index 6d8dce060ebe..8411f230bc5f 100644 --- a/docs/documentation/securing_apps/pom.xml +++ b/docs/documentation/securing_apps/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/securing_apps/topics/client-registration/client-registration-cli.adoc b/docs/documentation/securing_apps/topics/client-registration/client-registration-cli.adoc index 5b782ebf39a9..8dadc36db220 100644 --- a/docs/documentation/securing_apps/topics/client-registration/client-registration-cli.adoc +++ b/docs/documentation/securing_apps/topics/client-registration/client-registration-cli.adoc @@ -7,7 +7,7 @@ It is necessary to create or obtain a client configuration for any application t You can configure application clients from a command line with the Client Registration CLI, and you can use it in shell scripts. -To allow a particular user to use `Client Registration CLI` the {project_name} administrator typically uses the Admin Console to configure a new user with proper roles or to configure a new client and client secret to grant access to the Client Registration REST API. +To allow a particular user to use `Client Registration CLI`, the {project_name} administrator typically uses the Admin Console to configure a new user with proper roles or to configure a new client and client secret to grant access to the Client Registration REST API. [[_configuring_a_user_for_client_registration_cli]] @@ -18,8 +18,13 @@ To allow a particular user to use `Client Registration CLI` the {project_name} a . Log in to the Admin Console (for example, http://localhost:8080{kc_admins_path}) as [command]`admin`. . Select a realm to administer. . If you want to use an existing user, select that user to edit; otherwise, create a new user. -. Select *Role Mappings > Client Roles > realm-management*. If you are in the master realm, select *NAME-realm*, where `NAME` is the name of the target realm. You can grant access to any other realm to users in the master realm. -. Select *Available Roles > manage-client* to grant a full set of client management permissions. Another option is to choose *view-clients* for read-only or *create-client* to create new clients. + +. Select *Role Mapping*, *Assign role*. From the option list, click *Filter by clients*. In the search bar, type `manage-clients`. Select the role, or if you are in the master realm, select the one with *NAME-realm*, where `NAME` is the name of the target realm. You can grant access to any other realm to users in the master realm. + +. Click *Assign* to grant a full set of client management permissions. Another option is to choose *view-clients* for read-only or *create-client* to create new clients. + +. Select *Available Roles*, *manage-client* to grant a full set of client management permissions. Another option is to choose *view-clients* for read-only or *create-client* to create new clients. + + [NOTE] ==== @@ -28,8 +33,7 @@ These permissions grant the user the capability to perform operations without th It is possible to not assign any [command]`realm-management` roles to a user. In that case, a user can still log in with the Client Registration CLI but cannot use it without an Initial Access Token. Trying to perform any operations without a token results in a *403 Forbidden* error. -The Administrator can issue Initial Access Tokens from the Admin Console through the *Realm Settings > Client Registration > Initial Access Token* menu. - +The administrator can issue Initial Access Tokens from the Admin Console in the Clients area on the *Initial Access Token* tab. [[_configuring_a_client_for_use_with_client_registration_cli]] === Configuring a client for use with the Client Registration CLI @@ -39,21 +43,25 @@ By default, the server recognizes the Client Registration CLI as the [filename]` .Procedure . Create a client (for example, [filename]`reg-cli`) if you want to use a separate client configuration for the Client Registration CLI. -. Toggle the *Standard Flow Enabled* setting it to *Off*. -. Strengthen the security by configuring the client [filename]`Access Type` as [filename]`Confidential` and selecting *Credentials > ClientId and Secret*. +. Uncheck *Standard Flow Enabled*. +. Strengthen the security by toggling *Client authentication* to *On*. +. Choose the type of account that you want to use. +.. If you want to use a service account associated with the client, check *Service accounts roles*. +.. If you prefer to use a regular user account, check *Direct access grants*. +. Click *Next*. +. Click *Save*. +. Click the *Credentials* tab. + -[NOTE] -==== -You can configure either [filename]`Client Id and Secret` or [filename]`Signed JWT` under the *Credentials* tab . -==== -. Enable service accounts if you want to use a service account associated with the client by selecting a client to edit in the *Clients* section of the `Admin Console`. -.. Under *Settings*, change the *Access Type* to *Confidential*, toggle the *Service Accounts Enabled* setting to *On*, and click *Save*. -.. Click *Service Account Roles* and select desired roles to configure the access for the service account. For the details on what roles to select, see <<_configuring_a_user_for_client_registration_cli>>. -. Toggle the *Direct Access Grants Enabled* setting it to *On* if you want to use a regular user account instead of a service account. -. If the client is configured as [filename]`Confidential`, provide the configured secret when running [command]`kcreg config credentials` by using the [command]`--secret` option. -. Specify which [filename]`clientId` to use (for example, [command]`--client reg-cli`) when running [command]`kcreg config credentials`. -. With the service account enabled, you can omit specifying the user when running [command]`kcreg config credentials` and only provide the client secret or keystore information. +Configure either [filename]`Client Id and Secret` or [filename]`Signed JWT`. +. If you are using service account roles, click the *Service Account Roles* tab. ++ +Select the roles to configure the access for the service account. For the details on what roles to select, see <<_configuring_a_user_for_client_registration_cli>>. +. Click *Save*. + +When you run the [command]`kcreg config credentials`, use the [command]`--secret` option to supply the configured secret. +* Specify which [filename]`clientId` to use (for example, [command]`--client reg-cli`) when running [command]`kcreg config credentials`. +* With the service account enabled, you can omit specifying the user when running [command]`kcreg config credentials` and only provide the client secret or keystore information. [[_installing_client_registration_cli]] === Installing the Client Registration CLI diff --git a/docs/documentation/securing_apps/topics/oidc/fapi-support.adoc b/docs/documentation/securing_apps/topics/oidc/fapi-support.adoc index 7ee4b0614100..b261afde25c1 100644 --- a/docs/documentation/securing_apps/topics/oidc/fapi-support.adoc +++ b/docs/documentation/securing_apps/topics/oidc/fapi-support.adoc @@ -29,7 +29,7 @@ When enforcing the requirements of the FAPI CIBA specification, there is a need ==== Open Finance Brasil Financial-grade API Security Profile -{project_name} is compliant with the https://openfinancebrasil.atlassian.net/wiki/spaces/OF/pages/82083996/EN+Open+Finance+Brasil+Financial-grade+API+Security+Profile+1.0+Implementers+Draft+3[Open Finance Brasil Financial-grade API Security Profile 1.0 Implementers Draft 3]. +{project_name} is compliant with the https://openfinancebrasil.atlassian.net/wiki/spaces/OF/pages/245760001/EN+Open+Finance+Brasil+Financial-grade+API+Security+Profile+1.0+Implementers+Draft+3[Open Finance Brasil Financial-grade API Security Profile 1.0 Implementers Draft 3]. This one is stricter in some requirements than the <<_fapi-support,FAPI 1 Advanced>> specification and hence it may be needed to configure link:{adminguide_link}#_client_policies[Client Policies] in the more strict way to enforce some of the requirements. Especially: diff --git a/docs/documentation/securing_apps/topics/oidc/java/java-adapters-product.adoc b/docs/documentation/securing_apps/topics/oidc/java/java-adapters-product.adoc index 51f69183953d..bce81081edfe 100644 --- a/docs/documentation/securing_apps/topics/oidc/java/java-adapters-product.adoc +++ b/docs/documentation/securing_apps/topics/oidc/java/java-adapters-product.adoc @@ -30,6 +30,6 @@ Spring Security provides comprehensive support for OAuth 2 and OpenID Connect. F https://spring.io/projects/spring-security[Spring Security documentation]. Alternatively, for Spring Boot 2.x the Spring Boot adapter from Red Hat Single Sign-On 7.6 can be used in combination with the {project_name} server. For more information, see the -https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.6/html/securing_applications_and_services_guide/oidc#jboss_adapter[Red Hat Single Sign-On documentation]. +https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.6/html/securing_applications_and_services_guide/oidc#spring_boot_adapter[Red Hat Single Sign-On documentation]. diff --git a/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc index 68c46ceee1db..a7dbee363351 100644 --- a/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc +++ b/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc @@ -185,8 +185,6 @@ While this mode is easy to set up, it also has some disadvantages: * The InApp-Browser might also be slower, especially when rendering more complex themes. * There are security concerns to consider, before using this mode, such as that it is possible for the app to gain access to the credentials of the user, as it has full control of the browser rendering the login page, so do not allow its use in apps you do not trust. -Use this example app to help you get started: https://github.com/keycloak/keycloak/tree/master/examples/cordova - The alternative mode is`cordova-native`, which takes a different approach. It opens the login page using the system's browser. After the user has authenticated, the browser redirects back into the application using a special URL. From there, the {project_name} adapter can finish the login by reading the code or token from the URL. You can activate the native mode by passing the adapter type `cordova-native` to the `init()` method: @@ -221,8 +219,6 @@ Furthermore, we recommend the following steps to improve compatibility with the ---- -There is an example app that shows how to use the native-mode: https://github.com/keycloak/keycloak/tree/master/examples/cordova-native - [#custom-adapters] ==== Custom Adapters @@ -390,8 +386,11 @@ Options is an optional Object, where: * redirectUri - Specifies the uri to redirect to after login. * prompt - This parameter allows to slightly customize the login flow on the {project_name} server side. -For example enforce displaying the login screen in case of value `login`. See link:{adapterguide_link}#_params_forwarding[Parameters Forwarding Section] +For example, enforce displaying the login screen in case of value `login`. +ifeval::[{project_community}==true] +See the link:{adapterguide_link}#_params_forwarding[Parameters Forwarding Section] for the details and all the possible values of the `prompt` parameter. +endif::[] * maxAge - Used just if user is already authenticated. Specifies maximum time since the authentication of user happened. If user is already authenticated for longer time than `maxAge`, the SSO is ignored and he will need to re-authenticate again. * loginHint - Used to pre-fill the username/email field on the login form. * scope - Override the scope configured in `init` with a different value for this specific login. diff --git a/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc index 90bad646f10c..36ca659f8822 100644 --- a/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc +++ b/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc @@ -10,11 +10,11 @@ endif::[] To use the Node.js adapter, first you must create a client for your application in the {project_name} Admin Console. The adapter supports public, confidential, and bearer-only access type. Which one to choose depends on the use-case scenario. -Once the client is created click the `Installation` tab, select `{project_name} OIDC JSON` for `Format Option`, and then click `Download`. The downloaded `keycloak.json` file should be at the root folder of your project. +Once the client is created, click *Action* at the top right and choose *Download adapter config*. For *Format, choose *Keycloak OIDC JSON* and click *Download*. The downloaded `keycloak.json` file is at the root folder of your project. ==== Installation -Assuming you've already installed https://nodejs.org[Node.js], create a folder for your application: +Assuming you have already installed https://nodejs.org[Node.js], create a folder for your application: mkdir myapp && cd myapp diff --git a/docs/documentation/server_admin/images/add-realm-menu.png b/docs/documentation/server_admin/images/add-realm-menu.png index 4462459bbd56..cf8a82cd7354 100644 Binary files a/docs/documentation/server_admin/images/add-realm-menu.png and b/docs/documentation/server_admin/images/add-realm-menu.png differ diff --git a/docs/documentation/server_admin/images/initial-welcome-page.png b/docs/documentation/server_admin/images/initial-welcome-page.png old mode 100755 new mode 100644 index cfd0e6c58fc9..4674c5224305 Binary files a/docs/documentation/server_admin/images/initial-welcome-page.png and b/docs/documentation/server_admin/images/initial-welcome-page.png differ diff --git a/docs/documentation/server_admin/pom.xml b/docs/documentation/server_admin/pom.xml index cb0c3fdffef2..1268a213237f 100644 --- a/docs/documentation/server_admin/pom.xml +++ b/docs/documentation/server_admin/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/server_admin/topics/authentication/flows.adoc b/docs/documentation/server_admin/topics/authentication/flows.adoc index bfb611d5c3db..faf5396adbea 100644 --- a/docs/documentation/server_admin/topics/authentication/flows.adoc +++ b/docs/documentation/server_admin/topics/authentication/flows.adoc @@ -360,7 +360,7 @@ For more details see the https://openid.net/specs/openid-connect-core-1_0.html#a The logic for the previous configured authentication flow is as follows: + If a client request a high authentication level, meaning Level of Authentication 2 (LoA 2), a user has to perform full 2-factor authentication: Username/Password + OTP. -However, if a user already has a session in Keycloak, that was logged in with username and password (LoA 1), the user is only asked for the second authentication factor (OTP). +However, if a user already has a session in {project_name}, that was logged in with username and password (LoA 1), the user is only asked for the second authentication factor (OTP). The option *Max Age* in the condition determines how long (how much seconds) the subsequent authentication level is valid. This setting helps to decide whether the user will be asked to present the authentication factor again during a subsequent authentication. If the particular level X is requested diff --git a/docs/documentation/server_admin/topics/authentication/kerberos.adoc b/docs/documentation/server_admin/topics/authentication/kerberos.adoc index 0f01e71a504c..a8e558289e84 100644 --- a/docs/documentation/server_admin/topics/authentication/kerberos.adoc +++ b/docs/documentation/server_admin/topics/authentication/kerberos.adoc @@ -18,7 +18,7 @@ A typical use case for web authentication is the following: [WARNING] ==== -The https://www.ietf.org/rfc/rfc4559.txt[Negotiate] www-authenticate scheme allows NTLM as a fallback to Kerberos and on some web browsers in Windows NTLM is supported by default. If a www-authenticate challenge comes from a server outside a browsers permitted list, users may encounter an NTLM dialog prompt. A user would need to click the cancel button on the dialog to continue as Keycloak does not support this mechanism. This situation can happen if Intranet web browsers are not strictly configured or if Keycloak serves users in both the Intranet and Internet. A https://github.com/keycloak/keycloak/issues/8989[custom authenticator] can be used to restrict Negotiate challenges to a whitelist of hosts. +The https://www.ietf.org/rfc/rfc4559.txt[Negotiate] www-authenticate scheme allows NTLM as a fallback to Kerberos and on some web browsers in Windows NTLM is supported by default. If a www-authenticate challenge comes from a server outside a browsers permitted list, users may encounter an NTLM dialog prompt. A user would need to click the cancel button on the dialog to continue as {project_name} does not support this mechanism. This situation can happen if Intranet web browsers are not strictly configured or if {project_name} serves users in both the Intranet and Internet. A https://github.com/keycloak/keycloak/issues/8989[custom authenticator] can be used to restrict Negotiate challenges to a whitelist of hosts. ==== Perform the following steps to set up Kerberos authentication: diff --git a/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc b/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc index 6732d45c602b..4a35b8c290f6 100644 --- a/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc +++ b/docs/documentation/server_admin/topics/clients/oidc/con-basic-settings.adoc @@ -24,9 +24,13 @@ the name, set up a replacement string value. For example, a string value such as *Home URL*:: Provides the default URL for when the auth server needs to redirect or link back to the client. -*Valid Redirect URIs*:: Required field. Enter a URL pattern and click *+* to add and *-* to remove existing URLs and click *Save*. You can use wildcards at the end of the URL pattern. For example $$http://host.com/*$$ +*Valid Redirect URIs*:: Required field. Enter a URL pattern and click *+* to add and *-* to remove existing URLs and click *Save*. Exact (case sensitive) string matching is used to compare valid redirect URIs. + -Exclusive redirect URL patterns are typically more secure. See xref:unspecific-redirect-uris_{context}[Unspecific Redirect URIs] for more information. +You can use wildcards at the end of the URL pattern. For example `$$http://host.com/path/*$$`. To avoid security issues, if the passed redirect URI contains the *userinfo* part or its *path* manages access to parent directory (`/../`) no wildcard comparison is performed but the standard and secure exact string matching. ++ +The full wildcard `$$*$$` valid redirect URI can also be configured to allow any *http* or *https* redirect URI. Please do not use it in production environments. ++ +Exclusive redirect URI patterns are typically more secure. See xref:unspecific-redirect-uris_{context}[Unspecific Redirect URIs] for more information. Web Origins:: Enter a URL pattern and click + to add and - to remove existing URLs. Click Save. + diff --git a/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc b/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc index fbce9b35832b..4dcd07eed54f 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc @@ -38,8 +38,8 @@ grantMethod: prompt <4> ==== OpenShift 4 .Prerequisites -. A certificate of the OpenShift 4 instance stored in the Keycloak Truststore. -. A Keycloak server configured in order to use the truststore. For more information, see the https://www.keycloak.org/server/keycloak-truststore[Configuring a Truststore] {section}. +. A certificate of the OpenShift 4 instance stored in the {project_name} Truststore. +. A {project_name} server configured in order to use the truststore. For more information, see the https://www.keycloak.org/server/keycloak-truststore[Configuring a Truststore] {section}. .Procedure . Click *Identity Providers* in the menu. diff --git a/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc b/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc index 4f630ec1a63e..c41bc808b783 100644 --- a/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc +++ b/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc @@ -18,7 +18,7 @@ A `Forgot Password?` link displays in your login pages. .Forgot password link image:images/forgot-password-link.png[Forgot Password Link] + -. Specify `Host` and `From` in the *Email* tab in order for Keycloak to be able to send the reset email. +. Specify `Host` and `From` in the *Email* tab in order for {Project_Name} to be able to send the reset email. + . Click this link to bring users where they can enter their username or email address and receive an email with a link to reset their credentials. + diff --git a/docs/documentation/server_admin/topics/realms/proc-creating-a-realm.adoc b/docs/documentation/server_admin/topics/realms/proc-creating-a-realm.adoc index cf66e7cbbe2f..633e4a2c59d6 100644 --- a/docs/documentation/server_admin/topics/realms/proc-creating-a-realm.adoc +++ b/docs/documentation/server_admin/topics/realms/proc-creating-a-realm.adoc @@ -11,9 +11,7 @@ realm and only be able to interact with customer-facing apps. .Procedure -. Point to the top of the left pane. - -. Click *Create Realm*. +. Click *{project_name}* next to *master realm*, then click *Create Realm*. + .Add realm menu image:images/add-realm-menu.png[Add realm menu] diff --git a/docs/documentation/server_admin/topics/sso-protocols/con-oidc-auth-flows.adoc b/docs/documentation/server_admin/topics/sso-protocols/con-oidc-auth-flows.adoc index ed0a689a9b9a..7462c5902dae 100644 --- a/docs/documentation/server_admin/topics/sso-protocols/con-oidc-auth-flows.adoc +++ b/docs/documentation/server_admin/topics/sso-protocols/con-oidc-auth-flows.adoc @@ -98,7 +98,7 @@ profile and configure client policy to specify for which clients would be the pr This is used by clients running on internet-connected devices that have limited input capabilities or lack a suitable browser. Here's a brief summary of the protocol: . The application requests {project_name} a device code and a user code. {project_name} creates a device code and a user code. {project_name} returns a response including the device code and the user code to the application. -. The application provides the user with the user code and the verification URI. The user accesses a verification URI to be authenticated by using another browser. You could define a short verification_uri that will be redirected to Keycloak verification URI (/realms/realm_name/device)outside Keycloak - fe in a proxy. +. The application provides the user with the user code and the verification URI. The user accesses a verification URI to be authenticated by using another browser. You could define a short verification_uri that will be redirected to {project_name} verification URI (/realms/realm_name/device)outside {project_name} - fe in a proxy. . The application repeatedly polls {project_name} to find out if the user completed the user authorization. If user authentication is complete, the application exchanges the device code for an _identity_, _access_ and _refresh_ token. [[_client_initiated_backchannel_authentication_grant]] diff --git a/docs/documentation/server_admin/topics/user-federation/ldap.adoc b/docs/documentation/server_admin/topics/user-federation/ldap.adoc index 921fe1210789..4dfa9cdea43d 100644 --- a/docs/documentation/server_admin/topics/user-federation/ldap.adoc +++ b/docs/documentation/server_admin/topics/user-federation/ldap.adoc @@ -176,7 +176,7 @@ Note those messages are displayed just with the enabled DEBUG logging. the LDAP provider to value `all`. This will add lots of additional messages to server log with the included logging for the LDAP connection pooling. This can be used to track the issues related to connection pooling or performance. -NOTE: After changing the configuration of connection pooling, you may need to restart the Keycloak server to enforce re-initialization +NOTE: After changing the configuration of connection pooling, you may need to restart the {project_name} server to enforce re-initialization of the LDAP provider connection. If no more messages appear for connection pooling even after server restart, it can indicate that connection pooling does not work @@ -185,4 +185,4 @@ with your LDAP server. - For the case of reporting LDAP issue, you may consider to attach some part of your LDAP tree with the target data, which causes issues in your environment. For example if login of some user takes lot of time, you can consider attach his LDAP entry showing count of `member` attributes of various "group" entries. In this case, it might be useful to add if those group entries are mapped to some Group LDAP mapper (or Role LDAP Mapper) -in {project_name} etc. +in {project_name} and so on. diff --git a/docs/documentation/server_admin/topics/users/user-profile.adoc b/docs/documentation/server_admin/topics/users/user-profile.adoc index e37ed0aed013..5a5f2f09a4ef 100644 --- a/docs/documentation/server_admin/topics/users/user-profile.adoc +++ b/docs/documentation/server_admin/topics/users/user-profile.adoc @@ -120,7 +120,7 @@ To specify a different minimum or maximum length, change the unmanaged attribute WARNING: {project_name} caches user-related objects in its internal caches. The longer the attributes are, the more memory the cache consumes. Therefore, limiting the size of the length attributes is recommended. -Consider storing large objects outside Keycloak and reference them by ID or URL. +Consider storing large objects outside {project_Name} and reference them by ID or URL. == Managing the User Profile diff --git a/docs/documentation/server_admin/topics/vault.adoc b/docs/documentation/server_admin/topics/vault.adoc index 6206d6f2081d..4dabd3f65458 100644 --- a/docs/documentation/server_admin/topics/vault.adoc +++ b/docs/documentation/server_admin/topics/vault.adoc @@ -3,7 +3,7 @@ == Using a vault to obtain secrets -Keycloak currently provides two out-of-the-box implementations of the Vault SPI: a plain-text file-based vault and Java KeyStore-based vault. +{project_name} currently provides two out-of-the-box implementations of the Vault SPI: a plain-text file-based vault and Java KeyStore-based vault. To obtain a secret from a vault rather than entering it directly, enter the following specially crafted string into the appropriate field: diff --git a/docs/documentation/server_development/pom.xml b/docs/documentation/server_development/pom.xml index 6e0ec2e820c6..0f6b46eb3f40 100644 --- a/docs/documentation/server_development/pom.xml +++ b/docs/documentation/server_development/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/server_development/topics/extensions.adoc b/docs/documentation/server_development/topics/extensions.adoc index d636daf63427..d53525b98581 100644 --- a/docs/documentation/server_development/topics/extensions.adoc +++ b/docs/documentation/server_development/topics/extensions.adoc @@ -34,6 +34,8 @@ or <<_extensions_jpa,Extending the datamodel with custom JPA entities>>. For details on how to package and deploy a custom provider, refer to the <<_providers,Service Provider Interfaces>> chapter. +NOTE: While it is possible to install other JAX-RS components via the providers extension mechanism, such as filters and interceptors, these are not officially supported. + [[_extensions_spi]] === Add your own custom SPI @@ -144,7 +146,7 @@ EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityMan Company myCompany = em.find(Company.class, "123"); ---- -The methods `getChangelogLocation` and `getFactoryId` are important to support automatic updating of your entities by Liquibase. https://www.liquibase.org/[Liquibase] +The methods `getChangelogLocation` and `getFactoryId` are important to support automatic updating of your entities by Liquibase. https://www.liquibase.com/community/contributors[Liquibase] is a framework for updating the database schema, which {project_name} internally uses to create the DB schema and update the DB schema among versions. You may need to use it as well and create a changelog for your entities. Note that versioning of your own Liquibase changelog is independent of {project_name} versions. In other words, when you update to a new {project_name} version, you are not forced to update your diff --git a/docs/documentation/server_development/topics/providers.adoc b/docs/documentation/server_development/topics/providers.adoc index 103657446057..24f414b6b52f 100644 --- a/docs/documentation/server_development/topics/providers.adoc +++ b/docs/documentation/server_development/topics/providers.adoc @@ -220,21 +220,33 @@ Providers are registered with the server by simply copying the JAR file to the ` If your provider needs additional dependencies not already provided by Keycloak copy these to the `providers` directory. +After registering new providers or dependencies Keycloak needs to be re-built with a non-optimized start or the `kc.[sh|bat] build` command. + [NOTE] ==== Provider JARs are not loaded in isolated classloaders, so do not include resources or classes in your provider JARs that conflict with built-in resources or classes. -In particular the inclusion of an application.properties file will cause auto-build to fail if the provider JAR is removed. -If you have included conflicting classes, you will see a split package warning in the start log for the server. That should be resolved by removing or repackaging the offending classes. -However there is no warning if you have conflicting resource files. You should either ensure that your JAR's resource files have path names that contain something unique to that provider, +In particular the inclusion of an application.properties file or overriding the commons-lang3 dependency will cause auto-build to fail if the provider JAR is removed. +If you have included conflicting classes, you may see a split package warning in the start log for the server. Unfortunately not all built-in lib jars are checked by the split package warning logic, +so you'll need to check the lib directory JARs before bundling or including a transitive dependency. Should there be a conflict, that can be resolved by removing or repackaging the offending classes. + +There is no warning if you have conflicting resource files. You should either ensure that your JAR's resource files have path names that contain something unique to that provider, or you can check for the existence of `some.file` in the JAR contents under the `"install root"/lib/lib/main` directory with something like: [source,bash] ---- find . -type f -name "*.jar" -exec unzip -l {} \; | grep some.file ---- + +If you find that your server will not start due to a `NoSuchFileException` error related to a removed provider JAR, then run: + +[source,bash] +---- +./kc.sh -Dquarkus.launch.rebuild=true +---- + +This will force Quarkus to rebuild the classloading related index files. From there you should be able to perform a non-optimized start or build without an exception. ==== -After registering new providers or dependencies Keycloak needs to be re-built with a non-optimized start or the `kc.[sh|bat] build` command. ==== Disabling a provider diff --git a/docs/documentation/tests/pom.xml b/docs/documentation/tests/pom.xml index 3c2559aef269..105908a12b5c 100644 --- a/docs/documentation/tests/pom.xml +++ b/docs/documentation/tests/pom.xml @@ -60,7 +60,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/tests/src/test/resources/ignored-links b/docs/documentation/tests/src/test/resources/ignored-links index b449f0e9efe8..a0d459af1db4 100644 --- a/docs/documentation/tests/src/test/resources/ignored-links +++ b/docs/documentation/tests/src/test/resources/ignored-links @@ -13,7 +13,7 @@ http://host:port* https://host:port* http://broker-keycloak:8180* https://expressjs.com/ -https://github.com/keycloak/keycloak/tree/* +https://github.com/* http://node11:8080* http://node11:8080/auth/ http://node12:8080* @@ -21,10 +21,7 @@ http://node21:8080* http://node22:8080* http://web.example.com* https://keycloak.example.com* -https://github.com/keycloak/keycloak-documentation/blob/master/* https://openshift.example.com:8443/console -https://github.com/keycloak/keycloak-quickstarts.git -https://github.com/go-chi/chi#router-design https://accounts.google.com/o/oauth2/revoke https://keycloak.example.com/auth/realms/REALM_NAME/protocol/openid-connect/logout http://127.0.0.1:3000/oauth/callback @@ -38,9 +35,4 @@ https://developer.paypal.com/developer/applications https://account.live.com/developers/applications/create https://developer.twitter.com/apps/ https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#rolling-update -https://github.com/keycloak/keycloak/blob/main/docs/tests.md#kerberos-server -https://github.com/apache/felix-dev/tree/master/http#using-the-osgi-http-whiteboard -https://github.com/keycloak/keycloak/blob/025778fe9c745316f80b53fe3052aeb314e868ef/js/apps/admin-ui/public/locales/en/dashboard.json#L3 -https://github.com/keycloak/keycloak/issues/new?* -https://stackapps.com/apps/oauth/register -https://github.com/keycloak/keycloak-quickstarts/tree/release/24.0/extension/extend-account-console +https://stackapps.com/apps/oauth/register \ No newline at end of file diff --git a/docs/documentation/topics/templates/document-attributes.adoc b/docs/documentation/topics/templates/document-attributes.adoc index 8fa0d6842aa8..4cdc1c76fe81 100644 --- a/docs/documentation/topics/templates/document-attributes.adoc +++ b/docs/documentation/topics/templates/document-attributes.adoc @@ -2,10 +2,13 @@ :project_name_full: Keycloak :project_community: true :project_product: false -:project_version: DEV -:project_versionMvn: 999.0.0-SNAPSHOT -:project_versionNpm: 999.0.0-SNAPSHOT -:project_versionDoc: DEV +:project_version: 24.0.5-PS-2 +:project_versionMvn: 24.0.5-PS-2 +:project_versionNpm: 24.0.5-PS-2 +:project_versionDoc: 24.0.5-PS-2 + +:archivebasename: keycloak +:archivedownloadurl: https://github.com/keycloak/keycloak/releases/download/{project_version}/keycloak-{project_version}.zip :standalone: :api-management!: @@ -71,6 +74,8 @@ :gettingstarted_name_short: Getting Started :gettingstarted_link: https://www.keycloak.org/guides#getting-started :gettingstarted_link_latest: https://www.keycloak.org/guides#getting-started +:highavailabilityguide_name: High Availability Guide +:highavailabilityguide_link: https://www.keycloak.org/guides#high-availability :upgradingguide_name: Upgrading Guide :upgradingguide_name_short: Upgrading :upgradingguide_link: {project_doc_base_url}/upgrading/ diff --git a/docs/documentation/upgrading/pom.xml b/docs/documentation/upgrading/pom.xml index 9c081109a538..71184aa0807d 100644 --- a/docs/documentation/upgrading/pom.xml +++ b/docs/documentation/upgrading/pom.xml @@ -5,7 +5,7 @@ org.keycloak.documentation documentation-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/documentation/upgrading/topics.adoc b/docs/documentation/upgrading/topics.adoc index b49287785c20..07d57a0e4f70 100644 --- a/docs/documentation/upgrading/topics.adoc +++ b/docs/documentation/upgrading/topics.adoc @@ -1,3 +1,3 @@ -include::topics/keycloak/intro.adoc[leveloffset=0] -include::topics/keycloak/upgrading.adoc[leveloffset=0] -include::topics/keycloak/changes.adoc[leveloffset=0] \ No newline at end of file +include::topics/intro.adoc[leveloffset=0] +include::topics/changes/changes.adoc[leveloffset=0] +include::topics/upgrading.adoc[leveloffset=0] diff --git a/docs/documentation/upgrading/topics/keycloak/changes-16_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-16_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-16_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-16_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-17_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-17_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-17_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-17_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-18_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-18_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-18_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-18_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-19_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-19_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-19_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-19_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-19_0_2.adoc b/docs/documentation/upgrading/topics/changes/changes-19_0_2.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-19_0_2.adoc rename to docs/documentation/upgrading/topics/changes/changes-19_0_2.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-20_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-20_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-20_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-20_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-21_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-21_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-21_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-21_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-21_0_2.adoc b/docs/documentation/upgrading/topics/changes/changes-21_0_2.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-21_0_2.adoc rename to docs/documentation/upgrading/topics/changes/changes-21_0_2.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-21_1_0.adoc b/docs/documentation/upgrading/topics/changes/changes-21_1_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-21_1_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-21_1_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-22_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-22_0_0.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-22_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-22_0_0.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-22_0_2.adoc b/docs/documentation/upgrading/topics/changes/changes-22_0_2.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-22_0_2.adoc rename to docs/documentation/upgrading/topics/changes/changes-22_0_2.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-22_0_4.adoc b/docs/documentation/upgrading/topics/changes/changes-22_0_4.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-22_0_4.adoc rename to docs/documentation/upgrading/topics/changes/changes-22_0_4.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-23_0_0.adoc similarity index 90% rename from docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-23_0_0.adoc index 3981daa42054..8868d84f7d57 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-23_0_0.adoc @@ -8,24 +8,23 @@ However, some OpenID Connect / OAuth2 adapters, and especially older {project_na For example, the parameter will be always present in the browser URL after successful authentication to the client application. In these cases, it may be useful to disable adding the `iss` parameter to the authentication response. This can be done -for the particular client in the {project_name} Admin console, in client details in the section with `OpenID Connect Compatibility Modes`, +for the particular client in the {project_name} Admin Console, in client details in the section with `OpenID Connect Compatibility Modes`, described in <<_compatibility_with_older_adapters>>. Dedicated `Exclude Issuer From Authentication Response` switch exists, which can be turned on to prevent adding the `iss` parameter to the authentication response. = Wildcard characters handling -JPA allows wildcards `%` and `_` when searching, while other providers like LDAP allow only `*`. -As `*` is a natural wildcard character in LDAP, it works in all places, while with JPA it only +JPA allows wildcards `%` and `+_+` when searching, while other providers like LDAP allow only `+*+`. +As `+*+` is a natural wildcard character in LDAP, it works in all places, while with JPA it only worked at the beginning and the end of the search string. Starting with this release the only -wildcard character is `*` which work consistently across all providers in all places in the search -string. All special characters in a specific provider like `%` and `_` for JPA are escaped. For exact -search, with added quotes e.g. `"w*ord"`, the behavior remains the same as in previous releases. +wildcard character is `+*+` which work consistently across all providers in all places in the search +string. All special characters in a specific provider like `%` and `+_+` for JPA are escaped. For exact +search, with added quotes such as `+"w*ord"+`, the behavior remains the same as in previous releases. = Language files for themes default to UTF-8 encoding - This release now follows the standard mechanisms of Java and later, which assumes resource bundle files to be encoded in UTF-8. -Previous versions of Keycloak supported specifying the encoding in the first line with a comment like `# encoding: UTF-8`, which is no longer supported and is ignored. +Previous versions of {project_name} supported specifying the encoding in the first line with a comment like `# encoding: UTF-8`, which is no longer supported and is ignored. Message properties files for themes are now read in UTF-8 encoding, with an automatic fallback to ISO-8859-1 encoding. If you are using a different encoding, convert the files to UTF-8. @@ -155,7 +154,7 @@ Stream getTopLevelGroupsStream(RealmModel realm, * new field `subGroupCount` added to inform client how many subgroups are on any given group * `subGroups` list is now only populated on queries that request hierarchy data - * This field is populated from the "bottom up" so can't be relied on for getting all subgroups for a group. Use a `GroupProvider` or request the subgroups from `GET {keycloak server}/realms/{realm}/groups/{group_id}/children` + * This field is populated from the "bottom up" so cannot be relied on for getting all subgroups for a group. Use a `GroupProvider` or request the subgroups from `GET {keycloak server}/realms/{realm}/groups/{group_id}/children` = New endpoint for Group Admin API @@ -170,7 +169,7 @@ The endpoint `POST {keycloak server}/realms/{realm}/partial-export` and the corr = Removal of the options to trim the event's details length -Since this release, Keycloak supports long value for `EventEntity` details column. Therefore, it no longer supports options for trimming event detail length `--spi-events-store-jpa-max-detail-length` and `--spi-events-store-jpa-max-field-length`. +Since this release, {project_name} supports long value for `EventEntity` details column. Therefore, it no longer supports options for trimming event detail length `--spi-events-store-jpa-max-detail-length` and `--spi-events-store-jpa-max-field-length`. = User Profile updates @@ -178,8 +177,9 @@ This release includes many fixes and updates that are related to user profile as Minor changes exist for the SPI such as the newly added method `boolean isEnabled(RealmModel realm)` on `UserProfileProvider` interface. Also some user profile classes and some validator related classes (but not builtin validator implementations) were moved from `keycloak-server-spi-private` to `keycloak-server-spi` module. However, the packages for java classes remain the same. You might be affected in some corner cases, such as when you -are overriding the built-in implementation with your own `UserProfileProvider` implementation However, note that `UserProfileProvider` is an unsupported SPI. +are overriding the built-in implementation with your own `UserProfileProvider` implementation. However, note that `UserProfileProvider` is an unsupported SPI. +ifeval::[{project_community}==true] = Removal of the Map Store The Map Store has been an experimental feature in previous releases. @@ -187,10 +187,11 @@ Starting with this release, it is removed and users should continue to use the c Since this release, it is no longer possible to use `--storage` related CLI options. The modules `keycloak-model-map*` have been removed. +endif::[] = Removed namespaces from our translations -We moved all translations into one file for the admin-ui, if you have made your own translations or extended the admin ui you will need to migrate them to this new format. -Also if you have "overrides" in your database you'll have to remove the namespace from the keys. +All translations are moved into one file for the Admin Console. If you have made your own translations or extended the Admin Console you will need to migrate them to this new format. +Also if you have "overrides" in your database, you will have to remove the namespace from the keys. Some keys are the same only in different namespaces, this is most obvious to help. In these cases we have postfix the key with `Help`. diff --git a/docs/documentation/upgrading/topics/keycloak/changes-23_0_2.adoc b/docs/documentation/upgrading/topics/changes/changes-23_0_2.adoc similarity index 92% rename from docs/documentation/upgrading/topics/keycloak/changes-23_0_2.adoc rename to docs/documentation/upgrading/topics/changes/changes-23_0_2.adoc index 19cb057cbb39..34aeefab0704 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-23_0_2.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-23_0_2.adoc @@ -4,10 +4,12 @@ Version 1.8.0 introduced a lower-case for the hostname and scheme when comparing For realms relying on the old behavior, the valid redirect URIs for their clients should now hold separate entries for each URI that should be recognized by the server. -Although it introduces more steps and verbosity when configuring clients, the new behavior enables more secure deployments as pattern-based checks are frequently the cause of security issues. Not only due to how they are implemented but also how they are configured. +Although it introduces more steps and verbosity when configuring clients, the new behavior enables more secure deployments as pattern-based checks are frequently the cause of security issues. These issues are due to how they are implemented and how they are configured. +ifeval::[{project_community}==true] = Operator -secrets-store Secret Older versions of the operator created a Secret to track watched Secrets. Newer versions of the operator no longer use the -secrets-store Secret, so it may be deleted. -If you are on 23.0.0 or 23.0.1 and see "org.keycloak.operator.controllers.KeycloakAdminSecretDependentResource -> java.lang.IllegalStateException: More than 1 secondary resource related to primary" in the operator log then either delete the -secrets-store Secret, or upgrade to 23.0.2 where this is no longer an issue. \ No newline at end of file +If you are on 23.0.0 or 23.0.1 and see "org.keycloak.operator.controllers.KeycloakAdminSecretDependentResource -> java.lang.IllegalStateException: More than 1 secondary resource related to primary" in the operator log then either delete the -secrets-store Secret, or upgrade to 23.0.2 where this is no longer an issue. +endif::[] diff --git a/docs/documentation/upgrading/topics/keycloak/changes-23_0_4.adoc b/docs/documentation/upgrading/topics/changes/changes-23_0_4.adoc similarity index 100% rename from docs/documentation/upgrading/topics/keycloak/changes-23_0_4.adoc rename to docs/documentation/upgrading/topics/changes/changes-23_0_4.adoc diff --git a/docs/documentation/upgrading/topics/keycloak/changes-23_0_5.adoc b/docs/documentation/upgrading/topics/changes/changes-23_0_5.adoc similarity index 81% rename from docs/documentation/upgrading/topics/keycloak/changes-23_0_5.adoc rename to docs/documentation/upgrading/topics/changes/changes-23_0_5.adoc index 8f5608c42b35..2e03ee805912 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-23_0_5.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-23_0_5.adoc @@ -6,4 +6,4 @@ Because of issue https://github.com/keycloak/keycloak/issues/25078[#25078], the ./kc.sh start --spi-events-listener-jboss-logging-sanitize=false --spi-events-listener-jboss-logging-quotes=none ... ``` -More information about the options in the https://www.keycloak.org/server/all-provider-config#_jboss_logging[all provider configuration guide]. +For more information about the options, see https://www.keycloak.org/server/all-provider-config#_jboss_logging[all provider configuration guide]. diff --git a/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-24_0_0.adoc similarity index 91% rename from docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc rename to docs/documentation/upgrading/topics/changes/changes-24_0_0.adoc index 543509645a4e..fc0a54fe3536 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-24_0_0.adoc @@ -179,10 +179,14 @@ to the new template. = Truststore Changes -The `spi-truststore-file-*` options and the truststore related options `https-trust-store-*` are deprecated. Therefore, use the new default location for truststore material, `conf/truststores`, or specify your desired paths by using the `truststore-paths` option. For details, see the relevant https://www.keycloak.org/server/keycloak-truststore[guide]. +The `+spi-truststore-file-*+` options and the truststore related options `+https-trust-store-*+` are deprecated. Therefore, use the new default location for truststore material, `conf/truststores`, or specify your desired paths by using the `truststore-paths` option. For details, see the relevant https://www.keycloak.org/server/keycloak-truststore[guide]. The `tls-hostname-verifier` property should be used instead of the `spi-truststore-file-hostname-verification-policy` property. +A collateral effect of the changes is that now the truststore provider is always configured with some certificates (at least the default java trusted certificates are present). This new behavior can affect other parts of {project_name}. + +For example, *webauthn* registration can fail if *attestation conveyance* was configured to *Direct* validation. Previously, if the truststore provider was not configured the incoming certificate was not validated. But now this validation is always performed. The registration fails with `invalid cert path` error as the certificate chain sent by the dongle is not trusted by {project_name}. The Certificate Authorities of the authenticator need to be present in the truststore provider to correctly perform the attestation. + = Deprecated `--proxy` option The `--proxy` option has been deprecated and will be removed in a future release. The following table explains how the deprecated option maps to supported options. @@ -309,6 +313,9 @@ This change adds new indexes on the tables `USER_ATTRIBUTE` and `FED_USER_ATTRIB If those tables contain more than 300000 entries, {project_name} will skip the index creation by default during the automatic schema migration and instead log the SQL statement on the console during migration to be applied manually after {project_name}'s startup. See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to configure a different limit. +NOTE: The newly added indexes `USER_ATTR_LONG_VALUES_LOWER_CASE` and `FED_USER_ATTR_LONG_VALUES_LOWER_CASE` may exceed the maximum limit of 30 characters set by Oracle, +in case the database is running in compatibility mode. Since Oracle version 12.2, there is a support for longer index names. + == Additional migration steps for LDAP This is for installations that match all the following criteria: @@ -408,8 +415,9 @@ This can be done by configuring the hash iterations explicitly in the password p == Expected increased overall CPU usage and temporary increased database activity The Concepts for sizing CPU and memory resources in the {project_name} High Availability guide have been updated to reflect the new hashing defaults. -While the CPU usage per password-based login in our tests increased by 33% (which includes both the changed password hashing and unchanged TLS connection handling), the overall CPU increase should be around 10% to 15%. -This is due to the averaging effect of {project_name}'s other activities like refreshing access tokens and client credential grants, still this depends on the unique workload of an installation. +The CPU usage per password-based login in our tests increased by the factor of five, which includes both the changed password hashing and unchanged TLS connection handling. +The overall CPU increase should be around the factor of two to three due to the averaging effect of {project_name}'s other activities like refreshing access tokens and client credential grants. +Still, this depends on the unique workload of an installation. After the upgrade, during a password-based login, the user's passwords will be re-hashed with the new hash algorithm and hash iterations as a one-off activity and updated in the database. As this clears the user from {project_name}'s internal cache, you will also see an increased read activity on the database level. @@ -483,6 +491,17 @@ For custom extensions there may be some changes needed: The algorithm that {project_name} uses to sign internal tokens (a JWT which is consumed by {project_name} itself, for example a refresh or action token) is being changed from `HS256` to the more secure `HS512`. A new key provider named `hmac-generated-hs512` is now added for realms. Note that in migrated realms the old `hmac-generated` provider and the old `HS256` key are maintained and still validate tokens issued before the upgrade. The `HS256` provider can be manually deleted when no more old tokens exist following the {adminguide_link}#rotating-keys[rotating keys guidelines]. += Different JVM memory settings when running in a container + +The JVM options `-Xms` and `-Xmx` were replaced by `-XX:InitialRAMPercentage` and `-XX:MaxRAMPercentage` when running in a container. +Instead of the static maximum heap size settings, {project_name} specifies the maximum as 70% of the total container memory. + +As the heap size is dynamically calculated based on the total container memory, you should *always set the memory limit* for the container. + +WARNING: If the memory limit is not set, the memory consumption rapidly increases as the maximum heap size grows up to 70% of the total container memory. + +For more details, see the https://www.keycloak.org/server/containers#_specifying_different_memory_settings[Running Keycloak in a container] guide. + ifeval::[{project_community}==true] = GELF log handler has been deprecated diff --git a/docs/documentation/upgrading/topics/changes/changes-24_0_2.adoc b/docs/documentation/upgrading/topics/changes/changes-24_0_2.adoc new file mode 100644 index 000000000000..94ae4d88b10e --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-24_0_2.adoc @@ -0,0 +1,6 @@ +ifeval::[{project_community}==true] += Changes to Password Hashing + +The release notes for {project_name} 24.0.0 have been updated with corrected description of the expected performance impact for the change, as well as sizing guide. + +endif::[] diff --git a/docs/documentation/upgrading/topics/changes/changes-24_0_3.adoc b/docs/documentation/upgrading/topics/changes/changes-24_0_3.adoc new file mode 100644 index 000000000000..f61284a3d189 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-24_0_3.adoc @@ -0,0 +1,24 @@ += Changes in redirect URI verification when using wildcards + +Because of security concerns, the redirect URI verification now performs a exact string matching (no wildcard involved) if the passed redirect uri contains a `userinfo` part or its `path` accesses parent directory (`/../`). + +The full wildcard `*` can still be used as a valid redirect in development for http(s) URIs with those characteristics. In production environments a exact valid redirect URI without wildcard needs to be configured for any URI of that type. + +Please note that wildcard valid redirect URIs are not recommended for production and not covered by the OAuth 2.0 specification. + +ifeval::[{project_community}==true] += Changes to the `org.keycloak.userprofile.UserProfileDecorator` interface + +To properly support multiple user storage providers within a realm, the `org.keycloak.userprofile.UserProfileDecorator` +interface has changed. + +The `decorateUserProfile` method is no longer invoked when parsing the user profile configuration for the first time (and caching it), +but everytime a user is being managed through the user profile provider. As a result, the method changed its contract to: + +```java +List decorateUserProfile(String providerId, UserProfileMetadata metadata) +``` + +Differently than the previous contract and behavior, this method is only invoked for the user storage provider from where the user +was loaded from. +endif::[] \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/changes/changes-24_0_4.adoc b/docs/documentation/upgrading/topics/changes/changes-24_0_4.adoc new file mode 100644 index 000000000000..a90c36f7f596 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-24_0_4.adoc @@ -0,0 +1,10 @@ += Partial update to user attributes when updating users through the Admin User API is no longer supported + +When updating user attributes through the Admin User API, you cannot execute partial updates when updating the +user attributes, including the root attributes like `username`, `email`, `firstName`, and `lastName`. + +If you are updating user attributes through the Admin User API without passing all the attributes that the administrator +has write permissions, the missing attributes will be removed. On the other hand, if an attribute is marked as read-only for +administrators, not sending the attribute won't remove it. + +See the link:{adminguide_link}#user-profile[User Profile Documentation] for the details about the user profile settings. \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc new file mode 100644 index 000000000000..8a9601781f3a --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -0,0 +1,9 @@ += Nonce claim is only added to the ID token + +The nonce claim is now only added to the ID token strictly following the OpenID Connect Core 1.0 specification. As indicated in the specification, the claim is compulsory inside the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID token] when the same parameter was sent in the authorization request. The specification also recommends to not add the `nonce` after a https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse[refresh request]. Previously, the claim was set to all the tokens (Access, Refresh and ID) in all the responses (refresh included). + +A new `Nonce backwards compatible` mapper is also included in the software that can be assigned to client scopes to revert to the old behavior. For example, the JS adapter checked the returned `nonce` claim in all the tokens before fixing issue https://github.com/keycloak/keycloak/issues/26651[#26651] in version 24.0.0. Therefore, if an old version of the JS adapter is used, the mapper should be added to the required clients by using client scopes. + += Removed a model module + +The module `org.keycloak:keycloak-model-legacy` module was deprecated in a previous release and is removed in this release. Use the `org.keycloak:keycloak-model-storage` module instead. diff --git a/docs/documentation/upgrading/topics/keycloak/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc similarity index 99% rename from docs/documentation/upgrading/topics/keycloak/changes.adoc rename to docs/documentation/upgrading/topics/changes/changes.adoc index 1f299f46a67e..1c353b4870ce 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -1,5 +1,18 @@ +[[migration-changes]] == Migration Changes +=== Migrating to 24.0.4 + +include::changes-24_0_4.adoc[leveloffset=3] + +=== Migrating to 24.0.3 + +include::changes-24_0_3.adoc[leveloffset=3] + +=== Migrating to 24.0.2 + +include::changes-24_0_2.adoc[leveloffset=3] + === Migrating to 24.0.0 include::changes-24_0_0.adoc[leveloffset=3] diff --git a/docs/documentation/upgrading/topics/download.adoc b/docs/documentation/upgrading/topics/download.adoc new file mode 100644 index 000000000000..0910855f2293 --- /dev/null +++ b/docs/documentation/upgrading/topics/download.adoc @@ -0,0 +1,14 @@ +[[_install_new_version]] + +== Downloading the {project_name} server + +Once you have prepared for the upgrade, you can download the server. + +.Procedure + +. Download and extract {archivedownloadurl}[{archivebasename}-{project_version}.zip] +from the {project_name} website. ++ +After extracting this file, you should have a directory that is named `{archivebasename}-{project_version}`. +. Move this directory to the desired location. +. Copy `conf/`, `providers/` and `themes/` from the previous installation to the new installation. \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/install_new_version.adoc b/docs/documentation/upgrading/topics/install_new_version.adoc deleted file mode 100644 index de249c8f4161..000000000000 --- a/docs/documentation/upgrading/topics/install_new_version.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[[_install_new_version]] - -== Upgrading the {project_name} server - -It is important that you upgrade {project_name} server before upgrading the adapters. - -.Prerequisites -* Handle any open transactions and delete the data/tx-object-store/ transaction directory. - -.Procedure -. Download the new server archive -. Move the downloaded archive to the desired location. -. Extract the archive. This step installs a clean instance of the latest {project_name} release. -. Copy `conf/`, `providers/` and `themes/` from the previous installation to the new installation. \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/intro.adoc b/docs/documentation/upgrading/topics/intro.adoc new file mode 100644 index 000000000000..2daa2a91aefd --- /dev/null +++ b/docs/documentation/upgrading/topics/intro.adoc @@ -0,0 +1,11 @@ +[[intro]] + +== Upgrading {project_name} + +This guide describes how to upgrade {project_name}. Use the following procedures in this order: + +. Review the migration changes from the previous version of {project_name}. +. Upgrade the {project_name} server. +. Upgrade the {project_name} adapters. +. Upgrade the {project_name} Admin Client. + diff --git a/docs/documentation/upgrading/topics/keycloak/intro.adoc b/docs/documentation/upgrading/topics/keycloak/intro.adoc deleted file mode 100644 index e620d623235a..000000000000 --- a/docs/documentation/upgrading/topics/keycloak/intro.adoc +++ /dev/null @@ -1,9 +0,0 @@ -[[intro]] - -== Upgrading Keycloak - -This guide describes how to upgrade {project_name}. It is recommended that you start by upgrading the {project_name} -server first and {project_name} adapters second. - -Before upgrading make sure to read the instructions carefully and carefully review the changes listed in -<>. \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/keycloak/upgrading.adoc b/docs/documentation/upgrading/topics/keycloak/upgrading.adoc deleted file mode 100644 index e839cab8cd5a..000000000000 --- a/docs/documentation/upgrading/topics/keycloak/upgrading.adoc +++ /dev/null @@ -1,15 +0,0 @@ -[[_upgrading]] - -== Upgrading the {project_name} server - -include::../prep_migration.adoc[leveloffset=1] - -include::../install_new_version.adoc[leveloffset=1] - -include::../migrate_db.adoc[leveloffset=1] - -include::../migrate_themes.adoc[leveloffset=1] - -include::../upgrade_adapters.adoc[leveloffset=1] - -include::../upgrade_admin_client.adoc[leveloffset=1] diff --git a/docs/documentation/upgrading/topics/migrate_db.adoc b/docs/documentation/upgrading/topics/migrate_db.adoc index f5e7f93f1fb9..4e65d2e6cb40 100644 --- a/docs/documentation/upgrading/topics/migrate_db.adoc +++ b/docs/documentation/upgrading/topics/migrate_db.adoc @@ -1,21 +1,15 @@ [[_migrate_db]] -== Database migration +== Migrating the database {project_name} can automatically migrate the database schema, or you can choose to do it manually. By default the database is automatically migrated when you start the new installation for the first time. === Automatic relational database migration -To perform an automatic migration, start the server connected to the desired database. -If the database schema has changed for the new version of the server, it will be migrated. +To perform an automatic migration, start the server connected to the desired database. If the database schema has changed for the new server version, the migration starts automatically unless the database has too many records. -Creating an index on huge tables with millions of records can easily take a huge amount of time -and potentially cause major service disruption on upgrades. -For those cases, we added a threshold (the number of records) for automated index creation. -By default, this threshold is `300000` records. -When the number of records is higher than the threshold, the index is not created automatically, -and there will be a warning message in server logs including SQL commands which can be applied later manually. +For example, creating an index on tables with millions of records can be time-consuming and cause a major service disruption. Therefore, a threshold of `300000` records exists for automatic migration. If the number of records exceeds this threshold, the index is not created. Instead, you find a warning in the server logs with the SQL commands that you can apply manually. To change the threshold, set the `index-creation-threshold` property, value for the default `connections-liquibase` provider: @@ -34,7 +28,7 @@ default `connections-jpa` provider: kc.[sh|bat] start --spi-connections-jpa-quarkus-migration-strategy=manual ---- -When you start the server with this configuration it checks if the database needs to be migrated. +When you start the server with this configuration, the server checks if the database needs to be migrated. The required changes are written to the `bin/keycloak-database-update.sql` SQL file that you can review and manually run against the database. To change the path and name of the exported SQL file, set the `migration-export` property for the @@ -45,6 +39,6 @@ default `connections-jpa` provider: kc.[sh|bat] start --spi-connections-jpa-quarkus-migration-export=/ ---- -For further details on how to apply this file to the database, see the documentation for the relational database you're using. +For further details on how to apply this file to the database, see the documentation for your relational database. After the changes have been written to the file, the server exits. diff --git a/docs/documentation/upgrading/topics/migrate_themes.adoc b/docs/documentation/upgrading/topics/migrate_themes.adoc index 9126bfcfb1a0..869130cf83d6 100644 --- a/docs/documentation/upgrading/topics/migrate_themes.adoc +++ b/docs/documentation/upgrading/topics/migrate_themes.adoc @@ -1,80 +1,58 @@ [[_migrate_themes]] -== Theme migration +== Migrating themes -If you have created any custom themes they must be migrated to the new server. Any changes to the built-in themes might -need to be reflected in your custom themes, depending on which aspects you have customized. +If you created custom themes, those themes must be migrated to the new server. Also, any changes to the built-in themes might need to be reflected in your custom themes, depending on which aspects you customized. -You must copy your custom themes from the old server `themes` directory to the new server `themes` directory. -After that you need to review the changes below and consider if the changes need to be applied to your custom theme. +.Procedure -In summary: - -* If you have customized any of the changed templates listed below you need to compare the template from the base theme - to see if there are changes you need to apply. -* If you have customized any of the styles and are extending the {project_name} themes you need to review the changes to - the styles. If you are extending the base theme you can skip this step. -* If you have customized messages you might need to change the key or value or to add additional messages. - -ifeval::[{project_product}==true] -Each step is described in more detail below the list of changes. - -include::rhsso/migrate_themes-changes-73.adoc[leveloffset=0] -include::rhsso/migrate_themes-changes-72.adoc[leveloffset=0] -include::rhsso/migrate_themes-changes-71.adoc[leveloffset=0] -endif::[] +. Copy your custom themes from the old server `themes` directory to the new server `themes` directory. +. Use the following sections to migrate templates, messages, and styles. +* If you customized any of the updated templates listed in <>, compare the template from the base theme to check for any changes you need to apply. +* If you customized messages, you might need to change the key or value or to add additional messages. +* If you customized any styles and you are extending the {project_name} themes, review the changes to the styles. If you are extending the base theme, you can skip this step. === Migrating templates -If you have customized any of the templates you need to carefully review the changes that have been made to the templates -to decide if you need to apply these changes to your customized templates. Most likely you will need to apply the same -changes to your customized templates. If you have not customized any of the listed templates you can skip this section. - -A best practice is to use a diff tool to compare the templates to see what changes you might need to make to your -customized template. If you have only made minor changes it is simpler to compare the updated template to your -customized template. However, if you have made many changes it might be easier to compare the new template to your -customized old template, as this will show you what changes you need to make. +If you customized any template, review the new version to decide about updating your customized template. If you made minor changes, you could compare the updated template to your customized template. However, if you made many changes, consider comparing the new template to your customized template. This comparison will show you what changes you need to make. -The following screenshot compares the info.ftl template from the Login theme and an example custom theme: +You can use a diff tool to compare the templates. The following screenshot compares the `info.ftl` template from the Login theme and an example custom theme: -.Comparison of the updated version of a Login theme template with an example custom Login theme template -image:images/theme-migration-meld-info-1.png[] +.Updated version of a Login theme template versus a custom Login theme template +image:images/theme-migration-meld-info-1.png[Updated version of a Login theme template versus a custom Login theme template] -From this comparison it is easy to identify that the first change (`Hello world!!`) was a customization, while the +This comparison shows that the first change (`Hello world!!`) is a customization, while the second change (`if pageRedirectUri`) is a change to the base theme. By copying the second change to your custom template, you have successfully updated your customized template. -For the alternative approach the following screenshot compares the info.ftl template from the old installation with -the updated info.ftl template from the new installation: +In an alternative approach, the following screenshot compares the `info.ftl` template from the old installation with +the updated `info.ftl` template from the new installation: -.Comparison of the Login theme template from the old installation with the updated version of the Login theme template -image:images/theme-migration-meld-info-2.png[] +.Login theme template from the old installation versus the updated Login theme template +image:images/theme-migration-meld-info-2.png[Login theme template from the old installation versus the updated Login theme template] -From this comparison it is easy to identify what has been changed in the base template. You will then manually have to -make the same changes to your modified template. Since this approach is not as simple as the first approach, only use -this approach if the first one is not feasible. +This comparison shows what has been changed in the base template. You can then manually make the same changes to your modified template. Since this approach is more complex, use +this approach only if the first approach is not feasible. === Migrating messages -If you have added support for another language, you need to apply all the changes listed above. If you have not added -support for another language, you might not need to change anything; you only have to make changes if you have changed +If you added support for another language, you need to apply all the changes listed above. If you have not added +support for another language, you might not need to change anything. You need to make changes only if you have changed an affected message in your theme. -For added values, review the value of the message in the base theme to determine if you need to customize that message. +.Procedure -For renamed keys, rename the key in your custom theme. +. For added values, review the value of the message in the base theme to determine if you need to customize that message. -For changed values, check the value in the base theme to determine if you need to make changes to your custom theme. +. For renamed keys, rename the key in your custom theme. -=== Migrating styles +. For changed values, check the value in the base theme to determine if you need to make changes to your custom theme. -If you are inheriting styles from the keycloak or rh-sso themes you might need to update your custom styles to reflect -changes made to the styles from the built-in themes. +=== Migrating styles -A best practice is to use a diff tool to compare the changes to stylesheets between the old server installation and the -new server installation. +You might need to update your custom styles to reflect changes made to the styles from the built-in themes. Consider using a diff tool to compare the changes to stylesheets between the old server installation and the new server installation. -For example, using the diff command: +For example: [source,bash,subs=+attributes] ---- @@ -83,4 +61,3 @@ $ diff {project_dirref}_OLD/themes/keycloak/login/resources/css/login.css \ ---- Review the changes and determine if they affect your custom styling. - diff --git a/docs/documentation/upgrading/topics/prep_migration.adoc b/docs/documentation/upgrading/topics/prep_migration.adoc index 1a5fefd8db59..2a32389fc0b0 100644 --- a/docs/documentation/upgrading/topics/prep_migration.adoc +++ b/docs/documentation/upgrading/topics/prep_migration.adoc @@ -2,20 +2,18 @@ == Preparing for upgrading -Before you upgrade, be aware of the order in which you need to perform the upgrade steps. In particular, be sure to upgrade {project_name} server before you upgrade the adapters. - -[WARNING] -==== -In a minor upgrade of {project_name}, all user sessions are lost. After the upgrade, all users will have to log in again. -==== +Perform the following steps before you upgrade the server. .Procedure -. Back up the old installation (configuration, themes, and so on). +. Back up the old installation, such as configuration, themes, and so on. +. Handle any open transactions and delete the `data/tx-object-store/` transaction directory. + . Back up the database using instructions in the documentation for your relational database. -. Upgrade the {project_name} server. + -The database will no longer be compatible with the old server after the upgrade. -. If you need to revert the upgrade, first restore the old installation, and then restore the database from the backup copy. -. Upgrade the adapters. +The database will no longer be compatible with the old server after you upgrade the server. If you need to revert the upgrade, first restore the old installation, and then restore the database from the backup copy. +[WARNING] +==== +After upgrade of {project_name}, except for offline user sessions, user sessions are lost. Users will have to log in again. +==== diff --git a/docs/documentation/upgrading/topics/upgrade_adapters.adoc b/docs/documentation/upgrading/topics/upgrade_adapters.adoc index 315799d45125..7f7be9b09a8d 100644 --- a/docs/documentation/upgrading/topics/upgrade_adapters.adoc +++ b/docs/documentation/upgrading/topics/upgrade_adapters.adoc @@ -2,58 +2,30 @@ [[_upgrade_adapters]] -It is important that you upgrade {project_name} server first, and then upgrade the adapters. Earlier versions of the -adapter might work with later versions of {project_name} server, but earlier versions of {project_name} server might not +After you upgrade the {project_name} server, you can upgrade the adapters. Earlier versions of the +adapter might work with later versions of the {project_name} server, but earlier versions of the {project_name} server might not work with later versions of the adapter. [[_compatibility_with_older_adapters]] == Compatibility with older adapters -As mentioned above, we try to support newer release versions of {project_name} server working with older release versions of the adapters. -However, in some cases we need to include fixes on the {project_name} server side which may break compatibility with older versions -of the adapters. For example, when we implement new aspects of the OpenID Connect specification, which older client adapter versions -were not aware of. +Newer versions of the {project_name} server potentially work with older versions of the adapters. +However, some fixes of the {project_name} server may break compatibility with older versions +of the adapters. For example, a new implementation of the OpenID Connect specification may not match older client adapter versions. -In those cases, we added Compatibility modes. For OpenId Connect clients, there is a section named `OpenID Connect Compatibility Modes` -in the {project_name} admin console, on the page with client details. Here, you can disable some new aspects of the {project_name} server -to preserve compatibility with older client adapters. More details are available in the tool tips of individual switches. +For this situation, you can use Compatibility modes. For OpenID Connect clients, the Admin Console includes *OpenID Connect Compatibility Modes* on the page with client details. With this option, you can disable some new aspects of the {project_name} server +to preserve compatibility with older client adapters. For more details, see the tool tips of individual switches. [[_upgrade_eap_adapter]] == Upgrading the EAP adapter -ifeval::[{project_product}==true] - -.Procedure -If you originally installed the adapter using a downloaded archive, to upgrade the {appserver_name} adapter, perform the following procedure. - -. Download the new adapter archive. -. Remove the previous adapter modules by deleting the `{appserver_dirref}/modules/system/add-ons/keycloak/` directory. -. Unzip the downloaded archive into `{appserver_dirref}`. - -.Procedure -If you originally installed the adapter using RPM, to upgrade the adapter, complete the following steps, which are different depending on whether you are performing a minor or a micro upgrade: - -. For minor upgrades, use Yum to uninstall any adapters you currently have installed and then use Yum to install the new version of the adapters. -. For micro upgrades, use Yum to upgrade the adapter. This is the only step for micro upgrades. -+ -[source,bash,options="nowrap"] ----- -yum update ----- - -endif::[] - -ifeval::[{project_community}==true] - -.Procedure To upgrade the {appserver_name} adapter, complete the following steps: +.Procedure . Download the new adapter archive. . Remove the previous adapter modules by deleting the `{appserver_dirref}/modules/system/add-ons/keycloak/` directory. . Unzip the downloaded archive into `{appserver_dirref}`. -endif::[] - [[_upgrade_js_adapter]] == Upgrading the JavaScript adapter @@ -62,15 +34,15 @@ To upgrade a JavaScript adapter that has been copied to your web application, pe .Procedure . Download the new adapter archive. -. Overwrite the keycloak.js file in your application with the keycloak.js file from the downloaded archive. +. Overwrite the `keycloak.js` file in your application with the `keycloak.js` file from the downloaded archive. [[_upgrade_nodejs_adapter]] -== Upgrading the Node.js adapter +== Upgrading the `Node.js` adapter -To upgrade a Node.js adapter that has been copied to your web application, perform the following procedure. +To upgrade a `Node.js` adapter that has been copied to your web application, perform the following procedure. .Procedure . Download the new adapter archive. -. Remove the existing Node.js adapter directory +. Remove the existing `Node.js` adapter directory . Unzip the updated file into its place -. Change the dependency for keycloak-connect in the package.json of your application +. Change the dependency for keycloak-connect in the `package.json` of your application diff --git a/docs/documentation/upgrading/topics/upgrade_admin_client.adoc b/docs/documentation/upgrading/topics/upgrade_admin_client.adoc index 049e243b1d0e..86dd22aceb4c 100644 --- a/docs/documentation/upgrading/topics/upgrade_admin_client.adoc +++ b/docs/documentation/upgrading/topics/upgrade_admin_client.adoc @@ -2,7 +2,7 @@ [[_upgrade_admin_client]] -It is important that you upgrade the {project_name} server first, and then upgrade the admin-client. Earlier versions of the +Be sure that you upgrade the {project_name} server before you upgrade the admin-client. Earlier versions of the admin-client might work with later versions of {project_name} server, but earlier versions of {project_name} server might not -work with later versions of the admin-client. It is recommended to use the admin-client version that matches the used +work with later versions of the admin-client. Therefore, use the admin-client version that matches the current {project_name} server version. diff --git a/docs/documentation/upgrading/topics/upgrading.adoc b/docs/documentation/upgrading/topics/upgrading.adoc new file mode 100644 index 000000000000..324b980bbc00 --- /dev/null +++ b/docs/documentation/upgrading/topics/upgrading.adoc @@ -0,0 +1,17 @@ +[[_upgrading]] + +== Upgrading the {project_name} server + +You upgrade the server before you upgrade the adapters. + +include::prep_migration.adoc[leveloffset=1] + +include::download.adoc[leveloffset=1] + +include::migrate_db.adoc[leveloffset=1] + +include::migrate_themes.adoc[leveloffset=1] + +include::upgrade_adapters.adoc[leveloffset=1] + +include::upgrade_admin_client.adoc[leveloffset=1] diff --git a/docs/guides/attributes.adoc b/docs/guides/attributes.adoc index b3291dbbf530..ff8e4155fabc 100644 --- a/docs/guides/attributes.adoc +++ b/docs/guides/attributes.adoc @@ -7,3 +7,4 @@ :infinispan-operator-docs: https://infinispan.org/docs/infinispan-operator/main/operator.html :infinispan-xsite-docs: https://infinispan.org/docs/stable/titles/xsite/xsite.html :containerlabel: latest +:apidocs_adminrest_link: https://www.keycloak.org/docs-api/{version}/rest-api/ diff --git a/docs/guides/getting-started/templates/realm-config.adoc b/docs/guides/getting-started/templates/realm-config.adoc index 86f1b72aea1a..0842aef267d3 100644 --- a/docs/guides/getting-started/templates/realm-config.adoc +++ b/docs/guides/getting-started/templates/realm-config.adoc @@ -11,7 +11,7 @@ includes a single realm, called `master`. Use this realm only for managing {proj Use these steps to create the first realm. . Open the {links-admin-console}. -. Click the word *master* in the top-left corner, then click *Create Realm*. +. Click *{project_name}* next to *master realm*, then click *Create Realm*. . Enter `myrealm` in the *Realm name* field. . Click *Create*. @@ -21,8 +21,7 @@ image::add-realm.png[Add realm] Initially, the realm has no users. Use these steps to create a user: -. Open the {links-admin-console}. -. Click the word *master* in the top-left corner, then click *myrealm*. +. Verify that you are still in the *myrealm* realm, which is shown above the word *Manage*. . Click *Users* in the left-hand menu. . Click *Add user*. . Fill in the form with the following values: diff --git a/docs/guides/high-availability/bblocks-active-passive-sync.adoc b/docs/guides/high-availability/bblocks-active-passive-sync.adoc index 345941d0ed10..6b45af958f33 100644 --- a/docs/guides/high-availability/bblocks-active-passive-sync.adoc +++ b/docs/guides/high-availability/bblocks-active-passive-sync.adoc @@ -41,7 +41,7 @@ A synchronously replicated database across two sites. == {jdgserver_name} -An {jdgserver_name} deployment which leverages the {jdgserver_name}'s Cross-DC functionality. +A deployment of {jdgserver_name} that leverages the {jdgserver_name}'s Cross-DC functionality. *Blueprint:* <@links.ha id="deploy-infinispan-kubernetes-crossdc" /> using the {jdgserver_name} Operator, and connect the two sites using {jdgserver_name}'s Gossip Router. diff --git a/docs/guides/high-availability/concepts-active-passive-sync.adoc b/docs/guides/high-availability/concepts-active-passive-sync.adoc index 486bca09f248..08caba5c10ad 100644 --- a/docs/guides/high-availability/concepts-active-passive-sync.adoc +++ b/docs/guides/high-availability/concepts-active-passive-sync.adoc @@ -15,11 +15,11 @@ Use this setup to be able to fail over automatically in the event of a site fail Two independent {project_name} deployments running in different sites are connected with a low latency network connection. Users, realms, clients, offline sessions, and other entities are stored in a database that is replicated synchronously across the two sites. -The data is also cached in the {project_name} embedded {jdgserver_name} as local caches. +The data is also cached in the {project_name} Infinispan caches as local caches. When the data is changed in one {project_name} instance, that data is updated in the database, and an invalidation message is sent to the other site using the replicated `work` cache. -Session-related data is stored in the replicated caches of the embedded {jdgserver_name} of {project_name}, and forwarded to the external {jdgserver_name}, which forwards information to the external {jdgserver_name} running synchronously in the other site. -As session data of the external {jdgserver_name} is also cached in the embedded {jdgserver_name}, invalidation messages of the replicated `work` cache are needed for invalidation. +Session-related data is stored in the replicated caches of the Infinispan caches of {project_name}, and forwarded to the external {jdgserver_name}, which forwards information to the external {jdgserver_name} running synchronously in the other site. +As session data of the external {jdgserver_name} is also cached in the Infinispan caches, invalidation messages of the replicated `work` cache are needed for invalidation. In the following paragraphs and diagrams, references to deploying {jdgserver_name} apply to the external {jdgserver_name}. diff --git a/docs/guides/high-availability/concepts-infinispan-cli-batch.adoc b/docs/guides/high-availability/concepts-infinispan-cli-batch.adoc index 51902fc456aa..5e6497435015 100644 --- a/docs/guides/high-availability/concepts-infinispan-cli-batch.adoc +++ b/docs/guides/high-availability/concepts-infinispan-cli-batch.adoc @@ -19,7 +19,7 @@ For human interactions, the CLI shell might still be a better fit. == Example -The following `Batch` CR takes an {jdgserver_name} site offline as described in the operational procedure <@links.ha id="operate-switch-over" />. +The following `Batch` CR takes a site offline as described in the operational procedure <@links.ha id="operate-switch-over" />. [source,yaml,subs="+attributes"] ---- @@ -49,6 +49,6 @@ NOTE: Modifying a `Batch` CR instance has no effect. Batch operations are "`one- == Further reading -For more information, see the link:{infinispan-operator-docs}#batch-cr[{jdgserver_name} Operator Batch CR documentation]. +For more information, see the link:{infinispan-operator-docs}#batch-cr[{jdgserver_name} Operator `Batch` CR documentation]. diff --git a/docs/guides/high-availability/concepts-memory-and-cpu-sizing.adoc b/docs/guides/high-availability/concepts-memory-and-cpu-sizing.adoc index d2f62ae2f256..d61e5357bf57 100644 --- a/docs/guides/high-availability/concepts-memory-and-cpu-sizing.adoc +++ b/docs/guides/high-availability/concepts-memory-and-cpu-sizing.adoc @@ -39,9 +39,9 @@ Memory requirements increase with the number of client sessions per user session * In containers, Keycloak allocates 70% of the memory limit for heap based memory. It will also use approximately 300 MB of non-heap-based memory. To calculate the requested memory, use the calculation above. As memory limit, subtract the non-heap memory from the value above and divide the result by 0.7. -* For each 30 user logins per second, 1 vCPU per Pod in a three-node cluster (tested with up to 300 per second). +* For each 8 password-based user logins per second, 1 vCPU per Pod in a three-node cluster (tested with up to 300 per second). + -{project_name} spends most of the CPU time hashing the password provided by the user. +{project_name} spends most of the CPU time hashing the password provided by the user, and it is proportional to the number of hash iterations. * For each 450 client credential grants per second, 1 vCPU per Pod in a three node cluster (tested with up to 2000 per second). + @@ -58,25 +58,25 @@ Performance of {project_name} dropped significantly when its Pods were throttled Target size: * 50,000 active user sessions -* 30 logins per seconds +* 24 logins per seconds * 450 client credential grants per second * 350 refresh token requests per second Limits calculated: -* CPU requested: 3 vCPU +* CPU requested: 5 vCPU + -(30 logins per second = 1 vCPU, 450 client credential grants per second = 1 vCPU, 350 refresh token = 1 vCPU) +(24 logins per second = 3 vCPU, 450 client credential grants per second = 1 vCPU, 350 refresh token = 1 vCPU) -* CPU limit: 9 vCPU +* CPU limit: 15 vCPU + -(Allow for three times the CPU requested to handle peaks, startups and failover tasks, and also refresh token handling which we don't have numbers on, yet) +(Allow for three times the CPU requested to handle peaks, startups and failover tasks) * Memory requested: 1250 MB + (1000 MB base memory plus 250 MB RAM for 50,000 active sessions) -* Memory limit: 1360 GB +* Memory limit: 1360 MB + (1250 MB expected memory usage minus 300 non-heap-usage, divided by 0.7) @@ -86,15 +86,13 @@ The following setup was used to retrieve the settings above to run tests of abou * OpenShift 4.14.x deployed on AWS via ROSA. * Machinepool with `m5.4xlarge` instances. -* {project_name} deployed with the Operator and 3 pods. -* Default user password hashing with PBKDF2(SHA512) 210,000 hash iterations (which is the default). +* {project_name} deployed with the Operator and 3 pods in a high-availability setup with two sites in active/passive mode. +* OpenShift's reverse proxy running in passthrough mode were the TLS connection of the client is terminated at the Pod. +* Database Amazon Aurora PostgreSQL in a multi-AZ setup, with the writer instance in the availability zone of the primary site. +* Default user password hashing with PBKDF2(SHA512) 210,000 hash iterations which is the default https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2[as recommended by OWASP]. * Client credential grants don't use refresh tokens (which is the default). -* Database seeded with 100,000 users and 100,000 clients. -* Infinispan caches at default of 10,000 entries, so not all clients and users fit into the cache, and some requests will need to fetch the data from the database. +* Database seeded with 20,000 users and 20,000 clients. +* Infinispan local caches at default of 10,000 entries, so not all clients and users fit into the cache, and some requests will need to fetch the data from the database. * All sessions in distributed caches as per default, with two owners per entries, allowing one failing Pod without losing data. -* OpenShift's reverse proxy running in passthrough mode were the TLS connection of the client is terminated at the Pod. -* PostgreSQL deployed inside the same OpenShift with ephemeral storage. -+ -Using a database with persistent storage will have longer database latencies, which might lead to longer response times; still, the throughput should be similar. diff --git a/docs/guides/high-availability/connect-keycloak-to-external-infinispan.adoc b/docs/guides/high-availability/connect-keycloak-to-external-infinispan.adoc index 82acb0d0adf7..41379d2e1b23 100644 --- a/docs/guides/high-availability/connect-keycloak-to-external-infinispan.adoc +++ b/docs/guides/high-availability/connect-keycloak-to-external-infinispan.adoc @@ -4,10 +4,18 @@ <@tmpl.guide title="Connect {project_name} with an external {jdgserver_name}" summary="Building block for an Infinispan deployment on Kubernetes" -tileVisible="false" > +tileVisible="false" +includedOptions="cache-remote-*" > This topic describes advanced {jdgserver_name} configurations for {project_name} on Kubernetes. +== Architecture + +This connects {project_name} to {jdgserver_name} using TCP connections secured by TLS 1.3. +It uses the {project_name}'s truststore to verify {jdgserver_name}'s server certificate. +As {project_name} is deployed using its Operator on OpenShift in the prerequisites listed below, the Operator already added the `service-ca.crt` to the truststore which is used to sign {jdgserver_name}'s server certificates. +In other environments, add the necessary certificates to {project_name}'s truststore. + == Prerequisites * <@links.ha id="deploy-keycloak-kubernetes" /> as it will be extended. @@ -15,36 +23,6 @@ This topic describes advanced {jdgserver_name} configurations for {project_name} == Procedure -. Prepare an {jdgserver_name} Cache configuration XML from the file `cache-ispn.xml` which is part of the {project_name} distribution: -.. For each `distributed-cache` entry, add the tags `` as shown following. -+ -[source,xml,indent=0] ----- -include::examples/src/kcb-infinispan-cache-remote-store-config.xml[tag=keycloak-ispn-remotestore] ----- -<1> New tag `` to connect it to the remote store. -<2> This is a workaround for issue https://github.com/keycloak/keycloak/issues/27117[keycloak#27117] and will be removed in the following versions. -<3> For the address to the remote store, reference two environment variables for host name and port number. -<4> For authentication, reference two environment variables for username and password. -<5> To secure the remote store connection, use the Kubernetes mechanisms of the pre-configured truststore. - -.. Prepare an {jdgserver_name} Cache configuration XML from the file `cache-ispn.xml`, which is part of the {project_name} distribution. -For each `replicated-cache` entry, add the tag `` as shown below. -For additional information on the infinispan configuration options, see the https://docs.jboss.org/infinispan/14.0/configdocs/infinispan-config-14.0.html[infinispan configuration schema reference]. -+ -[source,xml,indent=0] ----- -include::examples/src/kcb-infinispan-cache-remote-store-config.xml[tag=keycloak-ispn-remotestore-work] ----- - -. Place the {jdgserver_name} Cache configuration XML in a ConfigMap. -+ -[source,yaml] ----- -include::examples/generated/keycloak-ispn.yaml[tag=keycloak-ispn-configmap] -... ----- - . Create a Secret with the username and password to connect to the external {jdgserver_name} deployment: + [source,yaml] @@ -56,9 +34,7 @@ include::examples/generated/keycloak-ispn.yaml[tag=keycloak-ispn-secret] + [NOTE] ==== -* The new `additionalOptions` entries starting with `remote-store` used here are not official {project_name} configurations. -Instead, they provide their values to environment variables that are then referenced in the {jdgserver_name} XML configuration. -* All the memory, resource and database configurations are skipped from the CR below as they have been described in <@links.ha id="deploy-keycloak-kubernetes" /> {section} already. +All the memory, resource and database configurations are skipped from the CR below as they have been described in <@links.ha id="deploy-keycloak-kubernetes" /> {section} already. Administrators should leave those configurations untouched. ==== + @@ -66,11 +42,13 @@ Administrators should leave those configurations untouched. ---- include::examples/generated/keycloak-ispn.yaml[tag=keycloak-ispn] ---- -<1> The `name` and `key` of the ConfigMap with the {jdgserver_name} Cache configuration XML created in the previous step. -<2> The hostname and port of the remote cache {jdgserver_name} cluster. -<3> The credentials required, username and password, to access the remote cache {jdgserver_name} cluster. -<4> The `spi-connections-infinispan-quarkus-site-name` is an arbitrary {jdgserver_name} site name which {project_name} needs for its embedded {jdgserver_name} deployment when a remote store is used. -This site-name is related only to the embedded {jdgserver_name} and does not need to match any value from the external {jdgserver_name} deployment. +<1> The hostname of the remote {jdgserver_name} cluster. +<2> The port of the remote {jdgserver_name} cluster. +This is optional and it default to `11222`. +<3> The Secret `name` and `key` with the {jdgserver_name} username credential. +<4> The Secret `name` and `key` with the {jdgserver_name} password credential. +<5> The `spi-connections-infinispan-quarkus-site-name` is an arbitrary {jdgserver_name} site name which {project_name} needs for its Infinispan caches deployment when a remote store is used. +This site-name is related only to the Infinispan caches and does not need to match any value from the external {jdgserver_name} deployment. If you are using multiple sites for {project_name} in a cross-DC setup such as <@links.ha id="deploy-infinispan-kubernetes-crossdc" />, the site name must be different in each site. diff --git a/docs/guides/high-availability/deploy-aurora-multi-az.adoc b/docs/guides/high-availability/deploy-aurora-multi-az.adoc index 8f0a7ccd6566..c8f0eb0625e7 100644 --- a/docs/guides/high-availability/deploy-aurora-multi-az.adoc +++ b/docs/guides/high-availability/deploy-aurora-multi-az.adoc @@ -52,7 +52,7 @@ include::partials/aurora/aurora-verify-peering-connections.adoc[] == Deploying {project_name} Now that an Aurora database has been established and linked with all of your ROSA clusters, the next step is to deploy {project_name} as described in the <@links.ha id="deploy-keycloak-kubernetes" /> {section} with the JDBC url configured to use the Aurora database writer endpoint. -To do this, create a `{project_name}` CR with the following adjustments: +To do this, create a `Keycloak` CR with the following adjustments: . Update `spec.db.url` to be `jdbc:aws-wrapper:postgresql://$HOST:5432/keycloak` where `$HOST` is the <>. diff --git a/docs/guides/high-availability/deploy-aws-route53-loadbalancer.adoc b/docs/guides/high-availability/deploy-aws-route53-loadbalancer.adoc index 32922cf889cc..86d22453d33d 100644 --- a/docs/guides/high-availability/deploy-aws-route53-loadbalancer.adoc +++ b/docs/guides/high-availability/deploy-aws-route53-loadbalancer.adoc @@ -217,13 +217,13 @@ For both the Primary and Backup cluster, perform the following steps: + .. Log in to the ROSA cluster + -.. Ensure the {project_name} CR has the following configuration +.. Ensure the `Keycloak` CR has the following configuration + [source,yaml] ---- <#noparse> apiVersion: k8s.keycloak.org/v2alpha1 -kind: {project_name} +kind: Keycloak metadata: name: keycloak spec: diff --git a/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc b/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc index 1b37f755f8ac..bc2b02ea68fb 100644 --- a/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc +++ b/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc @@ -137,13 +137,13 @@ kubectl -n {ns} create secret generic {ts-secret} \ + NOTE: Keystore and Truststore must be uploaded in both {ocp} clusters. -. Create an {jdgserver_name} Cluster with Cross-Site enabled +. Create a Cluster for {jdgserver_name} with Cross-Site enabled + The {infinispan-operator-docs}#setting-up-xsite[Setting Up Cross-Site] documentation provides all the information on how to create and configure your {jdgserver_name} cluster with cross-site enabled, including the previous steps. + A basic example is provided in this {section} using the credentials, tokens, and TLS Keystore/Truststore created by the commands from the previous steps. + -.The {jdgserver_name} CR for `{site-a}` +.The `Infinispan` CR for `{site-a}` [source,yaml] ---- include::examples/generated/ispn-site-a.yaml[tag=infinispan-crossdc] @@ -163,10 +163,10 @@ include::examples/generated/ispn-site-a.yaml[tag=infinispan-crossdc] <13> The {ocp} API URL for the remote site. <14> The secret with the access toke to authenticate into the remote site. + -For `{site-b}`, the {jdgserver_name} CR looks similar to the above. +For `{site-b}`, the `Infinispan` CR looks similar to the above. Note the differences in point 4, 11 and 13. + -.The {jdgserver_name} CR for `{site-b}` +.The `Infinispan` CR for `{site-b}` [source,yaml] ---- include::examples/generated/ispn-site-b.yaml[tag=infinispan-crossdc] @@ -179,7 +179,7 @@ include::examples/generated/ispn-site-b.yaml[tag=infinispan-crossdc] The {jdgserver_name} {infinispan-operator-docs}#creating-caches[Cache CR] allows deploying the caches in the {jdgserver_name} cluster. Cross-site needs to be enabled per cache as documented by {infinispan-xsite-docs}[Cross Site Documentation]. The documentation contains more details about the options used by this {section}. -The following example shows the Cache CR for `{site-a}`. +The following example shows the `Cache` CR for `{site-a}`. + .sessions in `{site-a}` [source,yaml] @@ -191,7 +191,7 @@ Set this for the caches `sessions`, `authenticationSessions`, `offlineSessions`, <2> The remote site name. <3> The cross-site communication, in this case, `SYNC`. + -For `{site-b}`, the Cache CR is similar except in point 2. +For `{site-b}`, the `Cache` CR is similar except in point 2. + .session in `{site-b}` [source,yaml] @@ -217,6 +217,6 @@ kubectl wait --for condition=CrossSiteViewFormed --timeout=300s infinispans.infi == Next steps -After infinispan is deployed and running, use the procedure in the <@links.ha id="connect-keycloak-to-external-infinispan"/> {section} to connect your {project_name} cluster with the {jdgserver_name} cluster. +After {jdgserver_name} is deployed and running, use the procedure in the <@links.ha id="connect-keycloak-to-external-infinispan"/> {section} to connect your {project_name} cluster with the {jdgserver_name} cluster. diff --git a/docs/guides/high-availability/examples/generated/ispn-single.yaml b/docs/guides/high-availability/examples/generated/ispn-single.yaml index 5971ac7dd5a8..a87e79f8f767 100644 --- a/docs/guides/high-availability/examples/generated/ispn-single.yaml +++ b/docs/guides/high-availability/examples/generated/ispn-single.yaml @@ -224,7 +224,7 @@ spec: expose: type: Route configMapName: "cluster-config" - image: quay.io/infinispan/server:14.0.25.Final + image: quay.io/infinispan/server:14.0.27.Final configListener: enabled: false container: diff --git a/docs/guides/high-availability/examples/generated/ispn-site-a.yaml b/docs/guides/high-availability/examples/generated/ispn-site-a.yaml index a65174d8b4c7..b8c66d22635e 100644 --- a/docs/guides/high-availability/examples/generated/ispn-site-a.yaml +++ b/docs/guides/high-availability/examples/generated/ispn-site-a.yaml @@ -363,7 +363,7 @@ spec: expose: type: Route configMapName: "cluster-config" - image: quay.io/infinispan/server:14.0.25.Final + image: quay.io/infinispan/server:14.0.27.Final configListener: enabled: false container: diff --git a/docs/guides/high-availability/examples/generated/ispn-site-b.yaml b/docs/guides/high-availability/examples/generated/ispn-site-b.yaml index 745e7c7d333f..7320a6ed8218 100644 --- a/docs/guides/high-availability/examples/generated/ispn-site-b.yaml +++ b/docs/guides/high-availability/examples/generated/ispn-site-b.yaml @@ -363,7 +363,7 @@ spec: expose: type: Route configMapName: "cluster-config" - image: quay.io/infinispan/server:14.0.25.Final + image: quay.io/infinispan/server:14.0.27.Final configListener: enabled: false container: diff --git a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml index a2359aa4cd63..b52e8571f24f 100644 --- a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml +++ b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml @@ -47,302 +47,6 @@ metadata: namespace: keycloak type: kubernetes.io/tls --- -# Source: keycloak/templates/keycloak-infinispan-configmap.yaml -# tag::keycloak-ispn-configmap[] -apiVersion: v1 -kind: ConfigMap -metadata: - name: kcb-infinispan-cache-config - namespace: keycloak -data: - kcb-infinispan-cache-remote-store-config.xml: | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ---- # Source: keycloak/templates/keycloak-providers-configmap.yaml apiVersion: v1 kind: ConfigMap @@ -746,12 +450,6 @@ spec: features: enabled: - multi-site # <3> - # tag::keycloak-ispn[] - cache: - configMapFile: - name: kcb-infinispan-cache-config # <1> - key: kcb-infinispan-cache-remote-store-config.xml # <1> - # end::keycloak-ispn[] transaction: xaEnabled: false # <4> # tag::keycloak-ispn[] @@ -768,19 +466,19 @@ spec: - name: http-pool-max-threads # <6> value: "200" # tag::keycloak-ispn[] - - name: remote-store-host # <2> + - name: cache-remote-host # <1> value: "infinispan.keycloak.svc" - - name: remote-store-port # <2> + - name: cache-remote-port # <2> value: "11222" - - name: remote-store-username # <3> + - name: cache-remote-username # <3> secret: name: remote-store-secret key: username - - name: remote-store-password # <3> + - name: cache-remote-password # <4> secret: name: remote-store-secret key: password - - name: spi-connections-infinispan-quarkus-site-name # <4> + - name: spi-connections-infinispan-quarkus-site-name # <5> value: keycloak # end::keycloak-ispn[] - name: db-driver @@ -793,7 +491,7 @@ spec: podTemplate: metadata: annotations: - checksum/config: ebe9b8c121995f449a1a4e339af244b2bb67769af84b3cbdff61159948447e20-4832924b47210161956e3b1718daf07ff52d801545186a76c391485eaf1897d3--dbc855dd9b7f7c0b828760ea8cd7427e8a2f5a5be303fba7dee0c6bbb68258d4-v1.27.0 + checksum/config: 385f54cb8e4bf326f6970aa2a0c8e573d35d9071e69ab2baee252728748bca76-4832924b47210161956e3b1718daf07ff52d801545186a76c391485eaf1897d3--01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b-v1.27.0 spec: containers: - env: diff --git a/docs/guides/high-availability/examples/src/kcb-infinispan-cache-remote-store-config.xml b/docs/guides/high-availability/examples/src/kcb-infinispan-cache-remote-store-config.xml deleted file mode 100644 index 675a29da5637..000000000000 --- a/docs/guides/high-availability/examples/src/kcb-infinispan-cache-remote-store-config.xml +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/guides/high-availability/introduction.adoc b/docs/guides/high-availability/introduction.adoc index 867f40f19085..bfbf02d28515 100644 --- a/docs/guides/high-availability/introduction.adoc +++ b/docs/guides/high-availability/introduction.adoc @@ -5,7 +5,7 @@ title="Multi-site deployments" summary="Connect multiple {project_name} deployments in different sites to increase the overall availability" > -{project_name} supports deployments that consist of multiple {project_name} instances that connect to each other using its embedded Infinispan; load balancers can distribute the load evenly across those instances. +{project_name} supports deployments that consist of multiple {project_name} instances that connect to each other using its Infinispan caches; load balancers can distribute the load evenly across those instances. Those setups are intended for a transparent network on a single site. The {project_name} high-availability guide goes one step further to describe setups across multiple sites. @@ -15,6 +15,7 @@ The different {sections} introduce the necessary concepts and building blocks. For each building block, a blueprint shows how to set a fully functional example. Additional performance tuning and security hardening are still recommended when preparing a production setup. +ifeval::[{project_community}==true] == Concept and building block overview * <@links.ha id="concepts-active-passive-sync" /> @@ -39,4 +40,6 @@ Additional performance tuning and security hardening are still recommended when * <@links.ha id="operate-network-partition-recovery" /> * <@links.ha id="operate-switch-back" /> +endif::[] + diff --git a/docs/guides/high-availability/partials/infinispan/infinispan-attributes.adoc b/docs/guides/high-availability/partials/infinispan/infinispan-attributes.adoc index 081f39907abc..686f3bda5738 100644 --- a/docs/guides/high-availability/partials/infinispan/infinispan-attributes.adoc +++ b/docs/guides/high-availability/partials/infinispan/infinispan-attributes.adoc @@ -20,6 +20,6 @@ // Other common attributes :ocp: OpenShift -:ispn-operator: Infinispan Operator +:ispn-operator: {jdgserver_name} Operator :site-a: Site-A :site-b: Site-B diff --git a/docs/guides/high-availability/partials/infinispan/infinispan-credentials.adoc b/docs/guides/high-availability/partials/infinispan/infinispan-credentials.adoc index e03d78b46c60..6f1e55570bab 100644 --- a/docs/guides/high-availability/partials/infinispan/infinispan-credentials.adoc +++ b/docs/guides/high-availability/partials/infinispan/infinispan-credentials.adoc @@ -1,7 +1,7 @@ [[infinispan-credentials]] -. Configure the credential to access the Infinispan cluster. +. Configure the credential to access the {jdgserver_name} cluster. + -{project_name} needs this credential to be able to authenticate with the Infinispan cluster. +{project_name} needs this credential to be able to authenticate with the {jdgserver_name} cluster. The following `identities.yaml` file sets the username and password with admin permissions + [source,yaml,subs="+attributes"] @@ -32,4 +32,4 @@ include::../../examples/generated/ispn-single.yaml[tag=infinispan-credentials] kubectl create secret generic connect-secret --from-file=identities.yaml ---- + -Check https://infinispan.org/docs/infinispan-operator/main/operator.html#configuring-authentication[Configuring Authentication] documentation for more details. +Check the {infinispan-operator-docs}#configuring-authentication[Configuring Authentication] documentation for more details. diff --git a/docs/guides/high-availability/partials/infinispan/infinispan-install-operator.adoc b/docs/guides/high-availability/partials/infinispan/infinispan-install-operator.adoc index 1f60df0e79d2..906c333d33ef 100644 --- a/docs/guides/high-availability/partials/infinispan/infinispan-install-operator.adoc +++ b/docs/guides/high-availability/partials/infinispan/infinispan-install-operator.adoc @@ -1 +1 @@ -. Install the https://infinispan.org/docs/infinispan-operator/main/operator.html#installation[Infinispan Operator] +. Install the {infinispan-operator-docs}#installation[{jdgserver_name} Operator] diff --git a/docs/guides/high-availability/partials/infinispan/infinispan-prerequisites.adoc b/docs/guides/high-availability/partials/infinispan/infinispan-prerequisites.adoc index c490694eda4f..4aacec9d0963 100644 --- a/docs/guides/high-availability/partials/infinispan/infinispan-prerequisites.adoc +++ b/docs/guides/high-availability/partials/infinispan/infinispan-prerequisites.adoc @@ -1,2 +1,2 @@ * OpenShift or Kubernetes cluster running -* Understanding of the https://infinispan.org/docs/infinispan-operator/main/operator.html[Infinispan Operator] +* Understanding of the {infinispan-operator-docs}[{jdgserver_name} Operator] diff --git a/docs/guides/images/add-realm.png b/docs/guides/images/add-realm.png index 75bd6410ff45..eb9a194b0a65 100644 Binary files a/docs/guides/images/add-realm.png and b/docs/guides/images/add-realm.png differ diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index aa090d1922a9..206c23458fb9 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -98,6 +98,8 @@ spec: - name: spi-email-template-mycustomprovider-enabled value: true # plain text value ---- +NOTE: The name format of options defined in this way is identical to the key format of options specified in the configuration file. + For details on various configuration formats, see <@links.server id="configuration"/>. === Secret References @@ -201,7 +203,43 @@ spec: Moreover, the {project_name} container manages the heap size more effectively by providing relative values for the heap size. It is achieved by providing certain JVM options. -For more details, check the -https://www.keycloak.org/server/containers[Running Keycloak in a container]. +For more details, see <@links.server id="containers" />. + +=== Truststores + +If you need to provide trusted certificates, the Keycloak CR provides a top level feature for configuring the server's truststore as discussed in <@links.server id="keycloak-truststore"/>. + +Use the truststores stanza of the Keycloak spec to specify Secrets containing PEM encoded files, or PKCS12 files with extension `.p12` or `.pfx`, for example: + +[source,yaml] +---- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc +spec: + ... + truststores: + my-truststore: + secret: + name: my-secret +---- + +Where the contents of my-secret could be a PEM file, for example: + +[source,yaml] +------ +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +stringData: + cert.pem: | + -----BEGIN CERTIFICATE----- + ... +------ + +When running on a Kubernetes or OpenShift environment well-known locations of trusted certificates are included automatically. +This includes /var/run/secrets/kubernetes.io/serviceaccount/ca.crt and the /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt when present. diff --git a/docs/guides/operator/basic-deployment.adoc b/docs/guides/operator/basic-deployment.adoc index 7a846826b787..0b62ea552e1f 100644 --- a/docs/guides/operator/basic-deployment.adoc +++ b/docs/guides/operator/basic-deployment.adoc @@ -52,11 +52,13 @@ spec: spec: containers: - name: postgresql-db - image: postgres:latest + image: postgres:15 volumeMounts: - mountPath: /data name: cache-volume env: + - name: POSTGRES_USER + value: testuser - name: POSTGRES_PASSWORD value: testpassword - name: PGDATA diff --git a/docs/guides/operator/realm-import.adoc b/docs/guides/operator/realm-import.adoc index f21a1693d716..5ce4bcee2f9e 100644 --- a/docs/guides/operator/realm-import.adoc +++ b/docs/guides/operator/realm-import.adoc @@ -36,7 +36,7 @@ spec: ---- This CR should be created in the same namespace as the Keycloak Deployment CR, defined in the field `keycloakCRName`. -The `realm` field accepts a full https://www.keycloak.org/docs-api/{version}/rest-api/index.html#RealmRepresentation[RealmRepresentation]. +The `realm` field accepts a full {apidocs_adminrest_link}/index.html#RealmRepresentation[RealmRepresentation]. The recommended way to obtain a `RealmRepresentation` is by leveraging the export functionality <@links.server id="importExport"/>. diff --git a/docs/guides/pom.xml b/docs/guides/pom.xml index 050466394dcb..46b050156893 100644 --- a/docs/guides/pom.xml +++ b/docs/guides/pom.xml @@ -19,7 +19,7 @@ keycloak-docs-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/guides/server/caching.adoc b/docs/guides/server/caching.adoc index a2e4bc40826b..b592c476afda 100644 --- a/docs/guides/server/caching.adoc +++ b/docs/guides/server/caching.adoc @@ -203,7 +203,7 @@ Instead, when you have a distributed cache setup running on AWS EC2 instances, y Cloud vendor specific stacks have additional dependencies for {project_name}. For more information and links to repositories with these dependencies, see the https://infinispan.org/docs/dev/titles/embedding/embedding.html#jgroups-cloud-discovery-protocols_cluster-transport[Infinispan documentation]. -To provide the dependencies to {project_name}, put the respective JAR in the `providers` directory and build Keycloak by entering this command: +To provide the dependencies to {project_name}, put the respective JAR in the `providers` directory and build {project_name} by entering this command: <@kc.build parameters="--cache-stack="/> diff --git a/docs/guides/server/configuration-metrics.adoc b/docs/guides/server/configuration-metrics.adoc index 346ddcab8201..70e413e3c1a0 100644 --- a/docs/guides/server/configuration-metrics.adoc +++ b/docs/guides/server/configuration-metrics.adoc @@ -22,7 +22,7 @@ It is possible to enable metrics using the build time option `metrics-enabled`: * `/metrics` -The response from the endpoint uses a `text/plain` content type and it is based on the Prometheus text format. The snippet bellow +The response from the endpoint uses a `application/openmetrics-text` content type and it is based on the Prometheus (OpenMetrics) text format. The snippet bellow is an example of a response: [source] @@ -69,9 +69,6 @@ The table below summarizes the available metrics groups: |Database |A set of metrics from the database connection pool, if using a database. -|HTTP -|A set of global and individual metrics from the HTTP endpoints - |Cache |A set of metrics from Infinispan caches. See <@links.server id="caching"/> for more details. diff --git a/docs/guides/server/configuration-production.adoc b/docs/guides/server/configuration-production.adoc index 51cc0143d60c..85a67da200f2 100644 --- a/docs/guides/server/configuration-production.adoc +++ b/docs/guides/server/configuration-production.adoc @@ -34,7 +34,7 @@ A production environment should protect itself from an overload situation, so th One way of doing this is rejecting additional requests once a certain threshold is reached. Load shedding should be implemented on all levels, including the load balancers in your environment. -In addition to that, there is a feature in Keycloak to limit the number of requests that can't be processed right away and need to be queued. +In addition to that, there is a feature in {project_Name} to limit the number of requests that can't be processed right away and need to be queued. By default, there is no limit set. Set the option `http-max-queued-requests` to limit the number of queued requests to a given threshold matching your environment. Any request that exceeds this limit would return with an immediate `503 Server not Available` response. diff --git a/docs/guides/server/configuration.adoc b/docs/guides/server/configuration.adoc index 170eee6cd06b..88626fd22527 100644 --- a/docs/guides/server/configuration.adoc +++ b/docs/guides/server/configuration.adoc @@ -146,7 +146,7 @@ You can use only a https://github.com/keycloak/keycloak/blob/main/quarkus/runtim Similarly, you can also store Quarkus properties in a Java KeyStore. -Note that some Quarkus properties are already mapped in the {project_name} configuration, such as `quarkus.http.port` and similar essential properties. If the property is used by Keycloak, defining that property key in `quarkus.properties` has no effect. The Keycloak configuration value takes precedence over the Quarkus property value. +Note that some Quarkus properties are already mapped in the {project_name} configuration, such as `quarkus.http.port` and similar essential properties. If the property is used by {project_Name}, defining that property key in `quarkus.properties` has no effect. The {project_Name} configuration value takes precedence over the Quarkus property value. === Using special characters in values diff --git a/docs/guides/server/containers.adoc b/docs/guides/server/containers.adoc index 55e7644ca1b2..a6b9a5221fd7 100644 --- a/docs/guides/server/containers.adoc +++ b/docs/guides/server/containers.adoc @@ -9,7 +9,7 @@ title="Running {project_name} in a container" summary="Learn how to run {project_name} from a container image" includedOptions="db db-url db-username db-password features hostname https-key-store-file https-key-store-password health-enabled metrics-enabled"> -This {section} describes how to optimize and run the {project_name} container image to provide the best experience running a {project_name} container. +This {section} describes how to optimize and run the {project_name} container image to provide the best experience running a container. <@profile.ifProduct> @@ -72,7 +72,7 @@ FROM quay.io/keycloak/keycloak:{containerlabel} as builder ... # Add the provider JAR file to the providers directory -ADD --chown=keycloak:keycloak /opt/keycloak/providers/myprovider.jar +ADD --chown=keycloak:keycloak --chmod=644 /opt/keycloak/providers/myprovider.jar ... @@ -82,7 +82,7 @@ RUN /opt/keycloak/bin/kc.sh build === Installing additional RPM packages -If you try to install new software in a stage `+FROM quay.io/keycloak/keycloak+`, you will notice that `+microdnf+`, `+dnf+`, and even `+rpm+` are not installed. Also, very few packages are available, only enough for a `+bash+` shell, and to run Keycloak itself. This is due to security hardening measures, which reduce the attack surface of the Keycloak container. +If you try to install new software in a stage `+FROM quay.io/keycloak/keycloak+`, you will notice that `+microdnf+`, `+dnf+`, and even `+rpm+` are not installed. Also, very few packages are available, only enough for a `+bash+` shell, and to run {project_name} itself. This is due to security hardening measures, which reduce the attack surface of the {project_name} container. First, consider if your use case can be implemented in a different way, and so avoid installing new RPMs into the final container: @@ -216,7 +216,7 @@ This approach significantly increases startup time and creates an image that is == Importing A Realm On Startup -The {project_name} containers have a directory `/opt/keycloak/data/import`. If you put one or more import files in that directory via a volume mount or other means and add the startup argument `--import-realm`, the Keycloak container will import that data on startup! This may only make sense to do in Dev mode. +The {project_name} containers have a directory `/opt/keycloak/data/import`. If you put one or more import files in that directory via a volume mount or other means and add the startup argument `--import-realm`, the {project_name} container will import that data on startup! This may only make sense to do in Dev mode. [source,bash,subs="attributes+"] ---- @@ -236,19 +236,27 @@ This behavior is achieved by JVM options `-XX:MaxRAMPercentage=70`, and `-XX:Ini The `-XX:MaxRAMPercentage` option represents the maximum heap size as 70% of the total container memory. The `-XX:InitialRAMPercentage` option represents the initial heap size as 50% of the total container memory. -These values were chosen based on a deeper analysis of Keycloak memory management. +These values were chosen based on a deeper analysis of {project_name} memory management. + +As the heap size is dynamically calculated based on the total container memory, you should *always set the memory limit* for the container. +Previously, the maximum heap size was set to 512 MB, and in order to approach similar values, you should set the memory limit to at least 750 MB. +For smaller production-ready deployments, the recommended memory limit is 2 GB. The JVM options related to the heap might be overridden by setting the environment variable `JAVA_OPTS_KC_HEAP`. You can find the default values of the `JAVA_OPTS_KC_HEAP` in the source code of the `kc.sh`, or `kc.bat` script. -For example, you can specify the environment variable as follows: + +For example, you can specify the environment variable and memory limit as follows: [source,bash,subs="attributes+"] ---- -podman|docker run --name mykeycloak -p 8080:8080 \ +podman|docker run --name mykeycloak -p 8080:8080 -m 1g \ -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=change_me \ -e JAVA_OPTS_KC_HEAP="-XX:MaxHeapFreeRatio=30 -XX:MaxRAMPercentage=65" \ quay.io/keycloak/keycloak:{containerlabel} \ start-dev ---- +WARNING: If the memory limit is not set, the memory consumption rapidly increases as the heap size can grow up to 70% of the total container memory. +Once the JVM allocates the memory, it is returned to the OS reluctantly with the current {project_name} GC settings. + diff --git a/docs/guides/server/db.adoc b/docs/guides/server/db.adoc index ff460745b161..a5554838e260 100644 --- a/docs/guides/server/db.adoc +++ b/docs/guides/server/db.adoc @@ -52,15 +52,15 @@ To install the Oracle Database driver for {project_name}: . When running the unzipped distribution: Place the `ojdbc11` and `orai18n` JAR files in {project_name}'s `providers` folder -. When running containers: Build a custom {project_name} image and add the JARs in the `providers` folder. When building a custom image for the Keycloak Operator, those images need to be optimized images with all build-time options of Keycloak set. +. When running containers: Build a custom {project_name} image and add the JARs in the `providers` folder. When building a custom image for the Operator, those images need to be optimized images with all build-time options of {project_name} set. + A minimal Dockerfile to build an image which can be used with the {project_name} Operator and includes Oracle Database JDBC drivers downloaded from Maven Central looks like the following: + [source,dockerfile,subs="attributes+"] ---- FROM quay.io/keycloak/keycloak:{containerlabel} -ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${properties["oracle-jdbc.version"]}/ojdbc11-${properties["oracle-jdbc.version"]}.jar /opt/keycloak/providers/ojdbc11.jar -ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${properties["oracle-jdbc.version"]}/orai18n-${properties["oracle-jdbc.version"]}.jar /opt/keycloak/providers/orai18n.jar +ADD --chown=keycloak:keycloak --chmod=644 https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${properties["oracle-jdbc.version"]}/ojdbc11-${properties["oracle-jdbc.version"]}.jar /opt/keycloak/providers/ojdbc11.jar +ADD --chown=keycloak:keycloak --chmod=644 https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${properties["oracle-jdbc.version"]}/orai18n-${properties["oracle-jdbc.version"]}.jar /opt/keycloak/providers/orai18n.jar # Setting the build parameter for the database: ENV KC_DB=oracle # Add all other build parameters needed, for example enable health and metrics: @@ -95,7 +95,7 @@ A minimal Dockerfile to build an image which can be used with the {project_name} [source,dockerfile,subs="attributes+"] ---- FROM quay.io/keycloak/keycloak:{containerlabel} -ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/microsoft/sqlserver/mssql-jdbc/${properties["mssql-jdbc.version"]}/mssql-jdbc-${properties["mssql-jdbc.version"]}.jar /opt/keycloak/providers/mssql-jdbc.jar +ADD --chown=keycloak:keycloak --chmod=644 https://repo1.maven.org/maven2/com/microsoft/sqlserver/mssql-jdbc/${properties["mssql-jdbc.version"]}/mssql-jdbc-${properties["mssql-jdbc.version"]}.jar /opt/keycloak/providers/mssql-jdbc.jar # Setting the build parameter for the database: ENV KC_DB=mssql # Add all other build parameters needed, for example enable health and metrics: @@ -258,7 +258,7 @@ See the <@links.server id="containers" /> {section} for details on how to build Beginning with MySQL 8.0.30, MySQL supports generated invisible primary keys for any InnoDB table that is created without an explicit primary key (more information https://dev.mysql.com/doc/refman/8.0/en/create-table-gipks.html[here]). If this feature is enabled, the database schema initialization and also migrations will fail with the error message `Multiple primary key defined (1068)`. -You then need to disable it by setting the parameter `sql_generate_invisible_primary_key` to `OFF` in your MySQL server configuration before installing or upgrading Keycloak. +You then need to disable it by setting the parameter `sql_generate_invisible_primary_key` to `OFF` in your MySQL server configuration before installing or upgrading {project_name}. == Changing database locking timeout in a cluster configuration @@ -269,7 +269,7 @@ The maximum timeout for this lock is 900 seconds. If a node waits on this lock f <@kc.start parameters="--spi-dblock-jpa-lock-wait-timeout 900"/> == Using Database Vendors without XA transaction support -{project_name} uses XA transactions and the appropriate database drivers by default. Certain vendors, such as Azure SQL and MariaDB Galera, do not support or rely on the XA transaction mechanism. To use Keycloak without XA transaction support using the appropriate JDBC driver, enter the following command: +{project_name} uses XA transactions and the appropriate database drivers by default. Certain vendors, such as Azure SQL and MariaDB Galera, do not support or rely on the XA transaction mechanism. To use {project_name} without XA transaction support using the appropriate JDBC driver, enter the following command: <@kc.build parameters="--db= --transaction-xa-enabled=false"/> diff --git a/docs/guides/server/fips.adoc b/docs/guides/server/fips.adoc index f605a5a90192..7ef76e66f74c 100644 --- a/docs/guides/server/fips.adoc +++ b/docs/guides/server/fips.adoc @@ -38,10 +38,10 @@ When {project_name} executes in fips mode, it will use the BCFIPS bits instead o === BouncyCastle FIPS bits BouncyCastle FIPS can be downloaded from the https://www.bouncycastle.org/fips-java/[BouncyCastle official page]. Then you can add them to the directory -`KEYCLOAK_HOME/providers` of your distribution. Make sure to use proper versions compatible with BouncyCastle Keycloak dependencies. The supported BCFIPS bits needed are: +`KEYCLOAK_HOME/providers` of your distribution. Make sure to use proper versions compatible with BouncyCastle {project_name} dependencies. The supported BCFIPS bits needed are: * `bc-fips-1.0.2.3.jar` -* `bctls-fips-1.0.16.jar` +* `bctls-fips-1.0.18.jar` * `bcpkix-fips-1.0.7.jar` == Generating keystore @@ -271,6 +271,6 @@ If you are still restricted to running {project_name} on such a system, you can at least the setup is closer to it. It can be done by providing a custom security file with only an overriden list of security providers as described earlier. For a list of recommended providers, see the https://access.redhat.com/documentation/en-us/openjdk/17/html/configuring_openjdk_17_on_rhel_with_fips/openjdk-default-fips-configuration[OpenJDK 17 documentation]. -You can check the {project_name} server log at startup to see if the correct security providers are used. TRACE logging should be enabled for crypto-related Keycloak packages as described in the Keycloak startup command earlier. +You can check the {project_name} server log at startup to see if the correct security providers are used. TRACE logging should be enabled for crypto-related {project_name} packages as described in the Keycloak startup command earlier. diff --git a/docs/guides/server/health.adoc b/docs/guides/server/health.adoc index 5bf518dff968..6ab73a4f3a06 100644 --- a/docs/guides/server/health.adoc +++ b/docs/guides/server/health.adoc @@ -8,7 +8,7 @@ title="Enabling {project_name} Health checks" summary="Learn how to enable and use {project_name} health checks" includedOptions="health-enabled"> -{project_name} has built in support for health checks. This {section} describes how to enable and use the Keycloak health checks. +{project_name} has built in support for health checks. This {section} describes how to enable and use the {project_name} health checks. == {project_name} health check endpoints diff --git a/docs/guides/server/importExport.adoc b/docs/guides/server/importExport.adoc index f8ccc7a76fb8..ac7c1d98917f 100644 --- a/docs/guides/server/importExport.adoc +++ b/docs/guides/server/importExport.adoc @@ -25,7 +25,7 @@ Use the `--help` command line option for each command to see the available optio Some of the configuration options are build time configuration options. As default, {project_name} will re-build automatically for the `export` and `import` commands if it detects a change of a build time parameter. -If you have built an optimized version of {project_name} with the `build` command as outlined in <@links.server id="configuration"/>, use the command line option `--optimized` to have Keycloak skip the build check for a faster startup time. +If you have built an optimized version of {project_name} with the `build` command as outlined in <@links.server id="configuration"/>, use the command line option `--optimized` to have {project_name} skip the build check for a faster startup time. When doing this, remove the build time options from the command line and keep only the runtime options. == Exporting a Realm to a Directory @@ -45,13 +45,13 @@ When exporting realms to a directory, the server is going to create separate fil You are also able to configure how users are going to be exported by setting the `--users ` option. The values available for this option are: -* *different_files*: Users export into different json files, depending on the maximum number of users per file set by `--users-per-file`. This is the default value. +`different_files`:: Users export into different json files, depending on the maximum number of users per file set by `--users-per-file`. This is the default value. -* *skip*: Skips exporting users. +`skip`:: Skips exporting users. -* *realm_file*: Users will be exported to the same file as the realm settings. For a realm named "foo", this would be "foo-realm.json" with realm data and users. +`realm_file`:: Users will be exported to the same file as the realm settings. For a realm named "foo", this would be "foo-realm.json" with realm data and users. -* *same_file*: All users are exported to one explicit file. So you will get two json files for a realm, one with realm data and one with users. +`same_file`:: All users are exported to one explicit file. So you will get two json files for a realm, one with realm data and one with users. If you are exporting users using the `different_files` strategy, you can set how many users per file you want by setting the `--users-per-file` option. The default value is `50`. @@ -127,4 +127,48 @@ When importing a realm at startup, you are able to use placeholders to resolve v In the example above, the value set to the `MY_REALM_NAME` environment variable is going to be used to set the `realm` property. - \ No newline at end of file +== Importing and Exporting by using the Admin Console + +You can also import and export a realm using the Admin Console. This functionality is +different from the other CLI options described in previous sections because the Admin Console offers only the capability to +_partially_ export a realm. In this case, the current realm settings, along with some resources like clients, +roles, and groups, can be exported. The users for that realm _cannot_ be exported using this method. + +NOTE: When using the Admin Console export, the realm and the selected resources are always exported to a file +named `realm-export.json`. Also, all sensitive values like passwords and client secrets will be masked with `+*+` symbols. + +To export a realm using the Admin Console, perform these steps: + +. Select a realm. +. Click *Realm settings* in the menu. +. Point to the *Action* menu in the top right corner of the realm settings screen, and select *Partial export*. ++ +A list of resources appears along with the realm configuration. +. Select the resources you want to export. +. Click *Export*. + +NOTE: Realms exported from the Admin Console are not suitable for backups or data transfer between servers. +Only CLI exports are suitable for backups or data transfer between servers. + +WARNING: If the realm contains many groups, roles, and clients, the operation may cause the server to be +unresponsive to user requests for a while. Use this feature with caution, especially on a production system. + +In a similar way, you can import a previously exported realm. Perform these steps: + +. Click *Realm settings* in the menu. +. Point to the *Action* menu in the top right corner of the realm settings screen, and select *Partial import*. ++ +A prompt appears where you can select the file you want to import. Based on this file, you see the resources you can import along with the realm settings. +. Click *Import*. + +You can also control what {project_name} should do if the imported resource already exists. These options exist: + +Fail import:: Abort the import. +Skip:: Skip the duplicate resources without aborting the process +Overwrite:: Replace the existing resources with the ones being imported. + +NOTE: The Admin Console partial import can also import files created by the CLI `export` command. In other words, full exports created +by the CLI can be imported by using the Admin Console. If the file contains users, those users will also be available for importing into the +current realm. + + diff --git a/docs/guides/server/keycloak-truststore.adoc b/docs/guides/server/keycloak-truststore.adoc index 3ef0f546bced..95702d855fb3 100644 --- a/docs/guides/server/keycloak-truststore.adoc +++ b/docs/guides/server/keycloak-truststore.adoc @@ -8,7 +8,7 @@ includedOptions="truststore-paths tls-hostname-verifier"> When {project_name} communicates with external services or has an incoming connection through TLS, it has to validate the remote certificate in order to ensure it is connecting to a trusted server. This is necessary in order to prevent man-in-the-middle attacks. -The certificates of these clients or servers, or the CA that signed these certificates, must be put in a truststore. This truststore is then configured for use by Keycloak. +The certificates of these clients or servers, or the CA that signed these certificates, must be put in a truststore. This truststore is then configured for use by {project_name}. == Configuring the System Truststore diff --git a/docs/guides/server/logging.adoc b/docs/guides/server/logging.adoc index c447fbe8f278..0fa496c3e1e9 100644 --- a/docs/guides/server/logging.adoc +++ b/docs/guides/server/logging.adoc @@ -167,7 +167,7 @@ Logging to a file is disabled by default. To enable it, enter the following comm <@kc.start parameters="--log=\"console,file\""/> -A log file named `keycloak.log` is created inside the `data/log` directory of your Keycloak installation. +A log file named `keycloak.log` is created inside the `data/log` directory of your {project_name} installation. === Configuring the location and name of the log file diff --git a/docs/maven-plugin/pom.xml b/docs/maven-plugin/pom.xml index 2c07a98d449c..7e79db8f6d66 100644 --- a/docs/maven-plugin/pom.xml +++ b/docs/maven-plugin/pom.xml @@ -20,7 +20,7 @@ keycloak-docs-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/docs/pom.xml b/docs/pom.xml index 35f5dfa64ff6..e721df31da61 100755 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -19,7 +19,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Docs Parent diff --git a/examples/admin-client/pom.xml b/examples/admin-client/pom.xml index 713d1db1d74c..90af2e89d5df 100755 --- a/examples/admin-client/pom.xml +++ b/examples/admin-client/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Examples - Admin Client diff --git a/examples/js-console/pom.xml b/examples/js-console/pom.xml index e26e650e56f2..f6d639ad831e 100755 --- a/examples/js-console/pom.xml +++ b/examples/js-console/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/examples/kerberos/pom.xml b/examples/kerberos/pom.xml index 985373bde0d7..0c13682e2a73 100755 --- a/examples/kerberos/pom.xml +++ b/examples/kerberos/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Examples - Kerberos Credential Delegation diff --git a/examples/ldap/pom.xml b/examples/ldap/pom.xml index 9e2bd4c20513..778b7a59cfea 100644 --- a/examples/ldap/pom.xml +++ b/examples/ldap/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/examples/pom.xml b/examples/pom.xml index b2a2186f7b9a..d1a4f4be5686 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Examples diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml index dc87b6705f50..6b84f960627f 100755 --- a/examples/providers/authenticator/pom.xml +++ b/examples/providers/authenticator/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Authenticator Example diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java index 1463ed629cdc..0323f7154e51 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java @@ -24,6 +24,8 @@ import org.keycloak.examples.authenticator.credential.SecretQuestionCredentialModel; import jakarta.ws.rs.core.Response; +import org.keycloak.models.KeycloakSession; +import org.keycloak.sessions.AuthenticationSessionModel; /** * @author Bill Burke @@ -37,6 +39,11 @@ public void evaluateTriggers(RequiredActionContext context) { } + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession) { + return SecretQuestionCredentialModel.TYPE; + } + @Override public void requiredActionChallenge(RequiredActionContext context) { Response challenge = context.form().createForm("secret-question-config.ftl"); diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml index 198116d57c1d..cf2a52a6bffd 100755 --- a/examples/providers/pom.xml +++ b/examples/providers/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Provider Examples diff --git a/examples/providers/rest/pom.xml b/examples/providers/rest/pom.xml index dead2d35943f..28491bcea0d6 100755 --- a/examples/providers/rest/pom.xml +++ b/examples/providers/rest/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 REST Example diff --git a/examples/saml/pom.xml b/examples/saml/pom.xml index c65cff5ac2d5..4474c6506542 100755 --- a/examples/saml/pom.xml +++ b/examples/saml/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 SAML Examples diff --git a/examples/saml/servlet-filter/pom.xml b/examples/saml/servlet-filter/pom.xml index 3b48824ae0a6..0ce593c7cc2a 100755 --- a/examples/saml/servlet-filter/pom.xml +++ b/examples/saml/servlet-filter/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-saml-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 saml-servlet-filter diff --git a/examples/themes/pom.xml b/examples/themes/pom.xml index d4190e3ac7a9..e15bfec5accd 100755 --- a/examples/themes/pom.xml +++ b/examples/themes/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Themes Examples diff --git a/federation/kerberos/pom.xml b/federation/kerberos/pom.xml index 95e483b06bf3..399ee9534048 100755 --- a/federation/kerberos/pom.xml +++ b/federation/kerberos/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml 4.0.0 diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index 137449a0794c..362945f69e04 100755 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -42,16 +42,16 @@ import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserLookupProvider; -import org.keycloak.userprofile.AttributeContext; import org.keycloak.userprofile.AttributeGroupMetadata; import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.UserProfileDecorator; import org.keycloak.userprofile.UserProfileMetadata; import org.keycloak.userprofile.UserProfileUtil; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.function.Predicate; import java.util.stream.Stream; import javax.security.auth.login.LoginException; @@ -302,22 +302,13 @@ public String toString() { } @Override - public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) { - Predicate kerberosUsersSelector = (attributeContext -> { - UserModel user = attributeContext.getUser(); - if (user == null) { - return false; - } - - return model.getId().equals(user.getFederationLink()); - }); - + public List decorateUserProfile(String providerId, UserProfileMetadata metadata) { int guiOrder = (int) metadata.getAttributes().stream() .map(AttributeMetadata::getName) .distinct() .count(); AttributeGroupMetadata metadataGroup = UserProfileUtil.lookupUserMetadataGroup(session); - UserProfileUtil.addMetadataAttributeToUserProfile(KerberosConstants.KERBEROS_PRINCIPAL, metadata, metadataGroup, kerberosUsersSelector, guiOrder++, model.getName()); + return Collections.singletonList(UserProfileUtil.createAttributeMetadata(KerberosConstants.KERBEROS_PRINCIPAL, metadata, metadataGroup, guiOrder++, model.getName())); } } diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index 6d976016a891..736dfc43da77 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml 4.0.0 diff --git a/federation/ldap/src/main/java/org/keycloak/services/resources/admin/TestLdapConnectionResource.java b/federation/ldap/src/main/java/org/keycloak/services/resources/admin/TestLdapConnectionResource.java index 3dcbe238185e..d0e52dd0bb0d 100644 --- a/federation/ldap/src/main/java/org/keycloak/services/resources/admin/TestLdapConnectionResource.java +++ b/federation/ldap/src/main/java/org/keycloak/services/resources/admin/TestLdapConnectionResource.java @@ -16,9 +16,7 @@ */ package org.keycloak.services.resources.admin; -import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; -import org.keycloak.common.ClientConnection; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; @@ -89,6 +87,7 @@ public Response testLDAPConnection(@FormParam("action") String action, @FormPara @NoCache @Consumes(MediaType.APPLICATION_JSON) public Response testLDAPConnection(TestLdapConnectionRepresentation config) { + auth.realm().requireManageRealm(); try { LDAPServerCapabilitiesManager.testLDAP(config, session, realm); return Response.noContent().build(); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java index fbb5da27d1e0..dea8cab938f0 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -1104,47 +1103,27 @@ public String toString() { } @Override - public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) { - Predicate ldapUsersSelector = (attributeContext -> { - UserModel user = attributeContext.getUser(); - if (user == null) { - return false; - } - - if (model.isImportEnabled()) { - return getModel().getId().equals(user.getFederationLink()); - } else { - return getModel().getId().equals(new StorageId(user.getId()).getProviderId()); - } - }); - - Predicate onlyAdminCondition = context -> metadata.getContext().isAdminContext(); - + public List decorateUserProfile(String providerId, UserProfileMetadata metadata) { int guiOrder = (int) metadata.getAttributes().stream() .map(AttributeMetadata::getName) .distinct() .count(); - + RealmModel realm = session.getContext().getRealm(); // 1 - get configured attributes from LDAP mappers and add them to the user profile (if they not already present) - Set attributes = new LinkedHashSet<>(); - realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName()) + List attributes = realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName()) .sorted(ldapMappersComparator.sortAsc()) - .forEachOrdered(mapperModel -> { + .flatMap(mapperModel -> { LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel); - attributes.addAll(ldapMapper.getUserAttributes()); - }); + return ldapMapper.getUserAttributes().stream(); + }).toList(); + + List metadatas = new ArrayList<>(); + for (String attrName : attributes) { - // In case that attributes from LDAP mappers are explicitly defined on user profile, we can prefer defined configuration - if (!metadata.getAttribute(attrName).isEmpty()) { - logger.debugf("Ignore adding attribute '%s' to user profile by LDAP provider '%s' as attribute is already defined on user profile.", attrName, getModel().getName()); - } else { - logger.debugf("Adding attribute '%s' to user profile by LDAP provider '%s' for user profile context '%s'.", attrName, getModel().getName(), metadata.getContext().toString()); - // Writable and readable only by administrators by default. Applied only for LDAP users - AttributeMetadata attributeMetadata = metadata.addAttribute(attrName, guiOrder++, Collections.emptyList()) - .addWriteCondition(onlyAdminCondition) - .addReadCondition(onlyAdminCondition) - .setRequired(AttributeMetadata.ALWAYS_FALSE); - attributeMetadata.setSelector(ldapUsersSelector); + AttributeMetadata attributeMetadata = UserProfileUtil.createAttributeMetadata(attrName, metadata, guiOrder++, getModel().getName()); + + if (attributeMetadata != null) { + metadatas.add(attributeMetadata); } } @@ -1157,17 +1136,20 @@ public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) AttributeGroupMetadata metadataGroup = UserProfileUtil.lookupUserMetadataGroup(session); for (String attrName : metadataAttributes) { - boolean attributeAdded = UserProfileUtil.addMetadataAttributeToUserProfile(attrName, metadata, metadataGroup, ldapUsersSelector, guiOrder++, getModel().getName()); - if (!attributeAdded) { + AttributeMetadata attributeAdded = UserProfileUtil.createAttributeMetadata(attrName, metadata, metadataGroup, guiOrder++, getModel().getName()); + if (attributeAdded == null) { guiOrder--; + } else { + metadatas.add(attributeAdded); } } // 3 - make all attributes read-only for LDAP users in case that LDAP itself is read-only if (getEditMode() == EditMode.READ_ONLY) { - for (AttributeMetadata attrMetadata : metadata.getAttributes()) { - attrMetadata.addWriteCondition(ldapUsersSelector.negate()); - } + Stream.concat(metadata.getAttributes().stream(), metadatas.stream()) + .forEach(attrMetadata -> attrMetadata.addWriteCondition(AttributeMetadata.ALWAYS_FALSE)); } + + return metadatas; } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java index 6bb287d4546a..63c345f2fddc 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java @@ -60,6 +60,7 @@ import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapper; import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory; import org.keycloak.storage.user.ImportSynchronization; import org.keycloak.storage.user.SynchronizationResult; @@ -434,7 +435,8 @@ public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel m // MSAD specific mapper for account state propagation if (activeDirectory) { - mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName()); + mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(), + MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); realm.addComponentModel(mapperModel); } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java index be463f42f85b..adf890c3eb5a 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; @@ -100,8 +99,7 @@ public void add(LDAPObject ldapObject) { } BasicAttributes ldapAttributes = extractAttributesForSaving(ldapObject, true); - this.operationManager.createSubContext(ldapObject.getDn().getLdapName(), ldapAttributes); - ldapObject.setUuid(getEntryIdentifier(ldapObject)); + ldapObject.setUuid(operationManager.createSubContext(ldapObject.getDn().getLdapName(), ldapAttributes)); if (logger.isDebugEnabled()) { logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), ldapObject.getDn()); @@ -603,23 +601,4 @@ private BasicAttribute createBinaryBasicAttribute(String attrName, Set a return attr; } - protected String getEntryIdentifier(final LDAPObject ldapObject) { - try { - // we need this to retrieve the entry's identifier from the ldap server - String uuidAttrName = getConfig().getUuidLDAPAttributeName(); - - List search = this.operationManager.search(ldapObject.getDn().getLdapName(), - new LDAPQueryConditionsBuilder().present(LDAPConstants.OBJECT_CLASS), - Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE); - Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName()); - - if (id == null) { - throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "]."); - } - - return this.operationManager.decodeEntryUUID(id.get()); - } catch (NamingException ne) { - throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "]."); - } - } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java index 4e9fda9572ac..8f61aaad41d1 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java @@ -600,7 +600,7 @@ public void modifyAttributes(final LdapName dn, final ModificationItem[] mods, L } } - public void createSubContext(final LdapName name, final Attributes attributes) { + public String createSubContext(final LdapName name, final Attributes attributes) { try { if (logger.isTraceEnabled()) { logger.tracef("Creating entry [%s] with attributes: [", name); @@ -622,14 +622,22 @@ public void createSubContext(final LdapName name, final Attributes attributes) { logger.tracef("]"); } - execute(new LdapOperation() { + return execute(new LdapOperation<>() { @Override - public Void execute(LdapContext context) throws NamingException { + public String execute(LdapContext context) throws NamingException { DirContext subcontext = context.createSubcontext(name, attributes); - - subcontext.close(); - - return null; + try { + String uuidLDAPAttributeName = config.getUuidLDAPAttributeName(); + Attribute id = subcontext.getAttributes("", new String[]{uuidLDAPAttributeName}).get(uuidLDAPAttributeName); + if (id == null) { + throw new ModelException("Could not retrieve identifier for entry [" + name + "]."); + } + return decodeEntryUUID(id.get()); + } catch (NamingException ne) { + throw new ModelException("Could not retrieve identifier for entry [" + name + "].", ne); + } finally { + subcontext.close(); + } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java index 4d5b1773a6ab..2e01c3b17712 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java @@ -49,6 +49,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdateCallback { public static final String LDAP_PASSWORD_POLICY_HINTS_ENABLED = "ldap.password.policy.hints.enabled"; + public static final String ALWAYS_READ_ENABLED_VALUE_FROM_LDAP = "always.read.enabled.value.from.ldap"; private static final Logger logger = Logger.getLogger(MSADUserAccountControlStorageMapper.class); @@ -112,7 +113,7 @@ public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, UserCreden @Override public UserModel proxy(LDAPObject ldapUser, UserModel delegate, RealmModel realm) { - return new MSADUserModelDelegate(delegate, ldapUser); + return new MSADUserModelDelegate(delegate, ldapUser, parseBooleanParameter(mapperModel, ALWAYS_READ_ENABLED_VALUE_FROM_LDAP)); } @Override @@ -122,7 +123,8 @@ public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, Realm @Override public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { - + // check if user is enabled in MSAD or not. + user.setEnabled(!getUserAccountControl(ldapUser).has(UserAccountControl.ACCOUNTDISABLE)); } @Override @@ -228,30 +230,24 @@ private String getRealmName() { public class MSADUserModelDelegate extends TxAwareLDAPUserModelDelegate { private final LDAPObject ldapUser; + private final boolean isAlwaysReadEnabledFromLdap; - public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) { + public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser, boolean isAlwaysReadEnabledFromLdap) { super(delegate, ldapProvider, ldapUser); this.ldapUser = ldapUser; + this.isAlwaysReadEnabledFromLdap = isAlwaysReadEnabledFromLdap; } @Override public boolean isEnabled() { - boolean kcEnabled = super.isEnabled(); - - if (getPwdLastSet() > 0) { - // Merge KC and MSAD - return kcEnabled && !getUserAccountControl(ldapUser).has(UserAccountControl.ACCOUNTDISABLE); - } else { - // If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway - return kcEnabled; + if (isAlwaysReadEnabledFromLdap) { + return !getUserAccountControl(ldapUser).has(UserAccountControl.ACCOUNTDISABLE); } + return super.isEnabled(); } @Override public void setEnabled(boolean enabled) { - // Always update DB - super.setEnabled(enabled); - if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && getPwdLastSet() > 0) { MSADUserAccountControlStorageMapper.logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString()); @@ -266,6 +262,8 @@ public void setEnabled(boolean enabled) { updateUserAccountControl(false, ldapUser, control); } + // Always update DB + super.setEnabled(enabled); } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java index 81566f776509..a4785d864883 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java @@ -22,12 +22,10 @@ import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; -import org.keycloak.storage.UserStorageProvider; -import org.keycloak.storage.ldap.LDAPConfig; +import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory; -import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper; import java.util.ArrayList; import java.util.List; @@ -44,17 +42,25 @@ public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStor configProperties = getConfigProps(null); } - private static List getConfigProps(ComponentModel parent) { - return ProviderConfigurationBuilder.create() + private static List getConfigProps(ComponentModel parentModel) { + UserStorageProviderModel parent = parentModel != null ? new UserStorageProviderModel(parentModel) : new UserStorageProviderModel(); + + ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create() .property().name(MSADUserAccountControlStorageMapper.LDAP_PASSWORD_POLICY_HINTS_ENABLED) .label("Password Policy Hints Enabled") .helpText("Applicable just for writable MSAD. If on, then updating password of MSAD user will use LDAP_SERVER_POLICY_HINTS_OID " + "extension, which means that advanced MSAD password policies like 'password history' or 'minimal password age' will be applied. This extension works just for MSAD 2008 R2 or newer.") .type(ProviderConfigProperty.BOOLEAN_TYPE) .defaultValue("false") - .add() - .build(); + .add(); + if (parent.isImportEnabled()) { + config + .property().name(MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP).label("Always Read Enabled Value From LDAP") + .helpText("If on, the user enabled/disabled state will always be read from MSAD by checking the proper userAccountControl") + .type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add(); + } + return config.build(); } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java index 3bae11aacdad..092297e24d6f 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java @@ -32,6 +32,7 @@ import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator; import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback; +import org.keycloak.storage.ldap.mappers.msad.UserAccountControl; import javax.naming.AuthenticationException; import java.util.Objects; @@ -39,6 +40,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import static org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP; + /** * Mapper specific to MSAD LDS. It's able to read the msDS-UserAccountDisabled, msDS-UserPasswordExpired and pwdLastSet attributes and set actions in Keycloak based on that. * It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 ) @@ -105,7 +108,7 @@ public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, UserCreden @Override public UserModel proxy(LDAPObject ldapUser, UserModel delegate, RealmModel realm) { - return new MSADUserModelDelegate(delegate, ldapUser); + return new MSADUserModelDelegate(delegate, ldapUser, parseBooleanParameter(mapperModel, ALWAYS_READ_ENABLED_VALUE_FROM_LDAP)); } @Override @@ -115,7 +118,8 @@ public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, Realm @Override public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { - + // check if user is enabled in MSAD or not. + user.setEnabled(!Boolean.parseBoolean(ldapUser.getAttributeAsString(LDAPConstants.MSDS_USER_ACCOUNT_DISABLED))); } @Override @@ -174,32 +178,24 @@ protected ModelException processFailedPasswordUpdateException(ModelException e) public class MSADUserModelDelegate extends UserModelDelegate { private final LDAPObject ldapUser; + private final boolean isAlwaysReadEnabledFromLdap; - public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) { + public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser, boolean isAlwaysReadEnabledFromLdap) { super(delegate); this.ldapUser = ldapUser; + this.isAlwaysReadEnabledFromLdap = isAlwaysReadEnabledFromLdap; } @Override public boolean isEnabled() { - boolean kcEnabled = super.isEnabled(); - - // getPwdLastSet() == -1 when is set but not commit in AD LDS (-1 set pwdLastSet time to now) - if (getPwdLastSet() > 0 - || getPwdLastSet() == -1) { - // Merge KC and MSAD LDS - return kcEnabled && !Boolean.parseBoolean(ldapUser.getAttributeAsString(LDAPConstants.MSDS_USER_ACCOUNT_DISABLED)); - } else { - // If new MSAD LDS user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway - return kcEnabled; + if (isAlwaysReadEnabledFromLdap) { + return !Boolean.parseBoolean(ldapUser.getAttributeAsString(LDAPConstants.MSDS_USER_ACCOUNT_DISABLED)); } + return super.isEnabled(); } @Override public void setEnabled(boolean enabled) { - // Always update DB - super.setEnabled(enabled); - if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && getPwdLastSet() > 0) { if (enabled) { logger.debugf("Removing msDS-UserAccountDisabled of user '%s'", ldapUser.getDn().toString()); @@ -209,9 +205,10 @@ public void setEnabled(boolean enabled) { logger.debugf("Setting msDS-UserAccountDisabled of user '%s' to value 'TRUE'", ldapUser.getDn().toString()); ldapUser.setSingleAttribute(LDAPConstants.MSDS_USER_ACCOUNT_DISABLED, "TRUE"); } - ldapProvider.getLdapIdentityStore().update(ldapUser); } + // Always update DB + super.setEnabled(enabled); } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapperFactory.java index 5f5d12e96543..189c9c47f0f8 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapperFactory.java @@ -21,9 +21,12 @@ import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapper; import java.util.ArrayList; import java.util.List; @@ -35,9 +38,23 @@ public class MSADLDSUserAccountControlStorageMapperFactory extends AbstractLDAPStorageMapperFactory { public static final String PROVIDER_ID = LDAPConstants.MSADLDS_USER_ACCOUNT_CONTROL_MAPPER; - protected static final List configProperties = new ArrayList<>(); + protected static final List configProperties; static { + configProperties = getConfigProps(null); + } + + private static List getConfigProps(ComponentModel parentModel) { + UserStorageProviderModel parent = parentModel != null ? new UserStorageProviderModel(parentModel) : new UserStorageProviderModel(); + if (parent.isImportEnabled()) { + ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create() + .property().name(MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP).label("Always Read Enabled Value From LDAP") + .helpText("If on, the user enabled/disabled state will always be read from MSAD LDS by checking the msDS-UserAccountDisabled attribute") + .type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add(); + + return config.build(); + } + return new ArrayList<>(); } @Override @@ -51,6 +68,11 @@ public List getConfigProperties() { return configProperties; } + @Override + public List getConfigProperties(RealmModel realm, ComponentModel parent) { + return getConfigProps(parent); + } + @Override public String getId() { return PROVIDER_ID; diff --git a/federation/pom.xml b/federation/pom.xml index 314e274a73b3..00d6d9eeea96 100755 --- a/federation/pom.xml +++ b/federation/pom.xml @@ -22,7 +22,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml index 72e1c71365b5..931c90ace950 100644 --- a/federation/sssd/pom.xml +++ b/federation/sssd/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml 4.0.0 diff --git a/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java b/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java index e56176b96aa9..cdc0bb48b2ad 100755 --- a/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java +++ b/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java @@ -33,16 +33,15 @@ import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserLookupProvider; -import org.keycloak.userprofile.AttributeContext; import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.UserProfileDecorator; import org.keycloak.userprofile.UserProfileMetadata; +import org.keycloak.userprofile.UserProfileUtil; -import java.util.Collections; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Stream; /** @@ -221,38 +220,29 @@ public Stream getDisableableCredentialTypesStream(RealmModel realm, User } @Override - public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) { - // selector by sssd - Predicate sssdUsersSelector = attributeContext -> - attributeContext.getUser() != null && model.getId().equals(attributeContext.getUser().getFederationLink()); - - // condition to view only by admin - Predicate onlyAdminCondition = context -> metadata.getContext().isAdminContext(); - + public List decorateUserProfile(String providerId, UserProfileMetadata metadata) { // guiOrder if new attributes are needed int guiOrder = (int) metadata.getAttributes().stream() .map(AttributeMetadata::getName) .distinct() .count(); + List metadatas = new ArrayList<>(); + // firstName, lastName, username and email should be read-only for (String attrName : List.of(UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL, UserModel.USERNAME)) { List attrMetadatas = metadata.getAttribute(attrName); if (attrMetadatas.isEmpty()) { logger.debugf("Adding user profile attribute '%s' for sssd provider and context '%s'.", attrName, metadata.getContext()); - AttributeMetadata sssdAttrMetadata = metadata.addAttribute(attrName, guiOrder++, Collections.emptyList()) - .addWriteCondition(AttributeMetadata.ALWAYS_FALSE) - .addReadCondition(onlyAdminCondition) - .setRequired(AttributeMetadata.ALWAYS_FALSE); - sssdAttrMetadata.setSelector(sssdUsersSelector); + metadatas.add(UserProfileUtil.createAttributeMetadata(attrName, metadata, null, guiOrder++, model.getName())); } else { for (AttributeMetadata attrMetadata : attrMetadatas) { logger.debugf("Cloning attribute '%s' as read-only for sssd provider and context '%s'.", attrName, metadata.getContext()); - AttributeMetadata sssdAttrMetadata = metadata.addAttribute(attrMetadata.clone()) - .addWriteCondition(AttributeMetadata.ALWAYS_FALSE); - sssdAttrMetadata.setSelector(sssdUsersSelector); + metadatas.add(attrMetadata.clone().addWriteCondition(AttributeMetadata.ALWAYS_FALSE)); } } } + + return metadatas; } } diff --git a/integration/admin-client-jee/pom.xml b/integration/admin-client-jee/pom.xml index 510faebcafb2..a3fa9c3fec51 100755 --- a/integration/admin-client-jee/pom.xml +++ b/integration/admin-client-jee/pom.xml @@ -22,7 +22,7 @@ keycloak-integration-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index 40293fb70b4f..f7cc6699d27b 100644 --- a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -17,6 +17,7 @@ package org.keycloak.admin.client.resource; +import jakarta.ws.rs.DefaultValue; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -268,7 +269,7 @@ Response testLDAPConnection(@FormParam("action") String action, @FormParam("conn @Path("sessions/{session}") @DELETE - void deleteSession(@PathParam("session") String sessionId); + void deleteSession(@PathParam("session") String sessionId, @DefaultValue("false") @QueryParam("isOffline") boolean offline); @Path("components") ComponentsResource components(); diff --git a/integration/admin-client/pom.xml b/integration/admin-client/pom.xml index b1b9a79cfa2c..9ac7f472974a 100755 --- a/integration/admin-client/pom.xml +++ b/integration/admin-client/pom.xml @@ -22,7 +22,7 @@ keycloak-integration-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 @@ -137,6 +137,20 @@ + + maven-clean-plugin + + + + + src + + **/*.java + + + + + diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml index c0d8ef6358f2..e50632332dcf 100755 --- a/integration/client-cli/admin-cli/pom.xml +++ b/integration/client-cli/admin-cli/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml index 5712241f7dd8..1a80fae45799 100755 --- a/integration/client-cli/client-cli-dist/pom.xml +++ b/integration/client-cli/client-cli-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-client-cli-dist diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml index 2072efa6c751..2536c5d075ba 100755 --- a/integration/client-cli/client-registration-cli/pom.xml +++ b/integration/client-cli/client-registration-cli/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/integration/client-cli/pom.xml b/integration/client-cli/pom.xml index 7ebc9baa4c07..4698ce739b8a 100644 --- a/integration/client-cli/pom.xml +++ b/integration/client-cli/pom.xml @@ -20,7 +20,7 @@ keycloak-integration-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Client CLI diff --git a/integration/client-registration/pom.xml b/integration/client-registration/pom.xml index 9546266379ba..2911874e6631 100755 --- a/integration/client-registration/pom.xml +++ b/integration/client-registration/pom.xml @@ -21,7 +21,7 @@ keycloak-integration-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/integration/pom.xml b/integration/pom.xml index dc6dde5da414..143f6df88ae6 100755 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Integration diff --git a/js/apps/account-ui/pom.xml b/js/apps/account-ui/pom.xml index 10a8081fbee7..6ba44bc644cc 100644 --- a/js/apps/account-ui/pom.xml +++ b/js/apps/account-ui/pom.xml @@ -7,7 +7,7 @@ keycloak-js-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml diff --git a/js/apps/account-ui/src/account-security/AccountRow.tsx b/js/apps/account-ui/src/account-security/AccountRow.tsx index aeac38ee4e76..aa2bd1b56ea8 100644 --- a/js/apps/account-ui/src/account-security/AccountRow.tsx +++ b/js/apps/account-ui/src/account-security/AccountRow.tsx @@ -22,7 +22,11 @@ type AccountRowProps = { refresh: () => void; }; -export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => { +export const AccountRow = ({ + account, + isLinked = false, + refresh, +}: AccountRowProps) => { const { t } = useTranslation(); const context = useEnvironment(); const { addAlert, addError } = useAlerts(); @@ -31,6 +35,7 @@ export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => { try { await unLinkAccount(context, account); addAlert(t("unLinkSuccess")); + refresh(); } catch (error) { addError(t("unLinkError", { error }).toString()); } diff --git a/js/apps/account-ui/src/account-security/SigningIn.tsx b/js/apps/account-ui/src/account-security/SigningIn.tsx index 6e927e0dac65..3b065d65b82b 100644 --- a/js/apps/account-ui/src/account-security/SigningIn.tsx +++ b/js/apps/account-ui/src/account-security/SigningIn.tsx @@ -17,12 +17,10 @@ import { } from "@patternfly/react-core"; import { CSSProperties, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { ContinueCancelModal, useAlerts } from "ui-shared"; -import { deleteCredentials, getCredentials } from "../api/methods"; +import { getCredentials } from "../api/methods"; import { CredentialContainer, CredentialMetadataRepresentation, - CredentialRepresentation, } from "../api/representations"; import { EmptyRow } from "../components/datalist/EmptyRow"; import { Page } from "../components/page/Page"; @@ -68,16 +66,15 @@ const MobileLink = ({ title, onClick, testid }: MobileLinkProps) => { export const SigningIn = () => { const { t } = useTranslation(); const context = useEnvironment(); - const { addAlert, addError } = useAlerts(); const { login } = context.keycloak; const [credentials, setCredentials] = useState(); - const [key, setKey] = useState(1); - const refresh = () => setKey(key + 1); - usePromise((signal) => getCredentials({ signal, context }), setCredentials, [ - key, - ]); + usePromise( + (signal) => getCredentials({ signal, context }), + setCredentials, + [], + ); const credentialRowCells = ( credMetadata: CredentialMetadataRepresentation, @@ -86,8 +83,8 @@ export const SigningIn = () => { const maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as CSSProperties; const items = [ @@ -97,7 +94,10 @@ export const SigningIn = () => { if (credential.createdDate) { items.push( - + {{ date: formatDate(new Date(credential.createdDate)) }} @@ -108,131 +108,126 @@ export const SigningIn = () => { return items; }; - const label = (credential: CredentialRepresentation) => - credential.userLabel || t(credential.type as TFuncKey); - if (!credentials) { return ; } + const credentialUniqueCategories = [ + ...new Set(credentials.map((c) => c.category)), + ]; + return ( - {credentials.map((container) => ( - - - {t(container.category as TFuncKey)} + {credentialUniqueCategories.map((category) => ( + <PageSection key={category} variant="light" className="pf-u-px-0"> + <Title headingLevel="h2" size="xl" id={`${category}-categ-title`}> + {t(category as TFuncKey)} - - - - <span className="cred-title pf-u-display-block"> - {t(container.displayName as TFuncKey)} - </span> - - {t(container.helptext as TFuncKey)} - - {container.createAction && ( - -
- - login({ - action: container.createAction, - }) - } - title={t("setUpNew", { - name: t(container.displayName as TFuncKey), - })} - testid={`${container.category}/create`} - /> -
-
- )} -
+ {credentials + .filter((cred) => cred.category == category) + .map((container) => ( + <> + + + + <span + className="cred-title pf-u-display-block" + data-testid={`${container.type}/title`} + > + {t(container.displayName as TFuncKey)} + </span> + + + {t(container.helptext as TFuncKey)} + + + {container.createAction && ( + +
+ + login({ + action: container.createAction, + }) + } + title={t("setUpNew", { + name: t( + `${container.type}-display-name` as TFuncKey, + ), + })} + testid={`${container.type}/create`} + /> +
+
+ )} +
- - {container.userCredentialMetadatas.length === 0 && ( - - )} + + {container.userCredentialMetadatas.length === 0 && ( + + )} - {container.userCredentialMetadatas.map((meta) => ( - - - - {container.removeable ? ( - { - try { - await deleteCredentials( - context, - meta.credential, - ); - addAlert( - t("successRemovedMessage", { - userLabel: label(meta.credential), - }), - ); - refresh(); - } catch (error) { - addError( - t("errorRemovedMessage", { - userLabel: label(meta.credential), - error, - }).toString(), - ); - } - }} - > - {t("stopUsingCred", { - name: label(meta.credential), - })} - - ) : ( - - )} - , - ]} - /> - - + {container.userCredentialMetadatas.map((meta) => ( + + + + {container.removeable ? ( + + ) : ( + + )} + , + ]} + /> + + + ))} + + ))} -
))}
diff --git a/js/apps/account-ui/src/api/methods.ts b/js/apps/account-ui/src/api/methods.ts index 2a991b3d4399..c735b94deade 100644 --- a/js/apps/account-ui/src/api/methods.ts +++ b/js/apps/account-ui/src/api/methods.ts @@ -124,6 +124,7 @@ export async function unLinkAccount( method: "DELETE", }, ); + if (response.ok) return; return parseResponse(response); } diff --git a/js/apps/account-ui/src/components/datalist/EmptyRow.tsx b/js/apps/account-ui/src/components/datalist/EmptyRow.tsx index 2a4a73d1d516..550767ae967e 100644 --- a/js/apps/account-ui/src/components/datalist/EmptyRow.tsx +++ b/js/apps/account-ui/src/components/datalist/EmptyRow.tsx @@ -9,12 +9,16 @@ type EmptyRowProps = { message: string; }; -export const EmptyRow = ({ message }: EmptyRowProps) => { +export const EmptyRow = ({ message, ...props }: EmptyRowProps) => { return ( {message}
]} + dataListCells={[ + + {message} + , + ]} /> diff --git a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx index 6044d33c8e01..b1fae4f73c7c 100644 --- a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx +++ b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx @@ -13,6 +13,7 @@ import { ErrorOption, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { UserProfileFields, + beerify, debeerify, setUserProfileServerError, useAlerts, @@ -39,7 +40,7 @@ export const PersonalInfo = () => { useState(); const [supportedLocales, setSupportedLocales] = useState([]); const form = useForm({ mode: "onChange" }); - const { handleSubmit, reset, setError } = form; + const { handleSubmit, reset, setValue, setError } = form; const { addAlert, addError } = useAlerts(); usePromise( @@ -52,6 +53,9 @@ export const PersonalInfo = () => { setUserProfileMetadata(personalInfo.userProfileMetadata); setSupportedLocales(supportedLocales); reset(personalInfo); + Object.entries(personalInfo.attributes || {}).forEach(([k, v]) => + setValue(`attributes[${beerify(k)}]`, v), + ); }, ); diff --git a/js/apps/account-ui/src/root/ErrorPage.tsx b/js/apps/account-ui/src/root/ErrorPage.tsx index 79aa3b88b7a3..97ee600e4a61 100644 --- a/js/apps/account-ui/src/root/ErrorPage.tsx +++ b/js/apps/account-ui/src/root/ErrorPage.tsx @@ -10,9 +10,13 @@ import { import { useTranslation } from "react-i18next"; import { isRouteErrorResponse, useRouteError } from "react-router-dom"; -export const ErrorPage = () => { +type ErrorPageProps = { + error?: unknown; +}; + +export const ErrorPage = (props: ErrorPageProps) => { const { t } = useTranslation(); - const error = useRouteError(); + const error = useRouteError() ?? props.error; const errorMessage = getErrorMessage(error); function onRetry() { diff --git a/js/apps/account-ui/src/root/KeycloakContext.tsx b/js/apps/account-ui/src/root/KeycloakContext.tsx index 5047235a553b..7be6bc6f8002 100644 --- a/js/apps/account-ui/src/root/KeycloakContext.tsx +++ b/js/apps/account-ui/src/root/KeycloakContext.tsx @@ -11,6 +11,7 @@ import { } from "react"; import { AlertProvider, Help } from "ui-shared"; import { Environment } from "../environment"; +import { ErrorPage } from "./ErrorPage"; export type KeycloakContext = KeycloakContextProps & { keycloak: Keycloak; @@ -39,6 +40,7 @@ export const KeycloakProvider = ({ }: PropsWithChildren) => { const calledOnce = useRef(false); const [init, setInit] = useState(false); + const [error, setError] = useState(); const keycloak = useMemo( () => new Keycloak({ @@ -54,6 +56,7 @@ export const KeycloakProvider = ({ if (calledOnce.current) { return; } + const init = () => { return keycloak.init({ onLoad: "check-sso", @@ -61,11 +64,21 @@ export const KeycloakProvider = ({ responseMode: "query", }); }; - init().then(() => setInit(true)); + + init() + .then(() => setInit(true)) + .catch((error) => setError(error)); + calledOnce.current = true; }, [keycloak]); - if (!init) return ; + if (error) { + return ; + } + + if (!init) { + return ; + } return ( diff --git a/js/apps/account-ui/test/account-security/signing-in.spec.ts b/js/apps/account-ui/test/account-security/signing-in.spec.ts index 1a6af4c9edc2..81ce20adf301 100644 --- a/js/apps/account-ui/test/account-security/signing-in.spec.ts +++ b/js/apps/account-ui/test/account-security/signing-in.spec.ts @@ -21,26 +21,22 @@ test.describe("Signing in", () => { page.getByTestId("account-security/signing-in").click(); await expect( - page - .getByTestId("basic-authentication/credential-list") - .getByRole("listitem"), + page.getByTestId("password/credential-list").getByRole("listitem"), ).toHaveCount(1); await expect( - page - .getByTestId("basic-authentication/credential-list") - .getByRole("listitem"), + page.getByTestId("password/credential-list").getByRole("listitem"), ).toContainText("My password"); - await expect(page.getByTestId("basic-authentication/create")).toBeHidden(); + await expect(page.getByTestId("password/create")).toBeHidden(); await expect( - page.getByTestId("two-factor/credential-list").getByRole("listitem"), + page.getByTestId("otp/credential-list").getByRole("listitem"), ).toHaveCount(1); await expect( - page.getByTestId("two-factor/credential-list").getByRole("listitem"), + page.getByTestId("otp/credential-list").getByRole("listitem"), ).toContainText("not set up"); - await expect(page.getByTestId("two-factor/create")).toBeVisible(); + await expect(page.getByTestId("otp/create")).toBeVisible(); - await page.getByTestId("two-factor/create").click(); + await page.getByTestId("otp/create").click(); await expect(page.locator("#kc-page-title")).toContainText( "Mobile Authenticator Setup", ); @@ -65,26 +61,22 @@ test.describe("Signing in 2", () => { page.getByTestId("account-security/signing-in").click(); await expect( - page - .getByTestId("basic-authentication/credential-list") - .getByRole("listitem"), + page.getByTestId("password/credential-list").getByRole("listitem"), ).toHaveCount(1); await expect( - page - .getByTestId("basic-authentication/credential-list") - .getByRole("listitem"), + page.getByTestId("password/credential-list").getByRole("listitem"), ).toContainText("not set up"); - await expect(page.getByTestId("basic-authentication/create")).toBeVisible(); + await expect(page.getByTestId("password/create")).toBeVisible(); await expect( - page.getByTestId("two-factor/credential-list").getByRole("listitem"), + page.getByTestId("otp/credential-list").getByRole("listitem"), ).toHaveCount(1); await expect( - page.getByTestId("two-factor/credential-list").getByRole("listitem"), + page.getByTestId("otp/credential-list").getByRole("listitem"), ).toContainText("not set up"); - await expect(page.getByTestId("two-factor/create")).toBeVisible(); + await expect(page.getByTestId("otp/create")).toBeVisible(); - await page.getByTestId("basic-authentication/create").click(); + await page.getByTestId("password/create").click(); await expect(page.locator("#kc-page-title")).toContainText( "Update password", ); diff --git a/js/apps/admin-ui/cypress.config.js b/js/apps/admin-ui/cypress.config.js index f80724266854..9f8b1969eb0f 100644 --- a/js/apps/admin-ui/cypress.config.js +++ b/js/apps/admin-ui/cypress.config.js @@ -1,6 +1,7 @@ import { defineConfig } from "cypress"; import cypressSplit from "cypress-split"; import fs from "node:fs"; +import { isAsyncFunction } from "node:util/types"; const isCI = process.env.CI === "true"; @@ -23,8 +24,10 @@ export default defineConfig({ slowTestThreshold: 30000, specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}", setupNodeEvents(on, config) { - on("after:spec", (spec, results) => { - if (results.video) { + // after:spec collides with cypressSplit function below and is overridden there + + function afterSpecRemoveSuccessfulVideos(spec, results) { + if (results?.video) { // Do we have failures for any retry attempts? const failures = results.tests.some((test) => test.attempts.some((attempt) => attempt.state === "failed"), @@ -32,12 +35,30 @@ export default defineConfig({ if (!failures) { // delete the video if the spec passed and no tests retried - fs.unlinkSync(results.video); + fs.rmSync(results.video, { force: true }); + } + } + } + + function chainedOn(event, callback) { + if (event === "after:spec") { + if (isAsyncFunction(callback)) { + on(event, async (spec, results) => { + afterSpecRemoveSuccessfulVideos(spec, results); + await callback(spec, results); + }); + } else { + on(event, (spec, results) => { + afterSpecRemoveSuccessfulVideos(spec, results); + callback(spec, results); + }); } + } else { + on(event, callback); } - }); + } - cypressSplit(on, config); + cypressSplit(chainedOn, config); return config; }, diff --git a/js/apps/admin-ui/cypress/downloads/downloads.html b/js/apps/admin-ui/cypress/downloads/downloads.html new file mode 100644 index 000000000000..4e6e883a5f39 Binary files /dev/null and b/js/apps/admin-ui/cypress/downloads/downloads.html differ diff --git a/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts b/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts index a245740e4381..082774290687 100644 --- a/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts @@ -102,7 +102,7 @@ describe("Clients test", () => { commonPage .tableUtils() .checkRowItemExists(clientScopeName + 0) - .checkRowItemsEqualTo(2); + .checkRowItemsEqualTo(1); }); it("Should search non-existent client scope by name", () => { @@ -187,7 +187,7 @@ describe("Clients test", () => { commonPage.modalUtils().confirmModal(); commonPage.masthead().checkNotificationMessage(msgScopeMappingRemoved); commonPage.tableToolbarUtils().searchItem(itemName, false); - commonPage.tableUtils().checkRowItemExists(itemName, false); + listingPage.assertNoResults(); }); it("Should remove multiple client scopes from search bar", () => { diff --git a/js/apps/admin-ui/cypress/e2e/realm_roles_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_roles_test.spec.ts index a354aa8a1d6c..b53de502a482 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_roles_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_roles_test.spec.ts @@ -151,7 +151,7 @@ describe("Realm roles test", () => { it("Should search non-existent associated role by name", () => { const itemName = "non-existent-associated-role"; listingPage.searchItem(itemName, false); - cy.findByTestId(listingPage.emptyState).should("exist"); + listingPage.assertNoResults(); }); it("Should hide inherited roles test", () => { diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts index 703f2424d59b..a3e68716ad6f 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts @@ -156,10 +156,10 @@ describe("User profile tabs", () => { it("Removes three validators with the editor", () => { getUserProfileTab(); getJsonEditorTab(); - userProfileTab.typeJSON(removedThree).saveJSON(); - masthead.checkNotificationMessage( - "User profile settings successfully updated.", - ); + userProfileTab + .typeJSON(removedThree) + .saveJSON() + .assertNotificationUpdated(); }); }); @@ -185,7 +185,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertAttributeFieldExists(attrName, false) - .setUsername("testuser7") + .setUsername(`testuser7-${uuid()}`) .create() .assertNotificationCreated() .assertAttributeFieldExists(attrName, false); @@ -218,7 +218,7 @@ describe("User profile tabs", () => { sidebarPage.goToUsers(); createUserPage .goToCreateUser() - .setAttributeValue(emailAttributeName, "testuser8@gmail.com") + .setAttributeValue(emailAttributeName, `testuser8-${uuid()}@gmail.com`) .assertAttributeFieldExists(attrName, false) .create() .assertNotificationCreated(); @@ -226,7 +226,7 @@ describe("User profile tabs", () => { // Edit user createUserPage .assertAttributeFieldExists(attrName, false) - .setAttributeValue(emailAttributeName, "testuser9@gmail.com") + .setAttributeValue(emailAttributeName, `testuser9-${uuid()}@gmail.com`) .update() .assertNotificationUpdated(); }); @@ -251,7 +251,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertAttributeFieldExists(attrName, true) - .setUsername("testuser10") + .setUsername(`testuser10-${uuid()}`) .create() .assertNotificationCreated() .assertAttributeFieldExists(attrName, true); @@ -270,7 +270,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertAttributeLabel(attrName, attrName) - .setUsername("testuser11") + .setUsername(`testuser11-${uuid()}`) .create() .assertValidationErrorRequired(attrName); @@ -293,7 +293,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertAttributeLabel(attrName, attrName) - .setUsername("testuser12") + .setUsername(`testuser12-${uuid()}`) .setAttributeValue(attrName, "MyAttribute") .create() .assertNotificationCreated(); @@ -306,7 +306,8 @@ describe("User profile tabs", () => { getAttributesGroupTab() .clickOnCreatesAttributesGroupButton() .createAttributeGroup(group, group) - .saveAttributesGroupCreation(); + .saveAttributesGroupCreation() + .assertNotificationUpdated(); getAttributesTab(); userProfileTab @@ -320,7 +321,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertGroupDisplayName(group, group) - .setUsername("testuser14") + .setUsername(`testuser14-${uuid()}`) .create() .assertNotificationCreated(); @@ -342,7 +343,8 @@ describe("User profile tabs", () => { getAttributesGroupTab() .clickOnCreatesAttributesGroupButton() .createAttributeGroup(group, group) - .saveAttributesGroupCreation(); + .saveAttributesGroupCreation() + .assertNotificationUpdated(); createAttributeDefinition(attrName, (attrConfigurer) => attrConfigurer.setAllAttributePermissions(), @@ -361,7 +363,7 @@ describe("User profile tabs", () => { .goToCreateUser() .assertGroupDisplayName(group, group) .assertAttributeLabel(attrName, attrName) - .setUsername("testuser13") + .setUsername(`testuser13-${uuid()}`) .setAttributeValue(attrName, initialAttrValue) .create() .assertNotificationCreated() diff --git a/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts index 169a70198c31..91084fc06fe8 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_test.spec.ts @@ -7,6 +7,7 @@ import adminClient from "../support/util/AdminClient"; import { keycloakBefore } from "../support/util/keycloak_hooks"; import RealmSettings from "../support/pages/admin-ui/configure/realm_settings/RealmSettings"; import ModalUtils from "../support/util/ModalUtils"; +import CommonPage from "../support/pages/CommonPage"; const masthead = new Masthead(); const loginPage = new LoginPage(); @@ -14,11 +15,13 @@ const sidebarPage = new SidebarPage(); const createRealmPage = new CreateRealmPage(); const realmSettings = new RealmSettings(); const modalUtils = new ModalUtils(); +const commonPage = new CommonPage(); const testRealmName = "Test-realm-" + uuid(); const newRealmName = "New-Test-realm-" + uuid(); const editedRealmName = "Edited-Test-realm-" + uuid(); const testDisabledName = "Test-Disabled"; +const specialCharsName = "%22-" + uuid(); describe("Realm tests", () => { beforeEach(() => { @@ -28,8 +31,8 @@ describe("Realm tests", () => { after(() => Promise.all( - [testRealmName, newRealmName, editedRealmName].map((realm) => - adminClient.deleteRealm(realm), + [testRealmName, newRealmName, editedRealmName, specialCharsName].map( + (realm) => adminClient.deleteRealm(realm), ), ), ); @@ -117,4 +120,13 @@ describe("Realm tests", () => { .getCurrentRealm() .should("eq", testRealmName); }); + + it("should create realm with special characters", () => { + sidebarPage.goToCreateRealm(); + createRealmPage.fillRealmName(specialCharsName).createRealm(); + + sidebarPage.goToRealm(specialCharsName); + sidebarPage.goToClients(); + commonPage.tableUtils().checkRowItemExists("account"); + }); }); diff --git a/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts b/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts index a3efd75994e0..113523f2f24f 100644 --- a/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/sessions_test.spec.ts @@ -96,11 +96,11 @@ describe("Sessions test", () => { sidebarPage.waitForPageLoad(); // Now check that offline session exists (online one has been logged off above) - // and that it is not possible to sign it out + // and that it is possible to revoke it commonPage .tableUtils() .checkRowItemExists(username) - .assertRowItemActionDoesNotExist(username, "Sign out"); + .selectRowItemAction(username, "Revoke"); }); }); diff --git a/js/apps/admin-ui/cypress/e2e/user_fed_ldap_mapper_test.spec.ts b/js/apps/admin-ui/cypress/e2e/user_fed_ldap_mapper_test.spec.ts index c0df367fe065..737c4323351c 100644 --- a/js/apps/admin-ui/cypress/e2e/user_fed_ldap_mapper_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/user_fed_ldap_mapper_test.spec.ts @@ -264,7 +264,7 @@ describe("User Fed LDAP mapper tests", () => { providersPage.clickExistingCard(ldapName); providersPage.goToMappers(); listingPage.searchItem(nonexistingSearchTerm, false); - cy.findByTestId(listingPage.emptyState).should("exist"); + listingPage.assertNoResults(); }); // *** test cleanup *** diff --git a/js/apps/admin-ui/cypress/e2e/users_test.spec.ts b/js/apps/admin-ui/cypress/e2e/users_test.spec.ts index 3f019a2af9d7..a9e9eb080c21 100644 --- a/js/apps/admin-ui/cypress/e2e/users_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/users_test.spec.ts @@ -127,7 +127,7 @@ describe("User creation", () => { it("Search non-existing user test", () => { listingPage.searchItem("user_DNE"); - cy.findByTestId(listingPage.emptyState).should("exist"); + listingPage.assertNoResults(); }); it("User details test", () => { diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts index 1f10227eebb1..5d05ebde4a5b 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts @@ -34,7 +34,7 @@ export default class ListingPage extends CommonElements { #itemsRows = "table:visible"; #deleteUserButton = "delete-user-btn"; #emptyListImg = '[role="tabpanel"]:not([hidden]) [data-testid="empty-state"]'; - public emptyState = "empty-state"; + #emptyState = "empty-state"; #itemRowDrpDwn = ".pf-c-dropdown__toggle"; #itemRowSelect = ".pf-c-select__toggle:nth-child(1)"; #itemRowSelectItem = ".pf-c-select__menu-item"; @@ -401,6 +401,10 @@ export default class ListingPage extends CommonElements { return this; } + assertNoResults() { + cy.findByTestId(this.#emptyState).should("exist"); + } + assertDefaultResource() { this.assertResource("Default Resource"); return this; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/Masthead.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/Masthead.ts index d79f4420d044..fa65da038280 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/Masthead.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/Masthead.ts @@ -94,16 +94,27 @@ export default class Masthead extends CommonElements { cy.get("#manage-account").click(); } - checkNotificationMessage(message: string, closeNotification = true) { - this.#getAlertsContainer() - .find(this.#alertMessage) - .should("contain.text", message); - - if (closeNotification) { + checkNotificationMessage(message: string | RegExp, closeNotification = true) { + if (typeof message === "string") { + this.#getAlertsContainer() + .find(this.#alertMessage) + .should("contain.text", message); + + if (closeNotification) { + this.#getAlertsContainer() + .find(`button[title="` + message.replaceAll('"', '\\"') + `"]`) + .last() + .click({ force: true }); + } + } else { this.#getAlertsContainer() - .find(`button[title="` + message.replaceAll('"', '\\"') + `"]`) - .last() - .click({ force: true }); + .find(this.#alertMessage) + .invoke("text") + .should("match", message); + + if (closeNotification) { + this.#getAlertsContainer().find("button").last().click({ force: true }); + } } return this; } diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts index fcae54db787c..9de6fd45cb06 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/components/TablePage.ts @@ -5,14 +5,12 @@ export default class TablePage extends CommonElements { #tableRowItemChckBx: string; #tableHeaderRowItem: string; #tableInModal: boolean; - static tableSelector = "table[aria-label]"; + static tableSelector = ".pf-c-table"; constructor(parentElement?: string) { super(parentElement ?? TablePage.tableSelector + ":visible"); - this.#tableRowItem = - this.parentSelector + "tbody tr[data-ouia-component-type]"; - this.#tableHeaderRowItem = - this.parentSelector + "thead tr[data-ouia-component-type]"; + this.#tableRowItem = this.parentSelector + "tbody tr"; + this.#tableHeaderRowItem = this.parentSelector + "thead tr"; this.#tableRowItemChckBx = ".pf-c-table__check"; this.#tableInModal = false; } @@ -53,24 +51,6 @@ export default class TablePage extends CommonElements { return this; } - assertRowItemActionDoesNotExist(itemName: string, actionItemName: string) { - cy.get( - (this.#tableInModal ? ".pf-c-modal-box.pf-m-md " : "") + - this.#tableRowItem, - ) - .contains(itemName) - .parentsUntil("tbody") - .then(($tbody) => { - if ($tbody.find(".pf-c-dropdown__toggle").length > 0) { - $tbody.find(".pf-c-dropdown__toggle").click(); - cy.get(this.dropdownMenuItem) - .contains(actionItemName) - .should("not.exist"); - } - }); - return this; - } - #getRowItemAction(itemName: string, actionItemName: string) { return cy .get( diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/SettingsTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/SettingsTab.ts index 0331c6958cfa..9fc1c94e6cfa 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/SettingsTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/SettingsTab.ts @@ -221,7 +221,7 @@ export default class SettingsTab extends PageObject { public assertAccessSettings() { const redirectUriError = - "Client could not be updated: A redirect URI is not a valid URI"; + /Client could not be updated:.*(Master SAML Processing URL is not a valid URL|A redirect URI is not a valid URI).*/i; cy.findByTestId(this.#idpInitiatedSsoUrlName).click().type("a"); cy.findByTestId(this.#idpInitiatedSsoRelayState).click().type("b"); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts index 70b689aebd64..7db74875431f 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts @@ -216,6 +216,14 @@ export default class UserProfile { return this; } + assertNotificationUpdated() { + this.masthead.checkNotificationMessage( + "User profile settings successfully updated.", + ); + + return this; + } + shouldHaveText(text: string) { this.#getText().should("have.text", text); return this; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts index a9d43a9ed17a..0ae12d78a987 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts @@ -186,7 +186,8 @@ export default class CreateUserPage { } setAttributeValue(attrName: string, value: string) { - cy.findByTestId(attrName).clear().type(value); + cy.findByTestId(attrName).as("attr").clear(); + cy.get("@attr").type(value); return this; } diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ca.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ca.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ca.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ca.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_de.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_de.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_de.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_de.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_es.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_es.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_fr.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_fr.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ja.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ja.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_lt.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_lt.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_lt.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_lt.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_no.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_no.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_no.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_no.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pl.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_pl.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pl.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_pl.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_pt_BR.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_pt_BR.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ru.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_ru.properties diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_zh_CN.properties b/js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_zh_CN.properties similarity index 100% rename from js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_zh_CN.properties rename to js/apps/admin-ui/maven-resources-community/theme/primesign.v2/admin/messages/messages_zh_CN.properties diff --git a/js/apps/admin-ui/maven-resources/META-INF/keycloak-themes.json b/js/apps/admin-ui/maven-resources/META-INF/keycloak-themes.json index 4bd655870214..4d95deafedd8 100644 --- a/js/apps/admin-ui/maven-resources/META-INF/keycloak-themes.json +++ b/js/apps/admin-ui/maven-resources/META-INF/keycloak-themes.json @@ -1,7 +1,7 @@ { "themes": [ { - "name": "keycloak.v2", + "name": "primesign.v2", "types": [ "admin" ] diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/primesign.v2/admin/messages/messages_en.properties similarity index 98% rename from js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties rename to js/apps/admin-ui/maven-resources/theme/primesign.v2/admin/messages/messages_en.properties index 371ff5c3cecb..1db665f69e9b 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/primesign.v2/admin/messages/messages_en.properties @@ -2126,7 +2126,7 @@ configureMappingDescription=Choose any of the mappings from this table keystorePassword=Keystore password mapperTypeHardcodedLdapRoleMapperHelp=Users imported from LDAP will be automatically added into this configured role. more={{count}} more -clientNameHelp=Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example\: ${my_client} +clientNameHelp=Specifies the display name of the client. For example 'My Client'. Supports keys for localized values as well. For example\: ${my_client} mappersList=Mappers list rootUrl=Root URL realmExplain=A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control. @@ -3075,4 +3075,68 @@ to the attribute. For that, make sure to use any of the built-in validators to p sendIdTokenOnLogout=Send 'id_token_hint' in logout requests sendIdTokenOnLogoutHelp=If the 'id_token_hint' parameter should be sent in logout requests. sendClientIdOnLogout=Send 'client_id' in logout requests -sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests. \ No newline at end of file +sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests. +searchClientRegistration=Search for policy +importFileHelp=File to import a key + +accountIdLabel=Account ID +accountIdHelpText=ID of the client, which is stored at the metering service. + +smsTitle=SMS Policy +smsDefaultGateway=Default gateway +smsFallbackGateway=Fallback gateway +smsTimeout=Timeout +smsTanLength=TAN length +smsRefLength=Reference value length +smsResendDelay=Resend TAN delay +smsResendMaxRetries=Resend TAN maximum retries +smsUpdateSuccess=SMS policy successfully updated +smsUpdateError=Could not update SMS policy: {{error}} +smsatTitle=SMS.AT +smsatProviderUrl=Override Provider URL +smsatSenderAddress=Sender address +smsatSenderAddressTypeTitle=Sender address type +smsatSenderAddressTypeNational=National +smsatSenderAddressTypeInternational=International +smsatSenderAddressTypeAlphanumeric=Alphanumeric +smsatSenderAddressTypeShortcode=Shortcode +smsatAuthMethodTitle=Authentication method +smsatAuthMethodBasic=Username / Password +smsatAuthMethodToken=API Token +smsatUsername=Username +smsatPassword=Password +smsatApiToken=API token +smsatConnectTimeout=Connect timeout +smsatReadTimeout=Read timeout +nexmoTitle=NEXMO +nexmoProviderUrl=Override Provider URL +nexmoSenderId=Sender ID +nexmoApiKey=API Key +nexmoApiSecret=API Secret +nexmoConnectTimeout=Connect timeout +nexmoReadTimeout=Read timeout +smsDefaultGatewayHelpText=The default gateway is used to send the first SMS. +smsFallbackGatewayHelpText=The fallback gateway is used to resend an SMS. +smsTimeoutHelpText=The lifetime of a SMS-TAN until it expires and is no longer valid. In case of resend TAN the lifetime is resetted, meaning every SMS-TAN is valid for 5 minutes. +smsTanLengthHelpText=The length of the SMS-TAN. +smsRefLengthHelpText=The length of the reference value (displayed as the transaction-ID in the SMS-TAN). +smsResendDelayHelpText=The delay a user has to wait until he can request a new SMS. +smsResendMaxRetriesHelpText=The maximum number of SMS-TAN send retries a user can request. +smsatProviderUrlHelpText=Overrides the default URL of the SMS.AT gateway provider., +smsatSenderAddressHelpText=Address of the sender (assigned to the account) from which the message is sent, e.g. PrimeSign., +smsatSenderAddressTypeHelpText=The sender address type. The following address types are supported: national, international, alphanumeric oshortcode). We currently use alphanumeric. +smsatAuthMethodHelpText=How to authenticate at the REST API. +smsatUsernameHelpText=Authenticate with this username. +smsatPasswordHelpText=Authenticate with this password. +smsatApiTokenHelpText=The API Token of SMS-AT. +smsatConnectTimeoutHelpText=How many seconds to wait for the server connection. +smsatReadTimeoutHelpText=How many seconds to wait for a response. +nexmoProviderUrlHelpText=Overrides the default URL for the NEXMO gateway provider. +nexmoSenderIdHelpText=Name or phone number of the sender. +nexmoApiKeyHelpText=Authenticate with this API key. +nexmoApiSecretHelpText=Authenticate with this API secret. +nexmoConnectTimeoutHelpText=How many seconds to wait for the server connection. +nexmoReadTimeoutHelpText=How many seconds to wait for a response. + +addIdgProvider=Add IDG Provider +identityProvidersHelp=The aliases of the allowed identity providers, seperated by comma. \ No newline at end of file diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/theme.properties b/js/apps/admin-ui/maven-resources/theme/primesign.v2/admin/theme.properties similarity index 100% rename from js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/theme.properties rename to js/apps/admin-ui/maven-resources/theme/primesign.v2/admin/theme.properties diff --git a/js/apps/admin-ui/pom.xml b/js/apps/admin-ui/pom.xml index 2407dc3daca8..cb290266d4a2 100644 --- a/js/apps/admin-ui/pom.xml +++ b/js/apps/admin-ui/pom.xml @@ -7,14 +7,14 @@ keycloak-js-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml keycloak-admin-ui Keycloak Admin UI - The user inferface to administrate the Keycloak server. + The user interface to administrate the Keycloak server. @@ -25,28 +25,28 @@ - - - maven-resources-plugin - - - copy-resources - generate-resources - - copy-resources - - - ${project.build.outputDirectory} - - - maven-resources-community - - - - - - - + + + maven-resources-plugin + + + copy-resources + generate-resources + + copy-resources + + + ${project.build.outputDirectory} + + + maven-resources-community + + + + + + + @@ -58,7 +58,7 @@ dist - theme/keycloak.v2/admin/resources + theme/primesign.v2/admin/resources index.html locales/** @@ -109,7 +109,7 @@ dist/index.html - target/classes/theme/keycloak.v2/admin/index.ftl + target/classes/theme/primesign.v2/admin/index.ftl false @@ -124,10 +124,6 @@ ]]> ]]> - - ]]> - ]]> - Keycloak Administration UI]]> diff --git a/js/apps/admin-ui/src/PageNav.tsx b/js/apps/admin-ui/src/PageNav.tsx index eb169fd251df..b00df3e24bc1 100644 --- a/js/apps/admin-ui/src/PageNav.tsx +++ b/js/apps/admin-ui/src/PageNav.tsx @@ -25,6 +25,7 @@ const LeftNav = ({ title, path, id }: LeftNavProps) => { const { t } = useTranslation(); const { hasAccess } = useAccess(); const { realm } = useRealm(); + const encodedRealm = encodeURIComponent(realm); const route = routes.find( (route) => route.path.replace(/\/:.+?(\?|(?:(?!\/).)*|$)/g, "") === (id || path), @@ -44,7 +45,7 @@ const LeftNav = ({ title, path, id }: LeftNavProps) => {
  • `pf-c-nav__link${isActive ? " pf-m-current" : ""}` } diff --git a/js/apps/admin-ui/src/authentication/execution-model.ts b/js/apps/admin-ui/src/authentication/execution-model.ts index 89e063c059d2..672fbfd00d18 100644 --- a/js/apps/admin-ui/src/authentication/execution-model.ts +++ b/js/apps/admin-ui/src/authentication/execution-model.ts @@ -98,17 +98,15 @@ export class ExecutionList { return found; } - #getParentNodes(level?: number) { - for (let index = 0; index < this.#list.length; index++) { - const ex = this.#list[index]; - if ( - index + 1 < this.#list.length && - this.#list[index + 1].level! > ex.level! && - ex.level! + 1 === level - ) { - return ex; + #getParentNodes(level: number, index: number) { + let parent = undefined; + for (let i = 0; i < index; i++) { + const ex = this.#list[i]; + if (level - 1 === ex.level) { + parent = ex; } } + return parent; } getChange( @@ -117,13 +115,14 @@ export class ExecutionList { ) { const currentOrder = this.order(); const newLocIndex = order.findIndex((id) => id === changed.id); - const oldLocation = - currentOrder[currentOrder.findIndex((ex) => ex.id === changed.id)]; + const oldLocIndex = currentOrder.findIndex((ex) => ex.id === changed.id); + const oldLocation = currentOrder[oldLocIndex]; const newLocation = currentOrder[newLocIndex]; - if (newLocation.level !== oldLocation.level) { + const currentParent = this.#getParentNodes(oldLocation.level!, oldLocIndex); + const parent = this.#getParentNodes(newLocation.level!, newLocIndex); + if (currentParent?.id !== parent?.id) { if (newLocation.level! > 0) { - const parent = this.#getParentNodes(newLocation.level); return new LevelChange( parent?.executionList?.length || 0, newLocation.index!, diff --git a/js/apps/admin-ui/src/authentication/form/CreateFlow.tsx b/js/apps/admin-ui/src/authentication/form/CreateFlow.tsx index 798d142ca11e..3c632f7650eb 100644 --- a/js/apps/admin-ui/src/authentication/form/CreateFlow.tsx +++ b/js/apps/admin-ui/src/authentication/form/CreateFlow.tsx @@ -69,7 +69,7 @@ export default function CreateFlow() { label={t("flowType")} labelIcon={t("topLevelFlowTypeHelp")} aria-label={t("selectFlowType")} - controller={{ defaultValue: "" }} + controller={{ defaultValue: TYPES[0] }} options={TYPES.map((type) => ({ key: type, value: t(`top-level-flow-type.${type}`), diff --git a/js/apps/admin-ui/src/authentication/policies/Policies.tsx b/js/apps/admin-ui/src/authentication/policies/Policies.tsx index bdb1bf384b45..7b5436cbbc61 100644 --- a/js/apps/admin-ui/src/authentication/policies/Policies.tsx +++ b/js/apps/admin-ui/src/authentication/policies/Policies.tsx @@ -11,6 +11,7 @@ import { CibaPolicy } from "./CibaPolicy"; import { OtpPolicy } from "./OtpPolicy"; import { PasswordPolicy } from "./PasswordPolicy"; import { WebauthnPolicy } from "./WebauthnPolicy"; +import { SmsPolicy } from "./SmsPolicy"; export const Policies = () => { const { t } = useTranslation(); @@ -78,6 +79,13 @@ export const Policies = () => { > + {t("smsTitle")}} + > + + ); }; diff --git a/js/apps/admin-ui/src/authentication/policies/SmsPolicy.tsx b/js/apps/admin-ui/src/authentication/policies/SmsPolicy.tsx new file mode 100644 index 000000000000..265b38d30af2 --- /dev/null +++ b/js/apps/admin-ui/src/authentication/policies/SmsPolicy.tsx @@ -0,0 +1,877 @@ +import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; +import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionToggle, + ActionGroup, + AlertVariant, + Button, + ButtonVariant, + FormGroup, + NumberInput, + PageSection, + Select, + SelectOption, + SelectVariant, + ValidatedOptions, +} from "@patternfly/react-core"; +import { FormAccess } from "../../components/form/FormAccess"; +import { Controller, useForm } from "react-hook-form"; +import useToggle from "../../utils/useToggle"; +import { adminClient } from "../../admin-client"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAlerts } from "../../components/alert/Alerts"; +import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; +import { HelpItem } from "ui-shared"; + +type SmsPolicyProps = { + realm: RealmRepresentation; + realmUpdated: (realm: RealmRepresentation) => void; +}; + +export const SmsPolicy = ({ realm, realmUpdated }: SmsPolicyProps) => { + const { t } = useTranslation(); + const { + control, + register, + reset, + handleSubmit, + formState: { isDirty }, + } = useForm({ mode: "onChange" }); + + const { realm: realmName } = useRealm(); + const { addAlert, addError } = useAlerts(); + const [openDefaultGateway, toggleDefaultGateway] = useToggle(); + const [openFallbackGateway, toggleFallbackGateway] = useToggle(); + const [openSenderAddressType, toggleSenderAddressType] = useToggle(); + const [openAuthMethod, toggleAuthMethod] = useToggle(); + + const [expanded, setExpanded] = useState([""]); + const [smsatAuthMethod, setSmsAuthMethod] = useState("basic"); + + const toggleAccordion = (id: any) => { + const index = expanded.indexOf(id); + const newExpanded: string[] = + index < 0 + ? [...expanded, id] + : [ + ...expanded.slice(0, index), + ...expanded.slice(index + 1, expanded.length), + ]; + setExpanded(newExpanded); + }; + + const SMS_GATEWAYS = [ + { + key: "smsat", + title: t("smsatTitle"), + }, + { + key: "nexmo", + title: t("nexmoTitle"), + }, + ] as const; + + const SENDER_ADDRESS_TYPES = [ + { + key: "national", + title: t("smsatSenderAddressTypeNational"), + }, + { + key: "international", + title: t("smsatSenderAddressTypeInternational"), + }, + { + key: "alphanumeric", + title: t("smsatSenderAddressTypeAlphanumeric"), + }, + { + key: "shortcode", + title: t("smsatSenderAddressTypeShortcode"), + }, + ] as const; + + const AUTH_METHOD = [ + { + key: "basic", + title: t("smsatAuthMethodBasic"), + }, + { + key: "token", + title: t("smsatAuthMethodToken"), + }, + ] as const; + + const setupForm = (realm: RealmRepresentation) => { + reset({ + ...realm, + }); + setSmsAuthMethod( + realm.attributes?.["smsatAuthMethod"] + ? realm.attributes["smsatAuthMethod"] + : "basic", + ); + }; + + useEffect(() => setupForm(realm), []); + + const prepareRealmAttributes = async ( + updatedAttributes: RealmRepresentation, + ) => { + const currentRealm = await adminClient.realms.findOne({ + realm: realmName, + }); + if (currentRealm) { + realm.attributes = { + ...currentRealm.attributes, + ...updatedAttributes.attributes, + }; + return realm; + } else { + throw "Realm not found."; + } + }; + + const save = async (input: RealmRepresentation) => { + try { + const merge = await prepareRealmAttributes(input); + await adminClient.realms.update({ realm: realmName }, merge); + const updatedRealm = await adminClient.realms.findOne({ + realm: realmName, + }); + realmUpdated(updatedRealm!); + setupForm(updatedRealm!); + addAlert(t("smsUpdateSuccess"), AlertVariant.success); + } catch (error) { + addError("smsUpdateError", error); + } + }; + + return ( + + + + } + fieldId="attributes.smsDefaultGateway" + > + ( + + )} + /> + + + } + fieldId="attributes.smsFallbackGateway" + > + ( + + )} + /> + + + } + fieldId="attributes.smsTimeout" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 5); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.smsTanLength" + > + { + const MIN_VALUE = 1; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 6); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.smsRefLength" + > + { + const MIN_VALUE = 1; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 6); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.smsResendDelay" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 30); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.smsResendMaxRetries" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 3); + }} + /> + ); + }} + /> + + + + + toggleAccordion("smsat-toggle")} + isExpanded={expanded.includes("smsat-toggle")} + id="smsat-toggle" + > + {t("smsatTitle")} + + + + } + label={t("smsatProviderUrl")} + fieldId="attributes.smsatProviderUrl" + validated={ValidatedOptions.default} + > + + + + } + label={t("smsatSenderAddress")} + fieldId="attributes.smsatSenderAddress" + validated={ValidatedOptions.default} + > + + + + } + fieldId="attributes.smsatSenderAddressType" + > + ( + + )} + /> + + + } + fieldId="attributes.smsatAuthMethod" + > + ( + + )} + /> + + + + + } + fieldId="attributes.smsatConnectTimeout" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 0); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.smsatReadTimeout" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 0); + }} + /> + ); + }} + /> + + + + + toggleAccordion("nexmo-toggle")} + isExpanded={expanded.includes("nexmo-toggle")} + id="nexmo-toggle" + > + {t("nexmoTitle")} + + + + } + label={t("nexmoProviderUrl")} + fieldId="attributes.nexmoProviderUrl" + validated={ValidatedOptions.default} + > + + + + } + label={t("nexmoSenderId")} + fieldId="attributes.nexmoSenderId" + validated={ValidatedOptions.default} + > + + + + } + label={t("nexmoApiKey")} + fieldId="attributes.nexmoApiKey" + validated={ValidatedOptions.default} + > + + + + } + label={t("nexmoApiSecret")} + fieldId="attributes.nexmoApiSecret" + validated={ValidatedOptions.default} + > + + + + } + fieldId="attributes.nexmoConnectTimeout" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 0); + }} + /> + ); + }} + /> + + + } + fieldId="attributes.nexmoReadTimeout" + > + { + const MIN_VALUE = 0; + const setValue = (newValue: number) => + field.onChange(Math.max(newValue, MIN_VALUE)); + + return ( + setValue(parseInt(field.value) + 1)} + onMinus={() => setValue(parseInt(field.value) - 1)} + onChange={(event) => { + const newValue = Number(event.currentTarget.value); + setValue(!isNaN(newValue) ? newValue : 0); + }} + /> + ); + }} + /> + + + + + + + + + + + + ); +}; diff --git a/js/apps/admin-ui/src/client-scopes/details/MappingDetails.tsx b/js/apps/admin-ui/src/client-scopes/details/MappingDetails.tsx index d372ef76e4cc..7143db9e303e 100644 --- a/js/apps/admin-ui/src/client-scopes/details/MappingDetails.tsx +++ b/js/apps/admin-ui/src/client-scopes/details/MappingDetails.tsx @@ -14,9 +14,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, useMatch, useNavigate } from "react-router-dom"; import { KeycloakTextInput, TextControl } from "ui-shared"; - import { adminClient } from "../../admin-client"; -import { toClient } from "../../clients/routes/Client"; import { useAlerts } from "../../components/alert/Alerts"; import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; import { DynamicComponents } from "../../components/dynamic/DynamicComponents"; @@ -29,6 +27,7 @@ import { useFetch } from "../../utils/useFetch"; import { useParams } from "../../utils/useParams"; import { toClientScope } from "../routes/ClientScope"; import { MapperParams, MapperRoute } from "../routes/Mapper"; +import { toDedicatedScope } from "../../clients/routes/DedicatedScopeDetails"; export default function MappingDetails() { const { t } = useTranslation(); @@ -53,7 +52,7 @@ export default function MappingDetails() { const toDetails = () => isOnClientScope ? toClientScope({ realm, id, tab: "mappers" }) - : toClient({ realm, clientId: id, tab: "mappers" }); + : toDedicatedScope({ realm, clientId: id, tab: "mappers" }); useFetch( async () => { diff --git a/js/apps/admin-ui/src/clients/ClientDescription.tsx b/js/apps/admin-ui/src/clients/ClientDescription.tsx index 4d08ac0a22a6..7277fa47c39e 100644 --- a/js/apps/admin-ui/src/clients/ClientDescription.tsx +++ b/js/apps/admin-ui/src/clients/ClientDescription.tsx @@ -37,6 +37,11 @@ export const ClientDescription = ({ }, }} /> + + + } + > + + { name="extendChildren" isChecked={field.value} onChange={field.onChange} - isDisabled={group.subGroups?.length === 0} + isDisabled={group.subGroupCount === 0} /> )} /> diff --git a/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx b/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx index 42084b7ca8ef..df9a52137ba4 100644 --- a/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx +++ b/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx @@ -104,7 +104,7 @@ export const ClientRegistrationList = ({ row.name); + setRest( clientScopes .filter((scope) => !names.includes(scope.name)) .filter((scope) => scope.protocol === protocol), ); - const filter = - searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType); - const firstNum = Number(first); - const page = localeSort(rows.filter(filter), mapByKey("name")); + rows = localeSort(rows, mapByKey("name")); if (isViewer) { - page.unshift({ + rows.unshift({ id: DEDICATED_ROW, name: t("dedicatedScopeName", { clientName }), type: AllClientScopes.none, @@ -187,7 +185,11 @@ export const ClientScopes = ({ }); } - return page.slice(firstNum, firstNum + Number(max)); + const filter = + searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType); + const firstNum = Number(first); + + return rows.filter(filter).slice(firstNum, firstNum + Number(max)); }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ diff --git a/js/apps/admin-ui/src/components/group/GroupPickerDialog.tsx b/js/apps/admin-ui/src/components/group/GroupPickerDialog.tsx index ad7fbb3429fd..6980434f44f2 100644 --- a/js/apps/admin-ui/src/components/group/GroupPickerDialog.tsx +++ b/js/apps/admin-ui/src/components/group/GroupPickerDialog.tsx @@ -226,18 +226,20 @@ export const GroupPickerDialog = ({ .slice(groupId ? first : 0, max + (groupId ? first : 0)) .map((group: SelectableGroup) => ( - + {(!isSearching || group.name?.includes(filter)) && ( + + )} {isSearching && group.subGroups?.map((g) => ( (); const [selectedRows, setSelectedRows] = useState([]); const [searchType, setSearchType] = useState("default"); + const [searchDropdownOpen, setSearchDropdownOpen] = useState(false); const [activeFilters, setActiveFilters] = useState([]); const [profile, setProfile] = useState({}); const [query, setQuery] = useState(""); @@ -153,7 +154,7 @@ export function UserDataTable() { params.search = searchParam; } - if (!listUsers && !searchParam) { + if (!listUsers && !(params.search || params.q)) { return []; } @@ -277,6 +278,8 @@ export function UserDataTable() { const toolbar = () => { return ( void; realm: RealmRepresentation; hasSelectedRows: boolean; toggleDeleteDialog: () => void; @@ -40,6 +42,8 @@ type UserDataTableToolbarItemsProps = { }; export function UserDataTableToolbarItems({ + searchDropdownOpen, + setSearchDropdownOpen, realm, hasSelectedRows, toggleDeleteDialog, @@ -59,7 +63,6 @@ export function UserDataTableToolbarItems({ }: UserDataTableToolbarItemsProps) { const { t } = useTranslation(); const [kebabOpen, setKebabOpen] = useState(false); - const [searchDropdownOpen, setSearchDropdownOpen] = useState(false); const { hasAccess } = useAccess(); @@ -130,13 +133,19 @@ export function UserDataTableToolbarItems({ setActiveFilters={setActiveFilters} profile={profile} createAttributeSearchChips={createAttributeSearchChips} - searchUserWithAttributes={searchUserWithAttributes} + searchUserWithAttributes={() => { + searchUserWithAttributes(); + setSearchDropdownOpen(false); + }} /> + + + + + + + ); +} diff --git a/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx index 0f724be18b4a..9b3e037b32f7 100644 --- a/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx @@ -66,6 +66,7 @@ import { OIDCAuthentication } from "./OIDCAuthentication"; import { OIDCGeneralSettings } from "./OIDCGeneralSettings"; import { ReqAuthnConstraints } from "./ReqAuthnConstraintsSettings"; import { SamlGeneralSettings } from "./SamlGeneralSettings"; +import { IdgGeneralSettings } from "./IdgGeneralSettings"; type HeaderProps = { onChange: (value: boolean) => void; @@ -323,10 +324,12 @@ export default function DetailSettings() { const save = async (savedProvider?: IdentityProviderRepresentation) => { const p = savedProvider || getValues(); + const origAuthnContextClassRefs = p.config?.authnContextClassRefs; if (p.config?.authnContextClassRefs) p.config.authnContextClassRefs = JSON.stringify( p.config.authnContextClassRefs, ); + const origAuthnContextDeclRefs = p.config?.authnContextDeclRefs; if (p.config?.authnContextDeclRefs) p.config.authnContextDeclRefs = JSON.stringify( p.config.authnContextDeclRefs, @@ -342,6 +345,12 @@ export default function DetailSettings() { providerId, }, ); + if (origAuthnContextClassRefs) { + p.config!.authnContextClassRefs = origAuthnContextClassRefs; + } + if (origAuthnContextDeclRefs) { + p.config!.authnContextDeclRefs = origAuthnContextDeclRefs; + } reset(p); addAlert(t("updateSuccessIdentityProvider"), AlertVariant.success); } catch (error) { @@ -395,6 +404,7 @@ export default function DetailSettings() { const isOIDC = provider.providerId!.includes("oidc"); const isSAML = provider.providerId!.includes("saml"); + const isIDG = provider.providerId!.includes("german-eid"); const loader = async () => { const [loaderMappers, loaderMapperTypes] = await Promise.all([ @@ -430,9 +440,12 @@ export default function DetailSettings() { isHorizontal onSubmit={handleSubmit(save)} > - {!isOIDC && !isSAML && } + {!isOIDC && !isSAML && !isIDG && ( + + )} {isOIDC && } {isSAML && } + {isIDG && } {providerInfo && ( )} diff --git a/js/apps/admin-ui/src/identity-providers/add/IdgGeneralSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/IdgGeneralSettings.tsx new file mode 100644 index 000000000000..011ad60e9053 --- /dev/null +++ b/js/apps/admin-ui/src/identity-providers/add/IdgGeneralSettings.tsx @@ -0,0 +1,52 @@ +import { FormGroup, ValidatedOptions } from "@patternfly/react-core"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; + +import { HelpItem } from "ui-shared"; +import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; +import { DisplayOrder } from "../component/DisplayOrder"; +import { RedirectUrl } from "../component/RedirectUrl"; +import { TextField } from "../component/TextField"; +import type { IdentityProviderParams } from "../routes/IdentityProvider"; + +export const IdgGeneralSettings = ({ id }: { id: string }) => { + const { t } = useTranslation(); + const { tab } = useParams(); + + const { + register, + formState: { errors }, + } = useFormContext(); + + return ( + <> + + + } + fieldId="alias" + isRequired + validated={ + errors.alias ? ValidatedOptions.error : ValidatedOptions.default + } + helperTextInvalid={t("required")} + > + + + + + + + ); +}; diff --git a/js/apps/admin-ui/src/identity-providers/routes.ts b/js/apps/admin-ui/src/identity-providers/routes.ts index 6ff67d1483d2..e16628990272 100644 --- a/js/apps/admin-ui/src/identity-providers/routes.ts +++ b/js/apps/admin-ui/src/identity-providers/routes.ts @@ -7,6 +7,7 @@ import { IdentityProvidersRoute } from "./routes/IdentityProviders"; import { IdentityProviderAddMapperRoute } from "./routes/AddMapper"; import { IdentityProviderEditMapperRoute } from "./routes/EditMapper"; import { IdentityProviderCreateRoute } from "./routes/IdentityProviderCreate"; +import { IdentityProviderIdgRoute } from "./routes/IdentityProviderIDG"; const routes: AppRouteObject[] = [ IdentityProviderAddMapperRoute, @@ -14,6 +15,7 @@ const routes: AppRouteObject[] = [ IdentityProvidersRoute, IdentityProviderOidcRoute, IdentityProviderSamlRoute, + IdentityProviderIdgRoute, IdentityProviderKeycloakOidcRoute, IdentityProviderCreateRoute, IdentityProviderRoute, diff --git a/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderIDG.tsx b/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderIDG.tsx new file mode 100644 index 000000000000..470b86f5416c --- /dev/null +++ b/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderIDG.tsx @@ -0,0 +1,23 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generatePath } from "react-router-dom"; +import type { AppRouteObject } from "../../routes"; + +export type IdentityProviderIdgParams = { realm: string }; + +const AddIdgConnect = lazy(() => import("../add/AddIdgConnect")); + +export const IdentityProviderIdgRoute: AppRouteObject = { + path: "/:realm/identity-providers/german-eid/add", + element: , + breadcrumb: (t) => t("addIdgProvider"), + handle: { + access: "manage-identity-providers", + }, +}; + +export const toIdentityProviderIdg = ( + params: IdentityProviderIdgParams, +): Partial => ({ + pathname: generatePath(IdentityProviderIdgRoute.path, params), +}); diff --git a/js/apps/admin-ui/src/sessions/SessionsTable.tsx b/js/apps/admin-ui/src/sessions/SessionsTable.tsx index 3eb6f594b6fc..b5328ba4b06b 100644 --- a/js/apps/admin-ui/src/sessions/SessionsTable.tsx +++ b/js/apps/admin-ui/src/sessions/SessionsTable.tsx @@ -146,13 +146,32 @@ export default function SessionsTable({ }, }); + async function onClickRevoke( + event: MouseEvent, + rowIndex: number, + rowData: IRowData, + ) { + const session = rowData.data as UserSessionRepresentation; + await adminClient.realms.deleteSession({ + realm, + session: session.id!, + isOffline: true, + }); + + refresh(); + } + async function onClickSignOut( event: MouseEvent, rowIndex: number, rowData: IRowData, ) { const session = rowData.data as UserSessionRepresentation; - await adminClient.realms.deleteSession({ realm, session: session.id! }); + await adminClient.realms.deleteSession({ + realm, + session: session.id!, + isOffline: false, + }); if (session.userId === whoAmI.getUserId()) { await keycloak.logout({ redirectUri: "" }); @@ -185,8 +204,16 @@ export default function SessionsTable({ } columns={columns} actionResolver={(rowData: IRowData) => { - if (rowData.data.type === "OFFLINE") { - return []; + if ( + rowData.data.type === "Offline" || + rowData.data.type === "OFFLINE" + ) { + return [ + { + title: t("revoke"), + onClick: onClickRevoke, + } as Action, + ]; } return [ { diff --git a/js/libs/keycloak-admin-client/package.json b/js/libs/keycloak-admin-client/package.json index e76b417cc15b..7225b6a513dc 100644 --- a/js/libs/keycloak-admin-client/package.json +++ b/js/libs/keycloak-admin-client/package.json @@ -1,6 +1,6 @@ { "name": "@keycloak/keycloak-admin-client", - "version": "999.0.0-SNAPSHOT", + "version": "24.0.5-PS-2", "description": "A client to interact with Keycloak's Administration API", "type": "module", "main": "lib/index.js", diff --git a/js/libs/keycloak-admin-client/pom.xml b/js/libs/keycloak-admin-client/pom.xml index d6b8c82c4e4a..f0b82e120b60 100644 --- a/js/libs/keycloak-admin-client/pom.xml +++ b/js/libs/keycloak-admin-client/pom.xml @@ -5,7 +5,7 @@ keycloak-js-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml diff --git a/js/libs/keycloak-admin-client/src/resources/realms.ts b/js/libs/keycloak-admin-client/src/resources/realms.ts index d760cfbf8dc3..49b10dc46e0b 100644 --- a/js/libs/keycloak-admin-client/src/resources/realms.ts +++ b/js/libs/keycloak-admin-client/src/resources/realms.ts @@ -311,12 +311,13 @@ export class Realms extends Resource { }); public deleteSession = this.makeRequest< - { realm: string; session: string }, + { realm: string; session: string; isOffline: boolean }, void >({ method: "DELETE", path: "/{realm}/sessions/{session}", urlParamKeys: ["realm", "session"], + queryParamKeys: ["isOffline"], }); public pushRevocation = this.makeRequest< diff --git a/js/libs/keycloak-js/package.json b/js/libs/keycloak-js/package.json index ebdbbe4a1ada..92470b27c769 100644 --- a/js/libs/keycloak-js/package.json +++ b/js/libs/keycloak-js/package.json @@ -1,6 +1,6 @@ { "name": "keycloak-js", - "version": "999.0.0-SNAPSHOT", + "version": "24.0.5-PS-2", "description": "A client-side JavaScript OpenID Connect library that can be used to secure web applications", "main": "./dist/keycloak.js", "module": "./dist/keycloak.mjs", diff --git a/js/libs/keycloak-js/pom.xml b/js/libs/keycloak-js/pom.xml index 2a6543760c11..d018097e5393 100644 --- a/js/libs/keycloak-js/pom.xml +++ b/js/libs/keycloak-js/pom.xml @@ -5,7 +5,7 @@ keycloak-js-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml diff --git a/js/libs/keycloak-js/src/keycloak.js b/js/libs/keycloak-js/src/keycloak.js index 35ff2d24de30..e4c18d6869bf 100755 --- a/js/libs/keycloak-js/src/keycloak.js +++ b/js/libs/keycloak-js/src/keycloak.js @@ -132,10 +132,11 @@ function Keycloak (config) { kc.silentCheckSsoFallback = true; } - if (initOptions.pkceMethod) { - if (initOptions.pkceMethod !== "S256") { - throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${initOptions.pkceMethod}'.`); + if (typeof initOptions.pkceMethod !== "undefined") { + if (initOptions.pkceMethod !== "S256" && initOptions.pkceMethod !== false) { + throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${initOptions.pkceMethod}.`); } + kc.pkceMethod = initOptions.pkceMethod; } else { kc.pkceMethod = "S256"; @@ -1004,7 +1005,7 @@ function Keycloak (config) { if (token) { kc.token = token; kc.tokenParsed = jwtDecode(token); - kc.sessionId = kc.tokenParsed.session_state; + kc.sessionId = kc.tokenParsed.sid; kc.authenticated = true; kc.subject = kc.tokenParsed.sub; kc.realmAccess = kc.tokenParsed.realm_access; diff --git a/js/libs/ui-shared/src/continue-cancel/ContinueCancelModal.tsx b/js/libs/ui-shared/src/continue-cancel/ContinueCancelModal.tsx index 5ff5c4f8ecf2..1ac3790dc0a5 100644 --- a/js/libs/ui-shared/src/continue-cancel/ContinueCancelModal.tsx +++ b/js/libs/ui-shared/src/continue-cancel/ContinueCancelModal.tsx @@ -7,6 +7,7 @@ export type ContinueCancelModalProps = Omit & { cancelLabel: string; buttonTitle: string | ReactNode; buttonVariant?: ButtonProps["variant"]; + buttonTestRole?: string; isDisabled?: boolean; onContinue: () => void; component?: React.ElementType | React.ComponentType; @@ -20,6 +21,7 @@ export const ContinueCancelModal = ({ buttonTitle, isDisabled, buttonVariant, + buttonTestRole, onContinue, component = Button, children, @@ -34,6 +36,7 @@ export const ContinueCancelModal = ({ variant={buttonVariant} onClick={() => setOpen(true)} isDisabled={isDisabled} + data-testrole={buttonTestRole} > {buttonTitle} diff --git a/js/libs/ui-shared/src/main.ts b/js/libs/ui-shared/src/main.ts index 89025a5a8c51..e4bb5d2bcbd5 100644 --- a/js/libs/ui-shared/src/main.ts +++ b/js/libs/ui-shared/src/main.ts @@ -20,6 +20,7 @@ export { setUserProfileServerError, isUserProfileError, label, + beerify, debeerify, } from "./user-profile/utils"; export type { UserFormFields } from "./user-profile/utils"; diff --git a/js/libs/ui-shared/src/user-profile/OptionsComponent.tsx b/js/libs/ui-shared/src/user-profile/OptionsComponent.tsx index f919fdd41166..98f03cf16538 100644 --- a/js/libs/ui-shared/src/user-profile/OptionsComponent.tsx +++ b/js/libs/ui-shared/src/user-profile/OptionsComponent.tsx @@ -1,8 +1,12 @@ import { Checkbox, Radio } from "@patternfly/react-core"; import { Controller } from "react-hook-form"; -import { Options, UserProfileFieldProps } from "./UserProfileFields"; +import { + OptionLabel, + Options, + UserProfileFieldProps, +} from "./UserProfileFields"; import { UserProfileGroup } from "./UserProfileGroup"; -import { fieldName, isRequiredAttribute } from "./utils"; +import { fieldName, isRequiredAttribute, unWrap } from "./utils"; export const OptionComponent = (props: UserProfileFieldProps) => { const { form, inputType, attribute } = props; @@ -12,6 +16,10 @@ export const OptionComponent = (props: UserProfileFieldProps) => { const options = (attribute.validators?.options as Options | undefined)?.options || []; + const optionLabel = attribute.annotations?.[ + "inputOptionLabels" + ] as OptionLabel; + return ( { key={option} id={option} data-testid={option} - label={option} + label={props.t(unWrap(optionLabel?.on || option))} value={option} isChecked={field.value.includes(option)} onChange={() => { diff --git a/js/libs/ui-shared/src/user-profile/SelectComponent.tsx b/js/libs/ui-shared/src/user-profile/SelectComponent.tsx index 6a2222a6c430..edbecb2c2465 100644 --- a/js/libs/ui-shared/src/user-profile/SelectComponent.tsx +++ b/js/libs/ui-shared/src/user-profile/SelectComponent.tsx @@ -1,7 +1,11 @@ import { Select, SelectOption } from "@patternfly/react-core"; import { useState } from "react"; import { Controller, ControllerRenderProps } from "react-hook-form"; -import { Options, UserProfileFieldProps } from "./UserProfileFields"; +import { + OptionLabel, + Options, + UserProfileFieldProps, +} from "./UserProfileFields"; import { UserProfileGroup } from "./UserProfileGroup"; import { UserFormFields, @@ -10,7 +14,6 @@ import { unWrap, } from "./utils"; -type OptionLabel = Record | undefined; export const SelectComponent = (props: UserProfileFieldProps) => { const { t, form, inputType, attribute } = props; const [open, setOpen] = useState(false); diff --git a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx index 38f06a5a6e5b..9aef0c72f910 100644 --- a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx +++ b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx @@ -54,6 +54,8 @@ export type UserProfileFieldProps = { renderer?: (attribute: UserProfileAttributeMetadata) => ReactNode; }; +export type OptionLabel = Record | undefined; + export const FIELDS: { [type in InputType]: (props: UserProfileFieldProps) => JSX.Element; } = { diff --git a/js/libs/ui-shared/src/user-profile/utils.ts b/js/libs/ui-shared/src/user-profile/utils.ts index 2f1ae4911755..07183bcc2a4b 100644 --- a/js/libs/ui-shared/src/user-profile/utils.ts +++ b/js/libs/ui-shared/src/user-profile/utils.ts @@ -49,6 +49,9 @@ export const fieldName = (name?: string) => "🍺", )}` as FieldPath; +export const beerify = (name: T) => + name.replaceAll(".", "🍺"); + export const debeerify = (name: T) => name.replaceAll("🍺", "."); diff --git a/js/pom.xml b/js/pom.xml index e23dfa7712c7..f2d8a2292c6c 100644 --- a/js/pom.xml +++ b/js/pom.xml @@ -5,7 +5,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml @@ -27,6 +27,8 @@ src ${project.basedir}/dist + + false @@ -56,6 +58,8 @@ maven-clean-plugin false + + ${js.skip.clean} ${basedir} @@ -76,4 +80,18 @@ + + + + clean-when-not-on-windows + + + windows + + + + true + + + diff --git a/maven-settings.xml b/maven-settings.xml index 950b2dba5333..a855e38dd133 100644 --- a/maven-settings.xml +++ b/maven-settings.xml @@ -49,4 +49,4 @@ jboss-public-repository - + \ No newline at end of file diff --git a/misc/keycloak-test-helper/pom.xml b/misc/keycloak-test-helper/pom.xml index aaed6d3a2de6..071c5d518f32 100644 --- a/misc/keycloak-test-helper/pom.xml +++ b/misc/keycloak-test-helper/pom.xml @@ -6,7 +6,7 @@ keycloak-misc-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak keycloak-test-helper diff --git a/misc/pom.xml b/misc/pom.xml index 8c813fdc5074..6dbb96632efd 100644 --- a/misc/pom.xml +++ b/misc/pom.xml @@ -3,7 +3,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Misc diff --git a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml index d3a97a4168c5..edcc23cec78c 100644 --- a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml +++ b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ org.keycloak keycloak-spring-boot-starter-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-spring-boot-starter Keycloak :: Spring :: Boot :: Default :: Starter diff --git a/misc/spring-boot-starter/pom.xml b/misc/spring-boot-starter/pom.xml index b94cafa4a6d0..5a3de2ba648e 100644 --- a/misc/spring-boot-starter/pom.xml +++ b/misc/spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ keycloak-misc-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 org.keycloak keycloak-spring-boot-starter-parent diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index 79ffa042d282..005746a890f7 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -21,7 +21,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 17 diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java index 7f9bc7bd1e84..d0907221d925 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java @@ -26,6 +26,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; @@ -36,6 +37,7 @@ import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; +import org.infinispan.client.hotrod.exceptions.HotRodClientException; import org.infinispan.context.Flag; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; @@ -54,7 +56,6 @@ import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory; import org.keycloak.executors.ExecutorsProvider; import org.keycloak.models.KeycloakSession; -import org.infinispan.client.hotrod.exceptions.HotRodClientException; import static org.keycloak.cluster.infinispan.InfinispanClusterProvider.TASK_KEY_PREFIX; @@ -67,13 +68,16 @@ public class InfinispanNotificationsManager { protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class); + private static final int BACKOFF_BASE_MILLIS = 10; + private static final int MAX_BACKOFF_RETRIES = 10; + private final ConcurrentMultivaluedHashMap listeners = new ConcurrentMultivaluedHashMap<>(); private final ConcurrentMap taskCallbacks = new ConcurrentHashMap<>(); private final Cache workCache; - private final RemoteCache workRemoteCache; + private final RemoteCache workRemoteCache; private final String myAddress; @@ -81,8 +85,7 @@ public class InfinispanNotificationsManager { private final ExecutorService listenersExecutor; - - protected InfinispanNotificationsManager(Cache workCache, RemoteCache workRemoteCache, String myAddress, String mySite, ExecutorService listenersExecutor) { + protected InfinispanNotificationsManager(Cache workCache, RemoteCache workRemoteCache, String myAddress, String mySite, ExecutorService listenersExecutor) { this.workCache = workCache; this.workRemoteCache = workRemoteCache; this.myAddress = myAddress; @@ -93,7 +96,7 @@ protected InfinispanNotificationsManager(Cache workCache, // Create and init manager including all listeners etc public static InfinispanNotificationsManager create(KeycloakSession session, Cache workCache, String myAddress, String mySite, Set remoteStores) { - RemoteCache workRemoteCache = null; + RemoteCache workRemoteCache = null; if (!remoteStores.isEmpty()) { RemoteStore remoteStore = remoteStores.iterator().next(); @@ -189,12 +192,12 @@ public void cacheEntryCreated(CacheEntryCreatedEvent event @CacheEntryModified public void cacheEntryModified(CacheEntryModifiedEvent event) { - eventReceived(event.getKey(), event.getValue()); + eventReceived(event.getKey(), event.getNewValue()); } @CacheEntryRemoved public void cacheEntryRemoved(CacheEntryRemovedEvent event) { - taskFinished(event.getKey(), true); + taskFinished(event.getKey()); } } @@ -203,31 +206,28 @@ public void cacheEntryRemoved(CacheEntryRemovedEvent event @ClientListener public class HotRodListener { - private final RemoteCache remoteCache; + private final RemoteCache remoteCache; - public HotRodListener(RemoteCache remoteCache) { + public HotRodListener(RemoteCache remoteCache) { this.remoteCache = remoteCache; } @ClientCacheEntryCreated - public void created(ClientCacheEntryCreatedEvent event) { - String key = event.getKey().toString(); - hotrodEventReceived(key); + public void created(ClientCacheEntryCreatedEvent event) { + hotrodEventReceived(event.getKey()); } @ClientCacheEntryModified - public void updated(ClientCacheEntryModifiedEvent event) { - String key = event.getKey().toString(); - hotrodEventReceived(key); + public void updated(ClientCacheEntryModifiedEvent event) { + hotrodEventReceived(event.getKey()); } @ClientCacheEntryRemoved - public void removed(ClientCacheEntryRemovedEvent event) { - String key = event.getKey().toString(); - taskFinished(key, true); + public void removed(ClientCacheEntryRemovedEvent event) { + taskFinished(event.getKey()); } @@ -235,11 +235,22 @@ private void hotrodEventReceived(String key) { // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request try { listenersExecutor.submit(() -> { - Object value = DefaultInfinispanConnectionProviderFactory.runWithReadLockOnCacheManager(() -> - // We've seen deadlocks in Infinispan 14.x when shutting down Infinispan concurrently, therefore wrapping this - remoteCache.get(key) - ); - eventReceived(key, (Serializable) value); + Supplier fetchEvent = () -> remoteCache.get(key); + Serializable event = DefaultInfinispanConnectionProviderFactory.runWithReadLockOnCacheManager(fetchEvent); + int iteration = 0; + // Event might have been generated from a node which is more up-to-date, so the fetch might return null. + // Retry until we find a node that is up-to-date and has the entry. + while (event == null && iteration < MAX_BACKOFF_RETRIES) { + ++iteration; + try { + Thread.sleep(Retry.computeBackoffInterval(BACKOFF_BASE_MILLIS, iteration)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + event = DefaultInfinispanConnectionProviderFactory.runWithReadLockOnCacheManager(fetchEvent); + } + eventReceived(key, event); }); } catch (RejectedExecutionException ree) { @@ -254,11 +265,10 @@ private void hotrodEventReceived(String key) { } } } - } private void eventReceived(String key, Serializable obj) { - if (!(obj instanceof WrapperClusterEvent)) { + if (!(obj instanceof WrapperClusterEvent event)) { // Items with the TASK_KEY_PREFIX might be gone fast once the locking is complete, therefore, don't log them. // It is still good to have the warning in case of real events return null because they have been, for example, expired if (obj == null && !key.startsWith(TASK_KEY_PREFIX)) { @@ -267,8 +277,6 @@ private void eventReceived(String key, Serializable obj) { return; } - WrapperClusterEvent event = (WrapperClusterEvent) obj; - if (event.isIgnoreSender()) { if (this.myAddress.equals(event.getSender())) { return; @@ -298,16 +306,15 @@ private void eventReceived(String key, Serializable obj) { } - void taskFinished(String taskKey, boolean success) { + void taskFinished(String taskKey) { TaskCallback callback = taskCallbacks.remove(taskKey); if (callback != null) { if (logger.isDebugEnabled()) { - logger.debugf("Finished task '%s' with '%b'", taskKey, success); + logger.debugf("Finished task '%s' with '%b'", taskKey, true); } - callback.setSuccess(success); + callback.setSuccess(true); callback.getTaskCompletedLatch().countDown(); } - } } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 95a4652b8223..659ffb464a12 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -25,7 +25,6 @@ import org.infinispan.configuration.global.GlobalConfigurationBuilder; import org.infinispan.eviction.EvictionStrategy; import org.infinispan.eviction.EvictionType; -import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; @@ -38,6 +37,7 @@ import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; +import org.keycloak.marshalling.Marshalling; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.infinispan.ClearCacheEvent; @@ -46,12 +46,9 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.InvalidationHandler.ObjectType; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ProviderEvent; import java.util.Iterator; -import java.util.List; import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; @@ -254,11 +251,7 @@ protected EmbeddedCacheManager initEmbedded() { gcb.jmx().domain(InfinispanConnectionProvider.JMX_DOMAIN).enable(); } - // For Infinispan 10, we go with the JBoss marshalling. - // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. - // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details - gcb.serialization().marshaller(new JBossUserMarshaller()); - + Marshalling.configure(gcb); EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build()); if (useKeycloakTimeService) { setTimeServiceToKeycloakTime(cacheManager); diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java new file mode 100644 index 000000000000..e710f09def92 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java @@ -0,0 +1,25 @@ +package org.keycloak.marshalling; + +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; +import org.keycloak.models.sessions.infinispan.changes.ReplaceFunction; + +@SuppressWarnings("removal") +public final class Marshalling { + + private Marshalling() { + } + + // Note: Min ID is 2500 + public static final Integer REPLACE_FUNCTION_ID = 2500; + + // For Infinispan 10, we go with the JBoss marshalling. + // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. + // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details + public static void configure(GlobalConfigurationBuilder builder) { + builder.serialization() + .marshaller(new JBossUserMarshaller()) + .addAdvancedExternalizer(ReplaceFunction.INSTANCE); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 7f3ae6a08488..c8fee2dbf36c 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -192,8 +192,10 @@ public void setAttribute(String name, List values) { @Override public void removeAttribute(String name) { - getDelegateForUpdate(); - updated.removeAttribute(name); + if (getFirstAttribute(name) != null) { + getDelegateForUpdate(); + updated.removeAttribute(name); + } } @Override @@ -229,8 +231,10 @@ public void addRequiredAction(RequiredAction action) { @Override public void removeRequiredAction(RequiredAction action) { - getDelegateForUpdate(); - updated.removeRequiredAction(action); + if (getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action.name()))) { + getDelegateForUpdate(); + updated.removeRequiredAction(action); + } } @Override @@ -241,8 +245,10 @@ public void addRequiredAction(String action) { @Override public void removeRequiredAction(String action) { - getDelegateForUpdate(); - updated.removeRequiredAction(action); + if (getRequiredActionsStream().anyMatch(s -> Objects.equals(s, action))) { + getDelegateForUpdate(); + updated.removeRequiredAction(action); + } } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 91df0c7de9b8..4ad8ed3e4382 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -63,6 +63,7 @@ import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.UserProfileDecorator; import org.keycloak.userprofile.UserProfileMetadata; @@ -950,9 +951,10 @@ public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel m } @Override - public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) { + public List decorateUserProfile(String providerId, UserProfileMetadata metadata) { if (getDelegate() instanceof UserProfileDecorator) { - ((UserProfileDecorator) getDelegate()).decorateUserProfile(realm, metadata); + return ((UserProfileDecorator) getDelegate()).decorateUserProfile(providerId, metadata); } + return List.of(); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java index 2df4073a2f69..b60c14093dc2 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java @@ -223,45 +223,32 @@ private void runOperationInCluster(K key, MergedUpdate task, SessionEntityWr private void replace(K key, MergedUpdate task, SessionEntityWrapper oldVersionEntity, long lifespanMs, long maxIdleTimeMs) { - boolean replaced = false; + SessionEntityWrapper oldVersion = oldVersionEntity; + SessionEntityWrapper returnValue = null; int iteration = 0; - V session = oldVersionEntity.getEntity(); - - while (!replaced && iteration < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) { - iteration++; - - SessionEntityWrapper newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata()); - - // Atomic cluster-aware replace - replaced = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache).replace(key, oldVersionEntity, newVersionEntity, lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS); - - // Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again - if (!replaced) { - if (logger.isDebugEnabled()) { - logger.debugf("Replace failed for entity: %s, old version %s, new version %s. Will try again", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion()); - } - - oldVersionEntity = cache.get(key); - - if (oldVersionEntity == null) { - logger.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key); - return; - } - - session = oldVersionEntity.getEntity(); + V session = oldVersion.getEntity(); + var writeCache = CacheDecorators.skipCacheStoreIfRemoteCacheIsEnabled(cache); + while (iteration++ < InfinispanUtil.MAXIMUM_REPLACE_RETRIES) { + SessionEntityWrapper newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersion.getLocalMetadata()); + returnValue = writeCache.computeIfPresent(key, new ReplaceFunction<>(oldVersion.getVersion(), newVersionEntity), lifespanMs, TimeUnit.MILLISECONDS, maxIdleTimeMs, TimeUnit.MILLISECONDS); + + if (returnValue == null) { + logger.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key); + return; + } - task.runUpdate(session); - } else { + if (returnValue.getVersion().equals(newVersionEntity.getVersion())) { if (logger.isTraceEnabled()) { - logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs()); + logger.tracef("Replace SUCCESS for entity: %s . old version: %s, new version: %s, Lifespan: %d ms, MaxIdle: %d ms", key, oldVersion.getVersion(), newVersionEntity.getVersion(), task.getLifespanMs(), task.getMaxIdleTimeMs()); } + return; } - } - if (!replaced) { - logger.warnf("Failed to replace entity '%s' in cache '%s'", key, cache.getName()); + oldVersion = returnValue; + session = oldVersion.getEntity(); + task.runUpdate(session); } - + logger.warnf("Failed to replace entity '%s' in cache '%s'. Expected: %s, Current: %s", key, cache.getName(), oldVersion, returnValue); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ReplaceFunction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ReplaceFunction.java new file mode 100644 index 000000000000..10aad0343329 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ReplaceFunction.java @@ -0,0 +1,73 @@ +package org.keycloak.models.sessions.infinispan.changes; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiFunction; + +import org.infinispan.commons.marshall.AdvancedExternalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.keycloak.marshalling.Marshalling; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * Performs an entity replacement in Infinispan, using its versions instead of equality. + * + * @param The Infinispan key type. + * @param The Infinispan value type (Keycloak entity) + */ +public class ReplaceFunction implements BiFunction, SessionEntityWrapper> { + + @SuppressWarnings({"removal", "rawtypes"}) + public static final AdvancedExternalizer INSTANCE = new Externalizer(); + private final UUID expectedVersion; + private final SessionEntityWrapper newValue; + + public ReplaceFunction(UUID expectedVersion, SessionEntityWrapper newValue) { + this.expectedVersion = Objects.requireNonNull(expectedVersion); + this.newValue = Objects.requireNonNull(newValue); + } + + @Override + public SessionEntityWrapper apply(K key, SessionEntityWrapper currentValue) { + assert currentValue != null; + return expectedVersion.equals(currentValue.getVersion()) ? newValue : currentValue; + } + + @SuppressWarnings({"removal", "rawtypes"}) + private static class Externalizer implements AdvancedExternalizer { + + private static final SessionEntityWrapper.ExternalizerImpl EXTERNALIZER = new SessionEntityWrapper.ExternalizerImpl(); + private static final byte VERSION_1 = 1; + + @Override + public Set> getTypeClasses() { + return Set.of(ReplaceFunction.class); + } + + @Override + public Integer getId() { + return Marshalling.REPLACE_FUNCTION_ID; + } + + @Override + public void writeObject(ObjectOutput output, ReplaceFunction object) throws IOException { + output.writeByte(VERSION_1); + MarshallUtil.marshallUUID(object.expectedVersion, output, false); + EXTERNALIZER.writeObject(output, object.newValue); + } + + @Override + public ReplaceFunction readObject(ObjectInput input) throws IOException, ClassNotFoundException { + var version = input.readByte(); + if (version != VERSION_1) { + throw new IOException("Invalid version: " + version); + } + //noinspection unchecked + return new ReplaceFunction(MarshallUtil.unmarshallUUID(input, false), EXTERNALIZER.readObject(input)); + } + } +} diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index ac4e28f787e9..b18c5e73e55b 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -21,7 +21,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index ba17fb9f0482..c138acd4a935 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -210,8 +210,9 @@ private void lazyInit(KeycloakSession session) { try { prepareOperationalInfo(connection); - String driverDialect = detectDialect(connection); - if (driverDialect != null) { + String driverDialect = config.get("driverDialect"); + // use configured dialect, else rely on Hibernate detection + if (driverDialect != null && !driverDialect.isBlank()) { properties.put("hibernate.dialect", driverDialect); } @@ -298,47 +299,6 @@ protected void prepareOperationalInfo(Connection connection) { } } - - protected String detectDialect(Connection connection) { - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - return driverDialect; - } else { - try { - String dbProductName = connection.getMetaData().getDatabaseProductName(); - String dbProductVersion = connection.getMetaData().getDatabaseProductVersion(); - - // For MSSQL2014, we may need to fix the autodetected dialect by hibernate - if (dbProductName.equals("Microsoft SQL Server")) { - String topVersionStr = dbProductVersion.split("\\.")[0]; - boolean shouldSet2012Dialect = true; - try { - int topVersion = Integer.parseInt(topVersionStr); - if (topVersion < 12) { - shouldSet2012Dialect = false; - } - } catch (NumberFormatException nfe) { - } - if (shouldSet2012Dialect) { - String sql2012Dialect = "org.hibernate.dialect.SQLServer2012Dialect"; - logger.debugf("Manually override hibernate dialect to %s", sql2012Dialect); - return sql2012Dialect; - } - } - - // For Oracle19c, we may need to set dialect explicitly to workaround https://hibernate.atlassian.net/browse/HHH-13184 - if (dbProductName.equals("Oracle") && connection.getMetaData().getDatabaseMajorVersion() > 12) { - logger.debugf("Manually specify dialect for Oracle to org.hibernate.dialect.Oracle12cDialect"); - return "org.hibernate.dialect.Oracle12cDialect"; - } - } catch (SQLException e) { - logger.warnf("Unable to detect hibernate dialect due database exception : %s", e.getMessage()); - } - - return null; - } - } - protected void startGlobalStats(KeycloakSession session, int globalStatsIntervalSecs) { logger.debugf("Started Hibernate statistics with the interval %s seconds", globalStatsIntervalSecs); TimerProvider timer = session.getProvider(TimerProvider.class); diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate24_0_2_FederatedTermsAndConditionsRequiredAction.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate24_0_2_FederatedTermsAndConditionsRequiredAction.java new file mode 100644 index 000000000000..f217dc708a6a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate24_0_2_FederatedTermsAndConditionsRequiredAction.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.connections.jpa.updater.liquibase.custom; + +import liquibase.exception.CustomChangeException; +import liquibase.statement.core.UpdateStatement; +import liquibase.structure.core.Table; +import org.keycloak.models.UserModel; + +/** + * Custom liquibase change to migrate legacy {@code terms and conditions} required action for federated users (table + * {@code FED_USER_REQUIRED_ACTION}, in line with what {@link JpaUpdate21_0_2_TermsAndConditionsRequiredAction} + * did to migrate the same action for regular users. + *

    + * The legacy value was in lowercase, and it was changed to upper case to match the other required actions in Keycloak. + * This class ensures that the legacy action set for federated users is properly migrated to upper case when upgrading + * the server. + * + * @author Stefan Guilhen + */ +public class JpaUpdate24_0_2_FederatedTermsAndConditionsRequiredAction extends CustomKeycloakTask { + + private static final String TERMS_AND_CONDITION_LEGACY_ALIAS = "terms_and_conditions"; + + @Override + protected void generateStatementsImpl() throws CustomChangeException { + statements.add( + new UpdateStatement(null, null, database.correctObjectName("FED_USER_REQUIRED_ACTION", Table.class)) + .addNewColumnValue("REQUIRED_ACTION", UserModel.RequiredAction.TERMS_AND_CONDITIONS.name()) + .setWhereClause("REQUIRED_ACTION=?") + .addWhereParameter(TERMS_AND_CONDITION_LEGACY_ALIAS) + ); + } + + @Override + protected String getTaskId() { + return "Federated Terms And Conditions required action alias change (25.0.0)"; + } + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 0dd32b2a10e6..c1df1e2a9ad4 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -865,12 +865,13 @@ public Stream searchClientsByAttributes(RealmModel realm, Map toRemove = new ArrayList<>(); + List customAttributesToRemove = new ArrayList<>(); for (UserAttributeEntity attr : user.getAttributes()) { if (attr.getName().equals(name)) { - toRemove.add(attr); + customAttributesToRemove.add(attr); } } - if (toRemove.isEmpty()) { + if (customAttributesToRemove.isEmpty()) { + // make sure root user attributes are set to null + if (UserModel.FIRST_NAME.equals(name)) { + setFirstName(null); + } else if (UserModel.LAST_NAME.equals(name)) { + setLastName(null); + } else if (UserModel.EMAIL.equals(name)) { + setEmail(null); + } return; } @@ -213,7 +221,7 @@ public void removeAttribute(String name) { query.setParameter("userId", user.getId()); query.executeUpdate(); // KEYCLOAK-3494 : Also remove attributes from local user entity - user.getAttributes().removeAll(toRemove); + user.getAttributes().removeAll(customAttributesToRemove); } @Override diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.0.xml index 406867019a6c..a5880ed8488b 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.0.xml @@ -66,12 +66,12 @@ + 9:bd2bd0fc7768cf0845ac96a8786fa735 - @@ -81,9 +81,6 @@ - - - diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.2.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.2.xml new file mode 100644 index 000000000000..1b13a52ef45e --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-24.0.2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 419f3541713b..a043d6a15216 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -80,5 +80,6 @@ + diff --git a/model/legacy/pom.xml b/model/legacy/pom.xml index 476a1c6c8b91..2c5b0253826a 100644 --- a/model/legacy/pom.xml +++ b/model/legacy/pom.xml @@ -3,7 +3,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/model/pom.xml b/model/pom.xml index 97b9188ac581..460f88e58ddd 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Model Parent diff --git a/model/storage-private/pom.xml b/model/storage-private/pom.xml index 851e136ede0a..f06e7b209c57 100644 --- a/model/storage-private/pom.xml +++ b/model/storage-private/pom.xml @@ -3,7 +3,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java index 44bfc92679cd..c9ec179326f9 100644 --- a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java +++ b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo23_0_0.java @@ -71,8 +71,11 @@ private void updateUserProfileConfig(RealmModel realm) { if (component.isPresent()) { ComponentModel userProfileComponent = component.get(); int count = userProfileComponent.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0); + if (count < 1) { + realm.removeComponent(userProfileComponent); + return; + } userProfileComponent.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY); - if (count < 1) return; // default config String configuration; if (count == 1) { configuration = userProfileComponent.get(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + "0"); diff --git a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_0.java b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_0.java index c7a4588be9fa..4e7b20c3d38a 100644 --- a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_0.java +++ b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_0.java @@ -116,6 +116,6 @@ private void bindFirstBrokerLoginFlow(KeycloakSession session) { return; } realm.setFirstBrokerLoginFlow(flow); - LOG.debugf("Flow '%s' has been bound to realm %s as 'First broker login' flow", realm.getName()); + LOG.debugf("Flow '%s' has been bound to realm %s as 'First broker login' flow", flow.getId(), realm.getName()); } } diff --git a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_3.java b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_3.java new file mode 100644 index 000000000000..cbbea545c3d8 --- /dev/null +++ b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo24_0_3.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.migration.migrators; + +import org.jboss.logging.Logger; +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.DefaultRequiredActions; +import org.keycloak.representations.idm.RealmRepresentation; + +/** + * @author Marek Posolda + */ +public class MigrateTo24_0_3 implements Migration { + + private static final Logger LOG = Logger.getLogger(MigrateTo24_0_3.class); + + public static final ModelVersion VERSION = new ModelVersion("24.0.3"); + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealmsStream().forEach(realm -> migrateRealm(session, realm)); + } + + @Override + public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { + migrateRealm(session, realm); + } + + @Override + public ModelVersion getVersion() { + return VERSION; + } + + private void migrateRealm(KeycloakSession session, RealmModel realm) { + DefaultRequiredActions.addDeleteCredentialAction(realm); + } +} diff --git a/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java b/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java index c6f21fcaa23b..e3dcb4ebdc2b 100755 --- a/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -21,6 +21,8 @@ import static org.keycloak.utils.StreamsUtil.distinctByKey; import static org.keycloak.utils.StreamsUtil.paginatedStream; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -71,6 +73,7 @@ import org.keycloak.storage.user.UserQueryMethodsProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; +import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.UserProfileDecorator; import org.keycloak.userprofile.UserProfileMetadata; @@ -857,10 +860,18 @@ public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) } @Override - public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) { - for (UserProfileDecorator decorator : getEnabledStorageProviders(session.getContext().getRealm(), UserProfileDecorator.class) - .collect(Collectors.toList())) { - decorator.decorateUserProfile(realm, metadata); + public List decorateUserProfile(String providerId, UserProfileMetadata metadata) { + RealmModel realm = session.getContext().getRealm(); + UserStorageProviderModel providerModel = getStorageProviderModel(realm, providerId); + + if (providerModel != null) { + UserProfileDecorator decorator = getStorageProviderInstance(providerModel, UserProfileDecorator.class); + + if (decorator != null) { + return decorator.decorateUserProfile(providerId, metadata); + } } + + return Collections.emptyList(); } } diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java index eb34a47efc4b..69eb2af486b7 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java @@ -1288,38 +1288,35 @@ private static WebAuthnPolicy getWebAuthnPolicyPasswordless(RealmRepresentation } public static Map importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) { Map mappedFlows = new HashMap<>(); - if (rep.getAuthenticationFlows() == null) { - // assume this is an old version being imported - DefaultAuthenticationFlows.migrateFlows(newRealm); - } else { - if (rep.getAuthenticatorConfig() != null) { - for (AuthenticatorConfigRepresentation configRep : rep.getAuthenticatorConfig()) { - if (configRep.getAlias() == null) { - // this can happen only during import json files from keycloak 3.4.0 and older - throw new IllegalStateException("Provided realm contains authenticator config with null alias. " - + "It should be resolved by adding alias to the authenticator config before exporting the realm."); - } - AuthenticatorConfigModel model = RepresentationToModel.toModel(configRep); - newRealm.addAuthenticatorConfig(model); - } - } - if (rep.getAuthenticationFlows() != null) { - for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { - AuthenticationFlowModel model = RepresentationToModel.toModel(flowRep); - String previousId = model.getId(); - model = newRealm.addAuthenticationFlow(model); - // store the mapped ids so that clients can reference the correct flow when importing the authenticationFlowBindingOverrides - mappedFlows.put(previousId, model.getId()); + + if (rep.getAuthenticatorConfig() != null) { + for (AuthenticatorConfigRepresentation configRep : rep.getAuthenticatorConfig()) { + if (configRep.getAlias() == null) { + // this can happen only during import json files from keycloak 3.4.0 and older + throw new IllegalStateException("Provided realm contains authenticator config with null alias. " + + "It should be resolved by adding alias to the authenticator config before exporting the realm."); } - for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { - AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); - for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { - AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep); - newRealm.addAuthenticatorExecution(execution); - } + AuthenticatorConfigModel model = RepresentationToModel.toModel(configRep); + newRealm.addAuthenticatorConfig(model); + } + } + if (rep.getAuthenticationFlows() != null) { + for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { + AuthenticationFlowModel model = RepresentationToModel.toModel(flowRep); + String previousId = model.getId(); + model = newRealm.addAuthenticationFlow(model); + // store the mapped ids so that clients can reference the correct flow when importing the authenticationFlowBindingOverrides + mappedFlows.put(previousId, model.getId()); + } + for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { + AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); + for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { + AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep); + newRealm.addAuthenticatorExecution(execution); } } } + DefaultAuthenticationFlows.migrateFlows(newRealm); if (rep.getBrowserFlow() == null) { AuthenticationFlowModel defaultFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); if (defaultFlow != null) { diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java index fbb4d65d952c..774404b1b6ad 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java @@ -38,6 +38,7 @@ import org.keycloak.migration.migrators.MigrateTo22_0_0; import org.keycloak.migration.migrators.MigrateTo23_0_0; import org.keycloak.migration.migrators.MigrateTo24_0_0; +import org.keycloak.migration.migrators.MigrateTo24_0_3; import org.keycloak.migration.migrators.MigrateTo2_0_0; import org.keycloak.migration.migrators.MigrateTo2_1_0; import org.keycloak.migration.migrators.MigrateTo2_2_0; @@ -113,7 +114,8 @@ public class DefaultMigrationManager implements MigrationManager { new MigrateTo21_0_0(), new MigrateTo22_0_0(), new MigrateTo23_0_0(), - new MigrateTo24_0_0() + new MigrateTo24_0_0(), + new MigrateTo24_0_3() }; private final KeycloakSession session; diff --git a/model/storage-services/pom.xml b/model/storage-services/pom.xml index 8d7acffc7fa9..12d4563da5cd 100644 --- a/model/storage-services/pom.xml +++ b/model/storage-services/pom.xml @@ -3,7 +3,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/model/storage-services/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java b/model/storage-services/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java index ee9a037e1f29..6ad575fe661e 100755 --- a/model/storage-services/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java +++ b/model/storage-services/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java @@ -71,6 +71,7 @@ public void exportModel() { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { Stream realms = session.realms().getRealmsStream() + .peek(realm -> session.getContext().setRealm(realm)) .map(realm -> ExportUtils.exportRealm(session, realm, true, true)); writeToFile(realms); @@ -88,6 +89,7 @@ private void exportRealm(final String realmName) { protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); Objects.requireNonNull(realm, "realm not found by realm name '" + realmName + "'"); + session.getContext().setRealm(realm); RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true, true); writeToFile(realmRep); } diff --git a/model/storage-services/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/model/storage-services/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java index 270dcb710aee..8802d3ac556f 100755 --- a/model/storage-services/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java +++ b/model/storage-services/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java @@ -95,6 +95,7 @@ protected void exportRealmImpl(final String realmName) { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); + session.getContext().setRealm(realm); RealmRepresentation rep = ExportUtils.exportRealm(session, realm, exportUsersIntoRealmFile, true); writeRealm(realmName + "-realm.json", rep); logger.info("Realm '" + realmName + "' - data exported"); @@ -131,6 +132,7 @@ protected void runExportImportTask(KeycloakSession session) throws IOException { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); + session.getContext().setRealm(realm); usersHolder.users = session.users() .searchForUserStream(realm, Collections.emptyMap(), usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart) .collect(Collectors.toList()); @@ -164,6 +166,7 @@ protected void runExportImportTask(KeycloakSession session) throws IOException { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); + session.getContext().setRealm(realm); federatedUsersHolder.users = UserStorageUtil.userFederatedStorage(session) .getStoredUsersStream(realm, federatedUsersHolder.currentPageStart, federatedUsersHolder.currentPageEnd - federatedUsersHolder.currentPageStart) .collect(Collectors.toList()); diff --git a/model/storage/pom.xml b/model/storage/pom.xml index bbdb17870d33..c319e6194ef8 100644 --- a/model/storage/pom.xml +++ b/model/storage/pom.xml @@ -3,7 +3,7 @@ keycloak-model-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/operator/pom.xml b/operator/pom.xml index 340445f62086..5b66c5c29466 100644 --- a/operator/pom.xml +++ b/operator/pom.xml @@ -7,7 +7,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java index 80a9cbbfb9be..761189351d61 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java @@ -21,12 +21,13 @@ import io.sundr.builder.annotations.Buildable; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; @JsonInclude(JsonInclude.Include.NON_NULL) @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class Truststore { - @Required + @JsonPropertyDescription("Not used. To be removed in later versions.") private String name; @Required private TruststoreSource secret; diff --git a/operator/src/main/kubernetes/kubernetes.yml b/operator/src/main/kubernetes/kubernetes.yml index 45c60438c585..888c2068dfb8 100644 --- a/operator/src/main/kubernetes/kubernetes.yml +++ b/operator/src/main/kubernetes/kubernetes.yml @@ -15,6 +15,14 @@ rules: - delete - patch - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java index a49e53f557f9..ae064ecc3a3a 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java @@ -22,10 +22,13 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.batch.v1.Job; import io.fabric8.kubernetes.api.model.events.v1.Event; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; @@ -79,6 +82,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -166,6 +170,8 @@ private static void createRBACresourcesAndOperatorDeployment() throws FileNotFou K8sUtils.set(k8sclient, new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + deploymentTarget + ".yml"), obj -> { if (obj instanceof ClusterRoleBinding) { ((ClusterRoleBinding)obj).getSubjects().forEach(s -> s.setNamespace(namespace)); + } else if (obj instanceof RoleBinding && "keycloak-operator-view".equals(((RoleBinding)obj).getMetadata().getName())) { + return null; // exclude this role since it's not present in olm } return obj; }); @@ -286,19 +292,39 @@ private static void setDefaultAwaitilityTimings() { } public void cleanup() { - Log.info("Deleting Keycloak CR"); - k8sclient.resources(Keycloak.class).delete(); - Awaitility.await() - .untilAsserted(() -> { - var kcDeployments = k8sclient - .apps() - .statefulSets() - .inNamespace(namespace) - .withLabels(Constants.DEFAULT_LABELS) - .list() - .getItems(); - assertThat(kcDeployments.size()).isZero(); - }); + Log.info("Deleting Keycloak CR"); + + // due to https://github.com/operator-framework/java-operator-sdk/issues/2314 we + // try to ensure that the operator has processed the delete event from root objects + // this can be simplified to just the root deletion after we pick up the fix + // it can be further simplified after https://github.com/fabric8io/kubernetes-client/issues/5838 + // to just a timed foreground deletion + var roots = List.of(Keycloak.class, KeycloakRealmImport.class); + var dependents = List.of(StatefulSet.class, Secret.class, Service.class, Pod.class, Job.class); + + var rootsDeleted = CompletableFuture.allOf(roots.stream() + .map(c -> k8sclient.resources(c).informOnCondition(List::isEmpty)).toArray(CompletableFuture[]::new)); + roots.stream().forEach(c -> k8sclient.resources(c).withGracePeriod(0).delete()); + try { + rootsDeleted.get(1, TimeUnit.MINUTES); + } catch (Exception e) { + // delete event should have arrived quickly because this is a background delete + throw new RuntimeException(e); + } + dependents.stream().map(c -> k8sclient.resources(c).withLabels(Constants.DEFAULT_LABELS)) + .forEach(r -> r.withGracePeriod(0).delete()); + // enforce that the dependents are gone + Awaitility.await().during(5, TimeUnit.SECONDS).until(() -> { + if (dependents.stream().anyMatch( + c -> !k8sclient.resources(c).withLabels(Constants.DEFAULT_LABELS).list().getItems().isEmpty())) { + // the operator must have recreated because it hasn't gotten the keycloak + // deleted event, keep cleaning + dependents.stream().map(c -> k8sclient.resources(c).withLabels(Constants.DEFAULT_LABELS)) + .forEach(r -> r.withGracePeriod(0).delete()); + return false; + } + return true; + }); } @Override @@ -316,6 +342,7 @@ public void afterEach(QuarkusTestMethodContext context) { return; } Log.warnf("Test failed with %s: %s", context.getTestStatus().getTestErrorCause().getMessage(), context.getTestStatus().getTestErrorCause().getClass().getName()); + Log.infof("Secrets %s", k8sclient.secrets().list().getItems().stream().map(s -> s.getMetadata().getName()).collect(Collectors.joining(", "))); logEvents(); savePodLogs(); // provide some helpful entries in the main log as well @@ -325,7 +352,7 @@ public void afterEach(QuarkusTestMethodContext context) { } logFailed(k8sclient.apps().statefulSets().withName(POSTGRESQL_NAME), StatefulSet::getStatus); k8sclient.pods().withLabel("app", "keycloak-realm-import").list().getItems().stream() - .forEach(pod -> logFailed(k8sclient.pods().resource(pod), Pod::getStatus)); + .forEach(pod -> log(k8sclient.pods().resource(pod), Pod::getStatus, false)); } finally { cleanup(); } @@ -339,11 +366,11 @@ private & Loggable> void log(R res } Log.warnf("%s failed to become ready %s", instance.getMetadata().getName(), Serialization.asYaml(statusExtractor.apply(instance))); } else { - Log.infof("%s is ready %s", instance.getMetadata().getName(), Serialization.asYaml(statusExtractor.apply(instance))); + Log.infof("%s is ready %s %s", instance.getMetadata().getName(), resource.isReady(), Serialization.asYaml(statusExtractor.apply(instance))); } try { String log = resource.getLog(); - log = log.substring(Math.max(0, log.length() - 5000)); + log = log.substring(Math.max(0, log.length() - 50000)); Log.warnf("%s log: %s", instance.getMetadata().getName(), log); } catch (KubernetesClientException e) { Log.warnf("No %s log: %s", instance.getMetadata().getName(), e.getMessage()); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/CacheTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/CacheTest.java new file mode 100644 index 000000000000..497146a6720b --- /dev/null +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/CacheTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.operator.testsuite.integration; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.quarkus.logging.Log; +import io.quarkus.test.junit.QuarkusTest; + +import org.apache.commons.io.IOUtils; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpecBuilder; +import org.keycloak.operator.testsuite.utils.CRAssert; +import org.keycloak.operator.testsuite.utils.K8sUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak; + +@QuarkusTest +public class CacheTest extends BaseOperatorTest { + + private static final String CONFIGMAP_NAME = "my-config"; + + @AfterEach + public void cleanupConfigMap() { + k8sclient.configMaps().withName(CONFIGMAP_NAME).delete(); + } + + @Test + public void testCreateCacheConfigMapFileAfterDeployment() { + var kc = getTestKeycloakDeployment(false); + var deploymentName = kc.getMetadata().getName(); + kc.getSpec().setCacheSpec(new CacheSpecBuilder().withNewConfigMapFile("file", CONFIGMAP_NAME, false).build()); + + deployKeycloak(k8sclient, kc, false); + + // Check Operator has deployed Keycloak and the statefulset exists, this allows + // for the watched configmap to be picked up + Log.info("Checking Operator has deployed Keycloak deployment"); + Resource stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName); + Resource keycloakResource = k8sclient.resources(Keycloak.class).withName(deploymentName); + // expect no errors and not ready, which means we'll keep reconciling + Awaitility.await().ignoreExceptions().atMost(2, TimeUnit.MINUTES).during(10, TimeUnit.SECONDS) + .untilAsserted(() -> { + StatefulSet deployment = stsResource.get(); + assertThat(deployment).isNotNull(); + Keycloak keycloak = keycloakResource.get(); + CRAssert.assertKeycloakStatusCondition(keycloak, KeycloakStatusCondition.HAS_ERRORS, false); + CRAssert.assertKeycloakStatusCondition(keycloak, KeycloakStatusCondition.READY, false); + var pod = k8sclient.pods().withLabelSelector(deployment.getSpec().getSelector()).list().getItems() + .get(0); + assertThat(pod.getStatus().getPhase()).isEqualTo("Pending"); + }); + + // should allow the deployment to proceed, but it won't become ready + Log.info("Checking Operator has picked up a bad configmap"); + createCacheConfigMap(false); + + Awaitility.await().ignoreExceptions().atMost(3, TimeUnit.MINUTES).untilAsserted(() -> { + StatefulSet deployment = stsResource.get(); + assertThat(deployment).isNotNull(); + // check the pod directly - it takes longer for us to update our Keycloak status + // with an error + var pod = k8sclient.pods().withLabelSelector(deployment.getSpec().getSelector()).list().getItems().get(0); + assertThat(Serialization.asYaml(pod.getStatus().getContainerStatuses().get(0))).contains("terminated"); + }); + + // should become fully ready + Log.info("Checking Operator has picked up a valid configmap"); + createCacheConfigMap(true); + + K8sUtils.waitForKeycloakToBeReady(k8sclient, kc); + } + + @Test + public void testCacheConfigMapFile() { + var kc = getTestKeycloakDeployment(true); + kc.getSpec().setCacheSpec(new CacheSpecBuilder().withNewConfigMapFile("file", CONFIGMAP_NAME, false).build()); + + createCacheConfigMap(true); + + // should immediately seem ready because probes are disabled + deployKeycloak(k8sclient, kc, true); + } + + private void createCacheConfigMap(boolean valid) { + try { + K8sUtils.set(k8sclient, + new ConfigMapBuilder().withNewMetadata().withName(CONFIGMAP_NAME).endMetadata() + .addToData("file", + valid ? IOUtils.resourceToString("/cache-ispn.xml", StandardCharsets.UTF_8) + : "this isn't right") + .build()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java index 61982aa3741d..890e939f0967 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java @@ -39,7 +39,7 @@ public class KeycloakTruststoresTests extends BaseOperatorTest { public void testTruststoreMissing() { var kc = getTestKeycloakDeployment(true); var deploymentName = kc.getMetadata().getName(); - kc.getSpec().getTruststores().put("xyz", new TruststoreBuilder().withName("xyz").withNewSecret().withName("xyz").endSecret().build()); + kc.getSpec().getTruststores().put("xyz", new TruststoreBuilder().withNewSecret().withName("xyz").endSecret().build()); deployKeycloak(k8sclient, kc, false); Resource stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName); @@ -58,7 +58,7 @@ public void testTrustroreExists() { var deploymentName = kc.getMetadata().getName(); K8sUtils.set(k8sclient, getResourceFromFile("example-truststore-secret.yaml", Secret.class)); - kc.getSpec().getTruststores().put("example", new TruststoreBuilder().withName("example").withNewSecret().withName("example-truststore-secret").endSecret().build()); + kc.getSpec().getTruststores().put("example", new TruststoreBuilder().withNewSecret().withName("example-truststore-secret").endSecret().build()); deployKeycloak(k8sclient, kc, true); Resource stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java index 40ba5adbe48a..2323f178d54f 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java @@ -26,11 +26,12 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; import io.quarkus.test.junit.QuarkusTest; + import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.testsuite.utils.CRAssert; import org.keycloak.operator.testsuite.utils.K8sUtils; -import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import java.util.Collections; @@ -68,10 +69,7 @@ public void testPodTemplateIsMerged() { var keycloakPod = k8sclient .pods() .inNamespace(namespace) - .withLabel("app", "keycloak") - .list() - .getItems() - .get(0); + .withName("example-podtemplate-kc-0").get(); var logs = k8sclient .pods() diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java b/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java index 8bb1561f1543..256ed3df09ff 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java @@ -68,7 +68,7 @@ public static List set(KubernetesClient client, InputStream stream) } public static List set(KubernetesClient client, InputStream stream, Function modifier) { - return client.load(stream).items().stream().map(modifier).map(i -> set(client, i)).collect(Collectors.toList()); + return client.load(stream).items().stream().map(modifier).filter(Objects::nonNull).map(i -> set(client, i)).collect(Collectors.toList()); } public static T set(KubernetesClient client, T hasMetadata) { diff --git a/operator/src/test/resources/cache-ispn.xml b/operator/src/test/resources/cache-ispn.xml new file mode 100644 index 000000000000..72cf71785ea5 --- /dev/null +++ b/operator/src/test/resources/cache-ispn.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index cad8830982f6..f6e5e486290e 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -65,6 +65,10 @@ spec: memory: "1500M" proxy: headers: forwarded + truststores: + x: + secret: + name: my-secret unsupported: podTemplate: metadata: diff --git a/pom.xml b/pom.xml index 45798d7c73c3..3ab201d5035e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,11 +31,11 @@
    org.keycloak keycloak-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 pom - 999.0.0-SNAPSHOT + 24.0.5-PS-2 1.5.8 @@ -45,8 +45,8 @@ jboss-snapshots-repository https://s01.oss.sonatype.org/content/repositories/snapshots/ - 3.8.1 - 3.8.1 + 3.8.4 + 3.8.4 ${timestamp} @@ -97,7 +97,7 @@ 2.2.224 6.2.13.Final 6.2.13.Final - 14.0.25.Final + 14.0.27.Final 2.1.1 @@ -256,17 +256,21 @@ - scm:git:git://github.com/keycloak/keycloak.git - scm:git:git@github.com:keycloak/keycloak.git - https://github.com/keycloak/keycloak/tree/master/ + scm:git:git@gitlab.intra.prime-sign.com:tc-dev/keycloak.git + HEAD - ${jboss.releases.repo.id} - JBoss Releases Repository - ${jboss.releases.repo.url} + releases + libs-releases-local + ${env.MAVEN_REPO_URL}/libs-releases-local + + snapshots + libs-snapshots-local + ${env.MAVEN_REPO_URL}/libs-snapshots-local + @@ -1218,6 +1222,11 @@ keycloak-model-jpa ${project.version} + + org.keycloak + keycloak-model-legacy + ${project.version} + org.keycloak keycloak-model-storage diff --git a/quarkus/config-api/pom.xml b/quarkus/config-api/pom.xml index 8b9737609a17..f84c5c2c383c 100755 --- a/quarkus/config-api/pom.xml +++ b/quarkus/config-api/pom.xml @@ -21,7 +21,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java index 745ea50a6a12..b6c3cc522631 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java @@ -60,20 +60,17 @@ public enum Stack { .category(OptionCategory.CACHE) .description("Encrypts the network communication between Keycloak servers.") .defaultValue(Boolean.FALSE) - .buildTime(true) .build(); public static final Option CACHE_EMBEDDED_MTLS_KEYSTORE = new OptionBuilder<>(CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY, String.class) .category(OptionCategory.CACHE) .description("The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. " + "By default, it lookup 'cache-mtls-keystore.p12' under conf/ directory.") - .buildTime(true) .build(); public static final Option CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD = new OptionBuilder<>(CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY, String.class) .category(OptionCategory.CACHE) .description("The password to access the Keystore.") - .buildTime(true) .build(); public static final Option CACHE_EMBEDDED_MTLS_TRUSTSTORE = new OptionBuilder<>(CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY, String.class) @@ -81,13 +78,11 @@ public enum Stack { .description("The Truststore file path. " + "It should contain the trusted certificates or the Certificate Authority that signed the certificates. " + "By default, it lookup 'cache-mtls-truststore.p12' under conf/ directory.") - .buildTime(true) .build(); public static final Option CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD = new OptionBuilder<>(CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY, String.class) .category(OptionCategory.CACHE) .description("The password to access the Truststore.") - .buildTime(true) .build(); public static final Option CACHE_REMOTE_HOST = new OptionBuilder<>(CACHE_REMOTE_HOST_PROPERTY, String.class) diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java index e2a55fbe29f7..1b3631910694 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java @@ -52,9 +52,9 @@ public static boolean isLiquibaseDatabaseSupported(String databaseType, String d return false; } - public static Optional getVendorByDbKind(String dbKind) { + public static Optional getVendor(String vendor) { return Arrays.stream(Vendor.values()) - .filter(v -> v.isOfKind(dbKind)) + .filter(v -> v.isOfKind(vendor) || asList(v.aliases).contains(vendor)) .findAny(); } diff --git a/quarkus/container/Dockerfile b/quarkus/container/Dockerfile index 0fe77a56d356..c86b5d1d5c2c 100644 --- a/quarkus/container/Dockerfile +++ b/quarkus/container/Dockerfile @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi9 AS ubi-micro-build -ENV KEYCLOAK_VERSION 999.0.0-SNAPSHOT +ENV KEYCLOAK_VERSION 24.0.5-PS-2 ARG KEYCLOAK_DIST=https://github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.tar.gz RUN dnf install -y tar gzip diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml index fcad78bde1a7..02214359186c 100644 --- a/quarkus/deployment/pom.xml +++ b/quarkus/deployment/pom.xml @@ -5,7 +5,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java index 7675d915e983..13b45bbda338 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java @@ -25,6 +25,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Collectors; + +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import jakarta.enterprise.context.ApplicationScoped; import org.infinispan.commons.util.FileLookupFactory; import org.keycloak.config.MetricsOptions; @@ -39,12 +42,16 @@ import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.ShutdownContextBuildItem; public class CacheBuildSteps { @Consume(ConfigBuildItem.class) - @Record(ExecutionTime.STATIC_INIT) + // Consume LoggingSetupBuildItem.class and record RUNTIME_INIT are necessary to ensure that logging is set up before the caches are initialized. + // This is to prevent the class TP in JGroups to pick up the trace logging at start up. While the logs will not appear on the console, + // they will still be created and use CPU cycles and create garbage collection. + // See: https://issues.redhat.com/browse/JGRP-2130 for the JGroups discussion, and https://github.com/keycloak/keycloak/issues/29129 for the issue Keycloak had with this. + @Consume(LoggingSetupBuildItem.class) + @Record(ExecutionTime.RUNTIME_INIT) @BuildStep void configureInfinispan(KeycloakRecorder recorder, BuildProducer syntheticBeanBuildItems, ShutdownContextBuildItem shutdownContext) { String configFile = getConfigValue("kc.spi-connections-infinispan-quarkus-config-file").getValue(); diff --git a/quarkus/dist/pom.xml b/quarkus/dist/pom.xml index 4189fb8967a4..8ead849777bc 100755 --- a/quarkus/dist/pom.xml +++ b/quarkus/dist/pom.xml @@ -21,7 +21,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-quarkus-dist diff --git a/quarkus/pom.xml b/quarkus/pom.xml index 4c0a5c76ca8f..4e85c34302ed 100644 --- a/quarkus/pom.xml +++ b/quarkus/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml Keycloak Quarkus Parent diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index 0b5cf28e074b..5d1b8575d582 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -5,7 +5,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 @@ -247,6 +247,16 @@ + + org.keycloak + keycloak-model-legacy + + + * + * + + + org.keycloak keycloak-model-storage-private diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index f91e3670758c..ca76823fe968 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -37,17 +37,11 @@ import io.quarkus.runtime.Quarkus; import org.jboss.logging.Logger; -import org.keycloak.Config; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler; import org.keycloak.quarkus.runtime.cli.PropertyException; import org.keycloak.quarkus.runtime.cli.Picocli; import org.keycloak.common.Version; import org.keycloak.quarkus.runtime.cli.command.Start; -import org.keycloak.services.ServicesLogger; -import org.keycloak.services.managers.ApplianceBootstrap; -import org.keycloak.services.resources.KeycloakApplication; import io.quarkus.runtime.QuarkusApplication; import io.quarkus.runtime.annotations.QuarkusMain; @@ -59,9 +53,6 @@ @ApplicationScoped public class KeycloakMain implements QuarkusApplication { - private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN"; - private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD"; - public static void main(String[] args) { System.setProperty("kc.version", Version.VERSION); List cliArgs = null; @@ -140,10 +131,6 @@ public static void start(ExecutionExceptionHandler errorHandler, PrintWriter err */ @Override public int run(String... args) throws Exception { - if (!isImportExportMode()) { - createAdminUser(); - } - if (isDevProfile()) { Logger.getLogger(KeycloakMain.class).warnf("Running the server in development mode. DO NOT use this configuration in production."); } @@ -161,23 +148,4 @@ public int run(String... args) throws Exception { return exitCode; } - private void createAdminUser() { - String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR); - String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR); - - if ((adminUserName == null || adminUserName.trim().length() == 0) - || (adminPassword == null || adminPassword.trim().length() == 0)) { - return; - } - - KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory(); - - try { - KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { - new ApplianceBootstrap(session).createMasterRealmUser(adminUserName, adminPassword); - }); - } catch (Throwable t) { - ServicesLogger.LOGGER.addUserFailed(t, adminUserName, Config.getAdminRealm()); - } - } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index d8120daf3433..af23eb040cf2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -323,7 +323,7 @@ public static void validateConfig(List cliArgs, AbstractCommand abstract } if (!deprecatedInUse.isEmpty()) { - logger.warn("The following used options or option values are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse)); + logger.warn("The following used options or option values are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details."); } } finally { PropertyMappingInterceptor.enable(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java index b2defe3d7a9b..e1b31f8f1fcc 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java @@ -92,7 +92,7 @@ private static Set fips() { public static final Set JDBC_MYSQL = Set.of( "io.quarkus:quarkus-jdbc-mysql", "io.quarkus:quarkus-jdbc-mysql-deployment", - "mysql:mysql-connector-java" + "com.mysql:mysql-connector-j" ); public static final Set JDBC_MSSQL = Set.of( @@ -120,18 +120,31 @@ private static Set fips() { .collect(Collectors.toUnmodifiableSet()); private static Set jdbcDrivers() { - final Database.Vendor vendor = Configuration.getOptionalValue("quarkus.datasource.db-kind") - .flatMap(Database::getVendorByDbKind) - .orElse(Database.Vendor.H2); - - final Set jdbcArtifacts = switch (vendor) { - case H2 -> JDBC_H2; - case MYSQL -> JDBC_MYSQL; - case MARIADB -> JDBC_MARIADB; - case POSTGRES -> JDBC_POSTGRES; - case MSSQL -> JDBC_MSSQL; - case ORACLE -> JDBC_ORACLE; - }; + final Set vendorsOfAllDatasources = new HashSet<>(); + + Configuration.getConfig().getPropertyNames().forEach(p -> { + if (p.startsWith("quarkus.datasource.") && p.endsWith(".db-kind")) { + Configuration.getOptionalValue(p) + .flatMap(Database::getVendor) + .ifPresent(vendorsOfAllDatasources::add); + } + }); + + if (vendorsOfAllDatasources.isEmpty()) { + vendorsOfAllDatasources.add(Database.Vendor.H2); + } + + final Set jdbcArtifacts = vendorsOfAllDatasources.stream() + .map(vendor -> switch (vendor) { + case H2 -> JDBC_H2; + case MYSQL -> JDBC_MYSQL; + case MARIADB -> JDBC_MARIADB; + case POSTGRES -> JDBC_POSTGRES; + case MSSQL -> JDBC_MSSQL; + case ORACLE -> JDBC_ORACLE; + }) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); final Set allJdbcDrivers = new HashSet<>(JDBC_DRIVERS); allJdbcDrivers.removeAll(jdbcArtifacts); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java index 965ee4853a20..dd155eb38a71 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java @@ -151,13 +151,7 @@ private static BiFunction, ConfigSourceInterceptorContext, Opti } private static Optional resolveFileLogLocation(Optional value, ConfigSourceInterceptorContext configSourceInterceptorContext) { - String location = value.get(); - - if (location.endsWith(File.separator)) { - return of(location + LoggingOptions.DEFAULT_LOG_FILENAME); - } - - return value; + return value.map(location -> location.endsWith(File.separator) ? location + LoggingOptions.DEFAULT_LOG_FILENAME : location); } private static Level toLevel(String categoryLevel) throws IllegalArgumentException { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index cbd7093e37e1..e9651c66b6e3 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -104,7 +104,7 @@ ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) // try to obtain the value for the property we want to map first ConfigValue config = convertValue(context.proceed(from)); - if (config == null) { + if (config == null || config.getValue() == null) { if (mapFrom != null) { // if the property we want to map depends on another one, we use the value from the other property to call the mapper String parentKey = MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + mapFrom; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java index 18840350d7ab..5a0a8773ae96 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java @@ -20,14 +20,17 @@ import java.util.HashSet; import java.util.Set; +import org.keycloak.Config; import jakarta.enterprise.event.Observes; import jakarta.ws.rs.ApplicationPath; -import org.keycloak.config.HostnameOptions; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.platform.Platform; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource; import org.keycloak.services.resources.KeycloakApplication; @@ -35,15 +38,23 @@ import io.quarkus.runtime.StartupEvent; import io.smallrye.common.annotation.Blocking; +import static org.keycloak.quarkus.runtime.Environment.isImportExportMode; + @ApplicationPath("/") @Blocking public class QuarkusKeycloakApplication extends KeycloakApplication { + private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN"; + private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD"; + void onStartupEvent(@Observes StartupEvent event) { QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); platform.started(); QuarkusPlatform.exitOnError(); startup(); + if (!isImportExportMode()) { + createAdminUser(); + } } void onShutdownEvent(@Observes ShutdownEvent event) { @@ -62,6 +73,26 @@ protected void loadConfig() { // no need to load config provider because we force quarkus impl } + private void createAdminUser() { + String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR); + String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR); + + if ((adminUserName == null || adminUserName.trim().length() == 0) + || (adminPassword == null || adminPassword.trim().length() == 0)) { + return; + } + + KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory(); + + try { + KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { + new ApplianceBootstrap(session).createMasterRealmUser(adminUserName, adminPassword); + }); + } catch (Throwable t) { + ServicesLogger.LOGGER.addUserFailed(t, adminUserName, Config.getAdminRealm()); + } + } + @Override public Set getSingletons() { return Set.of(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java index c3bbea9651d8..816637bcf5ca 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/CreateSessionHandler.java @@ -22,6 +22,7 @@ import jakarta.ws.rs.container.CompletionCallback; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; +import org.keycloak.common.util.Resteasy; import org.keycloak.models.KeycloakSession; import org.keycloak.quarkus.runtime.transaction.TransactionalSessionHandler; @@ -39,13 +40,20 @@ public void handle(ResteasyReactiveRequestContext requestContext) { if (currentSession == null) { // this handler might be invoked multiple times when resolving sub-resources // make sure the session is created once - routingContext.put(KeycloakSession.class.getName(), create()); + KeycloakSession session = create(); + routingContext.put(KeycloakSession.class.getName(), session); context.registerCompletionCallback(this); + Resteasy.pushContext(KeycloakSession.class, session); } } @Override public void onComplete(Throwable throwable) { + try { + close(Resteasy.getContextData(KeycloakSession.class)); + } catch (Exception e) { + + } clearContextData(); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java index 54f8d14aa5b8..32dbc7ed0663 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/resteasy/KeycloakHandlerChainCustomizer.java @@ -22,11 +22,8 @@ import static jakarta.ws.rs.HttpMethod.PUT; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.concurrent.Executor; -import java.util.function.Supplier; import org.jboss.resteasy.reactive.common.model.ResourceClass; import org.jboss.resteasy.reactive.server.handlers.FormBodyHandler; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; @@ -37,14 +34,7 @@ public final class KeycloakHandlerChainCustomizer implements HandlerChainCustomi private final CreateSessionHandler TRANSACTIONAL_SESSION_HANDLER = new CreateSessionHandler(); - private final FormBodyHandler formBodyHandler = new FormBodyHandler(true, new Supplier() { - @Override - public Executor get() { - // we always run in blocking mode and never run in an event loop thread - // we don't need to provide an executor to dispatch to a worker thread to parse the body - return null; - } - }, Set.of()); + private final FormBodyHandler formBodyHandler = new FormBodyHandler(true, () -> Runnable::run, Set.of()); @Override public List handlers(Phase phase, ResourceClass resourceClass, @@ -53,7 +43,7 @@ public List handlers(Phase phase, ResourceClass resourceClass switch (phase) { case BEFORE_METHOD_INVOKE: - if (!resourceMethod.isFormParamRequired() && + if (!resourceMethod.isFormParamRequired() && (PATCH.equalsIgnoreCase(resourceMethod.getHttpMethod()) || POST.equalsIgnoreCase(resourceMethod.getHttpMethod()) || PUT.equalsIgnoreCase(resourceMethod.getHttpMethod()))) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java index c6a91e2f8dbb..5feba1cb37c9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java @@ -30,7 +30,6 @@ import org.infinispan.configuration.global.GlobalConfiguration; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; -import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.metrics.config.MicrometerMeterRegisterConfigurationBuilder; import org.infinispan.persistence.remote.configuration.ExhaustedAction; @@ -41,6 +40,7 @@ import org.jgroups.protocols.UDP; import org.jgroups.util.TLS; import org.jgroups.util.TLSClientAuth; +import org.keycloak.marshalling.Marshalling; import org.keycloak.quarkus.runtime.configuration.Configuration; import javax.net.ssl.SSLContext; @@ -112,11 +112,7 @@ private DefaultCacheManager startCacheManager() { builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); } - // For Infinispan 10, we go with the JBoss marshalling. - // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. - // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details - builder.getGlobalConfigurationBuilder().serialization().marshaller(new JBossUserMarshaller()); - + Marshalling.configure(builder.getGlobalConfigurationBuilder()); return new DefaultCacheManager(builder, isStartEagerly()); } @@ -244,11 +240,7 @@ private void configureRemoteStores(ConfigurationBuilderHolder builder) { .saslMechanism(SCRAM_SHA_512) .addServer() .host(cacheRemoteHost) - .port(cacheRemotePort) - // This is a workaround for the following issue https://github.com/keycloak/keycloak/issues/27117 and should be removed when the issue is fixed - .async().enable().modificationQueueSize(1024) - // end of workaround - ; + .port(cacheRemotePort); }); } } diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/IgnoredArtifactsTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/IgnoredArtifactsTest.java index 072349fd80e7..7b2e5b25a6e0 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/IgnoredArtifactsTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/IgnoredArtifactsTest.java @@ -24,12 +24,16 @@ import org.keycloak.config.DatabaseOptions; import org.keycloak.config.HealthOptions; import org.keycloak.config.MetricsOptions; +import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.IgnoredArtifacts; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; +import java.util.Collection; import java.util.HashSet; import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -94,16 +98,41 @@ public void jdbcPostgres() { assertJdbc("postgres", JDBC_POSTGRES); } + // default ignored JDBC artifacts specified in quarkus.properties + private static final Set IGNORED_JDBC_FROM_PROPS = Stream.of(JDBC_MARIADB, JDBC_POSTGRES) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + @Test + public void multipleDatasources() { + var defaultDS = Configuration.getOptionalValue("quarkus.datasource.db-kind"); + assertThat(defaultDS.isPresent(), is(true)); + assertThat(defaultDS.get(), is("h2")); + + var dogStoreDS = Configuration.getOptionalValue("quarkus.datasource.dog-store.db-kind"); + assertThat(dogStoreDS.isPresent(), is(true)); + assertThat(dogStoreDS.get(), is("mariadb")); + + var catStoreDS = Configuration.getOptionalValue("quarkus.datasource.cat-store.db-kind"); + assertThat(catStoreDS.isPresent(), is(true)); + assertThat(catStoreDS.get(), is("postgresql")); + + assertJdbc("h2", JDBC_H2); + } + private void assertJdbc(String vendor, Set notIgnored) { + var notIgnoredWithDefaults = new HashSet<>(notIgnored); + notIgnoredWithDefaults.addAll(IGNORED_JDBC_FROM_PROPS); + System.setProperty(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + DatabaseOptions.DB.getKey(), vendor); try { final var resultArtifacts = IgnoredArtifacts.getDefaultIgnoredArtifacts(); assertThat(String.format("Ignored artifacts does not comply with the specified artifacts for '%s' JDBC driver", vendor), resultArtifacts, - not(CoreMatchers.hasItems(notIgnored.toArray(new String[0])))); + not(CoreMatchers.hasItems(notIgnoredWithDefaults.toArray(new String[0])))); final var includedArtifacts = new HashSet<>(IgnoredArtifacts.JDBC_DRIVERS); - includedArtifacts.removeAll(notIgnored); + includedArtifacts.removeAll(notIgnoredWithDefaults); assertThat("Ignored artifacts does not contain items for the other JDBC drivers", resultArtifacts, CoreMatchers.hasItems(includedArtifacts.toArray(new String[0]))); diff --git a/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties b/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties index 880d86225c2b..95d0635596bb 100644 --- a/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties +++ b/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties @@ -8,4 +8,8 @@ quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManage # For test nested properties quarkus.datasource.foo = jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/data/keycloakdb -quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}} \ No newline at end of file +quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}} + +# test multiple datasources db-kind +quarkus.datasource.dog-store.db-kind=mariadb +quarkus.datasource.cat-store.db-kind=postgresql \ No newline at end of file diff --git a/quarkus/server/pom.xml b/quarkus/server/pom.xml index 4cd6f2f88493..19abb81f004b 100644 --- a/quarkus/server/pom.xml +++ b/quarkus/server/pom.xml @@ -7,7 +7,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/quarkus/tests/integration/pom.xml b/quarkus/tests/integration/pom.xml index 1876cbaa773a..1c0ae45c0e2b 100644 --- a/quarkus/tests/integration/pom.xml +++ b/quarkus/tests/integration/pom.xml @@ -24,7 +24,7 @@ keycloak-quarkus-test-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java index 68c4d06cb2e7..2faf8e184a93 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FipsDistTest.java @@ -78,8 +78,7 @@ void testUnsupportedHttpsJksKeyStoreInStrictMode(KeycloakDistribution dist) { dist.copyOrReplaceFileFromClasspath("/server.keystore", Path.of("conf", "server.keystore")); CLIResult cliResult = dist.run("start", "--fips-mode=strict"); dist.assertStopped(); - // after https://issues.redhat.com/browse/JBTM-3830 reenable this check - //cliResult.assertMessage("ERROR: java.lang.IllegalArgumentException: malformed sequence"); + cliResult.assertMessage("ERROR: java.lang.IllegalArgumentException: malformed sequence"); }); } @@ -127,8 +126,7 @@ void testUnsupportedHttpsPkcs12KeyStoreInStrictMode(KeycloakDistribution dist) { dist.copyOrReplaceFileFromClasspath("/server.keystore.pkcs12", Path.of("conf", "server.keystore")); CLIResult cliResult = dist.run("start", "--fips-mode=strict", "--https-key-store-password=passwordpassword"); dist.assertStopped(); - // after https://issues.redhat.com/browse/JBTM-3830 reenable this check - //cliResult.assertMessage("ERROR: java.lang.IllegalArgumentException: malformed sequence"); + cliResult.assertMessage("ERROR: java.lang.IllegalArgumentException: malformed sequence"); }); } diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.unix.approved.txt index 6c43dce4ce5c..6ffbbac9e33b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.unix.approved.txt @@ -27,20 +27,6 @@ Cache: --cache-config-file Defines the file from which cache configuration should be loaded from. The configuration file is relative to the 'conf/' directory. ---cache-embedded-mtls-enabled - Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-stack Define the default stack to use for cluster communication and node discovery. This option only takes effect if 'cache' is set to 'ispn'. Default: udp. diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.unix.approved.txt index 93b6b3260189..9c2ed584d9a2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.unix.approved.txt @@ -18,6 +18,20 @@ Options: Cache: +--cache-embedded-mtls-enabled + Encrypts the network communication between Keycloak servers. Default: false. +--cache-embedded-mtls-key-store-file + The Keystore file path. The Keystore must contain the certificate to use by + the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under + conf/ directory. +--cache-embedded-mtls-key-store-password + The password to access the Keystore. +--cache-embedded-mtls-trust-store-file + The Truststore file path. It should contain the trusted certificates or the + Certificate Authority that signed the certificates. By default, it lookup + 'cache-mtls-truststore.p12' under conf/ directory. +--cache-embedded-mtls-trust-store-password + The password to access the Truststore. --cache-remote-host The hostname of the remote server for the remote store configuration. It replaces the 'host' attribute of 'remote-server' tag of the configuration diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.unix.approved.txt index 93b6b3260189..9c2ed584d9a2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.unix.approved.txt @@ -18,6 +18,20 @@ Options: Cache: +--cache-embedded-mtls-enabled + Encrypts the network communication between Keycloak servers. Default: false. +--cache-embedded-mtls-key-store-file + The Keystore file path. The Keystore must contain the certificate to use by + the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under + conf/ directory. +--cache-embedded-mtls-key-store-password + The password to access the Keystore. +--cache-embedded-mtls-trust-store-file + The Truststore file path. It should contain the trusted certificates or the + Certificate Authority that signed the certificates. By default, it lookup + 'cache-mtls-truststore.p12' under conf/ directory. +--cache-embedded-mtls-trust-store-password + The password to access the Truststore. --cache-remote-host The hostname of the remote server for the remote store configuration. It replaces the 'host' attribute of 'remote-server' tag of the configuration diff --git a/quarkus/tests/junit5/pom.xml b/quarkus/tests/junit5/pom.xml index 94b7d0e77557..00f419c6799a 100644 --- a/quarkus/tests/junit5/pom.xml +++ b/quarkus/tests/junit5/pom.xml @@ -24,7 +24,7 @@ keycloak-quarkus-test-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java index 5ed1160a9bc9..d8333a0ec41a 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java @@ -79,10 +79,6 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { - // TODO: reconsider the hardcoded timeout once https://issues.redhat.com/browse/JBTM-3830 is pulled into Keycloak - // ensures that the total wait time (two minutes for readiness + 200 seconds) is longer than the transaction timeout of 5 minutes - private static final int LONG_SHUTDOWN_WAIT = 200; - private static final int DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 10; private static final Logger LOG = Logger.getLogger(RawKeycloakDistribution.class); @@ -166,7 +162,7 @@ public void stop() { destroyDescendantsOnWindows(keycloak, false); keycloak.destroy(); - keycloak.waitFor(LONG_SHUTDOWN_WAIT, TimeUnit.SECONDS); + keycloak.waitFor(DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS); exitCode = keycloak.exitValue(); } catch (Exception cause) { destroyDescendantsOnWindows(keycloak, true); @@ -266,7 +262,7 @@ public String[] getCliArgs(List arguments) { public void assertStopped() { try { if (keycloak != null) { - keycloak.onExit().get(LONG_SHUTDOWN_WAIT, TimeUnit.SECONDS); + keycloak.onExit().get(DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -276,7 +272,7 @@ public void assertStopped() { } catch (TimeoutException e) { LOG.warn("Process did not exit as expected, will attempt a thread dump"); threadDump(); - LOG.warn("TODO: this should be a hard error / re-diagnosed after https://issues.redhat.com/browse/JBTM-3830 is pulled into Keycloak"); + throw new RuntimeException(e); } } @@ -297,9 +293,8 @@ private void waitForReadiness(String scheme, int port) throws MalformedURLExcept while (true) { if (System.currentTimeMillis() - startTime > getStartTimeout()) { threadDump(); - LOG.warn("Timeout [" + getStartTimeout() + "] while waiting for Quarkus server", ex); - LOG.warn("TODO: this should be a hard error / re-diagnosed after https://issues.redhat.com/browse/JBTM-3830 is pulled into Keycloak"); - return; + throw new IllegalStateException( + "Timeout [" + getStartTimeout() + "] while waiting for Quarkus server", ex); } if (!keycloak.isAlive()) { diff --git a/quarkus/tests/pom.xml b/quarkus/tests/pom.xml index 434605e1b9f2..3b616fc87c2b 100644 --- a/quarkus/tests/pom.xml +++ b/quarkus/tests/pom.xml @@ -24,7 +24,7 @@ keycloak-quarkus-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/release-details b/release-details new file mode 100644 index 000000000000..bfe0f9dfb36b --- /dev/null +++ b/release-details @@ -0,0 +1,3 @@ +VERSION=24.0.5-PS-2 +SHORT_VERSION=24.0.5-PS-2 +NPM_VERSION=24.0.5-PS-2 diff --git a/rest/admin-ui-ext/pom.xml b/rest/admin-ui-ext/pom.xml index caac25c83318..184b254ca98b 100644 --- a/rest/admin-ui-ext/pom.xml +++ b/rest/admin-ui-ext/pom.xml @@ -22,7 +22,7 @@ org.keycloak keycloak-rest-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-rest-admin-ui-ext diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java index b0c48c862529..3a0492112339 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AdminExtResource.java @@ -57,6 +57,6 @@ public UIRealmResource realm() { @Path("/users") public UsersResource users() { - return new UsersResource(session); + return new UsersResource(session, auth); } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java index a37dcd6b039b..73e22a6d95ad 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java @@ -17,9 +17,9 @@ import jakarta.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.NoCache; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.userprofile.config.UPConfig; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.utils.StringUtil; @@ -30,10 +30,12 @@ public class UserResource { private final KeycloakSession session; + private final AdminPermissionEvaluator auth; private final UserModel user; - public UserResource(KeycloakSession session, UserModel user) { + public UserResource(KeycloakSession session, AdminPermissionEvaluator auth, UserModel user) { this.session = session; + this.auth = auth; this.user = user; } @@ -42,6 +44,8 @@ public UserResource(KeycloakSession session, UserModel user) { @NoCache @Produces(MediaType.APPLICATION_JSON) public Map> getUnmanagedAttributes() { + auth.users().requireView(user); + UserProfileProvider provider = session.getProvider(UserProfileProvider.class); UserProfile profile = provider.create(USER_API, user); diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UsersResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UsersResource.java index f01b69f28caa..41ea079a86a4 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UsersResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UsersResource.java @@ -6,23 +6,40 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.light.LightweightUserAdapter; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import jakarta.ws.rs.ForbiddenException; public class UsersResource { private final KeycloakSession session; - public UsersResource(KeycloakSession session) { + private final AdminPermissionEvaluator auth; + + public UsersResource(KeycloakSession session, AdminPermissionEvaluator auth) { this.session = session; + this.auth = auth; } @Path("{id}") public UserResource getUser(@PathParam("id") String id) { RealmModel realm = session.getContext().getRealm(); - UserModel user = session.users().getUserById(realm, id); + UserModel user = null; + if (LightweightUserAdapter.isLightweightUser(id)) { + UserSessionModel userSession = session.sessions().getUserSession(realm, LightweightUserAdapter.getLightweightUserId(id)); + if (userSession != null) { + user = userSession.getUser(); + } + } else { + user = session.users().getUserById(realm, id); + } if (user == null) { - throw new NotFoundException(); + // we do this to make sure somebody can't phish ids + if (auth.users().canQuery()) throw new NotFoundException("User not found"); + else throw new ForbiddenException(); } - return new UserResource(session, user); + return new UserResource(session, auth, user); } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/AuthenticationMapper.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/AuthenticationMapper.java index 4d32cc4040f6..93c2728ecbc0 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/AuthenticationMapper.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/AuthenticationMapper.java @@ -43,7 +43,7 @@ public static Authentication convertToModel(AuthenticationFlowModel flow, RealmM final List useAsDefault = Stream.of(realm.getBrowserFlow(), realm.getRegistrationFlow(), realm.getDirectGrantFlow(), realm.getResetCredentialsFlow(), realm.getClientAuthenticationFlow(), realm.getDockerAuthenticationFlow(), realm.getFirstBrokerLoginFlow()) - .filter(f -> flow.getAlias().equals(f.getAlias())).map(AuthenticationFlowModel::getAlias).collect(Collectors.toList()); + .filter(f -> f != null && flow.getAlias().equals(f.getAlias())).map(AuthenticationFlowModel::getAlias).collect(Collectors.toList()); if (!useAsDefault.isEmpty()) { authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.DEFAULT, useAsDefault)); diff --git a/rest/pom.xml b/rest/pom.xml index c4d975748c08..eb6fdcc28887 100644 --- a/rest/pom.xml +++ b/rest/pom.xml @@ -22,7 +22,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Administration UI diff --git a/saml-core-api/pom.xml b/saml-core-api/pom.xml index 392c6b495ff3..c060d583cdd7 100755 --- a/saml-core-api/pom.xml +++ b/saml-core-api/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/saml-core/pom.xml b/saml-core/pom.xml index 6d1097dc5595..00c94d80f491 100755 --- a/saml-core/pom.xml +++ b/saml-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml index cafe65717241..8aae56df2077 100755 --- a/server-spi-private/pom.xml +++ b/server-spi-private/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java index ca82d3f3aeff..6a90ed4ee7a4 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java @@ -21,6 +21,7 @@ import org.keycloak.common.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -57,6 +58,11 @@ public interface AbstractAuthenticationFlowContext { */ AuthenticationExecutionModel getExecution(); + /** + * @return the top level flow (root flow) of this authentication + */ + AuthenticationFlowModel getTopLevelFlow(); + /** * Current realm * diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallback.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallback.java index 79006b62a07a..229bb17319ed 100644 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallback.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallback.java @@ -18,6 +18,8 @@ package org.keycloak.authentication; +import org.keycloak.models.AuthenticationFlowModel; + /** * Callback to be triggered during various lifecycle events of authentication flow. * @@ -40,7 +42,9 @@ public interface AuthenticationFlowCallback extends Authenticator { /** * Triggered after the top authentication flow is successfully finished. * It is really suitable for last verification of successful authentication + * + * @param topFlow which was successfully finished */ - default void onTopFlowSuccess() { + default void onTopFlowSuccess(AuthenticationFlowModel topFlow) { } } diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java index 612b859e563a..5f47a33f8832 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java @@ -47,5 +47,7 @@ public enum AuthenticationFlowError { DISPLAY_NOT_SUPPORTED, ACCESS_DENIED, - GENERIC_AUTHENTICATION_ERROR + GENERIC_AUTHENTICATION_ERROR, + + NOT_AUTHENTICATION_RELEVANT_ERROR } diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/CredentialAction.java b/server-spi-private/src/main/java/org/keycloak/authentication/CredentialAction.java new file mode 100644 index 000000000000..f1702bd45762 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authentication/CredentialAction.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.authentication; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.sessions.AuthenticationSessionModel; + +/** + * Marking any required action implementation, that is supposed to work with user credentials + * + * @author Marek Posolda + */ +public interface CredentialAction { + + /** + * @return credential type, which this action is able to register. This should refer to the same value as returned by {@link org.keycloak.credential.CredentialProvider#getType} of the + * corresponding credential provider and {@link AuthenticatorFactory#getReferenceCategory()} of the corresponding authenticator + */ + String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession); +} diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/CredentialRegistrator.java b/server-spi-private/src/main/java/org/keycloak/authentication/CredentialRegistrator.java index 2ee627aa8d8c..bce5a5d1e62e 100644 --- a/server-spi-private/src/main/java/org/keycloak/authentication/CredentialRegistrator.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/CredentialRegistrator.java @@ -1,4 +1,7 @@ package org.keycloak.authentication; -public interface CredentialRegistrator { +/** + * Marking implementation of the action, which is able to register credential of the particular type + */ +public interface CredentialRegistrator extends CredentialAction { } diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionProvider.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionProvider.java index f35a60bb445a..5ea665a2b697 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionProvider.java @@ -38,14 +38,14 @@ public interface RequiredActionProvider extends Provider { default InitiatedActionSupport initiatedActionSupport() { return InitiatedActionSupport.NOT_SUPPORTED; } - + /** * Callback to let the action know that an application-initiated action * was canceled. - * + * * @param session The Keycloak session. * @param authSession The authentication session. - * + * */ default void initiatedActionCanceled(KeycloakSession session, AuthenticationSessionModel authSession) { return; diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index db2db25c4e47..640aade3bec6 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -86,7 +86,9 @@ public interface Details { String CREDENTIAL_TYPE = "credential_type"; String SELECTED_CREDENTIAL_ID = "selected_credential_id"; + String CREDENTIAL_ID = "credential_id"; String AUTHENTICATION_ERROR_DETAIL = "authentication_error_detail"; + String CREDENTIAL_USER_LABEL = "credential_user_label"; String NOT_BEFORE = "not_before"; String NUM_FAILURES = "num_failures"; diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java index f0d513e564d4..7d62373e1de8 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java @@ -119,4 +119,8 @@ public interface Errors { String SLOW_DOWN = "slow_down"; String GENERIC_AUTHENTICATION_ERROR= "generic_authentication_error"; + String CREDENTIAL_NOT_FOUND = "credential_not_found"; + String MISSING_CREDENTIAL_ID = "missing_credential_id"; + String DELETE_CREDENTIAL_FAILED = "delete_credential_failed"; + } diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java b/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java index aa682b5ad634..57124ef5101c 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java @@ -68,6 +68,13 @@ public EventBuilder(RealmModel realm, KeycloakSession session) { this.listeners = getEventListeners(session, realm); realm(realm); + + String storeImmediatelyProperty = System.getenv("KC_EVENT_STOREIMMEDIATELY"); + + if (storeImmediatelyProperty != null) { + this.storeImmediately = Boolean.parseBoolean(storeImmediatelyProperty); + } + } private static EventStoreProvider getEventStoreProvider(KeycloakSession session) { @@ -270,4 +277,4 @@ private void sendNow(EventStoreProvider targetStore, Set eventTypes, Lis } } -} +} \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java index 2e6bb11983da..1456372bf0bc 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java @@ -28,6 +28,7 @@ public enum LoginFormsPages { LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM, LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, IDP_REVIEW_USER_PROFILE, LOGIN_RECOVERY_AUTHN_CODES_INPUT, LOGIN_RECOVERY_AUTHN_CODES_CONFIG, - FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL, LOGIN_RESET_OTP; + FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL, LOGIN_RESET_OTP, + LOGIN_SMS_TAN, ONBOARDING_SMS_TAN, GENERIC_SMS_TAN; } diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java index 5f9138b6b760..c659cd016d75 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java @@ -56,6 +56,18 @@ public interface LoginFormsProvider extends Provider { String getMessage(String message); + default Response createLoginSmsTan() { + throw new UnsupportedOperationException("The 'createLoginSmsTan' shouldn't be called."); + } + + default Response createOnboardingSmsTan() { + throw new UnsupportedOperationException("The 'createOnboardingSmsTan' shouldn't be called."); + } + + default Response createGenericSmsTanPage() { + throw new UnsupportedOperationException("The 'createGenericSmsTanPage' shouldn't be called."); + } + Response createLoginUsernamePassword(); Response createLoginUsername(); diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index 4c5d535510a2..bd580ff59dd6 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -85,6 +85,8 @@ public final class Constants { public static final String KEY = "key"; public static final String KC_ACTION = "kc_action"; + + public static final String KC_ACTION_PARAMETER = "kc_action_parameter"; public static final String KC_ACTION_STATUS = "kc_action_status"; public static final String KC_ACTION_EXECUTING = "kc_action_executing"; public static final int KC_ACTION_MAX_AGE = 300; diff --git a/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java b/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java index b220c1f50f0f..080854e15b88 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java +++ b/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java @@ -1,48 +1,129 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.keycloak.models; +import java.util.LinkedHashMap; +import java.util.Map; + public class ContentSecurityPolicyBuilder { - private String frameSrc = "'self'"; - private String frameAncestors = "'self'"; - private String objectSrc = "'none'"; + // constants for directive names used in the class + public static final String DIRECTIVE_NAME_FRAME_SRC = "frame-src"; + public static final String DIRECTIVE_NAME_FRAME_ANCESTORS = "frame-ancestors"; + public static final String DIRECTIVE_NAME_OBJECT_SRC = "object-src"; + + // constants for specific directive value keywords + public static final String DIRECTIVE_VALUE_SELF = "'self'"; + public static final String DIRECTIVE_VALUE_NONE = "'none'"; - private boolean first; - private StringBuilder sb; + private final Map directives = new LinkedHashMap<>(); public static ContentSecurityPolicyBuilder create() { - return new ContentSecurityPolicyBuilder(); + return new ContentSecurityPolicyBuilder() + .add(DIRECTIVE_NAME_FRAME_SRC, DIRECTIVE_VALUE_SELF) + .add(DIRECTIVE_NAME_FRAME_ANCESTORS, DIRECTIVE_VALUE_SELF) + .add(DIRECTIVE_NAME_OBJECT_SRC, DIRECTIVE_VALUE_NONE); + } + + public static ContentSecurityPolicyBuilder create(String directives) { + return new ContentSecurityPolicyBuilder().parse(directives); } public ContentSecurityPolicyBuilder frameSrc(String frameSrc) { - this.frameSrc = frameSrc; + if (frameSrc == null) { + directives.remove(DIRECTIVE_NAME_FRAME_SRC); + } else { + put(DIRECTIVE_NAME_FRAME_SRC, frameSrc); + } return this; } + public ContentSecurityPolicyBuilder addFrameSrc(String frameSrc) { + return add(DIRECTIVE_NAME_FRAME_SRC, frameSrc); + } + + public boolean isDefaultFrameAncestors() { + return DIRECTIVE_VALUE_SELF.equals(directives.get(DIRECTIVE_NAME_FRAME_ANCESTORS)); + } + public ContentSecurityPolicyBuilder frameAncestors(String frameancestors) { - this.frameAncestors = frameancestors; + if (frameancestors == null) { + directives.remove(DIRECTIVE_NAME_FRAME_ANCESTORS); + } else { + put(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors); + } return this; } - public String build() { - sb = new StringBuilder(); - first = true; - - build("frame-src", frameSrc); - build("frame-ancestors", frameAncestors); - build("object-src", objectSrc); + public ContentSecurityPolicyBuilder addFrameAncestors(String frameancestors) { + return add(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors); + } + public String build() { + StringBuilder sb = new StringBuilder(); + if (!directives.isEmpty()) { + for (Map.Entry entry : directives.entrySet()) { + sb.append(entry.getKey()); + if (!entry.getValue().isEmpty()) { + sb.append(" ").append(entry.getValue()); + } + sb.append("; "); + } + sb.setLength(sb.length() - 1); + } return sb.toString(); } - private void build(String k, String v) { - if (v != null) { - if (!first) { - sb.append(" "); - } - first = false; + private ContentSecurityPolicyBuilder put(String name, String value) { + if (name != null && value != null) { + directives.put(name, value); + } + return this; + } - sb.append(k).append(" ").append(v).append(";"); + private ContentSecurityPolicyBuilder add(String name, String value) { + if (name != null && value != null) { + String current = directives.get(name); + if (current != null && !current.isEmpty()) { + value = current + " " + value; + } + directives.put(name, value); } + return this; } + // W3C Working Draft: https://www.w3.org/TR/CSP/ + // Only managing spaces not the other whitespaces defined in the spec + private ContentSecurityPolicyBuilder parse(String value) { + if (value == null) { + return this; + } + String[] values = value.split(";"); + if (values != null) { + for (String directive : values) { + directive = directive.trim(); + int idx = directive.indexOf(' '); + if (idx > 0) { + add(directive.substring(0, idx), directive.substring(idx + 1, directive.length()).trim()); + } else if (!directive.isEmpty()) { + add(directive, ""); + } + } + } + return this; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index ab316f54ca19..14aaf1fe23f7 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -605,13 +605,15 @@ public static void firstBrokerLoginFlow(RealmModel realm, boolean migrate) { if (browserFlow == null) { browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); } - List browserExecutions = new LinkedList<>(); - KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions); - for (AuthenticationExecutionModel browserExecution : browserExecutions) { - if (browserExecution.isAuthenticatorFlow()){ - if (realm.getAuthenticationExecutionsStream(browserExecution.getFlowId()) - .anyMatch(e -> e.getAuthenticator().equals("auth-otp-form"))){ - execution.setRequirement(browserExecution.getRequirement()); + if (browserFlow != null) { + List browserExecutions = new LinkedList<>(); + KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions); + for (AuthenticationExecutionModel browserExecution : browserExecutions) { + if (browserExecution.isAuthenticatorFlow()){ + if (realm.getAuthenticationExecutionsStream(browserExecution.getFlowId()) + .anyMatch(e -> e.getAuthenticator().equals("auth-otp-form"))){ + execution.setRequirement(browserExecution.getRequirement()); + } } } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java index 3d81d69d3672..eb12522bb4c6 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java @@ -77,6 +77,7 @@ public enum Action { UPDATE_PASSWORD(UserModel.RequiredAction.UPDATE_PASSWORD.name(), DefaultRequiredActions::addUpdatePasswordAction), TERMS_AND_CONDITIONS(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name(), DefaultRequiredActions::addTermsAndConditionsAction), DELETE_ACCOUNT("delete_account", DefaultRequiredActions::addDeleteAccountAction), + DELETE_CREDENTIAL("delete_credential", DefaultRequiredActions::addDeleteCredentialAction), UPDATE_USER_LOCALE("update_user_locale", DefaultRequiredActions::addUpdateLocaleAction), UPDATE_EMAIL(UserModel.RequiredAction.UPDATE_EMAIL.name(), DefaultRequiredActions::addUpdateEmailAction, () -> isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)), CONFIGURE_RECOVERY_AUTHN_CODES(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name(), DefaultRequiredActions::addRecoveryAuthnCodesAction, () -> isFeatureEnabled(Profile.Feature.RECOVERY_CODES)), @@ -209,6 +210,19 @@ public static void addDeleteAccountAction(RealmModel realm) { } } + public static void addDeleteCredentialAction(RealmModel realm) { + if (realm.getRequiredActionProviderByAlias("delete_credential") == null) { + RequiredActionProviderModel deleteCredential = new RequiredActionProviderModel(); + deleteCredential.setEnabled(true); + deleteCredential.setAlias("delete_credential"); + deleteCredential.setName("Delete Credential"); + deleteCredential.setProviderId("delete_credential"); + deleteCredential.setDefaultAction(false); + deleteCredential.setPriority(100); + realm.addRequiredActionProvider(deleteCredential); + } + } + public static void addUpdateLocaleAction(RealmModel realm) { if (realm.getRequiredActionProviderByAlias("update_user_locale") == null) { RequiredActionProviderModel updateUserLocale = new RequiredActionProviderModel(); diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java index 321411e34261..f9cb740cdac6 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,9 +40,11 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; +import org.keycloak.storage.StorageId; import org.keycloak.utils.StringUtil; import org.keycloak.validate.ValidationContext; import org.keycloak.validate.ValidationError; @@ -84,7 +87,7 @@ public DefaultAttributes(UserProfileContext context, Map attributes, this.context = context; this.user = user; this.session = session; - this.metadataByAttribute = configureMetadata(profileMetadata.getAttributes()); + this.metadataByAttribute = configureMetadata(profileMetadata.getAttributes(), profileMetadata); this.upConfig = session.getProvider(UserProfileProvider.class).getConfiguration(); putAll(Collections.unmodifiableMap(normalizeAttributes(attributes))); } @@ -324,7 +327,7 @@ protected AttributeContext createAttributeContext(AttributeMetadata metadata) { return createAttributeContext(createAttribute(metadata.getName()), metadata); } - private Map configureMetadata(List attributes) { + private Map configureMetadata(List attributes, UserProfileMetadata profileMetadata) { Map metadatas = new HashMap<>(); for (AttributeMetadata metadata : attributes) { @@ -334,9 +337,35 @@ private Map configureMetadata(List } } + metadatas.putAll(getUserStorageProviderMetadata(profileMetadata)); + return metadatas; } + private Map getUserStorageProviderMetadata(UserProfileMetadata profileMetadata) { + if (user == null || (StorageId.isLocalStorage(user.getId()) && user.getFederationLink() == null)) { + // new user or not a user from a storage provider other than local + return Collections.emptyMap(); + } + + String providerId = user.getFederationLink(); + + if (providerId == null) { + providerId = StorageId.providerId(user.getId()); + } + + UserProvider userProvider = session.users(); + + if (userProvider instanceof UserProfileDecorator) { + // query the user provider from the source user storage provider for additional attribute metadata + UserProfileDecorator decorator = (UserProfileDecorator) userProvider; + return decorator.decorateUserProfile(providerId, profileMetadata).stream() + .collect(Collectors.toMap(AttributeMetadata::getName, Function.identity())); + } + + return Collections.emptyMap(); + } + private SimpleImmutableEntry> createAttribute(String name) { return new SimpleImmutableEntry>(name, null) { @Override diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java index 240f849901c4..420f817a1013 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java @@ -119,8 +119,7 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri String name = attribute.getKey(); List currentValue = user.getAttributeStream(name) .filter(Objects::nonNull).collect(Collectors.toList()); - List updatedValue = attribute.getValue().stream() - .filter(StringUtil::isNotBlank).collect(Collectors.toList()); + List updatedValue = attribute.getValue(); if (CollectionUtil.collectionEquals(currentValue, updatedValue)) { continue; @@ -132,7 +131,11 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri continue; } - user.setAttribute(name, updatedValue); + if (updatedValue.stream().allMatch(StringUtil::isBlank)) { + user.removeAttribute(name); + } else { + user.setAttribute(name, updatedValue.stream().filter(StringUtil::isNotBlank).collect(Collectors.toList())); + } if (UserModel.EMAIL.equals(name) && metadata.getContext().isResetEmailVerified()) { user.setEmailVerified(false); diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java index 0b716608d044..6c28ddd784e4 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java @@ -47,6 +47,8 @@ public class UserProfileUtil { public static final String USER_METADATA_GROUP = "user-metadata"; + public static final Predicate ONLY_ADMIN_CONDITION = context -> context.getContext().isAdminContext(); + /** * Find the metadata group "user-metadata" * @@ -69,30 +71,38 @@ public static AttributeGroupMetadata lookupUserMetadataGroup(KeycloakSession ses * @param attrName attribute name * @param metadata user-profile metadata where attribute would be added * @param metadataGroup metadata group in user-profile - * @param userFederationUsersSelector used to recognize if user belongs to this user-storage provider or not * @param guiOrder guiOrder to where to put the attribute * @param storageProviderName storageProviderName (just for logging purposes) - * @return true if attribute was added. False otherwise + * @return the attribute metadata if attribute was created. False otherwise */ - public static boolean addMetadataAttributeToUserProfile(String attrName, UserProfileMetadata metadata, AttributeGroupMetadata metadataGroup, Predicate userFederationUsersSelector, int guiOrder, String storageProviderName) { - // In case that attributes like LDAP_ID, KERBEROS_PRINCIPAL are explicitly defined on user profile, we can prefer defined configuration + public static AttributeMetadata createAttributeMetadata(String attrName, UserProfileMetadata metadata, AttributeGroupMetadata metadataGroup, int guiOrder, String storageProviderName) { + return createAttributeMetadata(attrName, metadata, metadataGroup, ONLY_ADMIN_CONDITION, AttributeMetadata.ALWAYS_FALSE, guiOrder, storageProviderName); + } + + public static AttributeMetadata createAttributeMetadata(String attrName, UserProfileMetadata metadata, int guiOrder, String storageProviderName) { + return createAttributeMetadata(attrName, metadata, null, ONLY_ADMIN_CONDITION, ONLY_ADMIN_CONDITION, guiOrder, storageProviderName); + } + + private static AttributeMetadata createAttributeMetadata(String attrName, UserProfileMetadata metadata, AttributeGroupMetadata metadataGroup, Predicate readCondition, Predicate writeCondition, int guiOrder, String storageProviderName) { if (!metadata.getAttribute(attrName).isEmpty()) { logger.tracef("Ignore adding metadata attribute '%s' to user profile by user storage provider '%s' as attribute is already defined on user profile.", attrName, storageProviderName); - return false; } else { logger.tracef("Adding metadata attribute '%s' to user profile by user storage provider '%s' for user profile context '%s'.", attrName, storageProviderName, metadata.getContext().toString()); - Predicate onlyAdminCondition = context -> metadata.getContext().isAdminContext(); - AttributeMetadata attributeMetadata = metadata.addAttribute(attrName, guiOrder, Collections.emptyList()) - .addWriteCondition(AttributeMetadata.ALWAYS_FALSE) // Not writable for anyone - .addReadCondition(onlyAdminCondition) // Read-only for administrators + + AttributeMetadata attributeMetadata = new AttributeMetadata(attrName, guiOrder) + .setValidators(Collections.emptyList()) + .addWriteCondition(writeCondition) + .addReadCondition(readCondition) .setRequired(AttributeMetadata.ALWAYS_FALSE); if (metadataGroup != null) { attributeMetadata.setAttributeGroupMetadata(metadataGroup); } - attributeMetadata.setSelector(userFederationUsersSelector); - return true; + + return attributeMetadata; } + + return null; } /** diff --git a/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java b/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java index e4b39afc4aee..cb88f12d0289 100644 --- a/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java +++ b/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java @@ -24,6 +24,24 @@ public void contentSecurityPolicyBuilderTest() { assertEquals("frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc(null).build()); assertEquals("frame-src 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameAncestors(null).build()); assertEquals("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc("'custom-frame-src'").frameAncestors("'custom-frame-ancestors'").build()); + assertEquals("frame-src localhost; frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc("localhost").build()); + assertEquals("frame-src 'self' localhost; frame-ancestors 'self'; object-src 'none';", + ContentSecurityPolicyBuilder.create().addFrameSrc("localhost").build()); + } + + private void assertParsedDirectives(String directives) { + assertEquals(directives, ContentSecurityPolicyBuilder.create(directives).build()); + } + + @Test + public void parseSecurityPolicyBuilderTest() { + assertParsedDirectives("frame-src 'self'; frame-ancestors 'self'; object-src 'none';"); + assertParsedDirectives("frame-ancestors 'self'; object-src 'none';"); + assertParsedDirectives("frame-src 'self'; object-src 'none';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none'; style-src 'self';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none'; sandbox;"); + assertEquals("frame-src 'custom-frame-src'; sandbox;", ContentSecurityPolicyBuilder.create("frame-src 'custom-frame-src' ; sandbox ; ").build()); } @Test diff --git a/server-spi/pom.xml b/server-spi/pom.xml index 3bc3dcb6d970..13ed544b748d 100755 --- a/server-spi/pom.xml +++ b/server-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/server-spi/src/main/java/org/keycloak/theme/ThemeSelectorProvider.java b/server-spi/src/main/java/org/keycloak/theme/ThemeSelectorProvider.java index 38f492a29436..482cb0536cf8 100755 --- a/server-spi/src/main/java/org/keycloak/theme/ThemeSelectorProvider.java +++ b/server-spi/src/main/java/org/keycloak/theme/ThemeSelectorProvider.java @@ -29,6 +29,7 @@ public interface ThemeSelectorProvider extends Provider { String DEFAULT = "keycloak"; String DEFAULT_V2 = "keycloak.v2"; String DEFAULT_V3 = "keycloak.v3"; + String PRIMESIGN_V2 = "primesign.v2"; /** * Return the theme name to use for the specified type @@ -53,7 +54,7 @@ default String getDefaultThemeName(Theme.Type type) { } if ((type == Theme.Type.ADMIN) && Profile.isFeatureEnabled(Profile.Feature.ADMIN2)) { - return DEFAULT_V2; + return PRIMESIGN_V2; } if ((type == Theme.Type.LOGIN) && Profile.isFeatureEnabled(Profile.Feature.LOGIN2)) { diff --git a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileDecorator.java b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileDecorator.java index acf7b5508f38..0675d769b7d7 100644 --- a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileDecorator.java +++ b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileDecorator.java @@ -19,18 +19,27 @@ package org.keycloak.userprofile; +import java.util.List; + import org.keycloak.models.RealmModel; /** + *

    This interface allows user storage providers to customize the user profile configuration and its attributes for realm + * on a per-user storage provider basis. + * * @author Pedro Igor */ public interface UserProfileDecorator { /** - * Decorates user profile with additional metadata. For instance, metadata attributes, which are available just for your user-storage - * provider can be added there, so they are available just for the users coming from your provider + *

    Decorates user profile with additional metadata. For instance, metadata attributes, which are available just for your user-storage + * provider can be added there, so they are available just for the users coming from your provider. + * + *

    This method is invoked every time a user is being managed through a user profile provider. * - * @param metadata to decorate + * @param providerId the id of the user storage provider to which the user is associated with + * @param metadata the current {@link UserProfileMetadata} for the current realm + * @return a list of attribute metadata.The {@link AttributeMetadata} returned from this method overrides any other metadata already set in {@code metadata} for a given attribute. */ - void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata); + List decorateUserProfile(String providerId, UserProfileMetadata metadata); } diff --git a/services/pom.xml b/services/pom.xml index 42fe00efb82e..1e988e0694ac 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 @@ -228,6 +228,16 @@ org.keycloak keycloak-model-storage-private + + org.mockito + mockito-core + test + + + org.mockito + mockito-core + test + diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 9640d926b54f..a7696ed77b68 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -42,6 +42,7 @@ import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol.Error; +import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPageException; @@ -50,12 +51,14 @@ import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.util.JsonSerialization; import jakarta.ws.rs.core.MultivaluedHashMap; @@ -345,6 +348,11 @@ public AuthenticationExecutionModel getExecution() { return execution; } + @Override + public AuthenticationFlowModel getTopLevelFlow() { + return AuthenticatorUtil.getTopParentFlow(realm, execution); + } + @Override public AuthenticatorConfigModel getAuthenticatorConfig() { if (execution.getAuthenticatorConfig() == null) return null; @@ -937,6 +945,22 @@ public static void resetFlow(AuthenticationSessionModel authSession, String flow authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath); } + // Recreate new root auth session and new auth session from the given auth session. + public static AuthenticationSessionModel recreate(KeycloakSession session, AuthenticationSessionModel authSession) { + AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session); + RootAuthenticationSessionModel rootAuthenticationSession = authenticationSessionManager.createAuthenticationSession(authSession.getRealm(), true); + AuthenticationSessionModel newAuthSession = rootAuthenticationSession.createAuthenticationSession(authSession.getClient()); + newAuthSession.setRedirectUri(authSession.getRedirectUri()); + newAuthSession.setProtocol(authSession.getProtocol()); + + for (Map.Entry clientNote : authSession.getClientNotes().entrySet()) { + newAuthSession.setClientNote(clientNote.getKey(), clientNote.getValue()); + } + + authenticationSessionManager.removeAuthenticationSession(authSession.getRealm(), authSession, true); + RestartLoginCookie.setRestartCookie(session, authSession); + return newAuthSession; + } // Clone new authentication session from the given authSession. New authenticationSession will have same parent (rootSession) and will use same client public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) { diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java index e7387dac288b..a6d7f1153721 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java @@ -21,8 +21,12 @@ import org.keycloak.authentication.actiontoken.ActionTokenContext; import org.keycloak.authentication.actiontoken.DefaultActionToken; import org.keycloak.common.ClientConnection; +import org.keycloak.common.util.reflections.Types; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.http.HttpRequest; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -38,6 +42,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.keycloak.services.managers.AuthenticationManager.FORCED_REAUTHENTICATION; import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH; @@ -129,6 +134,29 @@ public static List getExecutionsByType(RealmModel return executions; } + /** + * Useful if we need to find top-level flow from executionModel + * + * @param realm + * @param executionModel + * @return Top parent flow corresponding to given executionModel. + */ + public static AuthenticationFlowModel getTopParentFlow(RealmModel realm, AuthenticationExecutionModel executionModel) { + if (executionModel.getParentFlow() != null) { + AuthenticationFlowModel flow = realm.getAuthenticationFlowById(executionModel.getParentFlow()); + if (flow == null) throw new IllegalStateException("Flow '" + executionModel.getParentFlow() + "' referenced from execution '" + executionModel.getId() + "' not found in realm " + realm.getName()); + if (flow.isTopLevel()) return flow; + + AuthenticationExecutionModel execution = realm.getAuthenticationExecutionByFlowId(flow.getId()); + if (execution == null) throw new IllegalStateException("Not found execution referenced by flow '" + flow.getId() + "' in realm " + realm.getName()); + return getTopParentFlow(realm, execution); + } else { + throw new IllegalStateException("Execution '" + executionModel.getId() + "' does not have parent flow in realm " + realm.getName()); + } + } + + + /** * Logouts all sessions that are different to the current authentication session * managed in the action context. @@ -160,4 +188,14 @@ private static void logoutOtherSessions(KeycloakSession session, RealmModel real conn, req.getHttpHeaders(), true) ); } + + /** + * @param session + * @return all credential providers available + */ + public static Stream getCredentialProviders(KeycloakSession session) { + return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class) + .filter(f -> Types.supports(CredentialProvider.class, f, CredentialProviderFactory.class)) + .map(f -> session.getProvider(CredentialProvider.class, f.getId())); + } } diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index 592fd13153eb..696b95a8b58a 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -486,7 +486,14 @@ public Response processResult(AuthenticationProcessor.Result result, boolean isA return null; case FAILED: logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); - processor.logFailure(); + + + // ignore brute force protection for non-authentication relevant errors. + if (result.error != null + && !result.error.equals(AuthenticationFlowError.NOT_AUTHENTICATION_RELEVANT_ERROR)) { + processor.logFailure(); + } + setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.FAILED); if (result.getChallenge() != null) { return sendChallenge(result, execution); @@ -586,6 +593,6 @@ private void executeTopFlowSuccessCallbacks() { .filter(Objects::nonNull) .filter(AuthenticationFlowCallback.class::isInstance) .map(AuthenticationFlowCallback.class::cast) - .forEach(AuthenticationFlowCallback::onTopFlowSuccess); + .forEach(callback -> callback.onTopFlowSuccess(flow)); } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java index 510f3b02d013..469ce861e988 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java @@ -188,6 +188,11 @@ public void setUsername(String username) { public String getServiceAccountClientLink() { return null; } + + @Override + public String getFederationLink() { + return null; + } }; UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java index fcbe65c96636..7171494827f2 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java @@ -52,7 +52,7 @@ public void authenticate(AuthenticationFlowContext context) { LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol()); authSession.setAuthNote(Constants.LOA_MAP, authResult.getSession().getNote(Constants.LOA_MAP)); context.setUser(authResult.getUser()); - AcrStore acrStore = new AcrStore(authSession); + AcrStore acrStore = new AcrStore(context.getSession(), authSession); // Cookie re-authentication is skipped if re-authentication is required if (protocol.requireReauthentication(authResult.getSession(), authSession)) { @@ -65,10 +65,15 @@ public void authenticate(AuthenticationFlowContext context) { int previouslyAuthenticatedLevel = acrStore.getHighestAuthenticatedLevelFromPreviousAuthentication(); AuthenticatorUtils.updateCompletedExecutions(context.getAuthenticationSession(), authResult.getSession(), context.getExecution().getId()); - if (acrStore.getRequestedLevelOfAuthentication() > previouslyAuthenticatedLevel) { + if (acrStore.getRequestedLevelOfAuthentication(context.getTopLevelFlow()) > previouslyAuthenticatedLevel) { // Step-up authentication, we keep the loa from the existing user session. // The cookie alone is not enough and other authentications must follow. acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel); + + if (authSession.getClientNote(Constants.KC_ACTION) != null) { + context.setForwardedInfoMessage(Messages.AUTHENTICATE_STRONG); + } + context.attempted(); } else { // Cookie only authentication diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java index f916f1300292..8e26117c9b05 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java @@ -21,6 +21,7 @@ import org.keycloak.http.HttpRequest; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.Authenticator; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.events.Errors; @@ -61,6 +62,11 @@ public void authenticate(AuthenticationFlowContext context) { HttpRequest request = context.getHttpRequest(); String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader == null) { + if (context.getAuthenticationSession().getAuthNote(AuthenticationProcessor.FORKED_FROM) != null) { + // skip spnego authentication if it was forked (reset-credentials) + context.attempted(); + return; + } Response challenge = challengeNegotiation(context, null); context.forceChallenge(challenge); return; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java index f5eb81a7e094..1b8bd50bfd85 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java @@ -22,9 +22,9 @@ import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; -import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.authenticators.util.AcrStore; import org.keycloak.authentication.authenticators.util.LoAUtil; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -52,11 +52,11 @@ public ConditionalLoaAuthenticator(KeycloakSession session) { @Override public boolean matchCondition(AuthenticationFlowContext context) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); - AcrStore acrStore = new AcrStore(authSession); + AcrStore acrStore = new AcrStore(context.getSession(), authSession); int currentAuthenticationLoa = acrStore.getLevelOfAuthenticationFromCurrentAuthentication(); Integer configuredLoa = getConfiguredLoa(context); if (configuredLoa == null) configuredLoa = Constants.MINIMUM_LOA; - int requestedLoa = acrStore.getRequestedLevelOfAuthentication(); + int requestedLoa = acrStore.getRequestedLevelOfAuthentication(context.getTopLevelFlow()); if (currentAuthenticationLoa < Constants.MINIMUM_LOA) { logger.tracef("Condition '%s' evaluated to true due the user not yet reached any authentication level in this session, configuredLoa: %d, requestedLoa: %d", context.getAuthenticatorConfig().getAlias(), configuredLoa, requestedLoa); @@ -84,7 +84,7 @@ public boolean matchCondition(AuthenticationFlowContext context) { @Override public void onParentFlowSuccess(AuthenticationFlowContext context) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); - AcrStore acrStore = new AcrStore(authSession); + AcrStore acrStore = new AcrStore(context.getSession(), authSession); Integer newLoa = getConfiguredLoa(context); if (newLoa == null) { @@ -102,14 +102,14 @@ public void onParentFlowSuccess(AuthenticationFlowContext context) { } @Override - public void onTopFlowSuccess() { + public void onTopFlowSuccess(AuthenticationFlowModel topFlow) { AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); - AcrStore acrStore = new AcrStore(authSession); + AcrStore acrStore = new AcrStore(session, authSession); logger.tracef("Finished authentication at level %d when authenticating authSession '%s'.", acrStore.getLevelOfAuthenticationFromCurrentAuthentication(), authSession.getParentSession().getId()); - if (acrStore.isLevelOfAuthenticationForced() && !acrStore.isLevelOfAuthenticationSatisfiedFromCurrentAuthentication()) { + if (acrStore.isLevelOfAuthenticationForced() && !acrStore.isLevelOfAuthenticationSatisfiedFromCurrentAuthentication(topFlow)) { String details = String.format("Forced level of authentication did not meet the requirements. Requested level: %d, Fulfilled level: %d", - acrStore.getRequestedLevelOfAuthentication(), acrStore.getLevelOfAuthenticationFromCurrentAuthentication()); + acrStore.getRequestedLevelOfAuthentication(topFlow), acrStore.getLevelOfAuthenticationFromCurrentAuthentication()); throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, details, Messages.ACR_NOT_FULFILLED); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/util/AcrStore.java b/services/src/main/java/org/keycloak/authentication/authenticators/util/AcrStore.java index 7737ed974465..1539aba3476a 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/util/AcrStore.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/util/AcrStore.java @@ -20,18 +20,28 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; import com.fasterxml.jackson.core.type.TypeReference; import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.CredentialAction; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.JsonSerialization; +import static org.keycloak.models.Constants.NO_LOA; + /** * CRUD data in the authentication session, which are related to step-up authentication * @@ -41,9 +51,11 @@ public class AcrStore { private static final Logger logger = Logger.getLogger(AcrStore.class); + private final KeycloakSession session; private final AuthenticationSessionModel authSession; - public AcrStore(AuthenticationSessionModel authSession) { + public AcrStore(KeycloakSession session, AuthenticationSessionModel authSession) { + this.session = session; this.authSession = authSession; } @@ -53,21 +65,73 @@ public boolean isLevelOfAuthenticationForced() { } - public int getRequestedLevelOfAuthentication() { + public int getRequestedLevelOfAuthentication(AuthenticationFlowModel executionModel) { String requiredLoa = authSession.getClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION); - return requiredLoa == null ? Constants.NO_LOA : Integer.parseInt(requiredLoa); + int requestedLoaByClient = requiredLoa == null ? NO_LOA : Integer.parseInt(requiredLoa); + int requestedLoaByKcAction = getRequestedLevelOfAuthenticationByKcAction(executionModel); + logger.tracef("Level requested by client: %d, level requested by kc_action parameter: %d", requestedLoaByClient, requestedLoaByKcAction); + return Math.max(requestedLoaByClient, requestedLoaByKcAction); + } + + // + private int getRequestedLevelOfAuthenticationByKcAction(AuthenticationFlowModel topLevelFlow) { + RealmModel realm = authSession.getRealm(); + UserModel user = authSession.getAuthenticatedUser(); + String kcAction = authSession.getClientNote(Constants.KC_ACTION); + if (user != null && kcAction != null) { + RequiredActionProvider reqAction = session.getProvider(RequiredActionProvider.class, kcAction); + if (reqAction instanceof CredentialAction) { + String credentialType = ((CredentialAction) reqAction).getCredentialType(session, authSession); + if (credentialType != null) { + Map credentialTypesToLoa = LoAUtil.getCredentialTypesToLoAMap(session, realm, topLevelFlow); + + Integer credentialTypeLevel = credentialTypesToLoa.get(credentialType); + if (credentialTypeLevel != null) { + // We check if user has any credentials of given type available. For instance if user doesn't yet have any 2nd-factor configured, we don't request level2 from him + MultivaluedHashMap loaToCredentialTypes = reverse(credentialTypesToLoa); + return getHighestLevelAvailableForUser(user, loaToCredentialTypes, credentialTypeLevel); + } + } + } + } + return NO_LOA; + } + + private MultivaluedHashMap reverse(Map orig) { + MultivaluedHashMap reverse = new MultivaluedHashMap<>(); + orig.forEach((key, value) -> reverse.add(value, key)); + return reverse; } + private Integer getHighestLevelAvailableForUser(UserModel user, MultivaluedHashMap loaToCredentialTypes, int levelToTry) { + if (levelToTry <= NO_LOA) return levelToTry; + + List currentLevelCredentialTypes = loaToCredentialTypes.get(levelToTry); + if (currentLevelCredentialTypes == null || currentLevelCredentialTypes.isEmpty()) { + // No credentials required for authentication on this level + return levelToTry; + } + + boolean hasCredentialOfLevel = user.credentialManager().getStoredCredentialsStream() + .anyMatch(credentialModel -> currentLevelCredentialTypes.contains(credentialModel.getType())); + if (hasCredentialOfLevel) { + logger.tracef("User %s has credential of level %d available", user.getUsername(), levelToTry); + return levelToTry; + } else { + // Fallback to lower level + return getHighestLevelAvailableForUser(user, loaToCredentialTypes, levelToTry - 1); + } + } - public boolean isLevelOfAuthenticationSatisfiedFromCurrentAuthentication() { - return getRequestedLevelOfAuthentication() + public boolean isLevelOfAuthenticationSatisfiedFromCurrentAuthentication(AuthenticationFlowModel topFlow) { + return getRequestedLevelOfAuthentication(topFlow) <= getAuthenticatedLevelCurrentAuthentication(); } public static int getCurrentLevelOfAuthentication(AuthenticatedClientSessionModel clientSession) { String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION); - return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote); + return clientSessionLoaNote == null ? NO_LOA : Integer.parseInt(clientSessionLoaNote); } @@ -101,7 +165,7 @@ public boolean isLevelAuthenticatedInPreviousAuth(int level, int maxAge) { */ public int getLevelOfAuthenticationFromCurrentAuthentication() { String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION); - return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote); + return authSessionLoaNote == null ? NO_LOA : Integer.parseInt(authSessionLoaNote); } @@ -137,7 +201,7 @@ private void setLevelAuthenticatedToMap(int level) { private int getAuthenticatedLevelCurrentAuthentication() { String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION); - return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote); + return authSessionLoaNote == null ? NO_LOA : Integer.parseInt(authSessionLoaNote); } /** @@ -146,7 +210,7 @@ private int getAuthenticatedLevelCurrentAuthentication() { public int getHighestAuthenticatedLevelFromPreviousAuthentication() { // No map found. User was not yet authenticated in this session Map levels = getCurrentAuthenticatedLevelsMap(); - if (levels == null || levels.isEmpty()) return Constants.NO_LOA; + if (levels == null || levels.isEmpty()) return NO_LOA; // Map was already saved, so it is SSO authentication at minimum. Using "0" level as the minimum level in this case int maxLevel = Constants.MINIMUM_LOA; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/util/LoAUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/util/LoAUtil.java index 08329d6793ad..94722355b9eb 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/util/LoAUtil.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/util/LoAUtil.java @@ -19,21 +19,33 @@ package org.keycloak.authentication.authenticators.util; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jboss.logging.Logger; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory; +import org.keycloak.credential.CredentialProvider; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.cache.CachedRealmModel; + +import static org.keycloak.models.Constants.NO_LOA; /** * @author Marek Posolda @@ -48,7 +60,7 @@ public class LoAUtil { */ public static int getCurrentLevelOfAuthentication(AuthenticatedClientSessionModel clientSession) { String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION); - return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote); + return clientSessionLoaNote == null ? NO_LOA : Integer.parseInt(clientSessionLoaNote); } @@ -119,4 +131,75 @@ public static int getMaxAgeFromLoaConditionConfiguration(AuthenticatorConfigMode return 0; } } + + /** + * Return map where: + * - keys are credential types corresponding to authenticators available in given authentication flow + * - values are LoA levels of those credentials in the given flow (If not step-up authentication is used, values will be always Constants.NO_LOA) + * + * For instance if we have password as level1 and OTP or WebAuthn as available level2 authenticators it can return map like: + * { "password" -> 1, + * "otp" -> 2 + * "webauthn" -> 2 + * } + * + * @param session + * @param realm + * @param topFlow + * @return map as described above. Never returns null, but can return empty map. + */ + public static Map getCredentialTypesToLoAMap(KeycloakSession session, RealmModel realm, AuthenticationFlowModel topFlow) { + // Attempt to cache mapping, so it is not needed to compute it multiple times at every authentication + String cacheKey = "flow:" + topFlow.getId(); + if (realm instanceof CachedRealmModel) { + ConcurrentHashMap cachedWith = ((CachedRealmModel) realm).getCachedWith(); + Map result = (Map) cachedWith.get(cacheKey); + if (result != null) return result; + } + + Map result = new HashMap<>(); + AtomicReference currentLevel = new AtomicReference<>(NO_LOA); + Set availableCredentialTypes = AuthenticatorUtil.getCredentialProviders(session) + .map(CredentialProvider::getType) + .collect(Collectors.toSet()); + + fillCredentialsToLoAMap(session, realm, topFlow, availableCredentialTypes, currentLevel, result); + + logger.tracef("Computed credential types to LoA map for authentication flow '%s' in realm '%s'. Mapping: %s", topFlow.getAlias(), realm.getName(), result); + + if (realm instanceof CachedRealmModel) { + ConcurrentHashMap cachedWith = ((CachedRealmModel) realm).getCachedWith(); + cachedWith.put(cacheKey, result); + } + + return result; + } + + private static void fillCredentialsToLoAMap(KeycloakSession session, RealmModel realm, AuthenticationFlowModel authFlow, Set availableCredentialTypes, AtomicReference currentLevel, Map result) { + realm.getAuthenticationExecutionsStream(authFlow.getId()).forEachOrdered(execution -> { + if (execution.isAuthenticatorFlow()) { + AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId()); + + int levelWhenExecuted = currentLevel.get(); + fillCredentialsToLoAMap(session, realm, subFlow, availableCredentialTypes, currentLevel, result); + currentLevel.set(levelWhenExecuted); // Subflow is finished. We should "reset" current level and set it to the same value before we started to process the subflow + } else { + if (ConditionalLoaAuthenticatorFactory.PROVIDER_ID.equals(execution.getAuthenticator())) { + AuthenticatorConfigModel loaConditionConfig = realm.getAuthenticatorConfigById(execution.getAuthenticatorConfig()); + Integer level = getLevelFromLoaConditionConfiguration(loaConditionConfig); + if (level != null) { + currentLevel.set(level); + } + } else { + AuthenticatorFactory factory = (AuthenticatorFactory) session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, execution.getAuthenticator()); + if (factory == null) return; + // reference-category points to the credentialType + if (factory.getReferenceCategory() != null && availableCredentialTypes.contains(factory.getReferenceCategory())) { + result.put(factory.getReferenceCategory(), currentLevel.get()); + } + } + } + }); + } + } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java b/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java new file mode 100644 index 000000000000..23736b07a764 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java @@ -0,0 +1,194 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.authentication.requiredactions; + + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.CredentialAction; +import org.keycloak.authentication.InitiatedActionSupport; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.authenticators.util.AcrStore; +import org.keycloak.authentication.requiredactions.util.CredentialDeleteHelper; +import org.keycloak.credential.CredentialModel; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.utils.StringUtil; + +/** + * @author Marek Posolda + */ +public class DeleteCredentialAction implements RequiredActionProvider, RequiredActionFactory, CredentialAction { + + public static final String PROVIDER_ID = "delete_credential"; + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public InitiatedActionSupport initiatedActionSupport() { + return InitiatedActionSupport.SUPPORTED; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + + @Override + public void evaluateTriggers(RequiredActionContext context) { + + } + + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession) { + String credentialId = authenticationSession.getClientNote(Constants.KC_ACTION_PARAMETER); + if (credentialId == null) { + return null; + } + + UserModel user = authenticationSession.getAuthenticatedUser(); + if (user == null) { + return null; + } + + CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); + if (credential == null) { + if (credentialId.endsWith("-id")) { + return credentialId.substring(0, credentialId.length() - 3); + } else { + return null; + } + } else { + return credential.getType(); + } + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + String credentialId = context.getAuthenticationSession().getClientNote(Constants.KC_ACTION_PARAMETER); + UserModel user = context.getUser(); + if (credentialId == null) { + context.getEvent() + .error(Errors.MISSING_CREDENTIAL_ID); + context.ignore(); + return; + } + + String credentialLabel; + CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); + if (credential == null) { + // Backwards compatibility with account console 1 - When stored credential is not found, it may be federated credential. + // In this case, it's ID needs to be something like "otp-id", which is returned by account REST GET endpoint as a placeholder + // for federated credentials (See CredentialHelper.createUserStorageCredentialRepresentation ) + if (credentialId.endsWith("-id")) { + credentialLabel = credentialId.substring(0, credentialId.length() - 3); + } else { + context.getEvent() + .detail(Details.CREDENTIAL_ID, credentialId) + .error(Errors.CREDENTIAL_NOT_FOUND); + context.ignore(); + return; + } + } else { + credentialLabel = StringUtil.isNotBlank(credential.getUserLabel()) ? credential.getUserLabel() : credential.getType(); + } + + Response challenge = context.form() + .setAttribute("credentialLabel", credentialLabel) + .createForm("delete-credential.ftl"); + context.challenge(challenge); + } + + private void setupEvent(CredentialModel credential, EventBuilder event) { + if (credential != null) { + if (OTPCredentialModel.TYPE.equals(credential.getType())) { + event.event(EventType.REMOVE_TOTP); + } + event.detail(Details.CREDENTIAL_TYPE, credential.getType()) + .detail(Details.CREDENTIAL_ID, credential.getId()) + .detail(Details.CREDENTIAL_USER_LABEL, credential.getUserLabel()); + } + } + + @Override + public void processAction(RequiredActionContext context) { + EventBuilder event = context.getEvent(); + String credentialId = context.getAuthenticationSession().getClientNote(Constants.KC_ACTION_PARAMETER); + + CredentialModel credential = context.getUser().credentialManager().getStoredCredentialById(credentialId); + setupEvent(credential, event); + + try { + CredentialDeleteHelper.removeCredential(context.getSession(), context.getUser(), credentialId, () -> getCurrentLoa(context.getSession(), context.getAuthenticationSession())); + context.success(); + + } catch (WebApplicationException wae) { + Response response = context.getSession().getProvider(LoginFormsProvider.class) + .setAuthenticationSession(context.getAuthenticationSession()) + .setUser(context.getUser()) + .setError(wae.getMessage()) + .createErrorPage(Response.Status.BAD_REQUEST); + event.detail(Details.REASON, wae.getMessage()) + .error(Errors.DELETE_CREDENTIAL_FAILED); + context.challenge(response); + } + } + + private int getCurrentLoa(KeycloakSession session, AuthenticationSessionModel authSession) { + return new AcrStore(session, authSession).getLevelOfAuthenticationFromCurrentAuthentication(); + } + + @Override + public String getDisplayText() { + return "Delete Credential"; + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java b/services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java index 589ed7fa1e81..6d9abd563fd4 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java @@ -5,6 +5,7 @@ import java.util.List; import org.keycloak.Config; import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.CredentialRegistrator; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; @@ -21,8 +22,9 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import org.keycloak.sessions.AuthenticationSessionModel; -public class RecoveryAuthnCodesAction implements RequiredActionProvider, RequiredActionFactory, EnvironmentDependentProviderFactory { +public class RecoveryAuthnCodesAction implements RequiredActionProvider, RequiredActionFactory, EnvironmentDependentProviderFactory, CredentialRegistrator { private static final String FIELD_GENERATED_RECOVERY_AUTHN_CODES_HIDDEN = "generatedRecoveryAuthnCodes"; private static final String FIELD_GENERATED_AT_HIDDEN = "generatedAt"; @@ -35,6 +37,11 @@ public String getId() { return PROVIDER_ID; } + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession) { + return RecoveryAuthnCodesCredentialModel.TYPE; + } + @Override public String getDisplayText() { return "Recovery Authentication Codes"; diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java index 7c76eb908681..6a2da55dee7d 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java @@ -39,6 +39,7 @@ import org.keycloak.models.utils.FormMessage; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.utils.CredentialHelper; import jakarta.ws.rs.core.MultivaluedMap; @@ -161,6 +162,11 @@ public String getId() { return UserModel.RequiredAction.CONFIGURE_TOTP.name(); } + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession) { + return OTPCredentialModel.TYPE; + } + @Override public boolean isOneTimeAction() { return true; diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java index d44d246a5fb3..b6e0e9bc9869 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java @@ -54,6 +54,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.credential.WebAuthnCredentialModel; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.utils.StringUtil; import com.webauthn4j.converter.util.ObjectConverter; @@ -170,6 +171,11 @@ protected WebAuthnPolicy getWebAuthnPolicy(RequiredActionContext context) { return context.getRealm().getWebAuthnPolicy(); } + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel authenticationSession) { + return getCredentialType(); + } + protected String getCredentialType() { return WebAuthnCredentialModel.TYPE_TWOFACTOR; } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/CredentialDeleteHelper.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/CredentialDeleteHelper.java new file mode 100644 index 000000000000..8c5b437750a9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/CredentialDeleteHelper.java @@ -0,0 +1,111 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.authentication.requiredactions.util; + +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotFoundException; +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.authenticators.util.LoAUtil; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.CredentialTypeMetadata; +import org.keycloak.credential.CredentialTypeMetadataContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import static org.keycloak.models.Constants.NO_LOA; + +/** + * @author Marek Posolda + */ +public class CredentialDeleteHelper { + + private static final Logger logger = Logger.getLogger(CredentialDeleteHelper.class); + + /** + * Removing credential of given ID of specified user. It does the necessary validation to validate if specified credential can be removed. + * In case of step-up authentication enabled, it verifies if user authenticated with corresponding level in order to be able to remove this credential. + * + * For instance removing 2nd-factor credential require authentication with 2nd-factor as well for security reasons. + * + * @param session + * @param user + * @param credentialId + * @param currentLoAProvider supplier of current authenticated level. Can be retrieved for instance from session or from the token + * @return removed credential. It can return null if credential was not found or if it was legacy format of federated credential ID + */ + public static CredentialModel removeCredential(KeycloakSession session, UserModel user, String credentialId, Supplier currentLoAProvider) { + CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); + if (credential == null) { + // Backwards compatibility with account console 1 - When stored credential is not found, it may be federated credential. + // In this case, it's ID needs to be something like "otp-id", which is returned by account REST GET endpoint as a placeholder + // for federated credentials (See CredentialHelper.createUserStorageCredentialRepresentation ) + if (credentialId.endsWith("-id")) { + String credentialType = credentialId.substring(0, credentialId.length() - 3); + checkIfCanBeRemoved(session, user, credentialType, currentLoAProvider); + user.credentialManager().disableCredentialType(credentialType); + return null; + } + throw new NotFoundException("Credential not found"); + } + checkIfCanBeRemoved(session, user, credential.getType(), currentLoAProvider); + user.credentialManager().removeStoredCredentialById(credentialId); + return credential; + } + + private static void checkIfCanBeRemoved(KeycloakSession session, UserModel user, String credentialType, Supplier currentLoAProvider) { + CredentialProvider credentialProvider = AuthenticatorUtil.getCredentialProviders(session) + .filter(credentialProvider1 -> credentialType.equals(credentialProvider1.getType())) + .findAny().orElse(null); + if (credentialProvider == null) { + logger.warnf("Credential provider %s not found", credentialType); + throw new NotFoundException("Credential provider not found"); + } + CredentialTypeMetadataContext ctx = CredentialTypeMetadataContext.builder().user(user).build(session); + CredentialTypeMetadata metadata = credentialProvider.getCredentialTypeMetadata(ctx); + if (!metadata.isRemoveable()) { + logger.warnf("Credential type %s cannot be removed", credentialType); + throw new BadRequestException("Credential type cannot be removed"); + } + + // Check if current accessToken has permission to remove credential in case of step-up authentication was used + checkAuthenticatedLoASufficientForCredentialRemove(session, credentialType, currentLoAProvider); + } + + private static void checkAuthenticatedLoASufficientForCredentialRemove(KeycloakSession session, String credentialType, Supplier currentLoAProvider) { + int requestedLoaForCredentialRemove = getRequestedLoaForCredential(session, session.getContext().getRealm(), credentialType); + + int currentAuthenticatedLevel = currentLoAProvider.get(); + if (currentAuthenticatedLevel < requestedLoaForCredentialRemove) { + throw new ForbiddenException("Insufficient level of authentication for removing credential of type '" + credentialType + "'."); + } + } + + private static int getRequestedLoaForCredential(KeycloakSession session, RealmModel realm, String credentialType) { + Map credentialTypesToLoa = LoAUtil.getCredentialTypesToLoAMap(session, realm, realm.getBrowserFlow()); + return credentialTypesToLoa.getOrDefault(credentialType, NO_LOA); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index 96ecf9e19fb4..baa8ce87420b 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; +import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.ws.rs.HttpMethod; @@ -656,7 +657,12 @@ private void resolveResourcePermission(KeycloakAuthorizationRequest request, ResourcePermission resourcePermission = addPermission(request, resourceServer, authorization, permissionsToEvaluate, limit, requestedScopesModel, grantedResource); - + if (resourcePermission != null) { + Collection permissionScopes = resourcePermission.getScopes(); + if (permissionScopes != null) { + permissionScopes.retainAll(scopes); + } + } // the permission is explicitly granted by the owner, mark this permission as granted so that we don't run the evaluation engine on it resourcePermission.setGranted(true); } diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index 952040f4dfdb..215763ef9c18 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -64,6 +64,7 @@ import org.keycloak.services.resources.RealmsResource; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.JsonSerialization; +import org.keycloak.util.TokenUtil; import org.keycloak.vault.VaultStringSecret; import jakarta.ws.rs.GET; @@ -79,6 +80,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -97,6 +99,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider SUPPORTED_TOKEN_TYPES = Arrays.asList(TokenUtil.TOKEN_TYPE_ID, TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_JWT_ACCESS_TOKEN, TokenUtil.TOKEN_TYPE_JWT_ACCESS_TOKEN_PREFIXED); public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) { super(session, config); @@ -849,7 +852,7 @@ final protected BrokeredIdentityContext validateJwt(EventBuilder event, String s throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST); } - JsonWebToken parsedToken = null; + JsonWebToken parsedToken; try { parsedToken = validateToken(subjectToken, true); } catch (IdentityBrokerException e) { @@ -860,7 +863,9 @@ final protected BrokeredIdentityContext validateJwt(EventBuilder event, String s } try { - + if (!isTokenTypeSupported(parsedToken)) { + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type not supported", Response.Status.BAD_REQUEST); + } boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType); BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken); if (context == null) { @@ -880,12 +885,18 @@ final protected BrokeredIdentityContext validateJwt(EventBuilder event, String s return context; } catch (IOException e) { logger.debug("Unable to extract identity from identity token", e); + event.detail(Details.REASON, "Unable to extract identity from identity token: " + e.getMessage()); + event.error(Errors.INVALID_TOKEN); throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST); } } + protected static boolean isTokenTypeSupported(JsonWebToken parsedToken) { + return SUPPORTED_TOKEN_TYPES.contains(parsedToken.getType()); + } + @Override protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap params) { if (!supportsExternalExchange()) return null; diff --git a/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java b/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java index f8b7ec8e0308..fa9b665e3b50 100755 --- a/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java +++ b/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java @@ -94,6 +94,10 @@ private void logEvent(Event event) { sanitize(sb, event.getClientId()); sb.append(", userId="); sanitize(sb, event.getUserId()); + if (event.getSessionId() != null) { + sb.append(", sessionId="); + sanitize(sb, event.getSessionId()); + } sb.append(", ipAddress="); sanitize(sb, event.getIpAddress()); diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java index 6f20125c9ff2..a1cf03f45e02 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java @@ -86,6 +86,12 @@ public static String getTemplate(LoginFormsPages page) { return "frontchannel-logout.ftl"; case LOGOUT_CONFIRM: return "logout-confirm.ftl"; + case LOGIN_SMS_TAN: + return "sms-validation.ftl"; + case ONBOARDING_SMS_TAN: + return "onboarding-sms-validation.ftl"; + case GENERIC_SMS_TAN: + return "generic-sms-validation.ftl"; default: throw new IllegalArgumentException(); } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java index b055e198cd14..fdab733435da 100644 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java @@ -42,7 +42,7 @@ public abstract class AbstractUserProfileBean { return a1.compareTo(a2); } - return Comparator.nullsLast(AttributeGroup::compareTo).compare(g1, g2); + return Comparator.nullsFirst(AttributeGroup::compareTo).compare(g1, g2); }; protected final MultivaluedMap formData; diff --git a/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java b/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java index 58af8967d980..385446435688 100644 --- a/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java +++ b/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java @@ -106,20 +106,23 @@ private void addHtmlHeaders(MultivaluedMap headers) { // TODO This will be refactored as part of introducing a more strict CSP header if (options != null) { - ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create(); - if (options.isAllowAnyFrameAncestor()) { headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName()); - - csp.frameAncestors(null); } - String allowedFrameSrc = options.getAllowedFrameSrc(); - if (allowedFrameSrc != null) { - csp.frameSrc(allowedFrameSrc); - } + Object cspVal = headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()); + if (cspVal != null) { + ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create(cspVal.toString()); + if (options.isAllowAnyFrameAncestor() && csp.isDefaultFrameAncestors()) { + // only remove frame ancestors if defined to default 'self' + csp.frameAncestors(null); + } + + String allowedFrameSrc = options.getAllowedFrameSrc(); + if (allowedFrameSrc != null) { + csp.addFrameSrc(allowedFrameSrc); + } - if (CONTENT_SECURITY_POLICY.getDefaultValue().equals(headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()))) { headers.putSingle(CONTENT_SECURITY_POLICY.getHeaderName(), csp.build()); } } diff --git a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java index 4bd94b5b3528..a5a2703a7c33 100644 --- a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java +++ b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java @@ -273,6 +273,8 @@ private String getEncryptedToken(TokenCategory category, String encodedToken) { public String cekManagementAlgorithm(TokenCategory category) { if (category == null) return null; switch (category) { + case INTERNAL: + return Algorithm.AES; case ID: case LOGOUT: return getCekManagementAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ALG); @@ -300,6 +302,8 @@ public String encryptAlgorithm(TokenCategory category) { switch (category) { case ID: return getEncryptAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC, JWEConstants.A128CBC_HS256); + case INTERNAL: + return JWEConstants.A128CBC_HS256; case LOGOUT: return getEncryptAlgorithm(OIDCConfigAttributes.ID_TOKEN_ENCRYPTED_RESPONSE_ENC); case AUTHORIZATION_RESPONSE: diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java index d676a5121feb..9432bfee92fc 100644 --- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java +++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java @@ -23,13 +23,17 @@ import org.keycloak.TokenCategory; import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieType; +import org.keycloak.crypto.KeyUse; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel; +import org.keycloak.util.TokenUtil; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -118,7 +122,7 @@ public RestartLoginCookie(AuthenticationSessionModel authSession) { public static void setRestartCookie(KeycloakSession session, AuthenticationSessionModel authSession) { RestartLoginCookie restart = new RestartLoginCookie(authSession); - String encoded = session.tokens().encode(restart); + String encoded = encodeAndEncrypt(session, restart); session.getProvider(CookieProvider.class).set(CookieType.AUTH_RESTART, encoded); } @@ -138,7 +142,7 @@ public static String getRestartCookie(KeycloakSession session){ public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm, RootAuthenticationSessionModel rootSession, String expectedClientId, String encodedCookie) throws Exception { - RestartLoginCookie cookie = session.tokens().decode(encodedCookie, RestartLoginCookie.class); + RestartLoginCookie cookie = decryptAndDecode(session, encodedCookie); if (cookie == null) { logger.debug("Failed to verify encoded RestartLoginCookie"); return null; @@ -169,6 +173,36 @@ public static AuthenticationSessionModel restartSession(KeycloakSession session, return authSession; } + private static RestartLoginCookie decryptAndDecode(KeycloakSession session, String encodedToken) { + try { + String sigAlgorithm = session.tokens().signatureAlgorithm(TokenCategory.INTERNAL); + String algAlgorithm = session.tokens().cekManagementAlgorithm(TokenCategory.INTERNAL); + SecretKey encKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.ENC, algAlgorithm).getSecretKey(); + SecretKey signKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.SIG, sigAlgorithm).getSecretKey(); + + byte[] contentBytes = TokenUtil.jweDirectVerifyAndDecode(encKey, signKey, encodedToken); + String jwt = new String(contentBytes, StandardCharsets.UTF_8); + return session.tokens().decode(jwt, RestartLoginCookie.class); + } catch (Exception e) { + // Might be the cookie from the older version + return session.tokens().decode(encodedToken, RestartLoginCookie.class); + } + } + + private static String encodeAndEncrypt(KeycloakSession session, RestartLoginCookie cookie) { + try { + String sigAlgorithm = session.tokens().signatureAlgorithm(cookie.getCategory()); + String algAlgorithm = session.tokens().cekManagementAlgorithm(cookie.getCategory()); + SecretKey encKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.ENC, algAlgorithm).getSecretKey(); + SecretKey signKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.SIG, sigAlgorithm).getSecretKey(); + + String encodedJwt = session.tokens().encode(cookie); + return TokenUtil.jweDirectEncode(encKey, signKey, encodedJwt.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("Error encoding cookie.", e); + } + } + @Override public TokenCategory getCategory() { return TokenCategory.INTERNAL; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java b/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java index bcac389cc67b..f292aa89c0eb 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java @@ -67,7 +67,6 @@ private void configureCSP() { allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' '); } - session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor(); session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString()); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 3c26aa19409b..c0694af6f5c3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -412,7 +412,7 @@ public AccessTokenResponseBuilder refreshAccessToken(KeycloakSession session, Ur //if scope parameter is not null, remove every scope that is not part of scope parameter if (scopeParameter != null && ! scopeParameter.isEmpty()) { Set scopeParamScopes = Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet()); - oldTokenScope = Arrays.stream(oldTokenScope.split(" ")).filter(sc -> scopeParamScopes.contains(sc)) + oldTokenScope = Arrays.stream(oldTokenScope.split(" ")).filter(sc -> scopeParamScopes.contains(sc) || sc.equals(OAuth2Constants.OFFLINE_ACCESS)) .collect(Collectors.joining(" ")); } @@ -428,10 +428,6 @@ public AccessTokenResponseBuilder refreshAccessToken(KeycloakSession session, Ur validateTokenReuseForRefresh(session, realm, refreshToken, validation); - int currentTime = Time.currentTime(); - clientSession.setTimestamp(currentTime); - validation.userSession.setLastSessionRefresh(currentTime); - if (refreshToken.getAuthorization() != null) { validation.newToken.setAuthorization(refreshToken.getAuthorization()); } @@ -620,7 +616,7 @@ public static ClientSessionContext attachAuthenticationSession(KeycloakSession s userSession.setNote(entry.getKey(), entry.getValue()); } - clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(authSession).getLevelOfAuthenticationFromCurrentAuthentication())); + clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(session, authSession).getLevelOfAuthenticationFromCurrentAuthentication())); clientSession.setTimestamp(userSession.getLastSessionRefresh()); // Remove authentication session now (just current tab, not whole "rootAuthenticationSession" in case we have more browser tabs with "authentications in progress") @@ -1141,24 +1137,30 @@ public AccessTokenResponseBuilder generateRefreshToken(String scope) { } private void generateRefreshToken(boolean offlineTokenRequested) { + refreshToken = new RefreshToken(accessToken); + refreshToken.id(KeycloakModelUtils.generateId()); + refreshToken.issuedNow(); + int currentTime = Time.currentTime(); + AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession(); + clientSession.setTimestamp(currentTime); + UserSessionModel userSession = clientSession.getUserSession(); + userSession.setLastSessionRefresh(currentTime); + if (offlineTokenRequested) { UserSessionManager sessionManager = new UserSessionManager(session); if (!sessionManager.isOfflineTokenAllowed(clientSessionCtx)) { + event.detail(Details.REASON, "Offline tokens not allowed for the user or client"); event.error(Errors.NOT_ALLOWED); - throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST); + throw new ErrorResponseException(Errors.NOT_ALLOWED, "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST); } - - refreshToken = new RefreshToken(accessToken); refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE); - if (realm.isOfflineSessionMaxLifespanEnabled()) + if (realm.isOfflineSessionMaxLifespanEnabled()) { refreshToken.expiration(getExpiration(true)); + } sessionManager.createOrUpdateOfflineSession(clientSessionCtx.getClientSession(), userSession); } else { - refreshToken = new RefreshToken(accessToken); refreshToken.expiration(getExpiration(false)); } - refreshToken.id(KeycloakModelUtils.generateId()); - refreshToken.issuedNow(); } private int getExpiration(boolean offline) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 2ea246a7d322..4d6c01263228 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -64,6 +64,8 @@ import java.util.Map; import java.util.function.BiConsumer; +import static org.keycloak.OAuthErrorException.INVALID_REDIRECT_URI; + /** * @author Stian Thorgersen */ @@ -185,6 +187,11 @@ private Response process(final MultivaluedMap params) { try { session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri, params)); } catch (ClientPolicyException cpe) { + if (cpe.getError().equals(INVALID_REDIRECT_URI)) { + event.error(Errors.INVALID_REDIRECT_URI); + throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, + OIDCLoginProtocol.REDIRECT_URI_PARAM); + } return redirectErrorToClient(parsedResponseMode, cpe.getError(), cpe.getErrorDetail()); } @@ -363,7 +370,17 @@ private Response buildForgotCredential() { public static void performActionOnParameters(AuthorizationEndpointRequest request, BiConsumer paramAction) { paramAction.accept(AdapterConstants.KC_IDP_HINT, request.getIdpHint()); - paramAction.accept(Constants.KC_ACTION, request.getAction()); + + String kcAction = request.getAction(); + String kcActionParameter = null; + if (kcAction != null && kcAction.contains(":")) { + String[] splits = kcAction.split(":"); + kcAction = splits[0]; + kcActionParameter = splits[1]; + } + paramAction.accept(Constants.KC_ACTION, kcAction); + paramAction.accept(Constants.KC_ACTION_PARAMETER, kcActionParameter); + paramAction.accept(OAuth2Constants.DISPLAY, request.getDisplay()); paramAction.accept(OIDCLoginProtocol.ACR_PARAM, request.getAcr()); paramAction.accept(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 956014f19603..0201b925b68a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -442,6 +442,7 @@ private Response doBrowserLogout(AuthenticationSessionModel logoutSession) { } } catch (OAuthErrorException e) { event.event(EventType.LOGOUT); + event.detail(Details.REASON, e.getDescription()); event.error(Errors.INVALID_TOKEN); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); } @@ -538,9 +539,11 @@ private Response logoutToken() { } catch (OAuthErrorException e) { // KEYCLOAK-6771 Certificate Bound Token if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) { + event.detail(Details.REASON, e.getDescription()); event.error(Errors.NOT_ALLOWED); throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED); } else { + event.detail(Details.REASON, e.getDescription()); event.error(Errors.INVALID_TOKEN); throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index f81f28dc688f..7af0759fa625 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -171,6 +171,7 @@ private void checkToken() { String encodedToken = formParams.getFirst(PARAM_TOKEN); if (encodedToken == null) { + event.detail(Details.REASON, "Token not provided"); event.error(Errors.INVALID_REQUEST); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Token not provided", Response.Status.BAD_REQUEST); @@ -184,6 +185,7 @@ private void checkToken() { } if (!(TokenUtil.TOKEN_TYPE_REFRESH.equals(token.getType()) || TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType()) || TokenUtil.TOKEN_TYPE_BEARER.equals(token.getType())|| TokenUtil.TOKEN_TYPE_DPOP.equals(token.getType()))) { + event.detail(Details.REASON, "Unsupported token type"); event.error(Errors.INVALID_TOKEN_TYPE); throw new CorsErrorResponseException(cors, OAuthErrorException.UNSUPPORTED_TOKEN_TYPE, "Unsupported token type", Response.Status.BAD_REQUEST); @@ -193,11 +195,13 @@ private void checkToken() { private void checkIssuedFor() { String issuedFor = token.getIssuedFor(); if (issuedFor == null) { + event.detail(Details.REASON, "Issued for not set"); event.error(Errors.INVALID_TOKEN); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.OK); } if (!client.getClientId().equals(issuedFor)) { + event.detail(Details.REASON, "Unmatching clients"); event.error(Errors.INVALID_REQUEST); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Unmatching clients", Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java index 929d64027fdc..fa745cd3c237 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java @@ -54,7 +54,7 @@ public static AuthorizationEndpointRequest parseRequest(EventBuilder event, Keyc try { AuthorizationEndpointRequest request = new AuthorizationEndpointRequest(); boolean isResponseTypeParameterRequired = isResponseTypeParameterRequired(requestParams, endpointType); - AuthzEndpointQueryStringParser parser = new AuthzEndpointQueryStringParser(requestParams, isResponseTypeParameterRequired); + AuthzEndpointQueryStringParser parser = new AuthzEndpointQueryStringParser(session, requestParams, isResponseTypeParameterRequired); parser.parseRequest(request); if (parser.getInvalidRequestMessage() != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java index 3cb65424fada..120f7c6e22da 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java @@ -22,6 +22,7 @@ import java.util.Set; import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.OIDCLoginProtocol; /** @@ -39,7 +40,8 @@ public class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser { private String invalidRequestMessage = null; - public AuthzEndpointQueryStringParser(MultivaluedMap requestParams, boolean isResponseTypeParameterRequired) { + public AuthzEndpointQueryStringParser(KeycloakSession keycloakSession, MultivaluedMap requestParams, boolean isResponseTypeParameterRequired) { + super(keycloakSession); this.requestParams = requestParams; this.isResponseTypeParameterRequired = isResponseTypeParameterRequired; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java index 584230fba67f..c9ea65134b6c 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -41,6 +41,7 @@ public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser private final JsonNode requestParams; public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) { + super(session); this.requestParams = session.tokens().decodeClientJWT(requestObject, client, createRequestObjectValidator(session), JsonNode.class); if (this.requestParams == null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java index 8f696ea0ba7a..ab7c87c2e82e 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java @@ -19,15 +19,41 @@ import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; import org.keycloak.constants.AdapterConstants; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.ErrorResponseException; + +import jakarta.ws.rs.core.Response; import java.util.HashSet; import java.util.Map; import java.util.Set; /** + * This endpoint parser supports, per default, up to + * {@value #DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_MUMBER} parameters with each + * having a total size of {@value #DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_SIZE}. If + * there are more authentication request parameters, or a parameter has a size + * than allowed, those parameters are silently ignored. + *

    + * You can toggle the behavior by setting a realm specific attribute + * ({@code additionalReqParamsFailFast}) that enables the fail-fast principle. + * Any request parameter in violation of the configuration results in an + * error response, e.g., + *

      + *
    • for a Pushed Authorization Request (PAR) this results in a JSON response.
    • + *
    • For openid/auth in an error page with an "Back to Application" button using the client's base URL. (if valid) as redirect target.
    • + *
    + * + *

    + * Additionally a realm specific attribute ({@code additionalReqParamMaxOverallSize}) can be configured + * that sets the maximum of size of all parameters combined. If not provided, {@link Integer#MAX_VALUE} will be used. + * + * @author Manuel Schallar * @author Marek Posolda */ public abstract class AuthzEndpointRequestParser { @@ -35,22 +61,54 @@ public abstract class AuthzEndpointRequestParser { private static final Logger logger = Logger.getLogger(AuthzEndpointRequestParser.class); /** - * Max number of additional req params copied into client session note to prevent DoS attacks - * + * Default value for {@link #additionalReqParamsMaxNumber} if case no realm property is set. */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; + public static final int DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 10; + + /** + * Max number of additional request parameters copied into client session note to prevent DoS attacks. + */ + protected final int additionalReqParamsMaxNumber; + + /** + * Default value for {@link #additionalReqParamsMaxSize} if case no realm property is set. + */ + public static final int DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_SIZE = 2000; + + /** + * Max size of additional request parameters value copied into client session note to prevent DoS attacks. + */ + protected final int additionalReqParamsMaxSize; + + /** + * Default value for {@link #additionalReqParamsFailFast} in case no realm property is set. + */ + private static final boolean DEFAULT_ADDITIONAL_REQ_PARAMS_FAIL_FAST = false; + /** - * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored - * + * Whether the fail-fast strategy should be enforced. If false all additional request parameters + * that to not meet the configuration are silently ignored. If true an exception will be raised. */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 2000; + protected final boolean additionalReqParamsFailFast; + + /** + * Default value for {@link #additionalReqParamsMaxOverallSize} in case no realm property is set. + * + */ + private static final int DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_OVERALL_SIZE = Integer.MAX_VALUE; + + /** + * Max size of all additional request parameters value copied into client session note to prevent DoS attacks. + */ + protected final int additionalReqParamsMaxOverallSize; public static final String AUTHZ_REQUEST_OBJECT = "ParsedRequestObject"; public static final String AUTHZ_REQUEST_OBJECT_ENCRYPTED = "EncryptedRequestObject"; /** Set of known protocol GET params not to be stored into additionalReqParams} */ public static final Set KNOWN_REQ_PARAMS = new HashSet<>(); + public static final Set ADDITIONAL_REQ_PARAMS_MAX_SIZE_IGNORE = new HashSet<>(); static { KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM); KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); @@ -73,8 +131,25 @@ public abstract class AuthzEndpointRequestParser { // https://tools.ietf.org/html/rfc7636#section-6.1 KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM); KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM); + + // Ignore "hash" parameter for param size check, because more than 4 hashes get filtered by this check. + ADDITIONAL_REQ_PARAMS_MAX_SIZE_IGNORE.add("hash"); + ADDITIONAL_REQ_PARAMS_MAX_SIZE_IGNORE.add("dtbs"); + + // Those are not OAuth/OIDC parameters, but they should never be added to the additionalRequestParameters + KNOWN_REQ_PARAMS.add(OAuth2Constants.CLIENT_ASSERTION_TYPE); + KNOWN_REQ_PARAMS.add(OAuth2Constants.CLIENT_ASSERTION); + KNOWN_REQ_PARAMS.add(OAuth2Constants.CLIENT_SECRET); } + protected AuthzEndpointRequestParser(KeycloakSession keycloakSession) { + RealmModel realm = keycloakSession.getContext().getRealm(); + this.additionalReqParamsMaxNumber = realm.getAttribute("additionalReqParamsMaxNumber", DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_MUMBER); + this.additionalReqParamsMaxSize = realm.getAttribute("additionalReqParamsMaxSize", DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_SIZE); + this.additionalReqParamsFailFast = realm.getAttribute("additionalReqParamsFailFast", DEFAULT_ADDITIONAL_REQ_PARAMS_FAIL_FAST); + this.additionalReqParamsMaxOverallSize = realm.getAttribute("additionalReqParamsMaxOverallSize", DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_OVERALL_SIZE); + } + public void parseRequest(AuthorizationEndpointRequest request) { String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM); if (clientId != null && request.clientId != null && !request.clientId.equals(clientId)) { @@ -120,23 +195,61 @@ protected void validateResponseTypeParameter(String responseTypeParameter, Autho } protected void extractAdditionalReqParams(Map additionalReqParams) { + int currentAdditionalReqParamMaxOverallSize = 0; for (String paramName : keySet()) { - if (!KNOWN_REQ_PARAMS.contains(paramName)) { - String value = getParameter(paramName); - if (value != null && value.trim().isEmpty()) { - value = null; - } - if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { - if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { - logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); - break; - } - additionalReqParams.put(paramName, value); - } else { - logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); - } + + if (KNOWN_REQ_PARAMS.contains(paramName)) { + logger.debugv("The additional OIDC param ''{0}'' is well known. Continue with the other additional parameters.", paramName); + continue; + } + + final String value = getParameter(paramName); + + if (value == null || value.trim().isEmpty()) { + logger.debugv("The additional OIDC param ''{0}'' ignored because it's value is null or blank.", paramName); + continue; + } + + // Compare with ">=", as the currently processed parameter will be added at the END of this method. + if (additionalReqParams.size() >= additionalReqParamsMaxNumber) { + + if (additionalReqParamsFailFast) { + logger.infov("The maximum number of allowed parameters ({0}) is exceeded.", additionalReqParamsMaxNumber); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "The maximum number of allowed parameters (" + additionalReqParamsMaxNumber + ") is exceeded.", Response.Status.BAD_REQUEST); + } else { + logger.debugv("The maximum number of allowed parameters ({0}) is exceeded.", additionalReqParamsMaxNumber); + break; + } + + } + + if (value.length() + currentAdditionalReqParamMaxOverallSize > additionalReqParamsMaxOverallSize) { + + if (additionalReqParamsFailFast) { + logger.infov("The OIDC additional parameter '{0}''s size ({1}) exceeds the maximum allowed size of all parameters ({2}).", paramName, value.length(), additionalReqParamsMaxOverallSize); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "The OIDC additional parameter '" + paramName + "'s size (" + value.length() + ") exceeds the maximum allowed size of all parameters (" + additionalReqParamsMaxOverallSize + ").", Response.Status.BAD_REQUEST); + } else { + logger.debugv("The OIDC additional parameter '{0}''s size exceeds ({1}) the maximum allowed size of all parameters ({2}).", paramName, value.length(), additionalReqParamsMaxOverallSize); + break; } + } + + if (value.length() > additionalReqParamsMaxSize) { + + if (additionalReqParamsFailFast) { + logger.infov("The OIDC additional parameter '{0}''s size is longer ({1}) than allowed ({2}).", paramName, value.length(), additionalReqParamsMaxSize); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "The OIDC additional parameter '" + paramName + "'s size is longer (" + value.length() + ") than allowed (" + additionalReqParamsMaxSize + ").", Response.Status.BAD_REQUEST); + } else { + logger.debugv("The OIDC additional parameter '{0}''s size is longer ({1}) than allowed ({2}).", paramName, value.length(), additionalReqParamsMaxSize); + break; + } + + } + + logger.debugv("Adding OIDC additional parameter ''{0}'' as additional parameter.", paramName); + currentAdditionalReqParamMaxOverallSize += value.length(); + additionalReqParams.put(paramName, value); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java index 882e3c7620a6..efafc320cecb 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java @@ -25,10 +25,10 @@ import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.common.Profile; +import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessTokenResponse; @@ -65,6 +65,7 @@ public Response process(Context context) { session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams)); refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN); } catch (ClientPolicyException cpe) { + event.detail(Details.REASON, cpe.getErrorDetail()); event.error(cpe.getError()); throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } @@ -91,13 +92,16 @@ public Response process(Context context) { logger.trace(e.getMessage(), e); // KEYCLOAK-6771 Certificate Bound Token if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) { + event.detail(Details.REASON, e.getDescription()); event.error(Errors.NOT_ALLOWED); throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED); } else { + event.detail(Details.REASON, e.getDescription()); event.error(Errors.INVALID_TOKEN); throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); } } catch (ClientPolicyException cpe) { + event.detail(Details.REASON, cpe.getErrorDetail()); event.error(cpe.getError()); throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationCallbackEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationCallbackEndpoint.java index 069052dc1472..31100afed75f 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationCallbackEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationCallbackEndpoint.java @@ -56,6 +56,7 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo private static final Logger logger = Logger.getLogger(BackchannelAuthenticationCallbackEndpoint.class); private final HttpRequest httpRequest; + protected AuthenticationChannelResponse authenticationChannelResponse; public BackchannelAuthenticationCallbackEndpoint(KeycloakSession session, EventBuilder event) { super(session, event); @@ -69,7 +70,8 @@ public BackchannelAuthenticationCallbackEndpoint(KeycloakSession session, EventB @Produces(MediaType.APPLICATION_JSON) public Response processAuthenticationChannelResult(AuthenticationChannelResponse response) { event.event(EventType.LOGIN); - BackchannelAuthCallbackContext ctx = verifyAuthenticationRequest(httpRequest.getHttpHeaders()); + this.authenticationChannelResponse = response; + BackchannelAuthCallbackContext ctx = verifyAuthenticationRequest(getRawBearerToken()); AccessToken bearerToken = ctx.bearerToken; OAuth2DeviceCodeModel deviceModel = ctx.deviceModel; @@ -81,15 +83,21 @@ public Response processAuthenticationChannelResult(AuthenticationChannelResponse Response.Status.BAD_REQUEST); } + status = preApprove(response); + switch (status) { case SUCCEED: - approveRequest(bearerToken, response.getAdditionalParams()); + approveRequest(bearerToken.getId(), response.getAdditionalParams()); break; case CANCELLED: case UNAUTHORIZED: - denyRequest(bearerToken, status); + denyRequest(bearerToken.getId(), status); break; } + + if (!postApprove(response)) { + denyRequest(bearerToken.getId(), status); + } // Call the notification endpoint ClientModel client = session.getContext().getClient(); @@ -101,8 +109,7 @@ public Response processAuthenticationChannelResult(AuthenticationChannelResponse return Response.ok(MediaType.APPLICATION_JSON_TYPE).build(); } - private BackchannelAuthCallbackContext verifyAuthenticationRequest(HttpHeaders headers) { - String rawBearerToken = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers); + protected BackchannelAuthCallbackContext verifyAuthenticationRequest(String rawBearerToken) { if (rawBearerToken == null) { throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.UNAUTHORIZED); @@ -111,12 +118,7 @@ private BackchannelAuthCallbackContext verifyAuthenticationRequest(HttpHeaders h AccessToken bearerToken; try { - bearerToken = TokenVerifier.createWithoutSignature(session.tokens().decode(rawBearerToken, AccessToken.class)) - .withDefaultChecks() - .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())) - .checkActive(true) - .audience(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())) - .verify().getToken(); + bearerToken = getTokenVerifier(rawBearerToken).verify().getToken(); } catch (Exception e) { event.error(Errors.INVALID_TOKEN); // authentication channel id format is invalid or it has already been used @@ -152,26 +154,62 @@ private BackchannelAuthCallbackContext verifyAuthenticationRequest(HttpHeaders h return new BackchannelAuthCallbackContext(bearerToken, deviceCode); } - private void cancelRequest(String authResultId) { + protected void cancelRequest(String authResultId) { OAuth2DeviceCodeModel userCode = DeviceEndpoint.getDeviceByUserCode(session, realm, authResultId); DeviceGrantType.removeDeviceByDeviceCode(session, userCode.getDeviceCode()); DeviceGrantType.removeDeviceByUserCode(session, realm, authResultId); } + + protected Status preApprove(AuthenticationChannelResponse response) { + return response.getStatus(); + } - private void approveRequest(AccessToken authReqId, Map additionalParams) { - DeviceGrantType.approveUserCode(session, realm, authReqId.getId(), "fake", additionalParams); + protected void approveRequest(String authReqId, Map additionalParams) { + DeviceGrantType.approveUserCode(session, realm, authReqId, "fake", additionalParams); + } + + protected boolean postApprove(AuthenticationChannelResponse response) { + return true; } - private void denyRequest(AccessToken authReqId, Status status) { + protected void denyRequest(String authReqId, Status status) { if (CANCELLED.equals(status)) { event.error(Errors.NOT_ALLOWED); } else { event.error(Errors.CONSENT_DENIED); } - DeviceGrantType.denyUserCode(session, realm, authReqId.getId()); + DeviceGrantType.denyUserCode(session, realm, authReqId); + } + + protected String getRawBearerToken() { + return AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(httpRequest.getHttpHeaders()); } + /** + * Returns a tokenverifier for {@link AccessToken}s with + * {@link TokenVerifier#withDefaultChecks()} with configured realmurl, isactive + * check an audience. + * + * @param rawBearerToken The raw bearer token. (required; must not be + * {@code null}) + * + * @return The token verifier for {@link AccessToken}s. (never {@code null}) + * + * @implNote Note that the token will only verified once + * {@link TokenVerifier#verify()} is called. + */ + protected TokenVerifier getTokenVerifier(String rawBearerToken) { + + return TokenVerifier + .createWithoutSignature(session.tokens().decode(rawBearerToken, AccessToken.class)) + .withDefaultChecks() + .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())) + .checkActive(true) + .audience(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); + + } + protected void sendClientNotificationRequest(ClientModel client, CibaConfig cibaConfig, OAuth2DeviceCodeModel deviceModel) { String clientNotificationEndpoint = cibaConfig.getBackchannelClientNotificationEndpoint(client); if (clientNotificationEndpoint == null) { @@ -208,7 +246,7 @@ protected void sendClientNotificationRequest(ClientModel client, CibaConfig ciba } } - private class BackchannelAuthCallbackContext { + protected static class BackchannelAuthCallbackContext { private final AccessToken bearerToken; private final OAuth2DeviceCodeModel deviceModel; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java index a2b80c520b17..799c528a0f53 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/BackchannelAuthenticationEndpoint.java @@ -99,12 +99,12 @@ public Response processGrantRequest() { UserModel user = request.getUser(); String infoUsedByAuthentication = resolver.getInfoUsedByAuthentication(user); + CibaConfig cibaPolicy = realm.getCibaPolicy(); + int poolingInterval = cibaPolicy.getPoolingInterval(); - if (provider.requestAuthentication(request, infoUsedByAuthentication)) { - CibaConfig cibaPolicy = realm.getCibaPolicy(); - int poolingInterval = cibaPolicy.getPoolingInterval(); + storeAuthenticationRequest(request, cibaPolicy, authReqId); - storeAuthenticationRequest(request, cibaPolicy, authReqId); + if (provider.requestAuthentication(request, infoUsedByAuthentication)) { ObjectNode response = JsonSerialization.createObjectNode(); @@ -151,12 +151,19 @@ private void storeAuthenticationRequest(CIBAAuthenticationRequest request, CibaC // To inform "expired_token" to the client, the lifespan of the cache provider is longer than device code int lifespanSeconds = expiresIn + poolingInterval + 10; + + this.store(userCode, deviceCode, lifespanSeconds); + + } + + + protected void store(OAuth2DeviceUserCodeModel userCode, OAuth2DeviceCodeModel deviceCode, int lifespanSeconds) { - SingleUseObjectProvider singleUseStore = session.singleUseObjects(); + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); singleUseStore.put(deviceCode.serializeKey(), lifespanSeconds, deviceCode.toMap()); singleUseStore.put(userCode.serializeKey(), lifespanSeconds, userCode.serializeValue()); - } + } private CIBAAuthenticationRequest authorizeClient(MultivaluedMap params) { ClientModel client = null; @@ -167,7 +174,7 @@ private CIBAAuthenticationRequest authorizeClient(MultivaluedMap throw new ErrorResponseException(errorRep.getError(), errorRep.getErrorDescription(), Response.Status.UNAUTHORIZED); } BackchannelAuthenticationEndpointRequest endpointRequest = BackchannelAuthenticationEndpointRequestParserProcessor.parseRequest(event, session, client, params, realm.getCibaPolicy()); - UserModel user = resolveUser(endpointRequest, realm.getCibaPolicy().getAuthRequestedUserHint()); + UserModel user = resolveUser(endpointRequest, getAuthRequestedUserHint()); CIBAAuthenticationRequest request = new CIBAAuthenticationRequest(session, user, client); @@ -238,6 +245,10 @@ private CIBAAuthenticationRequest authorizeClient(MultivaluedMap return request; } + protected String getAuthRequestedUserHint() { + return realm.getCibaPolicy().getAuthRequestedUserHint(); + } + protected void extractAdditionalParams(BackchannelAuthenticationEndpointRequest endpointRequest, CIBAAuthenticationRequest request) { for (String paramName : endpointRequest.getAdditionalReqParams().keySet()) { request.setOtherClaims(paramName, endpointRequest.getAdditionalReqParams().get(paramName)); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java index 767e2c278729..99f90d21bf46 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestBodyParser.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc.grants.ciba.endpoints.request; import jakarta.ws.rs.core.MultivaluedMap; +import org.keycloak.models.KeycloakSession; import java.util.Set; @@ -32,7 +33,8 @@ class BackchannelAuthenticationEndpointRequestBodyParser extends BackchannelAuth private String invalidRequestMessage = null; - public BackchannelAuthenticationEndpointRequestBodyParser(MultivaluedMap requestParams) { + public BackchannelAuthenticationEndpointRequestBodyParser(KeycloakSession session, MultivaluedMap requestParams) { + super(session); this.requestParams = requestParams; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java index 6d734d2f8069..c7badcd5bec5 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParser.java @@ -20,7 +20,10 @@ import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser; import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; import java.util.HashSet; @@ -38,13 +41,13 @@ public abstract class BackchannelAuthenticationEndpointRequestParser { * Max number of additional req params copied into client session note to prevent DoS attacks * */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; + private final int additionalReqParamsMaxNumber; /** * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored * */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; + private final int additionalReqParamsMaxSize; public static final String CIBA_SIGNED_AUTHENTICATION_REQUEST = "ParsedSignedAuthenticationRequest"; @@ -77,6 +80,22 @@ public abstract class BackchannelAuthenticationEndpointRequestParser { // if these are included in Backchannel Authentication Request's body part for "client_secret_post" client authentication KNOWN_REQ_PARAMS.add(OAuth2Constants.CLIENT_ID); KNOWN_REQ_PARAMS.add(OAuth2Constants.CLIENT_SECRET); + + /* Ignore JTI as additional parameter. + * + * if not ignored it leads to a duplicate entry in the AUTH_REQ_ID JWE token + * (see BackchannelAuthenticationEndpoint:processGrantRequest) and due to + * deserialization to a wrong id in CibaGrantType:process + * which leads to an HTTP 400 response in the ciba token call. + */ + KNOWN_REQ_PARAMS.add("jti"); + KNOWN_REQ_PARAMS.add("hash"); + } + + BackchannelAuthenticationEndpointRequestParser(KeycloakSession keycloakSession) { + RealmModel realm = keycloakSession.getContext().getRealm(); + this.additionalReqParamsMaxNumber = realm.getAttribute("additionalReqParamsMaxNumber", AuthzEndpointRequestParser.DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_MUMBER); + this.additionalReqParamsMaxSize = realm.getAttribute("additionalReqParamsMaxSize", AuthzEndpointRequestParser.DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_SIZE); } public void parseRequest(BackchannelAuthenticationEndpointRequest request) { @@ -98,6 +117,8 @@ public void parseRequest(BackchannelAuthenticationEndpointRequest request) { request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM)); extractAdditionalReqParams(request.additionalReqParams); + + request.additionalReqParams.put("hash", getParameter("hash")); } protected void extractAdditionalReqParams(Map additionalReqParams) { @@ -107,14 +128,14 @@ protected void extractAdditionalReqParams(Map additionalReqParam if (value != null && value.trim().isEmpty()) { value = null; } - if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { - if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { - logger.debug("Maximal number of additional OIDC CIBA params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); + if (value != null && value.length() <= additionalReqParamsMaxSize) { + if (additionalReqParams.size() >= additionalReqParamsMaxNumber) { + logger.debug("Maximal number of additional OIDC CIBA params (" + additionalReqParamsMaxNumber + ") exceeded, ignoring rest of them!"); break; } additionalReqParams.put(paramName, value); } else { - logger.debug("OIDC CIBA Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); + logger.debug("OIDC CIBA Additional param " + paramName + " ignored because value is empty or longer than " + additionalReqParamsMaxSize); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java index 618db3569684..6e7ee64753cd 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointRequestParserProcessor.java @@ -45,7 +45,7 @@ public static BackchannelAuthenticationEndpointRequest parseRequest(EventBuilder try { BackchannelAuthenticationEndpointRequest request = new BackchannelAuthenticationEndpointRequest(); - BackchannelAuthenticationEndpointRequestBodyParser parser = new BackchannelAuthenticationEndpointRequestBodyParser(requestParams); + BackchannelAuthenticationEndpointRequestBodyParser parser = new BackchannelAuthenticationEndpointRequestBodyParser(session, requestParams); parser.parseRequest(request); if (parser.getInvalidRequestMessage() != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java index 4baef290d406..1d86ea3203ec 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java @@ -43,6 +43,7 @@ class BackchannelAuthenticationEndpointSignedRequestParser extends BackchannelAu private final JsonNode requestParams; public BackchannelAuthenticationEndpointSignedRequestParser(KeycloakSession session, String signedAuthReq, ClientModel client, CibaConfig config) throws Exception { + super(session); JOSE jwt = JOSEParser.parse(signedAuthReq); if (jwt instanceof JWE) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java new file mode 100755 index 000000000000..ac4ac07a2c8f --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.protocol.oidc.mappers; + +import org.jboss.logging.Logger; + +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.IDToken; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Giuseppe Graziano + */ +public class SessionStateMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, TokenIntrospectionTokenMapper { + + + public static final String PROVIDER_ID = "oidc-session-state-mapper"; + + private static final Logger logger = Logger.getLogger(SessionStateMapper.class); + + private static final List configProperties = new ArrayList<>(); + + static { + OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, SessionStateMapper.class); + } + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Session State (session_state)"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Add Session State (session_state) claim"; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, + ClientSessionContext clientSessionCtx) { + if (userSession != null) { + token.getOtherClaims().put(IDToken.SESSION_STATE, userSession.getId()); + } + } + + public static ProtocolMapperModel create(String name, boolean accessToken, boolean idToken, boolean userInfo, boolean introspectionEndpoint) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + Map config = new HashMap<>(); + if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + if (userInfo) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); + if (introspectionEndpoint) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "true"); + mapper.setConfig(config); + return mapper; + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java index 57882c408f3a..36b6f9fb5413 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java @@ -47,6 +47,7 @@ public class AuthzEndpointParParser extends AuthzEndpointRequestParser { private String invalidRequestMessage = null; public AuthzEndpointParParser(KeycloakSession session, ClientModel client, String requestUri) { + super(session); this.session = session; this.client = client; SingleUseObjectProvider singleUseStore = session.singleUseObjects(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/ParEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/ParEndpointRequestParserProcessor.java index 1efdf24feef6..b10e2591e005 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/ParEndpointRequestParserProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/ParEndpointRequestParserProcessor.java @@ -50,7 +50,7 @@ public static AuthorizationEndpointRequest parseRequest(EventBuilder event, Keyc try { AuthorizationEndpointRequest request = new AuthorizationEndpointRequest(); - AuthzEndpointQueryStringParser parser = new AuthzEndpointQueryStringParser(requestParams, false); + AuthzEndpointQueryStringParser parser = new AuthzEndpointQueryStringParser(session, requestParams, false); parser.parseRequest(request); if (parser.getInvalidRequestMessage() != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java index cafebb9ff1e1..8611093622e4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java @@ -102,7 +102,7 @@ private static Map getAcrLoaMapForClientOnly(ClientModel client try { return JsonSerialization.readValue(map, new TypeReference>() {}); } catch (IOException e) { - LOGGER.warnf("Invalid client configuration (ACR-LOA map) for client '%s'", client.getClientId()); + LOGGER.warnf("Invalid client configuration (ACR-LOA map) for client '%s'. Error details: %s", client.getClientId(), e.getMessage()); return Collections.emptyMap(); } } @@ -119,7 +119,7 @@ public static Map getAcrLoaMap(RealmModel realm) { try { return JsonSerialization.readValue(map, new TypeReference>() {}); } catch (IOException e) { - LOGGER.warn("Invalid realm configuration (ACR-LOA map)"); + LOGGER.warnf("Invalid realm configuration (ACR-LOA map). Details: %s", e.getMessage()); return Collections.emptyMap(); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java index e171d8da8951..de8fd84e0a56 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -18,8 +18,6 @@ package org.keycloak.protocol.oidc.utils; import org.jboss.logging.Logger; -import org.keycloak.common.util.Encode; -import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -34,6 +32,7 @@ import java.util.Collection; import java.util.Set; import java.util.TreeSet; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -111,16 +110,13 @@ public static String verifyRedirectUri(KeycloakSession session, String rootUrl, return null; } - // Make the validations against fully decoded and normalized redirect-url. This also allows wildcards (case when client configured "Valid redirect-urls" contain wildcards) - String decodedRedirectUri = decodeRedirectUri(redirectUri); - URI decodedRedirect = toUri(decodedRedirectUri); - decodedRedirectUri = getNormalizedRedirectUri(decodedRedirect); - if (decodedRedirectUri == null) return null; + // check if the passed URI allows wildcards + boolean allowWildcards = areWildcardsAllowed(originalRedirect); - String r = decodedRedirectUri; + String r = redirectUri; Set resolveValidRedirects = resolveValidRedirects(session, rootUrl, validRedirects); - String valid = matchesRedirects(resolveValidRedirects, r, true); + String valid = matchesRedirects(resolveValidRedirects, r, allowWildcards); if (valid == null && (r.startsWith(Constants.INSTALLED_APP_URL) || r.startsWith(Constants.INSTALLED_APP_LOOPBACK)) && r.indexOf(':', Constants.INSTALLED_APP_URL.length()) >= 0) { int i = r.indexOf(':', Constants.INSTALLED_APP_URL.length()); @@ -135,15 +131,7 @@ public static String verifyRedirectUri(KeycloakSession session, String rootUrl, r = sb.toString(); - valid = matchesRedirects(resolveValidRedirects, r, true); - } - - // Return the original redirectUri, which can be partially encoded - for example http://localhost:8280/foo/bar%20bar%2092%2F72/3 . Just make sure it is normalized - redirectUri = getNormalizedRedirectUri(originalRedirect); - - // We try to check validity also for original (encoded) redirectUrl, but just in case it exactly matches some "Valid Redirect URL" specified for client (not wildcards allowed) - if (valid == null) { - valid = matchesRedirects(resolveValidRedirects, redirectUri, false); + valid = matchesRedirects(resolveValidRedirects, r, allowWildcards); } if (valid != null && !originalRedirect.isAbsolute()) { @@ -154,7 +142,7 @@ public static String verifyRedirectUri(KeycloakSession session, String rootUrl, redirectUri = relativeToAbsoluteURI(session, rootUrl, redirectUri); } - String scheme = decodedRedirect.getScheme(); + String scheme = originalRedirect.getScheme(); if (valid != null && scheme != null) { // check the scheme is valid, it should be http(s) or explicitly allowed by the validation if (!valid.startsWith(scheme + ":") && !"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { @@ -179,51 +167,22 @@ private static URI toUri(String redirectUri) { try { uri = URI.create(redirectUri); } catch (IllegalArgumentException cause) { - logger.debug("Invalid redirect uri", cause); + logger.debugf(cause, "Invalid redirect uri %s", redirectUri); } catch (Exception cause) { - logger.debug("Unexpected error when parsing redirect uri", cause); + logger.debugf(cause, "Unexpected error when parsing redirect uri %s", redirectUri); } } return uri; } - private static String getNormalizedRedirectUri(URI uri) { - String redirectUri = null; - if (uri != null) { - redirectUri = uri.normalize().toString(); - } - return redirectUri; - } - - // Decode redirectUri. Only path is decoded as other elements can be encoded in the original URL or cannot be encoded at all. - // URL can be decoded multiple times (in case it was encoded multiple times, or some of it's parts were encoded multiple times) - private static String decodeRedirectUri(String redirectUri) { - if (redirectUri == null) return null; - int MAX_DECODING_COUNT = 5; // Max count of attempts for decoding URL (in case it was encoded multiple times) + // any access to parent folder /../ is unsafe with or without encoding + private final static Pattern UNSAFE_PATH_PATTERN = Pattern.compile( + "(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|\\.){2}(/|%2[fF]|%5[cC]|\\\\)|(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|\\.){2}$"); - try { - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(redirectUri, false).preserveDefaultPort(); - if (uriBuilder.getPath() == null) { - return redirectUri; - } - String encodedPath = uriBuilder.getPath(); - String decodedPath; - - for (int i = 0; i < MAX_DECODING_COUNT; i++) { - decodedPath = Encode.decode(encodedPath); - if (decodedPath.equals(encodedPath)) { - // URL path is decoded. We can return it in the original redirect URI - return uriBuilder.replacePath(decodedPath, false).buildAsString(); - } else { - // Next attempt - encodedPath = decodedPath; - } - } - } catch (IllegalArgumentException iae) { - logger.debugf("Illegal redirect URI used: %s, Details: %s", redirectUri, iae.getMessage()); - } - logger.debugf("Was not able to decode redirect URI: %s", redirectUri); - return null; + private static boolean areWildcardsAllowed(URI redirectUri) { + // wildcars are only allowed if no user-info and no unsafe pattern in path + return redirectUri.getRawUserInfo() == null + && (redirectUri.getRawPath() == null || !UNSAFE_PATH_PATTERN.matcher(redirectUri.getRawPath()).find()); } private static String relativeToAbsoluteURI(KeycloakSession session, String rootUrl, String relative) { @@ -240,24 +199,20 @@ private static String relativeToAbsoluteURI(KeycloakSession session, String root return sb.toString(); } - // removes the queryString, fragment and userInfo from the redirect - // to avoid comparing this when wildcards are used - private static String stripOffRedirectForWildcard(String redirect) { - return KeycloakUriBuilder.fromUri(redirect, false) - .preserveDefaultPort() - .userInfo(null) - .replaceQuery(null) - .fragment(null) - .buildAsString(); - } - // return the String that matched the redirect or null if not matched private static String matchesRedirects(Set validRedirects, String redirect, boolean allowWildcards) { logger.tracef("matchesRedirects: redirect URL to check: %s, allow wildcards: %b, Configured valid redirect URLs: %s", redirect, allowWildcards, validRedirects); for (String validRedirect : validRedirects) { - if (validRedirect.endsWith("*") && !validRedirect.contains("?") && allowWildcards) { - // strip off the userInfo, query or fragment components - we don't check them when wildcards are effective - String r = stripOffRedirectForWildcard(redirect); + if ("*".equals(validRedirect)) { + // the valid redirect * is a full wildcard for http(s) even if the redirect URI does not allow wildcards + return validRedirect; + } else if (validRedirect.endsWith("*") && !validRedirect.contains("?") && allowWildcards) { + // strip off the query or fragment components - we don't check them when wildcards are effective + int idx = redirect.indexOf('?'); + if (idx == -1) { + idx = redirect.indexOf('#'); + } + String r = idx == -1 ? redirect : redirect.substring(0, idx); // strip off * int length = validRedirect.length() - 1; validRedirect = validRedirect.substring(0, length); diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsCondition.java new file mode 100644 index 000000000000..ab3224bfee00 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsCondition.java @@ -0,0 +1,80 @@ +package org.keycloak.services.clientpolicy.condition; + +import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.ClientPolicyVote; + +import java.util.List; + +public class ClientIdsCondition extends AbstractClientPolicyConditionProvider { + + private static final Logger logger = Logger.getLogger(ClientIdsCondition.class); + + public ClientIdsCondition(KeycloakSession session) { + super(session); + } + + @Override public Class getConditionConfigurationClass() { + return ClientIdsCondition.Configuration.class; + } + + @Override public String getProviderId() { + return ClientIdsConditionFactory.PROVIDER_ID; + } + + @Override public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case AUTHORIZATION_REQUEST: + case TOKEN_REQUEST: + case TOKEN_REFRESH: + case TOKEN_REVOKE: + case TOKEN_INTROSPECT: + case USERINFO_REQUEST: + case LOGOUT_REQUEST: + if (clientIdMatched(session.getContext().getClient())) + return ClientPolicyVote.YES; + return ClientPolicyVote.NO; + default: + return ClientPolicyVote.ABSTAIN; + } + } + + private boolean clientIdMatched(ClientModel client) { + if (client == null || client.getClientId() == null) + return false; + + List configuredClientIds = configuration.getClientIds(); + + if (configuredClientIds == null) + return false; + + String clientId = client.getClientId(); + + for (String configuredClientId : configuredClientIds) { + if (clientId.equals(configuredClientId)) { + return true; + } + } + + return false; + + } + + public static class Configuration extends ClientPolicyConditionConfigurationRepresentation { + + protected List clientIds; + + public List getClientIds() { + return clientIds; + } + + public void setClientIds(List clientIds) { + this.clientIds = clientIds; + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsConditionFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsConditionFactory.java new file mode 100644 index 000000000000..e1eab4e4e942 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientIdsConditionFactory.java @@ -0,0 +1,52 @@ +package org.keycloak.services.clientpolicy.condition; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.List; + +public class ClientIdsConditionFactory implements ClientPolicyConditionProviderFactory { + + public static final String PROVIDER_ID = "client-ids"; + public static final String CLIENT_IDS = "clientIds"; + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(CLIENT_IDS, PROVIDER_ID + ".label", PROVIDER_ID + "-condition.tooltip", + ProviderConfigProperty.MULTIVALUED_STRING_TYPE, null); + configProperties.add(property); + } + + @Override public ClientPolicyConditionProvider create(KeycloakSession session) { + return new ClientIdsCondition(session); + } + + @Override public void init(Config.Scope config) { + } + + @Override public void postInit(KeycloakSessionFactory factory) { + } + + @Override public void close() { + } + + @Override public String getId() { + return PROVIDER_ID; + } + + @Override public String getHelpText() { + return "The condition checks whether one of the specified client ids matches the client-id of the client to determine whether the policy is applied."; + } + + @Override public List getConfigProperties() { + return configProperties; + } + +} + + diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutor.java new file mode 100644 index 000000000000..b918dd07f8b1 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutor.java @@ -0,0 +1,83 @@ +package org.keycloak.services.clientpolicy.executor; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.logging.Logger; +import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyEvent; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.keycloak.OAuthErrorException.INVALID_REDIRECT_URI; +import static org.keycloak.services.clientpolicy.executor.RegexRedirectUriExecutorFactory.REGEX_PATTERNS_CONFIG_FIELD; + +public class RegexRedirectUriExecutor + implements ClientPolicyExecutorProvider { + + private static final Logger logger = Logger.getLogger(RegexRedirectUriExecutor.class); + + private Configuration configuration; + + public RegexRedirectUriExecutor() { + } + + public void setupConfiguration(Configuration config) { + this.configuration = config; + } + + public Class getExecutorConfigurationClass() { + return Configuration.class; + } + + public String getProviderId() { + return "regex-redirect-uri"; + } + + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + if (context.getEvent().equals(ClientPolicyEvent.AUTHORIZATION_REQUEST)) { + this.checkRedirectUri(((AuthorizationRequestContext) context).getRedirectUri()); + } + } + + private void checkRedirectUri(String redirectUri) throws ClientPolicyException { + if (redirectUri != null && !redirectUri.isEmpty()) { + logger.tracev("Redirect URI = {0}", redirectUri); + + List patterns = this.configuration.getRedirectUriRegexPatterns(); + + for (String pattern : patterns) { + Pattern regexPattern = Pattern.compile(pattern); + Matcher matcher = regexPattern.matcher(redirectUri); + if (matcher.matches()) { + return; + } + } + + throw new ClientPolicyException(INVALID_REDIRECT_URI, "Invalid redirect_uri"); + + } else { + throw new ClientPolicyException(INVALID_REDIRECT_URI, "no redirect_uri specified."); + } + } + + public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation { + @JsonProperty(REGEX_PATTERNS_CONFIG_FIELD) + protected List redirectUriRegexPatterns; + + public Configuration() { + } + + public List getRedirectUriRegexPatterns() { + return redirectUriRegexPatterns; + } + + public void setRedirectUriRegexPatterns(List redirectUriRegexPatterns) { + this.redirectUriRegexPatterns = redirectUriRegexPatterns; + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutorFactory.java new file mode 100644 index 000000000000..989944f9858f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegexRedirectUriExecutorFactory.java @@ -0,0 +1,50 @@ +package org.keycloak.services.clientpolicy.executor; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.List; + +public class RegexRedirectUriExecutorFactory implements ClientPolicyExecutorProviderFactory { + public static final String PROVIDER_ID = "regex-redirect-uri"; + public static final String REGEX_PATTERNS_CONFIG_FIELD = "redirect-uri-regex-patterns"; + private List configProperties = new ArrayList(); + + public RegexRedirectUriExecutorFactory() { + } + + public ClientPolicyExecutorProvider create(KeycloakSession session) { + return new RegexRedirectUriExecutor(); + } + + public void init(Config.Scope config) { + } + + public void postInit(KeycloakSessionFactory factory) { + ProviderConfigProperty regexPatterns = + new ProviderConfigProperty(REGEX_PATTERNS_CONFIG_FIELD, "Redirect URI Regex Patterns", + "Regex-Patterns with which the redirect-URI is checked against", + "MultivaluedString", null); + + this.configProperties.add(regexPatterns); + } + + public void close() { + } + + public String getId() { + return PROVIDER_ID; + } + + public String getHelpText() { + return "Checks if the redirect URI matches a configured regex pattern."; + } + + public List getConfigProperties() { + return this.configProperties; + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicy.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicy.java index 6d79c701b4b9..65d26fbc661e 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicy.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicy.java @@ -23,7 +23,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; -import java.util.LinkedList; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -97,6 +97,10 @@ protected void verifyHost() throws ClientRegistrationPolicyException { String hostAddress = session.getContext().getConnection().getRemoteAddr(); + verifyHost(hostAddress); + } + + protected void verifyHost(String hostAddress) throws ClientRegistrationPolicyException { logger.debugf("Verifying remote host : %s", hostAddress); List trustedHosts = getTrustedHosts(); @@ -130,20 +134,9 @@ protected List getTrustedHosts() { protected List getTrustedDomains() { - List trustedHostsConfig = componentModel.getConfig().getList(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS); - List domains = new LinkedList<>(); - - for (String hostname : trustedHostsConfig) { - if (hostname.startsWith("*.")) { - hostname = hostname.substring(2); - domains.add(hostname); - } - } - - return domains; + return componentModel.getConfig().getList(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS); } - protected String verifyHostInTrustedHosts(String hostAddress, List trustedHosts) { for (String confHostName : trustedHosts) { try { @@ -162,23 +155,39 @@ protected String verifyHostInTrustedHosts(String hostAddress, List trust return null; } + private boolean checkTrustedDomain(String hostname, String trustedDomain) { + if (trustedDomain.startsWith("*.")) { + String domain = trustedDomain.substring(2); + return hostname.equals(domain) || hostname.endsWith("." + domain); + } + return hostname.equals(trustedDomain); + } protected String verifyHostInTrustedDomains(String hostAddress, List trustedDomains) { - if (!trustedDomains.isEmpty()) { - try { - String hostname = InetAddress.getByName(hostAddress).getHostName(); + try { + InetAddress address = InetAddress.getByName(hostAddress); + String hostname = address.getHostName(); + + logger.debugf("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname); - logger.debugf("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname); + if (hostname.equals(address.getHostAddress())) { + logger.debugf("The hostAddress '%s' was not resolved to a hostname", hostAddress); + return null; + } + + if (Arrays.stream(InetAddress.getAllByName(hostname)).filter(a -> address.equals(a)).findAny().isEmpty()) { + logger.debugf("The hostAddress '%s' is not among the direct lookups returned resolving '%s'", hostAddress, hostname); + return null; + } - for (String confDomain : trustedDomains) { - if (hostname.endsWith(confDomain)) { - logger.debugf("Successfully verified host '%s' by trusted domain '%s'", hostname, confDomain); - return hostname; - } + for (String confDomain : trustedDomains) { + if (checkTrustedDomain(hostname, confDomain)) { + logger.debugf("Successfully verified host '%s' by trusted domain '%s'", hostname, confDomain); + return hostname; } - } catch (UnknownHostException uhe) { - logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains", hostAddress); } + } catch (UnknownHostException uhe) { + logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains", hostAddress); } return null; @@ -260,7 +269,7 @@ private boolean checkHostTrusted(String host, List trustedHosts, List credentialTypes(@QueryParam(TYPE) String type return new CredentialContainer(metadata, userCredentialMetadataModels); }; - return getCredentialProviders() + return AuthenticatorUtil.getCredentialProviders(session) .filter(p -> type == null || Objects.equals(p.getType(), type)) .filter(p -> enabledCredentialTypes.contains(p.getType())) .map(toCredentialContainer) @@ -223,12 +235,6 @@ public Stream credentialTypes(@QueryParam(TYPE) String type .sorted(Comparator.comparing(CredentialContainer::getMetadata)); } - private Stream getCredentialProviders() { - return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class) - .filter(f -> Types.supports(CredentialProvider.class, f, CredentialProviderFactory.class)) - .map(f -> session.getProvider(CredentialProvider.class, f.getId())); - } - // Going through all authentication flows and their authentication executions to see if there is any authenticator of the corresponding // credential type. private Set getEnabledCredentialTypes() { @@ -261,18 +267,25 @@ private boolean isFlowEffectivelyDisabled(AuthenticationFlowModel flow) { return false; } - private void checkIfCanBeRemoved(String credentialType) { - Set enabledCredentialTypes = getEnabledCredentialTypes(); - CredentialProvider credentialProvider = getCredentialProviders() - .filter(p -> credentialType.equals(p.getType()) && enabledCredentialTypes.contains(p.getType())) - .findAny().orElse(null); - if (credentialProvider == null) { - throw new NotFoundException("Credential provider " + credentialType + " not found"); + private Integer getCurrentAuthenticatedLevel() { + ClientModel client = realm.getClientByClientId(auth.getToken().getIssuedFor()); + Map acrLoaMap = AcrUtils.getAcrLoaMap(client); + String tokenAcr = auth.getToken().getAcr(); + if (tokenAcr == null) { + logger.warnf("Not able to remove credential of user '%s' as no acr claim on the token", user.getUsername()); + throw new ForbiddenException("No LoA on the token"); } - CredentialTypeMetadataContext ctx = CredentialTypeMetadataContext.builder().user(user).build(session); - CredentialTypeMetadata metadata = credentialProvider.getCredentialTypeMetadata(ctx); - if (!metadata.isRemoveable()) { - throw new BadRequestException("Credential type " + credentialType + " cannot be removed"); + Integer currentAuthenticatedLevel = acrLoaMap.get(tokenAcr); + if (currentAuthenticatedLevel != null) { + return currentAuthenticatedLevel; + } else { + try { + return Integer.parseInt(tokenAcr); + } catch (NumberFormatException nfe) { + logger.warnf("Token acr '%s' not found in acrLoaMap of client '%s' or realm '%s'. Not able to remove credential of user '%s'", + tokenAcr, client.getClientId(), realm.getName(), user.getUsername()); + throw new ForbiddenException("Unsupported acr on the token"); + } } } @@ -280,28 +293,23 @@ private void checkIfCanBeRemoved(String credentialType) { * Remove a credential of current user * * @param credentialId ID of the credential, which will be removed + * @deprecated It is recommended to delete credentials with the use of "delete_credential" kc_action. + * Action can be used for instance by adding parameter like "kc_action=delete_credential:123" to the login URL where 123 is ID of the credential to delete. */ @Path("{credentialId}") @DELETE @NoCache + @Deprecated public void removeCredential(final @PathParam("credentialId") String credentialId) { auth.require(AccountRoles.MANAGE_ACCOUNT); - CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); - if (credential == null) { - // Backwards compatibility with account console 1 - When stored credential is not found, it may be federated credential. - // In this case, it's ID needs to be something like "otp-id", which is returned by account REST GET endpoint as a placeholder - // for federated credentials (See CredentialHelper.createUserStorageCredentialRepresentation ) - if (credentialId.endsWith("-id")) { - String credentialType = credentialId.substring(0, credentialId.length() - 3); - checkIfCanBeRemoved(credentialType); - user.credentialManager().disableCredentialType(credentialType); - return; - } + CredentialModel credential = CredentialDeleteHelper.removeCredential(session, user, credentialId, this::getCurrentAuthenticatedLevel); - throw new NotFoundException("Credential not found"); + if (credential != null && OTPCredentialModel.TYPE.equals(credential.getType())) { + event.event(EventType.REMOVE_TOTP) + .detail(Details.SELECTED_CREDENTIAL_ID, credentialId) + .detail(Details.CREDENTIAL_USER_LABEL, credential.getUserLabel()); + event.success(); } - checkIfCanBeRemoved(credential.getType()); - user.credentialManager().removeStoredCredentialById(credentialId); } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index eb7e3a0d5c3a..a49a57fcb746 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -214,7 +214,7 @@ public SessionResource sessions() { @Path("/credentials") public AccountCredentialResource credentials() { checkAccountApiEnabled(); - return new AccountCredentialResource(session, user, auth); + return new AccountCredentialResource(session, user, auth, event); } @Path("/resources") diff --git a/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java b/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java index aa0c211eff66..dcf796c5e68a 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.resources.account; +import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -60,6 +61,7 @@ import org.keycloak.services.managers.Auth; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; +import org.keycloak.theme.Theme; import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID; @@ -201,12 +203,12 @@ public Response removeLinkedAccount(@PathParam("providerAlias") String providerA FederatedIdentityModel link = session.users().getFederatedIdentity(realm, user, providerAlias); if (link == null) { - throw ErrorResponse.error(Messages.FEDERATED_IDENTITY_NOT_ACTIVE, Response.Status.BAD_REQUEST); + throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_NOT_ACTIVE), Response.Status.BAD_REQUEST); } // Removing last social provider is not possible if you don't have other possibility to authenticate if (!(session.users().getFederatedIdentitiesStream(realm, user).count() > 1 || user.getFederationLink() != null || isPasswordSet())) { - throw ErrorResponse.error(Messages.FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER, Response.Status.BAD_REQUEST); + throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER), Response.Status.BAD_REQUEST); } session.users().removeFederatedIdentity(realm, user, providerAlias); @@ -239,6 +241,14 @@ private String checkCommonPreconditions(String providerAlias) { return null; } + + private String translateErrorMessage(String errorCode) { + try { + return session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(session.getContext().resolveLocale(user)).getProperty(errorCode); + } catch (IOException e) { + return errorCode; + } + } private boolean isPasswordSet() { return user.credentialManager().isConfiguredFor(PasswordCredentialModel.TYPE); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java index 86693805d381..fc1f15ee5c3b 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java @@ -74,6 +74,7 @@ public ClientRegistrationPolicyResource(KeycloakSession session, AdminPermission @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_REGISTRATION_POLICY) @Operation( summary="Base path for retrieve providers with the configProperties properly filled") public Stream getProviders() { + auth.realm().requireViewRealm(); return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientRegistrationPolicy.class) .map((ProviderFactory factory) -> { ClientRegistrationPolicyFactory clientRegFactory = (ClientRegistrationPolicyFactory) factory; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 2abdfa896d9b..5cc70a3ecf42 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -30,6 +30,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; +import jakarta.ws.rs.DefaultValue; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; @@ -625,14 +627,19 @@ public GlobalRequestResult logoutAll() { @DELETE @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @Operation( summary = "Remove a specific user session.", description = "Any client that has an admin url will also be told to invalidate this particular session.") - public void deleteSession(@PathParam("session") String sessionId) { + public void deleteSession(@PathParam("session") String sessionId, @DefaultValue("false") @QueryParam("isOffline") boolean offline) { auth.users().requireManage(); - UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); - if (userSession == null) throw new NotFoundException("Sesssion not found"); + UserSessionModel userSession = offline ? session.sessions().getOfflineUserSession(realm, sessionId) : session.sessions().getUserSession(realm, sessionId); + if (userSession == null) { + throw new NotFoundException("Sesssion not found"); + } + AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true); - adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).success(); + Map eventRep = new HashMap<>(); + eventRep.put("offline", offline); + adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).representation(eventRep).success(); } /** diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index 2e7e035d0301..22ff3913015d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -24,6 +24,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.NoCache; import jakarta.ws.rs.NotFoundException; +import org.keycloak.common.util.Encode; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.ClientModel; @@ -202,7 +203,7 @@ public Response createRole(final RoleRepresentation rep) { adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, role.getName()).representation(rep).success(); - return Response.created(uriInfo.getAbsolutePathBuilder().path(role.getName()).build()).build(); + return Response.created(uriInfo.getAbsolutePathBuilder().path(Encode.encodePathSegmentAsIs(role.getName())).build()).build(); } catch (ModelDuplicateException e) { throw ErrorResponse.exists("Role with name " + rep.getName() + " already exists"); } diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index d9edb6b207ec..65503a9fb687 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -176,13 +176,9 @@ public UserModel apply(Attributes attributes) { protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) { UserProfileContext context = metadata.getContext(); UserProfileMetadata decoratedMetadata = metadata.clone(); - RealmModel realm = session.getContext().getRealm(); - ComponentModel component = getComponentModel().orElse(null); if (component == null) { - // makes sure user providers can override metadata for any attribute - decorateUserProfileMetadataWithUserStorage(realm, decoratedMetadata); return decoratedMetadata; } @@ -411,23 +407,10 @@ public boolean test(AttributeContext context) { } } - if (session != null) { - // makes sure user providers can override metadata for any attribute - decorateUserProfileMetadataWithUserStorage(session.getContext().getRealm(), decoratedMetadata); - } - return decoratedMetadata; } - private void decorateUserProfileMetadataWithUserStorage(RealmModel realm, UserProfileMetadata userProfileMetadata) { - // makes sure user providers can override metadata for any attribute - UserProvider users = session.users(); - if (users instanceof UserProfileDecorator) { - ((UserProfileDecorator) users).decorateUserProfile(realm, userProfileMetadata); - } - } - private Map asHashMap(List groups) { return groups.stream().collect(Collectors.toMap(g -> g.getName(), g -> g)); } @@ -459,16 +442,18 @@ private boolean isServiceAccountUser(UserModel user) { /** * Get parsed config file configured in model. Default one used if not configured. */ - protected UPConfig getParsedConfig(String rawConfig) { - if (!isBlank(rawConfig)) { - try { - return UPConfigUtils.parseConfig(rawConfig); - } catch (IOException e) { - throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid:" + e.getMessage(), e); - } + protected UPConfig parseConfigOrDefault(ComponentModel component) { + String rawConfig = component.get(UP_COMPONENT_CONFIG_KEY); + + if (isBlank(rawConfig)) { + return parsedDefaultRawConfig; } - return null; + try { + return UPConfigUtils.parseConfig(rawConfig); + } catch (IOException e) { + throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid:" + e.getMessage(), e); + } } /** @@ -492,22 +477,27 @@ protected AttributeValidatorMetadata createConfiguredValidator(String validator, } private UPConfig getConfigFromComponentModel(ComponentModel model) { - if (model == null) - return null; + UPConfig cached = getParsedConfigFromCache(model); - UPConfig cfg = model.getNote(PARSED_UP_CONFIG_COMPONENT_KEY); - if (cfg != null) { - return cfg; + if (cached == null) { + cached = parseAndCacheConfig(model); } - String rawConfig = model.get(UP_COMPONENT_CONFIG_KEY); - if (rawConfig == null) { + return cached; + } + + private UPConfig parseAndCacheConfig(ComponentModel model) { + UPConfig cfg = parseConfigOrDefault(model); + model.setNote(PARSED_UP_CONFIG_COMPONENT_KEY, cfg); + return cfg; + } + + private UPConfig getParsedConfigFromCache(ComponentModel component) { + if (component == null) { return null; - } else { - cfg = getParsedConfig(rawConfig); - model.setNote(PARSED_UP_CONFIG_COMPONENT_KEY, cfg); - return cfg; } + + return component.getNote(PARSED_UP_CONFIG_COMPONENT_KEY); } private void removeConfigJsonFromComponentModel(ComponentModel model) { diff --git a/services/src/main/java/org/keycloak/userprofile/validator/DuplicateEmailValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/DuplicateEmailValidator.java index 5b8e6cab9b77..469f0404c909 100644 --- a/services/src/main/java/org/keycloak/userprofile/validator/DuplicateEmailValidator.java +++ b/services/src/main/java/org/keycloak/userprofile/validator/DuplicateEmailValidator.java @@ -18,6 +18,7 @@ import jakarta.ws.rs.core.Response; import java.util.List; +import java.util.Objects; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -62,10 +63,11 @@ public ValidationContext validate(Object input, String inputHint, ValidationCont KeycloakSession session = context.getSession(); RealmModel realm = session.getContext().getRealm(); + UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser(); - if (!realm.isDuplicateEmailsAllowed()) { + // Only check if duplicate email addresses are not allowed, and the user is either new or changed their email address + if (!realm.isDuplicateEmailsAllowed() && (user == null || !Objects.equals(user.getFirstAttribute(inputHint), value))) { UserModel userByEmail = session.users().getUserByEmail(realm, value); - UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser(); // check for duplicated email if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) { context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS) diff --git a/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java b/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java index b2202e2ed781..954da121e7b1 100644 --- a/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java +++ b/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java @@ -27,6 +27,7 @@ import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils; import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator; import org.keycloak.protocol.oidc.utils.SubjectType; +import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.services.util.ResolveRelative; @@ -76,7 +77,52 @@ private enum FieldMessages { TOS_URI(ClientModel.TOS_URI, "Terms of service URL is not a valid URL", "tosURLInvalid", null, null, - "Terms of service URL uses an illegal scheme", "tosURLIllegalSchemeError"); + "Terms of service URL uses an illegal scheme", "tosURLIllegalSchemeError"), + + ADMIN_URL("masterSamlProcessingUrl", + "Master SAML Processing URL is not a valid URL", "adminUrlURLInvalid", + null, null, + "Master SAML Processing URL uses an illegal scheme", "adminUrlURLIllegalSchemeError"), + + SAML_ASSERTION_CONSUMER_URL_POST_URI(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, + "Assertion Consumer Service POST Binding URL is not a valid URL", "samlAssertionConsumerUrlPostURLInvalid", + null, null, + "Assertion Consumer Service POST Binding URL uses an illegal scheme", "samlAssertionConsumerUrlPostURLIllegalSchemeError"), + + SAML_ASSERTION_CONSUMER_URL_REDIRECT_URI(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, + "Assertion Consumer Service Redirect Binding URL is not a valid URL", "samlAssertionConsumerUrlRedirectURLInvalid", + null, null, + "Assertion Consumer Service Redirect Binding URL uses an illegal scheme", "samlAssertionConsumerUrlRedirectURLIllegalSchemeError"), + + SAML_ASSERTION_CONSUMER_URL_ARTIFACT_URI(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, + "Artifact Binding URL is not a valid URL", "samlAssertionConsumerUrlArtifactURLInvalid", + null, null, + "Artifact Binding URL uses an illegal scheme", "samlAssertionConsumerUrlArtifactURLIllegalSchemeError"), + + SAML_SINGLE_LOGOUT_SERVICE_URL_POST_URI(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, + "Logout Service POST Binding URL is not a valid URL", "samlLogoutServiceUrlPostURLInvalid", + null, null, + "Logout Service POST Binding URL uses an illegal scheme", "samlLogoutServiceUrlPostURLIllegalSchemeError"), + + SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_URI(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, + "Logout Service ARTIFACT Binding URL is not a valid URL", "samlLogoutServiceUrlArtifactURLInvalid", + null, null, + "Logout Service ARTIFACT Binding URL uses an illegal scheme", "samlLogoutServiceUrlArtifactURLIllegalSchemeError"), + + SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_URI(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, + "Logout Service Redirect Binding URL is not a valid URL", "samlLogoutServiceUrlRedirectURLInvalid", + null, null, + "Logout Service Redirect Binding URL uses an illegal scheme", "samlLogoutServiceUrlRedirectURLIllegalSchemeError"), + + SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_URI(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, + "Logout Service SOAP Binding URL is not a valid URL", "samlLogoutServiceUrlSoapURLInvalid", + null, null, + "Logout Service SOAP Binding URL uses an illegal scheme", "samlAssertionConsumerUrlPostURLIllegalSchemeError"), + + SAML_ARTIFACT_RESOLUTION_SERVICE_URL_URI(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, + "Artifact Resolution Service is not a valid URL", "samlAssertionConsumerUrlPostURLInvalid", + null, null, + "Artifact Resolution Service uses an illegal scheme", "samlAssertionConsumerUrlPostURLIllegalSchemeError"); private String fieldId; @@ -174,6 +220,19 @@ private void validateUrls(ValidationContext context) { checkUriLogo(FieldMessages.LOGO_URI, client.getAttribute(ClientModel.LOGO_URI), context); checkUri(FieldMessages.POLICY_URI, client.getAttribute(ClientModel.POLICY_URI), context, true, false); checkUri(FieldMessages.TOS_URI, client.getAttribute(ClientModel.TOS_URI), context, true, false); + + // extra validation URLs for SAML clients + if (SamlProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) { + checkUri(FieldMessages.ADMIN_URL, client.getManagementUrl(), context, true, false); + checkUri(FieldMessages.SAML_ASSERTION_CONSUMER_URL_POST_URI, client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_ASSERTION_CONSUMER_URL_REDIRECT_URI, client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_URI, client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_URI, client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_URI, client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_URI, client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_URI, client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE), context, true, false); + checkUri(FieldMessages.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_URI, client.getAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), context, true, false); + } } private void checkUri(FieldMessages field, String url, ValidationContext context, boolean checkValidUrl, boolean checkFragment) { diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory index ac3961633aa5..fb61a458049b 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -24,6 +24,7 @@ org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory org.keycloak.authentication.requiredactions.UpdateUserLocaleAction org.keycloak.authentication.requiredactions.DeleteAccount +org.keycloak.authentication.requiredactions.DeleteCredentialAction org.keycloak.authentication.requiredactions.VerifyUserProfile org.keycloak.authentication.requiredactions.RecoveryAuthnCodesAction org.keycloak.authentication.requiredactions.UpdateEmail diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 544200a95b0a..6508d71ae91f 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -45,4 +45,5 @@ org.keycloak.protocol.saml.mappers.SAMLAudienceProtocolMapper org.keycloak.protocol.saml.mappers.SAMLAudienceResolveProtocolMapper org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper -org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper \ No newline at end of file +org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper +org.keycloak.protocol.oidc.mappers.SessionStateMapper \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory index 0b5e3cffdece..9a9312fdeb7c 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory @@ -5,4 +5,5 @@ org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceHostsConditionFactory org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceGroupsConditionFactory org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesConditionFactory -org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory \ No newline at end of file +org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory +org.keycloak.services.clientpolicy.condition.ClientIdsConditionFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory index 4f0320c276b7..370e888c94b8 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory @@ -13,6 +13,7 @@ org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFactory org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSessionEnforceExecutorFactory org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutorFactory org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaAuthenticationRequestSigningAlgorithmExecutorFactory +org.keycloak.services.clientpolicy.executor.RegexRedirectUriExecutorFactory org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutorFactory org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory diff --git a/services/src/main/resources/org/keycloak/protocol/oidc/endpoints/login-status-iframe.html b/services/src/main/resources/org/keycloak/protocol/oidc/endpoints/login-status-iframe.html index ee640cffa0fa..73ac4bc95355 100755 --- a/services/src/main/resources/org/keycloak/protocol/oidc/endpoints/login-status-iframe.html +++ b/services/src/main/resources/org/keycloak/protocol/oidc/endpoints/login-status-iframe.html @@ -28,6 +28,7 @@ } let init; + let preventAdditionalRequests = false; async function checkState(clientId, origin, sessionState) { // Check if the browser has granted us access to 3rd-party storage (such as cookies). @@ -41,6 +42,13 @@ // If not initialized, verify this client is allowed access with a call to the server. if (!init) { + // Prevent additional requests to the server to avoid potential DoS attacks. + if (preventAdditionalRequests) { + return "error"; + } else { + preventAdditionalRequests = true; + } + const url = new URL(`${location.origin}${location.pathname}/init`); url.searchParams.set("client_id", clientId); diff --git a/services/src/test/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParserTest.java b/services/src/test/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParserTest.java new file mode 100644 index 000000000000..bdd90cf781d5 --- /dev/null +++ b/services/src/test/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParserTest.java @@ -0,0 +1,89 @@ +package org.keycloak.protocol.oidc.endpoints.request; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class AuthzEndpointRequestParserTest { + + @Mock + KeycloakSession keycloakSession; + + @Mock + RealmModel realmModel; + + @Mock + KeycloakContext keycloakContext; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Mockito.lenient().when(keycloakSession.getContext()).thenReturn(keycloakContext); + Mockito.lenient().when(keycloakContext.getRealm()).thenReturn(realmModel); + Mockito.lenient().when(realmModel.getAttribute("additionalReqParamsMaxNumber", 10)).thenReturn(10); + Mockito.lenient().when(realmModel.getAttribute("additionalReqParamsMaxSize", 2000)).thenReturn(2000); + Mockito.lenient().when(realmModel.getAttribute("additionalReqParamsFailFast", false)).thenReturn(false); + Mockito.lenient().when(realmModel.getAttribute("additionalReqParamsMaxOverallSize", Integer.MAX_VALUE)).thenReturn(Integer.MAX_VALUE); + } + + @Test + public void testExtractAdditionalReqParams() { + + TestAuthzEndpointRequestParser test = new TestAuthzEndpointRequestParser(keycloakSession); + + Map additionalReqParams = new HashMap<>(); + + test.extractAdditionalReqParams(additionalReqParams); + + assertNotNull(additionalReqParams); + assertEquals(1, additionalReqParams.size()); + assertTrue(additionalReqParams.containsKey("hash")); + + } + + + private class TestAuthzEndpointRequestParser extends AuthzEndpointRequestParser { + + protected TestAuthzEndpointRequestParser(KeycloakSession keycloakSession) { + super(keycloakSession); + } + + @Override + protected String getParameter(String paramName) { + return "VER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaMVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaMVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaMVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaMVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaMVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM%2CVER2CFYJFMliVLAFlYUUE9Vj4FkbIhHq1Ufe_VnWcsY%2C7P2xRG3WGTXogxuhXOfIrZsfL8lrnCE-G7E95chcwBw%2C9ba29mAU6sK4O9cBk22sGe9bK0Y5ilNycVVx0yij5SM%2Cw6HfN_o5gO7s39azUBWTyCBQiEbbihAGddwr9eUteFA%2CjYYPTQT4nz79AYMNOM67TcnDABQXOMWK0yJ_91EXwaM"; + } + + @Override + protected Integer getIntParameter(String paramName) { + return 10; + } + + @Override + protected Set keySet() { + + Set parameterSet = new HashSet<>(); + parameterSet.add("hash"); + parameterSet.add("customParameter1"); + parameterSet.add("customParameter2"); + parameterSet.add("client_id"); + + return parameterSet; + } + } + + +} diff --git a/services/src/test/java/org/keycloak/protocol/oidc/utils/RedirectUtilsTest.java b/services/src/test/java/org/keycloak/protocol/oidc/utils/RedirectUtilsTest.java index 814d29920690..fcc501934d3c 100644 --- a/services/src/test/java/org/keycloak/protocol/oidc/utils/RedirectUtilsTest.java +++ b/services/src/test/java/org/keycloak/protocol/oidc/utils/RedirectUtilsTest.java @@ -87,6 +87,8 @@ public void testverifyRedirectUriMixedSchemes() { Assert.assertEquals("custom1:/parent/child", RedirectUtils.verifyRedirectUri(session, null, "custom1:/parent/child", set, false)); Assert.assertEquals("custom2:/something", RedirectUtils.verifyRedirectUri(session, null, "custom2:/something", set, false)); Assert.assertEquals("https://keycloak.org/test", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test", set, false)); + Assert.assertEquals("https://keycloak.org/", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/", set, false)); + Assert.assertEquals("https://keycloak.org", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "custom1:/test", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "custom1:/test1/test", set, false)); @@ -172,6 +174,8 @@ public void testRelativeRedirectUri() { Assert.assertEquals("https://keycloak.org/path", RedirectUtils.verifyRedirectUri(session, "https://keycloak.org", "/path", set, false)); Assert.assertEquals("https://keycloak.org/path", RedirectUtils.verifyRedirectUri(session, "https://keycloak.org", "path", set, false)); + Assert.assertEquals("https://keycloak.org/test/../other", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/../other", set, false)); + Assert.assertEquals("http://keycloak.org/test%2Fother", RedirectUtils.verifyRedirectUri(session, null, "http://keycloak.org/test%2Fother", set, false)); } @Test @@ -184,8 +188,6 @@ public void testUserInfo() { Assert.assertEquals("https://keycloak.org/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/index.html", set, false)); Assert.assertEquals("https://test.com/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://test.com/index.html", set, false)); - Assert.assertEquals("https://something@keycloak.org/path", RedirectUtils.verifyRedirectUri(session, null, "https://something@keycloak.org/path", set, false)); - Assert.assertEquals("https://some%20thing@test.com/path", RedirectUtils.verifyRedirectUri(session, null, "https://some%20thing@test.com/path", set, false)); Assert.assertEquals("https://something@keycloak.com/exact", RedirectUtils.verifyRedirectUri(session, null, "https://something@keycloak.com/exact", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://something@other.com/", set, false)); @@ -193,12 +195,15 @@ public void testUserInfo() { Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org%2F@other.com", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://test@other.com", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://test.com@other.com", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://something@keycloak.org/path", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://some%20thing@test.com/path", set, false)); } @Test public void testEncodedRedirectUri() { Set set = Stream.of( - "https://keycloak.org/test/*" + "https://keycloak.org/test/*", + "https://keycloak.org/exact/%5C%2F/.." ).collect(Collectors.toSet()); Assert.assertEquals("https://keycloak.org/test/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/index.html", set, false)); @@ -206,16 +211,30 @@ public void testEncodedRedirectUri() { Assert.assertEquals("https://keycloak.org/test?encodeTest=a%3Cb#encode2=a%3Cb", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test?encodeTest=a%3Cb#encode2=a%3Cb", set, false)); Assert.assertEquals("https://keycloak.org/test/#encode2=a%3Cb", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/#encode2=a%3Cb", set, false)); - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/../", set, false)); // direct - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2E%2E/", set, false)); // encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2F%2E%2E%2F", set, false)); // encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%252E%252E/", set, false)); // double-encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%252E%252E/?some_query_param=some_value", set, false)); // double-encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%252E%252E/#encodeTest=a%3Cb", set, false)); // double-encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%25252E%25252E/", set, false)); // triple-encoded - Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2525252525252E%2525252525252E/", set, false)); // seventh-encoded + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/../", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test\\..\\", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2E%2E/", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2e%2e/", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2E./", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2E.", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test\\%2E.", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2f%2E%2e%2F", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%5C%2E.%5c", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%5C..", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2F%2E%2E%2Fdocumentation", set, false)); + Assert.assertEquals("https://keycloak.org/test/.../", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/.../", set, false)); + Assert.assertEquals("https://keycloak.org/test/%2E../", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%2E../", set, false)); // encoded + Assert.assertEquals("https://keycloak.org/test/some%2Fthing/", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/some%2Fthing/", set, false)); // encoded + Assert.assertEquals("https://keycloak.org/test/./", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/./", set, false)); + Assert.assertEquals("https://keycloak.org/test/%252E%252E/", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%252E%252E/", set, false)); // double-encoded + Assert.assertEquals("https://keycloak.org/test/%252E%252E/#encodeTest=a%3Cb", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%252E%252E/#encodeTest=a%3Cb", set, false)); // double-encoded + Assert.assertEquals("https://keycloak.org/test/%25252E%25252E/", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test/%25252E%25252E/", set, false)); // triple-encoded + Assert.assertEquals("https://keycloak.org/exact/%5C%2F/..", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/exact/%5C%2F/..", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak%2Eorg/test/", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org%2Ftest%2F%40sample.com", set, false)); + + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2Fanother/../any/path/", set, false)); + Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2Fanother/%2E%2E/any/path/", set, false)); } } diff --git a/services/src/test/java/org/keycloak/services/clientregistration/policies/impl/HostsTest.java b/services/src/test/java/org/keycloak/services/clientregistration/policy/impl/HostsTest.java similarity index 97% rename from services/src/test/java/org/keycloak/services/clientregistration/policies/impl/HostsTest.java rename to services/src/test/java/org/keycloak/services/clientregistration/policy/impl/HostsTest.java index 5ca4407f6b96..98699f7a3bfa 100644 --- a/services/src/test/java/org/keycloak/services/clientregistration/policies/impl/HostsTest.java +++ b/services/src/test/java/org/keycloak/services/clientregistration/policy/impl/HostsTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.services.clientregistration.policies.impl; +package org.keycloak.services.clientregistration.policy.impl; import java.net.InetAddress; diff --git a/services/src/test/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyTest.java b/services/src/test/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyTest.java new file mode 100644 index 000000000000..9c444137aba0 --- /dev/null +++ b/services/src/test/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.services.clientregistration.policy.impl; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.crypto.CryptoProvider; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.DefaultKeycloakSession; +import org.keycloak.services.DefaultKeycloakSessionFactory; +import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException; + +/** + * + * @author rmartinc + */ +public class TrustedHostClientRegistrationPolicyTest { + + private static KeycloakSession session; + + @BeforeClass + public static void beforeClass() { + Profile.defaults(); + CryptoIntegration.init(CryptoProvider.class.getClassLoader()); + DefaultKeycloakSessionFactory sessionFactory = new DefaultKeycloakSessionFactory(); + sessionFactory.init(); + session = new DefaultKeycloakSession(sessionFactory); + } + + @Test + public void testLocalhostName() { + TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory(); + ComponentModel model = createComponentModel("localhost"); + TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model); + + policy.verifyHost("127.0.0.1"); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.verifyHost("10.0.0.1")); + policy.checkURLTrusted("https://localhost", policy.getTrustedHosts(), policy.getTrustedDomains()); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.checkURLTrusted("https://otherhost", + policy.getTrustedHosts(), policy.getTrustedDomains())); + } + + @Test + public void testLocalhostDomain() { + TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory(); + ComponentModel model = createComponentModel("*.localhost"); + TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model); + + policy.verifyHost("127.0.0.1"); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.verifyHost("10.0.0.1")); + policy.checkURLTrusted("https://localhost", policy.getTrustedHosts(), policy.getTrustedDomains()); + policy.checkURLTrusted("https://other.localhost", policy.getTrustedHosts(), policy.getTrustedDomains()); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.checkURLTrusted("https://otherlocalhost", + policy.getTrustedHosts(), policy.getTrustedDomains())); + } + + @Test + public void testLocalhostIP() { + TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory(); + ComponentModel model = createComponentModel("127.0.0.1"); + TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model); + + policy.verifyHost("127.0.0.1"); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.verifyHost("10.0.0.1")); + policy.checkURLTrusted("https://127.0.0.1", policy.getTrustedHosts(), policy.getTrustedDomains()); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.checkURLTrusted("https://localhost", + policy.getTrustedHosts(), policy.getTrustedDomains())); + } + + @Test + public void testGoogleCrawlBot() { + // https://developers.google.com/search/blog/2006/09/how-to-verify-googlebot + TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory(); + ComponentModel model = createComponentModel("*.googlebot.com"); + TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model); + + policy.verifyHost("66.249.66.1"); + policy.checkURLTrusted("https://www.googlebot.com", policy.getTrustedHosts(), policy.getTrustedDomains()); + policy.checkURLTrusted("https://googlebot.com", policy.getTrustedHosts(), policy.getTrustedDomains()); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.checkURLTrusted("https://www.othergooglebot.com", + policy.getTrustedHosts(), policy.getTrustedDomains())); + } + + @Test + public void testGithubDomain() throws UnknownHostException { + TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory(); + ComponentModel model = createComponentModel("*.github.com"); + TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model); + + policy.verifyHost(InetAddress.getByName("www.github.com").getHostAddress()); + policy.verifyHost(InetAddress.getByName("github.com").getHostAddress()); + policy.checkURLTrusted("https://www.github.com", policy.getTrustedHosts(), policy.getTrustedDomains()); + policy.checkURLTrusted("https://github.com", policy.getTrustedHosts(), policy.getTrustedDomains()); + Assert.assertThrows(ClientRegistrationPolicyException.class, () -> policy.checkURLTrusted("https://othergithub.com", + policy.getTrustedHosts(), policy.getTrustedDomains())); + } + + private ComponentModel createComponentModel(String... hosts) { + ComponentModel model = new ComponentModel(); + model.put(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "true"); + model.put(TrustedHostClientRegistrationPolicyFactory.CLIENT_URIS_MUST_MATCH, "true"); + model.getConfig().addAll(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, hosts); + return model; + } +} diff --git a/set-version.sh b/set-version.sh index 21886a202163..28443058ed1b 100755 --- a/set-version.sh +++ b/set-version.sh @@ -33,3 +33,14 @@ echo "$(jq '. += {"version": "'$NEW_NPM_VERSION'"}' js/libs/keycloak-admin-clien echo "New Mvn Version: $NEW_VERSION" >&2 echo "New NPM Version: $NEW_NPM_VERSION" >&2 + +# js-ci.yml remove 999 +sed -i -E 's/keycloak-[0-9]+\.[0-9]+\.[0-9]+(-PS-[0-9]+){0,1}(-SNAPSHOT){0,1}\.tar.gz/keycloak-'$NEW_VERSION'.tar.gz/g' .github/workflows/js-ci.yml +sed -i -E 's/keycloak-[0-9]+\.[0-9]+\.[0-9]+(-PS-[0-9]+){0,1}(-SNAPSHOT){0,1}/keycloak-'$NEW_VERSION'/g' .github/workflows/js-ci.yml + + +cat < release-details +VERSION=$NEW_VERSION +SHORT_VERSION=$NEW_VERSION +NPM_VERSION=$NEW_VERSION +EOT diff --git a/testsuite/db-allocator-plugin/pom.xml b/testsuite/db-allocator-plugin/pom.xml index c82daf8462b7..626960aa01ed 100644 --- a/testsuite/db-allocator-plugin/pom.xml +++ b/testsuite/db-allocator-plugin/pom.xml @@ -22,7 +22,7 @@ keycloak-testsuite-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index 3ec43f7bb981..c0e5fca2365e 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -22,7 +22,7 @@ org.keycloak keycloak-testsuite-pom - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml @@ -85,6 +85,13 @@ + + org.keycloak + keycloak-parent + ${project.version} + pom + import + @@ -229,6 +236,14 @@ com.google.guava guava + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + diff --git a/testsuite/integration-arquillian/servers/adapter-spi/pom.xml b/testsuite/integration-arquillian/servers/adapter-spi/pom.xml index 7f641d873404..7a2b57bc8c77 100644 --- a/testsuite/integration-arquillian/servers/adapter-spi/pom.xml +++ b/testsuite/integration-arquillian/servers/adapter-spi/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 pom diff --git a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-jakarta/pom.xml b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-jakarta/pom.xml index 7539a2caf5a0..bc4674f6d5eb 100644 --- a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-jakarta/pom.xml +++ b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-jakarta/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-adapter-spi org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-saml-jakarta/pom.xml b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-saml-jakarta/pom.xml index 27455823417e..df7683a7ac40 100644 --- a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-saml-jakarta/pom.xml +++ b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-saml-jakarta/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-adapter-spi org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-spi-jakarta/pom.xml b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-spi-jakarta/pom.xml index 3fe965d90478..43233b630a2c 100644 --- a/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-spi-jakarta/pom.xml +++ b/testsuite/integration-arquillian/servers/adapter-spi/undertow-adapter-spi-jakarta/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-adapter-spi org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml b/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml index a00eae01d9ec..0c3d6650383b 100644 --- a/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml index 34afca828528..10982bc6f9b7 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml index e5bbcdd78b13..79272ae46869 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/galleon/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/galleon/pom.xml index 50ab5e2be1b6..f9cf3748c864 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/galleon/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/galleon/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml index 30fa7cb29d50..a996a6eadd12 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml @@ -22,7 +22,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml index 4932980a169d..954601335b26 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jetty/94/pom.xml b/testsuite/integration-arquillian/servers/app-server/jetty/94/pom.xml index 14fc2f005fa7..3a0daafbc2de 100644 --- a/testsuite/integration-arquillian/servers/app-server/jetty/94/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jetty/94/pom.xml @@ -18,7 +18,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jetty - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jetty/common/pom.xml b/testsuite/integration-arquillian/servers/app-server/jetty/common/pom.xml index ffb12cebace8..b10a9c9d0f17 100644 --- a/testsuite/integration-arquillian/servers/app-server/jetty/common/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jetty/common/pom.xml @@ -18,7 +18,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jetty - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jetty/pom.xml b/testsuite/integration-arquillian/servers/app-server/jetty/pom.xml index 1c938b754ea7..a43cf381e473 100644 --- a/testsuite/integration-arquillian/servers/app-server/jetty/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jetty/pom.xml @@ -18,7 +18,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml index b57b22e505b3..df360634a3dc 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/pom.xml index 7b2e45786b9e..7a0e3517415c 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/src/main/resources/product-portal-keycloak.json b/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/src/main/resources/product-portal-keycloak.json new file mode 100644 index 000000000000..0a28d8c5c4d2 --- /dev/null +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse7x/src/main/resources/product-portal-keycloak.json @@ -0,0 +1,10 @@ + +{ + "realm": "demo", + "resource": "product-portal", + "auth-server-url": "http://localhost:8080/auth", + "ssl-required" : "external", + "credentials": { + "secret": "password" + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml index fc156da61e25..0c843bb73220 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 @@ -119,6 +119,7 @@ keycloak-direct-access.json keycloak-hawtio-client.json keycloak-hawtio.json + product-portal-keycloak.json diff --git a/testsuite/integration-arquillian/servers/app-server/pom.xml b/testsuite/integration-arquillian/servers/app-server/pom.xml index 3ee99ab0d9cc..9b21c3cde54a 100644 --- a/testsuite/integration-arquillian/servers/app-server/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/common/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/common/pom.xml index a0019ea56da5..3c4b4918d58f 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/common/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/common/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-app-server-tomcat org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml index ceb6f924d843..39a627459510 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml index ebf2b1cf6bef..6161639d5cf8 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-tomcat - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml index cbb1c5b0bccf..2ea7daa7e350 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-tomcat - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/app-server/undertow/pom.xml index 1f708815e3f4..47a51b23c8ba 100644 --- a/testsuite/integration-arquillian/servers/app-server/undertow/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/undertow/pom.xml @@ -18,7 +18,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/pom.xml b/testsuite/integration-arquillian/servers/auth-server/pom.xml index a5ed91a6c734..1f1bea543b92 100644 --- a/testsuite/integration-arquillian/servers/auth-server/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml b/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml index 6bbd8ab3e41e..897e2753b669 100644 --- a/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-auth-server org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml index a096973d211c..2f44ef6dc36f 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers-deployment/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers-deployment/pom.xml index 20063ee9442d..966e1b125f8a 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers-deployment/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers-deployment/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server-services - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-testsuite-providers-deployment diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml index 0d0277216cc4..02c8296927ee 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server-services - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-testsuite-providers diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/CustomAuthenticationFlowCallback.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/CustomAuthenticationFlowCallback.java index b0a2173e576d..82bbb63d4586 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/CustomAuthenticationFlowCallback.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/CustomAuthenticationFlowCallback.java @@ -21,6 +21,7 @@ import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -33,7 +34,7 @@ public class CustomAuthenticationFlowCallback implements AuthenticationFlowCallb public static final String EXPECTED_ERROR_MESSAGE = "Custom Authentication Flow Callback message"; @Override - public void onTopFlowSuccess() { + public void onTopFlowSuccess(AuthenticationFlowModel topFlow) { throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, "detail", EXPECTED_ERROR_MESSAGE); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java index 5fc050ca3641..c52b047c08db 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java @@ -59,7 +59,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { private final BlockingQueue adminLogoutActions; private final BlockingQueue frontChannelLogoutTokens; - private final BlockingQueue backChannelLogoutTokens; + private final BlockingQueue backChannelLogoutTokens; private final BlockingQueue adminPushNotBeforeActions; private final BlockingQueue adminTestAvailabilityAction; private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData; @@ -71,7 +71,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { private final HttpRequest request; public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue adminLogoutActions, - BlockingQueue backChannelLogoutTokens, + BlockingQueue backChannelLogoutTokens, BlockingQueue frontChannelLogoutTokens, BlockingQueue adminPushNotBeforeActions, BlockingQueue adminTestAvailabilityAction, @@ -102,8 +102,8 @@ public void adminLogout(String data) throws JWSInputException { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Path("/admin/backchannelLogout") - public void backchannelLogout() throws JWSInputException { - backChannelLogoutTokens.add(new JWSInput(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)).readJsonContent(LogoutToken.class)); + public void backchannelLogout() { + backChannelLogoutTokens.add(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)); } @GET @@ -139,7 +139,14 @@ public LogoutAction getAdminLogoutAction() throws InterruptedException { @GET @Produces(MediaType.APPLICATION_JSON) @Path("/poll-backchannel-logout") - public LogoutToken getBackChannelLogoutAction() throws InterruptedException { + public LogoutToken getBackChannelLogoutAction() throws InterruptedException, JWSInputException { + return new JWSInput(backChannelLogoutTokens.poll(20, TimeUnit.SECONDS)).readJsonContent(LogoutToken.class); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/poll-backchannel-raw-logout") + public String getBackChanneRawlLogoutAction() throws InterruptedException { return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java index e15772ad90cd..a1a8db9c0858 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java @@ -45,7 +45,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProviderFactory { private BlockingQueue adminLogoutActions = new LinkedBlockingDeque<>(); - private BlockingQueue backChannelLogoutTokens = new LinkedBlockingDeque<>(); + private BlockingQueue backChannelLogoutTokens = new LinkedBlockingDeque<>(); private BlockingQueue frontChannelLogoutTokens = new LinkedBlockingDeque<>(); private BlockingQueue pushNotBeforeActions = new LinkedBlockingDeque<>(); private BlockingQueue testAvailabilityActions = new LinkedBlockingDeque<>(); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java index b1adb22bcbf0..e9a62b15841c 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java @@ -187,6 +187,14 @@ public static ComponentModel getLdapProviderModel(RealmModel realm) { .orElse(null); } + public static ComponentModel getLdapProviderModel(RealmModel realm, String providerName) { + return realm.getComponentsStream(realm.getId(), UserStorageProvider.class.getName()) + .filter(component -> Objects.equals(component.getProviderId(), LDAPStorageProviderFactory.PROVIDER_NAME)) + .filter(component -> providerName == null || component.getName().equals(providerName)) + .findFirst() + .orElse(null); + } + public static LDAPStorageProvider getLdapProvider(KeycloakSession keycloakSession, ComponentModel ldapFedModel) { return (LDAPStorageProvider)keycloakSession.getProvider(UserStorageProvider.class, ldapFedModel); } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml index cdfe83d80212..f50e1df51c00 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/infinispan/datagrid/pom.xml b/testsuite/integration-arquillian/servers/cache-server/infinispan/datagrid/pom.xml index 39ee8e345f63..553c0f189b3b 100644 --- a/testsuite/integration-arquillian/servers/cache-server/infinispan/datagrid/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/infinispan/datagrid/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-infinispan - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/infinispan/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/infinispan/infinispan/pom.xml index 493512a20957..0ebbcc317131 100644 --- a/testsuite/integration-arquillian/servers/cache-server/infinispan/infinispan/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/infinispan/infinispan/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-infinispan - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml index 80b68a9d8278..8498b5f697de 100644 --- a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 pom diff --git a/testsuite/integration-arquillian/servers/cache-server/legacy/datagrid/pom.xml b/testsuite/integration-arquillian/servers/cache-server/legacy/datagrid/pom.xml index 56494399a8e5..a822438c685f 100644 --- a/testsuite/integration-arquillian/servers/cache-server/legacy/datagrid/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/legacy/datagrid/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-legacy - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/legacy/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/legacy/infinispan/pom.xml index cb0f8056bfb1..55592fdaf7e5 100644 --- a/testsuite/integration-arquillian/servers/cache-server/legacy/infinispan/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/legacy/infinispan/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-legacy - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/legacy/pom.xml b/testsuite/integration-arquillian/servers/cache-server/legacy/pom.xml index d5f7c46fe695..3b90020ff397 100644 --- a/testsuite/integration-arquillian/servers/cache-server/legacy/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/legacy/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/pom.xml b/testsuite/integration-arquillian/servers/cache-server/pom.xml index 22779a9af5e2..44665453e7cf 100644 --- a/testsuite/integration-arquillian/servers/cache-server/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/migration/pom.xml b/testsuite/integration-arquillian/servers/migration/pom.xml index 94cf6e625c65..d71fbd53c07b 100644 --- a/testsuite/integration-arquillian/servers/migration/pom.xml +++ b/testsuite/integration-arquillian/servers/migration/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/servers/pom.xml b/testsuite/integration-arquillian/servers/pom.xml index 70c336cdd481..7ede4cc5515c 100644 --- a/testsuite/integration-arquillian/servers/pom.xml +++ b/testsuite/integration-arquillian/servers/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml index d25c47c91ca4..0e3188904c34 100644 --- a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml +++ b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 keycloak-test-app-profile-jee diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml index ef95d30e4f3e..ab7f6d923e20 100755 --- a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-cors-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml index e6bb20846a94..faa6fc2b4513 100755 --- a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-cors-parent - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/cors/pom.xml b/testsuite/integration-arquillian/test-apps/cors/pom.xml index ce8f9da676d2..667c5a1a61bc 100644 --- a/testsuite/integration-arquillian/test-apps/cors/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/camel-fuse7-undertow/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/camel-fuse7-undertow/pom.xml index 201c50a85380..9fa6374fedff 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/camel-fuse7-undertow/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/camel-fuse7-undertow/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 @@ -38,7 +38,6 @@ javax.servlet;version="[3.1,5)", javax.servlet.http;version="[3.1,5)", javax.net.ssl, - org.apache.camel.*, io.undertow.*;version="[1.4,3)", org.apache.camel;version="[2.13,3)", org.keycloak.*;version="${fuse.adapter.version}", diff --git a/testsuite/integration-arquillian/test-apps/fuse/camel/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/camel/pom.xml index c55d5b01db2b..2f2514f8bde2 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/camel/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/camel/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/customer-app-fuse/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/customer-app-fuse/pom.xml index a7d0ad92fcb7..1908c4954c6a 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/customer-app-fuse/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/customer-app-fuse/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 @@ -113,8 +113,8 @@ war - customer-portal - customer-portal + /customer-portal + /customer-portal WEB-INF/lib .,WEB-INF/classes ${project.name} diff --git a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/pom.xml index 6ae6300c2e72..42c7a4de22f7 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 @@ -63,6 +63,12 @@ cxf-rt-transports-http-undertow ${cxf.undertow.version} + + javax.ws.rs + javax.ws.rs-api + 2.1 + compile + diff --git a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/src/main/java/org/keycloak/example/rs/CxfCustomerService.java b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/src/main/java/org/keycloak/example/rs/CxfCustomerService.java index f737bc42463c..184391fe0d6b 100644 --- a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/src/main/java/org/keycloak/example/rs/CxfCustomerService.java +++ b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs-fuse7-undertow/src/main/java/org/keycloak/example/rs/CxfCustomerService.java @@ -17,9 +17,9 @@ package org.keycloak.example.rs; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; import java.util.ArrayList; import java.util.List; diff --git a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs/pom.xml index 2ab0bcf48be1..ef5eae77e9f8 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxrs/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws-fuse7-undertow/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws-fuse7-undertow/pom.xml index 714b0d3f383b..8f6782bdd193 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws-fuse7-undertow/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws-fuse7-undertow/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws/pom.xml index 67547eb4295e..1fdf85e19e44 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/cxf-jaxws/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/demorealm.json b/testsuite/integration-arquillian/test-apps/fuse/demorealm.json index 1f609e60f2cc..d63cb39663ea 100644 --- a/testsuite/integration-arquillian/test-apps/fuse/demorealm.json +++ b/testsuite/integration-arquillian/test-apps/fuse/demorealm.json @@ -198,7 +198,10 @@ "redirectUris": [ "http://localhost:8181/customer-portal/*" ], - "secret": "password" + "secret": "password", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "product-portal", @@ -208,7 +211,10 @@ "redirectUris": [ "http://localhost:8181/product-portal/*" ], - "secret": "password" + "secret": "password", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "builtin-cxf-app", @@ -218,28 +224,40 @@ "redirectUris": [ "http://localhost:8181/cxf/*" ], - "secret": "password" + "secret": "password", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "custom-cxf-endpoint", "enabled": true, "adminUrl": "http://localhost:8282/PersonServiceCF", "baseUrl": "http://localhost:8282/PersonServiceCF", - "bearerOnly": true + "bearerOnly": true, + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "admin-camel-endpoint", "enabled": true, "adminUrl": "http://localhost:8383/admin-camel-endpoint", "baseUrl": "http://localhost:8383/admin-camel-endpoint", - "bearerOnly": true + "bearerOnly": true, + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "admin-camel-restdsl", "enabled": true, "adminUrl": "http://localhost:8484/restdsl", "baseUrl": "http://localhost:8484/restdsl", - "bearerOnly": true + "bearerOnly": true, + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "ssh-jmx-admin-client", @@ -247,7 +265,10 @@ "publicClient": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": true, - "secret": "password" + "secret": "password", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId": "external-config", @@ -258,7 +279,10 @@ "http://localhost:8181/external-config", "http://localhost:8181/external-config/*" ], - "secret": "password" + "secret": "password", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } }, { "clientId" : "hawtio-client", @@ -269,7 +293,10 @@ "webOrigins" : [ "http://localhost:8080", "http://localhost:8181", "http://localhost:8081" ], "bearerOnly" : false, "publicClient" : true, - "protocol" : "openid-connect" + "protocol" : "openid-connect", + "attributes": { + "exclude.issuer.from.auth.response": "true" + } } ], diff --git a/testsuite/integration-arquillian/test-apps/fuse/external-config/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/external-config/pom.xml index eb970fb64ea9..72da7ac6c39f 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/external-config/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/external-config/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Keycloak Examples - External Config @@ -116,8 +116,8 @@ war - external-config - external-config + /external-config + /external-config WEB-INF/lib ${project.name} ${project.groupId}.${project.artifactId} diff --git a/testsuite/integration-arquillian/test-apps/fuse/features/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/features/pom.xml index f5efd8bad550..587e75782c83 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/features/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/features/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/features/src/main/resources/features.xml b/testsuite/integration-arquillian/test-apps/fuse/features/src/main/resources/features.xml index 4d1109c41ca2..7ff60fad40e8 100644 --- a/testsuite/integration-arquillian/test-apps/fuse/features/src/main/resources/features.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/features/src/main/resources/features.xml @@ -20,7 +20,7 @@

    The Keycloak / Fuse 7.0 on Undertow example
    - pax-http-undertow + pax-web-http-undertow war camel camel-undertow diff --git a/testsuite/integration-arquillian/test-apps/fuse/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/pom.xml index 2989cc4b7980..2ea0d335f235 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/pom.xml @@ -20,7 +20,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 Fuse Test Applications @@ -32,7 +32,7 @@ pom 2.21.2 - 18.0.7 + 18.0.12 customer-app-fuse diff --git a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse/pom.xml index 8e85c189e528..ff127a4d34e6 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/pom.xml b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/pom.xml index d35dfd83c2a5..8cf4e1de02d7 100755 --- a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/pom.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/pom.xml @@ -21,7 +21,7 @@ integration-arquillian-test-apps-fuse-parent org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/java/org/keycloak/example/ProductPortalServlet.java b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/java/org/keycloak/example/ProductPortalServlet.java index ee023fa88cfd..b3084a2c5307 100644 --- a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/java/org/keycloak/example/ProductPortalServlet.java +++ b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/java/org/keycloak/example/ProductPortalServlet.java @@ -55,7 +55,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se out.println("Product Portal Page"); String logoutUri = KeycloakUriBuilder.fromUri("http://localhost:8080/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH) - .queryParam("redirect_uri", "http://localhost:8181/product-portal").build("demo").toString(); + .build("demo").toString(); String acctUri = KeycloakUriBuilder.fromUri("http://localhost:8080/auth").path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH) .queryParam("referrer", "product-portal").build("demo").toString(); diff --git a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 2166764892f4..8fc629fd289b 100644 --- a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -42,12 +42,25 @@ - + + + + + + + + /product-portal/* + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/keycloak.json b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/keycloak.json new file mode 100644 index 000000000000..0808feafef6c --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/fuse/product-app-fuse7-undertow/src/main/resources/keycloak.json @@ -0,0 +1,9 @@ +{ + "realm": "demo", + "resource": "product-portal", + "auth-server-url": "http://localhost:8080/auth", + "ssl-required" : "external", + "credentials": { + "secret": "password" + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml index 6fe8f7897bb4..e778184efb72 100755 --- a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml +++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 hello-world-authz-service diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml index 320a3133d23d..e1f71bbaf67a 100644 --- a/testsuite/integration-arquillian/test-apps/pom.xml +++ b/testsuite/integration-arquillian/test-apps/pom.xml @@ -5,7 +5,7 @@ integration-arquillian org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml index a2b12820e213..c244e5743ae3 100755 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 servlet-authz-app diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml index a5fb0fe51153..7ad5c4668231 100755 --- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 servlet-policy-enforcer diff --git a/testsuite/integration-arquillian/test-apps/servlets-jakarta/pom.xml b/testsuite/integration-arquillian/test-apps/servlets-jakarta/pom.xml index ff0e3f1d8c4a..a5e767847869 100644 --- a/testsuite/integration-arquillian/test-apps/servlets-jakarta/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlets-jakarta/pom.xml @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-test-apps-servlets-jakarta diff --git a/testsuite/integration-arquillian/test-apps/servlets/pom.xml b/testsuite/integration-arquillian/test-apps/servlets/pom.xml index 88e482771d6a..d1f6e0ee9223 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlets/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter-app/pom.xml b/testsuite/integration-arquillian/test-apps/spring-boot-adapter-app/pom.xml index 08ac13d183d6..8bbfe25843ab 100644 --- a/testsuite/integration-arquillian/test-apps/spring-boot-adapter-app/pom.xml +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter-app/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml index e937de7a6752..660ca20e0200 100644 --- a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml +++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 2b0db696463c..4fab313a7373 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-tests - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/account/AccountRestClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/account/AccountRestClient.java new file mode 100644 index 000000000000..93b27e3e884a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/account/AccountRestClient.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.account; + +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.representations.account.CredentialMetadataRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.resources.account.AccountCredentialResource; +import org.keycloak.testsuite.arquillian.SuiteContext; +import org.keycloak.testsuite.util.TokenUtil; + +/** + * Helper client for account REST API + * + * @author Marek Posolda + */ +public class AccountRestClient implements AutoCloseable { + + private final SuiteContext suiteContext; + private final CloseableHttpClient httpClient; + private final Supplier tokenProvider; + private final String apiVersion; + private final String realmName; + + private AccountRestClient(SuiteContext suiteContext, CloseableHttpClient httpClient, Supplier tokenProvider, String apiVersion, String realmName) { + this.suiteContext = suiteContext; + this.httpClient = httpClient; + this.tokenProvider = tokenProvider; + this.apiVersion = apiVersion; + this.realmName = realmName; + } + + public List getCredentials() { + try { + return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient) + .auth(tokenProvider.get()).asJson(new TypeReference>() { + }); + } catch (IOException ioe) { + throw new RuntimeException("Failed to get credentials", ioe); + } + } + + public CredentialRepresentation getCredentialByUserLabel(String userLabel) { + return getCredentials().stream() + .flatMap(credentialContainer -> credentialContainer.getUserCredentialMetadatas().stream()) + .map(CredentialMetadataRepresentation::getCredential) + .filter(credentialRep -> userLabel.equals(credentialRep.getUserLabel())) + .findFirst() + .orElse(null); + } + + public SimpleHttp.Response removeCredential(String credentialId) { + try { + return SimpleHttp + .doDelete(getAccountUrl("credentials/" + credentialId), httpClient) + .acceptJson() + .auth(tokenProvider.get()) + .asResponse(); + } catch (IOException ioe) { + throw new RuntimeException("Failed to delete credential", ioe); + } + } + + // TODO: Other objects... + + + @Override + public void close() { + if (httpClient != null) { + try { + httpClient.close(); + } catch (IOException ioe) { + throw new RuntimeException("Error closing httpClient", ioe); + } + } + } + + + private String getAccountUrl(String resource) { + String url = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + realmName + "/account"; + if (apiVersion != null) { + url += "/" + apiVersion; + } + if (resource != null) { + url += "/" + resource; + } + return url; + } + + public static AccountRestClientBuilder builder(SuiteContext suiteContext) { + return new AccountRestClientBuilder(suiteContext); + } + + + public static class AccountRestClientBuilder { + + private SuiteContext suiteContext; + private CloseableHttpClient httpClient; + private Supplier tokenProvider; + private String apiVersion; + private String realmName; + + private AccountRestClientBuilder(SuiteContext suiteContext) { + this.suiteContext = suiteContext; + } + + public AccountRestClientBuilder httpClient(CloseableHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + public AccountRestClientBuilder tokenUtil(TokenUtil tokenUtil) { + this.tokenProvider = tokenUtil::getToken; + return this; + } + + public AccountRestClientBuilder accessToken(String accessToken) { + this.tokenProvider = () -> accessToken; + return this; + } + + public AccountRestClientBuilder apiVersion(String apiVersion) { + this.apiVersion = apiVersion; + return this; + } + + public AccountRestClientBuilder realmName(String realmName) { + this.realmName = realmName; + return this; + } + + public AccountRestClient build() { + if (httpClient == null) { + httpClient = HttpClientBuilder.create().build(); + } + if (realmName == null) { + realmName = "test"; + } + if (tokenProvider == null) { + TokenUtil tokenUtil = new TokenUtil(); + tokenProvider = tokenUtil::getToken; + } + return new AccountRestClient(suiteContext, httpClient, tokenProvider, apiVersion, realmName); + } + + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java index c0d4d3a59484..698741083eb0 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java @@ -45,6 +45,11 @@ public interface TestApplicationResource { @Path("/poll-backchannel-logout") LogoutToken getBackChannelLogoutToken(); + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/poll-backchannel-raw-logout") + String getBackChannelRawLogoutToken(); + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/poll-frontchannel-logout") diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/DeleteCredentialPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/DeleteCredentialPage.java new file mode 100644 index 000000000000..1bbf2fe72ba1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/DeleteCredentialPage.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.pages; + +import org.junit.Assert; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Marek Posolda + */ +public class DeleteCredentialPage extends AbstractPage { + + @FindBy(id = "kc-accept") + private WebElement submitButton; + + @FindBy(id = "kc-decline") + private WebElement cancelButton; + + @FindBy(id = "kc-delete-text") + private WebElement message; + + public boolean isCurrent() { + return PageUtils.getPageTitle(driver).startsWith("Delete "); + } + + public void confirm() { + submitButton.click(); + } + public void cancel() { + cancelButton.click(); + } + + public void assertCredentialInMessage(String expectedLabel) { + Assert.assertEquals("Do you want to delete " + expectedLabel + "?", message.getText()); + } + + @Override + public void open() { + throw new UnsupportedOperationException(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java index bb5d87e3621b..4d0d77c0aa6a 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java @@ -68,6 +68,11 @@ public ClientAttributeUpdater setClientId(String clientId) { return this; } + public ClientAttributeUpdater setName(String name) { + this.rep.setName(name); + return this; + } + public ClientAttributeUpdater setAttribute(String name, String value) { this.rep.getAttributes().put(name, value); if (value != null && !this.origRep.getAttributes().containsKey(name)) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java index 56ad1317501a..505a7d5432a1 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java @@ -169,4 +169,9 @@ public RealmAttributeUpdater setSmtpServer(String name, String value) { rep.getSmtpServer().put(name, value); return this; } + + public RealmAttributeUpdater setBrowserSecurityHeader(String name, String value) { + rep.getBrowserSecurityHeaders().put(name, value); + return this; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 501cd2414f9e..5b74459a4989 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -1198,10 +1198,10 @@ public ParResponse doPushedAuthorizationRequest(String clientId, String clientSe parameters.add(new BasicNameValuePair(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode)); } if (clientId != null && clientSecret != null) { - String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); - post.setHeader("Authorization", authorization); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, clientSecret)); } - if (clientId != null) { + else if (clientId != null) { parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId)); } if (redirectUri != null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 2d5c53147689..1fbc41c99e40 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -44,6 +44,7 @@ import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.account.ClientRepresentation; import org.keycloak.representations.account.ConsentRepresentation; import org.keycloak.representations.account.ConsentScopeRepresentation; @@ -789,6 +790,79 @@ public void testCRUDCredentialOfDifferentUser() throws IOException { Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel())); } + @Test + public void testRemoveCredentialWithNonOtpCredentialTriggeringNoEvent() throws IOException { + + List credentials = getCredentials(); + + UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost"); + assertEquals(1, user.credentials().size()); + + // Add non-OTP credential to the user through admin REST API + CredentialRepresentation nonOtpCredential = ModelToRepresentation.toRepresentation( + WebAuthnCredentialModel.create(WebAuthnCredentialModel.TYPE_TWOFACTOR, "foo", "foo", "foo", "foo", "foo", 2L, "foo")); + org.keycloak.representations.idm.UserRepresentation userRep = UserBuilder.edit(user.toRepresentation()) + .secret(nonOtpCredential) + .build(); + user.update(userRep); + + credentials = getCredentials(); + Assert.assertEquals(2, credentials.size()); + Assert.assertTrue(credentials.get(1).isRemoveable()); + + // Remove credential + CredentialRepresentation credential = user.credentials().stream() + .filter(credentialRep -> WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(credentialRep.getType())) + .findFirst() + .get(); + Assert.assertNotNull(credential); + user.removeCredential(credential.getId()); + + events.poll(); + events.assertEmpty(); + } + + @Test + public void testRemoveCredentialWithOtpCredentialTriggeringEvent() throws IOException { + + List credentials = getCredentials(); + + UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost"); + assertEquals(1, user.credentials().size()); + + // Add OTP credential to the user through admin REST API + org.keycloak.representations.idm.UserRepresentation userRep = UserBuilder.edit(user.toRepresentation()) + .totpSecret("totpSecret") + .build(); + userRep.getCredentials().get(0).setUserLabel("totpCredentialUserLabel"); + user.update(userRep); + + credentials = getCredentials(); + Assert.assertEquals(2, credentials.size()); + Assert.assertTrue(credentials.get(1).isRemoveable()); + + // Remove credential + CredentialRepresentation otpCredential = user.credentials().stream() + .filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType())) + .findFirst() + .get(); + SimpleHttp.Response response = SimpleHttp + .doDelete(getAccountUrl("credentials/" + otpCredential.getId()), httpClient) + .acceptJson() + .auth(tokenUtil.getToken()) + .asResponse(); + assertEquals(204, response.getStatus()); + + events.poll(); + events.expect(EventType.REMOVE_TOTP) + .client("account") + .user(user.toRepresentation().getId()) + .detail(Details.SELECTED_CREDENTIAL_ID, otpCredential.getId()) + .detail(Details.CREDENTIAL_USER_LABEL, "totpCredentialUserLabel") + .assertEvent(); + events.assertEmpty(); + } + // Send REST request to get all credential containers and credentials of current user private List getCredentials() throws IOException { return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient) @@ -914,7 +988,7 @@ public void testCredentialsForUserWithoutPassword() throws IOException { .auth(tokenUtil.getToken()) .asResponse()) { assertEquals(400, response.getStatus()); - Assert.assertEquals("Credential type password cannot be removed", response.asJson(OAuth2ErrorRepresentation.class).getError()); + Assert.assertEquals("Credential type cannot be removed", response.asJson(OAuth2ErrorRepresentation.class).getError()); } // Remove password from the user now @@ -1688,7 +1762,7 @@ public void testCustomAccountResourceTheme() throws Exception { testRealm().update(realmRep); } } - + @EnableFeature(Profile.Feature.UPDATE_EMAIL) public void testEmailWhenUpdateEmailEnabled() throws Exception { reconnectAdminClient(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java new file mode 100644 index 000000000000..e1877990d341 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java @@ -0,0 +1,267 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.actions; + +import java.util.List; + +import jakarta.ws.rs.core.Response; +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.authentication.requiredactions.DeleteCredentialAction; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.models.utils.TimeBasedOTP; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.DeleteCredentialPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginConfigTotpPage; +import org.keycloak.testsuite.pages.LoginTotpPage; +import org.keycloak.testsuite.util.UserBuilder; + +/** + * @author Marek Posolda + */ +public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiatedActionTest { + + @Override + protected String getAiaAction() { + return DeleteCredentialAction.PROVIDER_ID; + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.setResetPasswordAllowed(Boolean.TRUE); + } + + @Page + protected LoginTotpPage loginTotpPage; + + @Page + protected LoginConfigTotpPage totpPage; + + @Page + protected DeleteCredentialPage deleteCredentialPage; + + @Page + protected ErrorPage errorPage; + + protected TimeBasedOTP totp = new TimeBasedOTP(); + + private String userId; + + @Before + public void beforeTest() { + UserRepresentation user = UserBuilder.create() + .username("john") + .email("john@email.cz") + .firstName("John") + .lastName("Bar") + .enabled(true) + .password("password") + .totpSecret("mySecret").build(); + Response response = testRealm().users().create(user); + userId = ApiUtil.getCreatedId(response); + response.close(); + getCleanup().addUserId(userId); + } + + @Test + public void removeTotpSuccess() throws Exception { + String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); + oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); + + loginPasswordAndOtp(); + + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage(OTPCredentialModel.TYPE); + + deleteCredentialPage.confirm(); + + appPage.assertCurrent(); + assertKcActionStatus("success"); + + Assert.assertNull(getCredentialIdByType(OTPCredentialModel.TYPE)); + + events.expect(EventType.REMOVE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); + } + + @Test + public void removeTotpCancel() throws Exception { + String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); + + loginPasswordAndOtp(); + + appPage.assertCurrent(); + events.clear(); + + oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); + oauth.openLoginForm(); + + // Cancel on the confirmation page + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage(OTPCredentialModel.TYPE); + deleteCredentialPage.cancel(); + + appPage.assertCurrent(); + + Assert.assertNotNull(getCredentialIdByType(OTPCredentialModel.TYPE)); + } + + @Test + public void removePasswordShouldFail() throws Exception { + String credentialId = getCredentialIdByType(PasswordCredentialModel.TYPE); + loginPasswordAndOtp(); + + appPage.assertCurrent(); + events.clear(); + + oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); + oauth.openLoginForm(); + + // Cancel on the confirmation page + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage(PasswordCredentialModel.TYPE); + deleteCredentialPage.confirm(); + + errorPage.assertCurrent(); + + events.expect(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .detail(Details.REASON, "Credential type cannot be removed") + .error(Errors.DELETE_CREDENTIAL_FAILED) + .assertEvent(); + } + + @Test + public void missingActionId() throws Exception { + loginPasswordAndOtp(); + + appPage.assertCurrent(); + events.clear(); + + oauth.kcAction(DeleteCredentialAction.PROVIDER_ID); + oauth.openLoginForm(); + + events.expect(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .error(Errors.MISSING_CREDENTIAL_ID); + + // Redirected to the application. Action will be ignored + appPage.assertCurrent(); + } + + @Test + public void incorrectId() throws Exception { + loginPasswordAndOtp(); + + appPage.assertCurrent(); + events.clear(); + + oauth.kcAction(getKcActionParamForDeleteCredential("incorrect")); + oauth.openLoginForm(); + + // Redirected to the application. Action will be ignored + appPage.assertCurrent(); + + events.expect(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .detail(Details.CREDENTIAL_ID, "incorrect") + .error(Errors.CREDENTIAL_NOT_FOUND); + } + + @Test + public void requiredActionByAdmin() throws Exception { + // Add required action by admin. It will be ignored as there is no credentialId + UserRepresentation user = testRealm().users().get(userId).toRepresentation(); + user.setRequiredActions(List.of(DeleteCredentialAction.PROVIDER_ID)); + testRealm().users().get(userId).update(user); + + loginPasswordAndOtp(); + appPage.assertCurrent(); + + events.expect(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .error(Errors.MISSING_CREDENTIAL_ID); + } + + @Test + public void removeTotpCustomLabel() throws Exception { + String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); + testRealm().users().get(userId).setCredentialUserLabel(credentialId, "custom-otp-authenticator"); + + oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); + loginPasswordAndOtp(); + + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage("custom-otp-authenticator"); + + deleteCredentialPage.confirm(); + + appPage.assertCurrent(); + assertKcActionStatus("success"); + + Assert.assertNull(getCredentialIdByType(OTPCredentialModel.TYPE)); + + events.expect(EventType.REMOVE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CREDENTIAL_USER_LABEL, "custom-otp-authenticator") + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); + } + + private String getCredentialIdByType(String type) { + List credentials = testRealm().users().get(userId).credentials(); + return credentials.stream() + .filter(credential -> type.equals(credential.getType())) + .findFirst() + .map(CredentialRepresentation::getId) + .orElse(null); + } + + public static String getKcActionParamForDeleteCredential(String credentialId) { + return DeleteCredentialAction.PROVIDER_ID + ":" + credentialId; + } + + private void loginPasswordAndOtp() { + oauth.openLoginForm(); + loginPage.login("john", "password"); + loginTotpPage.assertCurrent(); + loginTotpPage.login(totp.generateTOTP("mySecret")); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 1841d9f5fbfd..b8202b1d66f2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -59,6 +59,7 @@ import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.userprofile.UserProfileContext; +import org.keycloak.utils.StringUtil; import org.openqa.selenium.htmlunit.HtmlUnitDriver; /** @@ -513,6 +514,10 @@ public void testMultivaluedAttributes() { for (String value : values) { updateProfilePage.clickRemoveAttributeValue(attribute + "-0"); } + // make sure the last attribute is set with a value + if (StringUtil.isBlank(updateProfilePage.getAttribute(attribute + "-0"))) { + updateProfilePage.setAttribute(attribute + "-0", values.get(values.size() - 1)); + } updateProfilePage.update("f", "l", "e@keycloak.org"); userRep = ActionUtil.findUserWithAdminClient(adminClient, "john-doh@localhost"); assertThat(userRep.getAttributes().get(attribute), Matchers.containsInAnyOrder(values.get(values.size() - 1))); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java index 2364a65dc133..7ba2f0a3494d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java @@ -174,45 +174,45 @@ public void testAttributeGrouping() { updateProfilePage.assertCurrent(); String htmlFormId="kc-update-profile-form"; - //assert fields and groups location in form, attributes without a group are the last + //assert fields and groups location in form, attributes without a group appear first Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(1) > label#header-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > label#description-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#department") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(1) > label#header-contact") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#email") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#lastName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > input#username") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#firstName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email") ).isDisplayed() ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/fuse/FuseAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/fuse/FuseAdapterTest.java index d035427285b2..c1d6be746319 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/fuse/FuseAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/fuse/FuseAdapterTest.java @@ -16,39 +16,13 @@ */ package org.keycloak.testsuite.adapter.example.fuse; -import static org.hamcrest.Matchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; -import static org.keycloak.testsuite.utils.fuse.FuseUtils.assertCommand; -import static org.keycloak.testsuite.utils.fuse.FuseUtils.getCommandOutput; -import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.management.InstanceNotFoundException; -import javax.management.MBeanException; -import javax.management.MBeanServerConnection; -import javax.management.ObjectName; -import javax.management.ReflectionException; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXServiceURL; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.Test; -import org.keycloak.common.Profile; +import org.junit.Ignore; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; @@ -59,21 +33,49 @@ import org.keycloak.testsuite.adapter.page.fuse.CustomerPortalFuseExample; import org.keycloak.testsuite.adapter.page.fuse.ProductPortalFuseExample; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; -import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.auth.page.AuthRealm; -import org.keycloak.testsuite.pages.LogoutConfirmPage; -import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.auth.page.login.OIDCLogin; +import org.keycloak.testsuite.pages.LogoutConfirmPage; import org.keycloak.testsuite.util.DroneUtils; import org.keycloak.testsuite.util.JavascriptBrowser; import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.utils.fuse.FuseUtils.Result; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf; +import static org.keycloak.testsuite.utils.fuse.FuseUtils.assertCommand; +import static org.keycloak.testsuite.utils.fuse.FuseUtils.getCommandOutput; +import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm; + @AppServerContainer(ContainerConstants.APP_SERVER_FUSE63) @AppServerContainer(ContainerConstants.APP_SERVER_FUSE7X) -@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228) public class FuseAdapterTest extends AbstractExampleAdapterTest { @@ -165,6 +167,7 @@ public void hawtio1LoginTest() throws Exception { } @Test + @Ignore @AppServerContainer(value = ContainerConstants.APP_SERVER_FUSE63, skip = true) public void hawtio2LoginTest() throws Exception { @@ -345,9 +348,9 @@ public void testCustomerListingAndAccountManagement() { DroneUtils.getCurrentDriver().navigate().back(); customerListing.clickLogOut(); + logoutConfirmPage.confirmLogout(); - WaitUtils.pause(2500); customerPortal.navigateTo();//needed for phantomjs WaitUtils.waitForPageToLoad(); customerPortal.clickCustomerListingLink(); @@ -374,6 +377,7 @@ public void testAdminInterface() { WaitUtils.waitForPageToLoad(); customerListing.clickLogOut(); logoutConfirmPage.confirmLogout(); + WaitUtils.waitForPageToLoad(); WaitUtils.pause(2500); customerPortal.navigateTo();//needed for phantomjs @@ -403,7 +407,8 @@ public void testProductPortal() { assertThat(productPortal.getProduct2SecuredText(), containsString("Product received: id=2")); productPortal.clickLogOutLink(); + logoutConfirmPage.confirmLogout(); WaitUtils.waitForPageToLoad(); - assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); + assertCurrentUrlStartsWith(testRealmPage); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java index bf3fe5c6df71..37eb5c6e94e6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java @@ -48,7 +48,7 @@ public void landingPage() throws IOException { Assert.assertEquals(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", authUrl); String resourceUrl = config.get("resourceUrl"); - Assert.assertTrue(resourceUrl.matches("/auth/resources/[^/]*/admin/keycloak.v2")); + Assert.assertTrue(resourceUrl.matches("/auth/resources/[^/]*/admin/primesign.v2")); String consoleBaseUrl = config.get("consoleBaseUrl"); Assert.assertEquals(consoleBaseUrl, "/auth/admin/master/console/"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index 40fdd085908a..e9967f641cb4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -51,6 +51,7 @@ import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; +import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; @@ -72,6 +73,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -95,11 +97,18 @@ public void getClients() { } private ClientRepresentation createClient() { + return createClient(null); + } + + private ClientRepresentation createClient(String protocol) { ClientRepresentation rep = new ClientRepresentation(); rep.setClientId("my-app"); rep.setDescription("my-app description"); rep.setEnabled(true); rep.setPublicClient(true); + if (protocol != null) { + rep.setProtocol(protocol); + } Response response = realm.clients().create(rep); response.close(); String id = ApiUtil.getCreatedId(response); @@ -185,6 +194,49 @@ public void testFragmentProhibitedClientValidation() { ); } + @Test + public void testSamlSpecificUrls() { + testSamlSpecificUrls(true, "javascript:alert('TEST')", "data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+"); + testSamlSpecificUrls(false, "javascript:alert('TEST')", "data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+"); + } + + private void testSamlSpecificUrls(boolean create, String... testUrls) { + ClientRepresentation rep; + if (create) { + rep = new ClientRepresentation(); + rep.setClientId("my-app2"); + rep.setEnabled(true); + rep.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + } + else { + rep = createClient(SamlProtocol.LOGIN_PROTOCOL); + } + rep.setAttributes(new HashMap<>()); + + Map attrs = Map.of( + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "Assertion Consumer Service POST Binding URL", + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "Assertion Consumer Service Redirect Binding URL", + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "Artifact Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "Logout Service POST Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "Logout Service ARTIFACT Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "Logout Service Redirect Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, "Logout Service SOAP Binding URL", + SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "Artifact Resolution Service"); + + for (String testUrl : testUrls) { + // admin url + rep.setAdminUrl(testUrl); + createOrUpdateClientExpectingValidationErrors(rep, create, "Master SAML Processing URL uses an illegal scheme"); + rep.setAdminUrl(null); + // attributes + for (Map.Entry entry : attrs.entrySet()) { + rep.getAttributes().put(entry.getKey(), testUrl); + createOrUpdateClientExpectingValidationErrors(rep, create, entry.getValue() + " uses an illegal scheme"); + rep.getAttributes().remove(entry.getKey()); + } + } + } + private void testClientUriValidation(String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) { testClientUriValidation(false, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls); testClientUriValidation(true, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index 905a1841cc1b..5487dd6af1c7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.BearerAuthFilter; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.Profile; import org.keycloak.models.AdminRoles; @@ -51,6 +52,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.TestLdapConnectionRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; @@ -73,6 +75,8 @@ import org.keycloak.userprofile.DeclarativeUserProfileProvider; import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.lang.reflect.Method; import java.util.Arrays; @@ -336,7 +340,7 @@ public void invoke(RealmResource realm) { }, Resource.REALM, true); invoke(new Invocation() { public void invoke(RealmResource realm) { - realm.deleteSession("nosuch"); + realm.deleteSession("nosuch", false); } }, Resource.USER, true); invoke(new Invocation() { @@ -375,7 +379,11 @@ public void invoke(RealmResource realm) { invoke(new InvocationWithResponse() { public void invoke(RealmResource realm, AtomicReference response) { - response.set(realm.testLDAPConnection("nosuch", "nosuch", "nosuch", "nosuch", "nosuch", "nosuch")); + TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation( + "nosuch", "nosuch", "nosuch", "nosuch", "nosuch", "nosuch"); + response.set(realm.testLDAPConnection(config.getAction(), config.getConnectionUrl(), config.getBindDn(), + config.getBindCredential(), config.getUseTruststoreSpi(), config.getConnectionTimeout())); + response.set(realm.testLDAPConnection(config)); } }, Resource.REALM, true); @@ -1458,6 +1466,21 @@ public void invoke(RealmResource realm) { realm.users().get(user.getId()).toRepresentation(); } }, Resource.USER, false); + invoke(new InvocationWithResponse() { + public void invoke(RealmResource realm, AtomicReference response) { + // no-op + } + public void invoke(Keycloak keycloak, RealmResource realm, AtomicReference response) { + try (Client client = Keycloak.getClientProvider().newRestEasyClient(null, null, true)) { + Response resp = client.target(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth") + .path("/admin/realms/" + realm.toRepresentation().getRealm() + "/ui-ext/users/" + user.getId() + "/unmanagedAttributes") + .register(new BearerAuthFilter(keycloak.tokenManager())) + .request(MediaType.APPLICATION_JSON) + .get(); + response.set(resp); + } + } + }, Resource.USER, false); invoke(new Invocation() { public void invoke(RealmResource realm) { realm.users().get(user.getId()).update(user); @@ -1757,6 +1780,11 @@ public void invoke(RealmResource realm) { realm.components().query("nosuch"); } }, Resource.REALM, false); + invoke(new Invocation() { + public void invoke(RealmResource realm) { + realm.clientRegistrationPolicy().getProviders(); + } + }, Resource.REALM, false); invoke(new InvocationWithResponse() { public void invoke(RealmResource realm, AtomicReference response) { response.set(realm.components().add(new ComponentRepresentation())); @@ -1945,7 +1973,7 @@ private void invoke(InvocationWithResponse invocation, Keycloak client, boolean int statusCode; try { AtomicReference responseReference = new AtomicReference<>(); - invocation.invoke(client.realm(REALM_NAME), responseReference); + invocation.invoke(client, client.realm(REALM_NAME), responseReference); Response response = responseReference.get(); if (response != null) { statusCode = response.getStatus(); @@ -2054,6 +2082,9 @@ public interface InvocationWithResponse { void invoke(RealmResource realm, AtomicReference response); + default void invoke(Keycloak keycloak, RealmResource realm, AtomicReference response) { + invoke(realm, response); + } } private void assertGettersEmpty(RealmRepresentation rep) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java index 57de87ba0de6..446cf01d225b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java @@ -57,7 +57,7 @@ public void testServerInfo() { assertNotNull(info.getThemes()); assertNotNull(info.getThemes().get("account")); Assert.assertNames(info.getThemes().get("account"), "base", "keycloak.v3", "custom-account-provider"); - Assert.assertNames(info.getThemes().get("admin"), "base", "keycloak.v2"); + Assert.assertNames(info.getThemes().get("admin"), "base", "primesign.v2"); Assert.assertNames(info.getThemes().get("email"), "base", "keycloak"); Assert.assertNames(info.getThemes().get("login"), "address", "base", "environment-agnostic", "keycloak"); Assert.assertNames(info.getThemes().get("welcome"), "keycloak"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java index ab9dc6ddb07e..473d0a4649c6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java @@ -58,6 +58,7 @@ public void testRequiredActions() { addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", true, false, null); addRequiredAction(expected, "VERIFY_PROFILE", "Verify Profile", true, false, null); addRequiredAction(expected, "delete_account", "Delete Account", false, false, null); + addRequiredAction(expected, "delete_credential", "Delete Credential", true, false, null); addRequiredAction(expected, "update_user_locale", "Update User Locale", true, false, null); addRequiredAction(expected, "webauthn-register", "Webauthn Register", true, false, null); addRequiredAction(expected, "webauthn-register-passwordless", "Webauthn Register Passwordless", true, false, null); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java index 07f60ac0d3d6..618e8fffa5df 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java @@ -110,6 +110,12 @@ public void createRoleWithSameName() { rolesRsc.create(role); } + @Test + public void createRoleWithNamePattern() { + RoleRepresentation role = RoleBuilder.create().name("role-a-{pattern}").build(); + rolesRsc.create(role); + } + @Test public void testRemoveRole() { RoleRepresentation role2 = makeRole("role2"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java index ace6e2869d39..c5d6fdf4ec9f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java @@ -229,30 +229,30 @@ public void testSamlMetadataSpDescriptorPost() throws Exception { attrNamesAndValues.clear(); //fallback to adminUrl - updater.setAdminUrl("admin-url").update(); + updater.setAdminUrl("https://admin-url").update(); assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR)); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - attrNamesAndValues.put("Location", "admin-url"); + attrNamesAndValues.put("Location", "https://admin-url"); assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues); assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues); attrNamesAndValues.clear(); //fine grained - updater.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "saml-assertion-post-url") - .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "saml-logout-post-url") - .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "saml-assertion-redirect-url") - .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "saml-logout-redirect-url") + updater.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "https://saml-assertion-post-url") + .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "https://saml-logout-post-url") + .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "https://saml-assertion-redirect-url") + .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "https://saml-logout-redirect-url") .update(); assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR)); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - attrNamesAndValues.put("Location", "saml-logout-post-url"); + attrNamesAndValues.put("Location", "https://saml-logout-post-url"); assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues); attrNamesAndValues.clear(); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - attrNamesAndValues.put("Location", "saml-assertion-post-url"); + attrNamesAndValues.put("Location", "https://saml-assertion-post-url"); assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues); } assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); @@ -277,29 +277,29 @@ public void testSamlMetadataSpDescriptorRedirect() throws Exception { attrNamesAndValues.clear(); //fallback to adminUrl - updater.setAdminUrl("admin-url").update(); + updater.setAdminUrl("https://admin-url").update(); assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR)); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - attrNamesAndValues.put("Location", "admin-url"); + attrNamesAndValues.put("Location", "https://admin-url"); assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues); assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues); attrNamesAndValues.clear(); //fine grained - updater.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "saml-assertion-post-url") - .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "saml-logout-post-url") - .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "saml-assertion-redirect-url") - .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "saml-logout-redirect-url") + updater.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "https://saml-assertion-post-url") + .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "https://saml-logout-post-url") + .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "https://saml-assertion-redirect-url") + .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "https://saml-logout-redirect-url") .update(); assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR)); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - attrNamesAndValues.put("Location", "saml-logout-redirect-url"); + attrNamesAndValues.put("Location", "https://saml-logout-redirect-url"); assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues); attrNamesAndValues.clear(); attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - attrNamesAndValues.put("Location", "saml-assertion-redirect-url"); + attrNamesAndValues.put("Location", "https://saml-assertion-redirect-url"); assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues); } assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index e39653fce975..df3ada02f9b8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -947,10 +947,10 @@ public void deleteSession() { EventRepresentation event = events.poll(); assertNotNull(event); - realm.deleteSession(event.getSessionId()); + realm.deleteSession(event.getSessionId(), false); assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.deleteSessionPath(event.getSessionId()), ResourceType.USER_SESSION); try { - realm.deleteSession(event.getSessionId()); + realm.deleteSession(event.getSessionId(), false); fail("Expected 404"); } catch (NotFoundException e) { // Expected diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzEndpointRequestParserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzEndpointRequestParserTest.java new file mode 100644 index 000000000000..b5e3ea7d80cc --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzEndpointRequestParserTest.java @@ -0,0 +1,348 @@ +package org.keycloak.testsuite.authz; + +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.keycloak.testsuite.admin.AbstractAdminTest.loadJson; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Response; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Test; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.RealmBuilder; + +public class AuthzEndpointRequestParserTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + + testRealm.setAttributes(new HashMap<>()); // no realm specific attributes yet + + RealmBuilder.edit(testRealm); + + } + + private void updateTestRealm(Map newAttributes) { + RealmRepresentation testRealm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + testRealm.setAttributes(newAttributes); + adminClient.realm("test").update(testRealm); + } + + @Test + public void test_authentication_backwards_compatible() { + + // no realm specific attribute set - test backwards compatibility + updateTestRealm(Collections.emptyMap()); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + oauth.addCustomParameter("paramkey1_too_long", RandomStringUtils.random(2000 + 1)); + oauth.addCustomParameter("paramkey2", "paramvalue2"); + oauth.addCustomParameter("paramkey3", "paramvalue3"); + oauth.addCustomParameter("paramkey4", "paramvalue4"); + oauth.addCustomParameter("paramkey5", "paramvalue5"); + oauth.addCustomParameter("paramkey6_too_many", "paramvalue6"); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_size_exceeds_failfast() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + oauth.addCustomParameter("param_too_long", RandomStringUtils.random(42 + 1)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(400))); + assertThat(response, Matchers.body(containsString("Back to Application"))); + + } + + } + + } + + @Test + public void test_authentication_size_accepted() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + oauth.addCustomParameter("param_accepted", RandomStringUtils.random(42)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_size_exceeds_ignore() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "false", + "additionalReqParamsMaxSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // well known params are ignored anyway + oauth.addCustomParameter(OIDCLoginProtocol.NONCE_PARAM, RandomStringUtils.random(100)); + oauth.addCustomParameter("param_too_long_silently_ignored", RandomStringUtils.random(42 + 1)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_maxnumber_exceeds_failfast() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxNumber", "2" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // well known params are ignored + oauth.addCustomParameter(OIDCLoginProtocol.NONCE_PARAM, RandomStringUtils.random(42)); + oauth.addCustomParameter("paramkey1", "paramvalue1"); + oauth.addCustomParameter("paramkey2", "paramvalue2"); + oauth.addCustomParameter("paramkey3", "paramvalue3"); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(400))); + assertThat(response, Matchers.body(containsString("Back to Application"))); + + } + + } + + } + + @Test + public void test_authentication_maxnumber_accepted() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxNumber", "2" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // well known params are ignored + oauth.addCustomParameter(OIDCLoginProtocol.NONCE_PARAM, RandomStringUtils.random(42)); + oauth.addCustomParameter("paramkey1", "paramvalue1"); + oauth.addCustomParameter("paramkey2", "paramvalue2"); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_maxnumber_exceeds_ignore() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "false", + "additionalReqParamsMaxNumber", "2" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // well known params are ignored anyway + oauth.addCustomParameter(OIDCLoginProtocol.NONCE_PARAM, RandomStringUtils.random(42)); + oauth.addCustomParameter("paramkey1", "paramvalue1"); + oauth.addCustomParameter("paramkey2", "paramvalue2"); + oauth.addCustomParameter("paramkey3", "paramvalue3"); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_maxoverallsize_exceeds_failfast() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxOverallSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // 21 + 21 + 1 = 43 + oauth.addCustomParameter("paramkey1", RandomStringUtils.random(21)); + oauth.addCustomParameter("paramkey2", RandomStringUtils.random(21)); + oauth.addCustomParameter("paramkey3", RandomStringUtils.random(1)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(400))); + assertThat(response, Matchers.body(containsString("Back to Application"))); + + } + + } + + } + + @Test + public void test_authentication_maxoverallsize_accepted() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxOverallSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // 21 + 21 = 42 + oauth.addCustomParameter("paramkey1", RandomStringUtils.random(21)); + oauth.addCustomParameter("paramkey2", RandomStringUtils.random(21)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_maxoverallsize_exceeds_ignore() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "false", + "additionalReqParamsMaxOverallSize", "42" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + // 21 + 21 + 1 = 43 + oauth.addCustomParameter("paramkey1", RandomStringUtils.random(21)); + oauth.addCustomParameter("paramkey2", RandomStringUtils.random(21)); + oauth.addCustomParameter("paramkey3", RandomStringUtils.random(1)); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + + @Test + public void test_authentication_knownparameters_dont_count() { + + updateTestRealm( + Map.of( + "additionalReqParamsFailFast", "true", + "additionalReqParamsMaxOverallSize", "42", + "additionalReqParamsMaxNumber", "2" + ) + ); + + try (Client client = AdminClientUtil.createResteasyClient()) { + + /* + * Well known parameter will neither be counted towards additionalReqParamsMaxSize nor + * additionalReqParamsMaxOverallSize. + */ + oauth.addCustomParameter(OIDCLoginProtocol.NONCE_PARAM, RandomStringUtils.random(100)); + oauth.addCustomParameter("paramkey1", RandomStringUtils.random(21)); + oauth.addCustomParameter(OIDCLoginProtocol.CODE_PARAM, ""); + oauth.addCustomParameter("paramkey2", RandomStringUtils.random(21)); + oauth.addCustomParameter(OIDCLoginProtocol.MAX_AGE_PARAM, "42"); + + try (Response response = client.target(oauth.getLoginFormUrl()).request().get()) { + + assertThat(response.getStatus(), is(equalTo(200))); + assertThat(response, Matchers.body(containsString("Sign in"))); + + } + + } + + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java index 429329ee5b11..cdd36b7064db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java @@ -35,7 +35,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClients; @@ -2046,6 +2048,32 @@ public void testRefreshTokenFromClientOtherThanAudience() throws Exception { assertFalse(token.getAuthorization().getPermissions().isEmpty()); } + @Test + public void testTokenExpirationRenewalWhenIssuingTokens() { + oauth.realm("authz-test"); + oauth.clientId(PUBLIC_TEST_CLIENT); + oauth.doLogin("marta", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, null); + assertNotNull(accessTokenResponse.getAccessToken()); + assertNotNull(accessTokenResponse.getRefreshToken()); + + try { + for (int i = 0; i < 3; i++) { + AuthorizationRequest request = new AuthorizationRequest(); + request.setAudience(RESOURCE_SERVER_TEST); + AuthorizationResponse authorizationResponse = getAuthzClient(PUBLIC_TEST_CLIENT_CONFIG).authorization(accessTokenResponse.getAccessToken()).authorize(request); + AccessToken refreshToken = toAccessToken(authorizationResponse.getRefreshToken()); + AccessToken accessTokenToken = toAccessToken(authorizationResponse.getToken()); + assertEquals(refreshToken.getExp() - refreshToken.getIat(), 1800); + assertEquals(accessTokenToken.getExp() - accessTokenToken.getIat(), 300); + setTimeOffset(i); + } + } finally { + resetTimeOffset(); + } + } + @Test public void testUsingExpiredToken() throws Exception { ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST); @@ -2389,6 +2417,65 @@ public void testPermissionOrder() throws Exception { .getScopes().contains("entity:read"))); } + @Test + public void testSameResultRegardlessOPermissionParameterValue() throws Exception { + ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST); + AuthorizationResource authorization = client.authorization(); + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName(KeycloakModelUtils.generateId()); + resource.addScope("scope1", "scope2"); + resource.setOwnerManagedAccess(true); + + try (Response response = authorization.resources().create(resource)) { + resource = response.readEntity(ResourceRepresentation.class); + } + + UserPolicyRepresentation policy = new UserPolicyRepresentation(); + + policy.setName(KeycloakModelUtils.generateId()); + policy.addUser("marta"); + + authorization.policies().user().create(policy).close(); + + ScopePermissionRepresentation representation = new ScopePermissionRepresentation(); + + representation.setName(KeycloakModelUtils.generateId()); + representation.addScope("scope1"); + representation.addPolicy(policy.getName()); + + authorization.permissions().scope().create(representation).close(); + + AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG); + PermissionTicketRepresentation ticket = new PermissionTicketRepresentation(); + + ticket.setResource(resource.getId()); + ticket.setRequesterName("marta"); + ticket.setGranted(true); + ticket.setScopeName("scope1"); + + authzClient.protection().permission().create(ticket); + + AuthorizationRequest request = new AuthorizationRequest(); + request.addPermission(resource.getId()); + AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(request); + AccessToken rpt = toAccessToken(response.getToken()); + ResourceRepresentation finalResource = resource; + List permissions = rpt.getAuthorization().getPermissions().stream().filter(permission -> permission.getResourceId().equals(finalResource.getId())).collect(Collectors.toList()); + assertEquals(1, permissions.size()); + assertEquals(1, permissions.get(0).getScopes().size()); + assertEquals("scope1", permissions.get(0).getScopes().iterator().next()); + + request = new AuthorizationRequest(); + request.addPermission(resource.getName()); + response = authzClient.authorization("marta", "password").authorize(request); + rpt = toAccessToken(response.getToken()); + permissions = rpt.getAuthorization().getPermissions().stream().filter(permission -> permission.getResourceId().equals(finalResource.getId())).collect(Collectors.toList()); + assertEquals(1, permissions.size()); + assertEquals(1, permissions.get(0).getScopes().size()); + assertEquals("scope1", permissions.get(0).getScopes().iterator().next()); + } + private void testRptRequestWithResourceName(String configFile) { Metadata metadata = new Metadata(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLdapTest.java new file mode 100644 index 000000000000..df5c073433c2 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLdapTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import jakarta.ws.rs.core.Response; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.storage.UserStorageProvider.EditMode; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig; +import org.keycloak.testsuite.KerberosEmbeddedServer; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.federation.kerberos.KeycloakSPNegoSchemeFactory; +import org.keycloak.testsuite.util.KerberosRule; + +public final class KcOidcBrokerLdapTest extends AbstractInitializedBaseBrokerTest { + + private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-ldap-connection.properties"; + + private KeycloakSPNegoSchemeFactory spnegoSchemeFactory; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return KcOidcBrokerConfiguration.INSTANCE; + } + + @ClassRule + public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM); + + @Before + public void onBefore() { + getKerberosRule().setKrb5ConfPath(testingClient.testing()); + spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig()); + oauth.clientId("kerberos-app"); + ComponentRepresentation rep = getUserStorageConfiguration(); + Response resp = adminClient.realm(bc.consumerRealmName()).components().add(rep); + getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); + resp.close(); + } + + @Test + public void testUpdateProfileOnFirstLogin() { + driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS); + updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin); + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + logInWithBroker(bc); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "f", "l"); + Assert.assertFalse(errorPage.isCurrent()); + } + + private ComponentRepresentation getUserStorageConfiguration(String providerName, String providerId) { + Map kerberosConfig = getKerberosRule().getConfig(); + kerberosConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "false"); + kerberosConfig.put(LDAPConstants.EDIT_MODE, EditMode.UNSYNCED.name()); + kerberosConfig.put(UserStorageProviderModel.IMPORT_ENABLED, "true"); + MultivaluedHashMap config = toComponentConfig(kerberosConfig); + + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName(providerName); + model.setPriority(0); + model.setProviderId(providerId); + model.setConfig(config); + + return ModelToRepresentation.toRepresentationWithoutConfig(model); + } + + private static MultivaluedHashMap toComponentConfig(Map ldapConfig) { + MultivaluedHashMap config = new MultivaluedHashMap<>(); + for (Map.Entry entry : ldapConfig.entrySet()) { + config.add(entry.getKey(), entry.getValue()); + + } + return config; + } + + private KerberosRule getKerberosRule() { + return kerberosRule; + } + + + private CommonKerberosConfig getKerberosConfig() { + return new LDAPProviderKerberosConfig(getUserStorageConfiguration()); + } + + private ComponentRepresentation getUserStorageConfiguration() { + return getUserStorageConfiguration("kerberos-ldap", LDAPStorageProviderFactory.PROVIDER_NAME); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java index 6899c587ce95..c7f78de3b270 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java @@ -23,12 +23,15 @@ import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS; import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim; +import java.util.concurrent.TimeUnit; + import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.ClientResource; @@ -45,13 +48,16 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeatures; import org.keycloak.testsuite.util.AdminClientUtil; @@ -127,6 +133,61 @@ public void testExternalInternalTokenExchange() throws Exception { } } + @Test + public void testSupportedTokenTypesWhenValidatingSubjectToken() throws Exception { + testingClient.server().run(KcOidcBrokerTokenExchangeTest::setupRealm); + RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName()); + ClientsResource clients = providerRealm.clients(); + ClientRepresentation brokerApp = clients.findByClientId("brokerapp").get(0); + brokerApp.setDirectAccessGrantsEnabled(true); + ClientResource brokerAppResource = providerRealm.clients().get(brokerApp.getId()); + brokerAppResource.update(brokerApp); + RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName()); + IdentityProviderResource identityProviderResource = consumerRealm.identityProviders().get(bc.getIDPAlias()); + IdentityProviderRepresentation idpRep = identityProviderResource.toRepresentation(); + idpRep.getConfig().put("disableUserInfo", "true"); + identityProviderResource.update(idpRep); + getCleanup().addCleanup(() -> { + idpRep.getConfig().put("disableUserInfo", "false"); + identityProviderResource.update(idpRep); + }); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(bc.providerRealmName(), bc.getUserLogin(), bc.getUserPassword(), null, brokerApp.getClientId(), brokerApp.getSecret()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + String idTokenString = tokenResponse.getIdToken(); + oauth.realm(bc.providerRealmName()); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + String logoutToken = testingClient.testApp().getBackChannelRawLogoutToken(); + Assert.assertNotNull(logoutToken); + + Client httpClient = AdminClientUtil.createResteasyClient(); + try { + WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(bc.consumerRealmName()) + .path("protocol/openid-connect/token"); + // test user info validation. + try (Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader( + "test-app", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, logoutToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + .param(OAuth2Constants.SUBJECT_ISSUER, bc.getIDPAlias()) + .param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID) + + ))) { + assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode())); + } + } finally { + httpClient.close(); + } + } + private static void setupRealm(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(BrokerTestConstants.REALM_CONS_NAME); IdentityProviderModel idp = realm.getIdentityProviderByAlias(IDP_OIDC_ALIAS); @@ -149,5 +210,10 @@ private static void setupRealm(KeycloakSession session) { ResourceServer server = management.realmResourceServer(); Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep); management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy); + + realm = session.realms().getRealmByName(BrokerTestConstants.REALM_PROV_NAME); + client = realm.getClientByClientId("brokerapp"); + client.addRedirectUri(OAuthClient.APP_ROOT + "/auth"); + client.setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, OAuthClient.APP_ROOT + "/admin/backchannelLogout"); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java index b265b0b2f564..f666eee26ce4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java @@ -407,45 +407,45 @@ public void testAttributeGrouping() { //assert fields location in form String htmlFormId = "kc-idp-review-profile-form"; - //assert fields and groups location in form, attributes without a group are the last + //assert fields and groups location in form, attributes without a group appear first org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(1) > label#header-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > label#description-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#department") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(1) > label#header-contact") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#email") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#lastName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > input#username") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact") ).isDisplayed() ); org.junit.Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#firstName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email") ).isDisplayed() ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java index 85f8ebe4e1c0..6a240b71ccb3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java @@ -389,7 +389,6 @@ public void githubPrivateEmailLogin() throws InterruptedException { public void twitterLogin() { setTestProvider(TWITTER); performLogin(); - navigateToLoginPage(); assertUpdateProfile(false, false, true); appPage.assertCurrent(); } @@ -643,7 +642,7 @@ protected void testTokenExchange() { Assert.assertEquals(1, users.size()); String username = users.get(0).getUsername(); - checkFeature(501, username); + checkFeature(400, username); testingClient.enableFeature(Profile.Feature.TOKEN_EXCHANGE); @@ -730,7 +729,7 @@ protected void testTokenExchange() { } finally { httpClient.close(); testingClient.disableFeature(Profile.Feature.TOKEN_EXCHANGE); - checkFeature(501, username); + checkFeature(400, username); } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java index bff7d220d66c..1f8436dee77c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java @@ -36,6 +36,8 @@ import org.keycloak.models.Constants; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -52,7 +54,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.HashSet; import java.util.Set; @@ -70,7 +74,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import static org.keycloak.services.clientregistration.ErrorCodes.INVALID_CLIENT_METADATA; import static org.keycloak.services.clientregistration.ErrorCodes.INVALID_REDIRECT_URI; import static org.keycloak.utils.MediaType.APPLICATION_JSON; @@ -302,6 +305,48 @@ public void testFragmentProhibitedClientValidation() { ); } + @Test + public void testSamlSpecificUrls() throws ClientRegistrationException { + testSamlSpecificUrls(true, "javascript:alert('TEST')", "data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+"); + testSamlSpecificUrls(false, "javascript:alert('TEST')", "data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+"); + } + + private void testSamlSpecificUrls(boolean register, String... testUrls) throws ClientRegistrationException { + ClientRepresentation rep = buildClient(); + rep.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + if (register) { + authCreateClients(); + } else { + authManageClients(); + registerClient(rep); + rep = reg.get(CLIENT_ID); + } + rep.setAttributes(new HashMap<>()); + + Map attrs = Map.of( + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, "Assertion Consumer Service POST Binding URL", + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, "Assertion Consumer Service Redirect Binding URL", + SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "Artifact Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "Logout Service POST Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "Logout Service ARTIFACT Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "Logout Service Redirect Binding URL", + SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, "Logout Service SOAP Binding URL", + SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "Artifact Resolution Service"); + + for (String testUrl : testUrls) { + // admin url + rep.setAdminUrl(testUrl); + registerOrUpdateClientExpectingValidationErrors(rep, register, false, "Master SAML Processing URL uses an illegal scheme"); + rep.setAdminUrl(null); + // attributes + for (Map.Entry entry : attrs.entrySet()) { + rep.getAttributes().put(entry.getKey(), testUrl); + registerOrUpdateClientExpectingValidationErrors(rep, register, false, entry.getValue() + " uses an illegal scheme"); + rep.getAttributes().remove(entry.getKey()); + } + } + } + @Test public void testUpdateAuthorizationSettings() throws ClientRegistrationException { authManageClients(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java index e9f8e29d49af..316f2079239a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java @@ -425,7 +425,7 @@ public void testLogoutUser( // Logout single session of user first UserResource user = ApiUtil.findUserByUsernameId(getAdminClient().realm(REALM_NAME), "login-test"); UserSessionRepresentation userSession = user.getUserSessions().get(0); - getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId()); + getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId(), false); // Just one session expired. assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index e9b6486f92de..67d12c8d7a04 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -257,6 +257,13 @@ public void testImportFromPartialExport() { addTestRealmToTestRealmReps("import-without-clients"); } + @Test + public void testImportFromRealmWithPartialAuthenticationFlows() { + // import a realm with no built-in authentication flows + importRealmFromFile("/import/partial-authentication-flows-import.json"); + Assert.assertTrue("Imported realm hasn't been found!", isRealmPresent("partial-authentication-flows-import")); + } + @Test public void testImportWithNullAuthenticatorConfigAndNoDefaultBrowserFlow() { importRealmFromFile("/import/testrealm-authenticator-config-null.json"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index f9a8c848adf6..a9657af95bf5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -17,19 +17,30 @@ package org.keycloak.testsuite.federation.kerberos; -import java.net.URI; -import java.util.List; +import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import org.keycloak.testsuite.Assert; +import java.net.URI; +import java.util.List; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.jboss.arquillian.graphene.page.Page; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.constants.KerberosConstants; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; +import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; @@ -37,10 +48,16 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.ActionURIUtils; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.KerberosEmbeddedServer; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.KerberosRule; +import org.keycloak.testsuite.util.MailUtils; +import org.keycloak.testsuite.util.OAuthClient; import static org.keycloak.userprofile.UserProfileUtil.USER_METADATA_GROUP; @@ -56,6 +73,14 @@ public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest { @ClassRule public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM); + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected LoginPasswordUpdatePage loginPasswordUpdatePage; + + @Page + protected InfoPage infoPage; @Override protected KerberosRule getKerberosRule() { @@ -213,4 +238,40 @@ public void testUserProfile() throws Exception { johnResource.update(john); } + @Test + public void testResetCredentials() throws Exception { + // request reset-credentials + String resetUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"; + String actionUri; + try (Response response = client.target(resetUri).queryParam(Constants.CLIENT_ID, oauth.getClientId()).request().get()) { + Assert.assertEquals(200, response.getStatus()); + Document theResponsePage = Jsoup.parse(response.readEntity(String.class)); + Elements forms = theResponsePage.select("form[id=kc-reset-password-form]"); + Assert.assertEquals(1, forms.size()); + actionUri = forms.get(0).attr("action"); + Assert.assertNotNull(actionUri); + } + + // continue the reset providing the user to change email + spnegoSchemeFactory.setCredentials("hnelson", "incorrectpassword"); // this should not be used, error if auth requested + Form form = new Form(); + form.param("username", "test-user@localhost"); + try (Response response = client.target(actionUri).request().post(Entity.form(form))) { + Assert.assertEquals(200, response.getStatus()); + MatcherAssert.assertThat(response.readEntity(String.class), Matchers.containsString("You should receive an email shortly with further instructions.")); + } + + // get the email from green mail + MimeMessage message = greenMail.getLastReceivedMessage(); + Assert.assertNotNull(message); + String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message); + + // perform the password change using the email url + driver.navigate().to(changePasswordUrl.trim()); + loginPasswordUpdatePage.assertCurrent(); + loginPasswordUpdatePage.changePassword("resetPassword", "resetPassword"); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).client(oauth.getClientId()).detail(Details.USERNAME, "test-user@localhost"); + infoPage.assertCurrent(); + Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAccountRestApiTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAccountRestApiTest.java index e66f0b408319..8f89449c2ab0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAccountRestApiTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAccountRestApiTest.java @@ -123,6 +123,11 @@ public void testUpdateProfile() throws IOException { List origLdapEntryDn = adminRestUserRep.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN); Assert.assertNotNull(origLdapId.get(0)); Assert.assertNotNull(origLdapEntryDn.get(0)); + adminRestUserRep = testRealm().users().get(adminRestUserRep.getId()).toRepresentation(); + origLdapId = adminRestUserRep.getAttributes().get(LDAPConstants.LDAP_ID); + origLdapEntryDn = adminRestUserRep.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN); + Assert.assertNotNull(origLdapId.get(0)); + Assert.assertNotNull(origLdapEntryDn.get(0)); // Trying to add KERBEROS_PRINCIPAL (Adding attribute, which was not yet present). Request does not fail, but attribute is not updated user.setFirstName("JohnUpdated"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADMapperTest.java index 764c97df3d9a..5cb0e35f1557 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADMapperTest.java @@ -28,13 +28,18 @@ import org.junit.Test; import org.junit.runners.MethodSorters; import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.component.ComponentModel; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; +import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapper; +import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory; import org.keycloak.storage.ldap.mappers.msad.UserAccountControl; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; @@ -262,7 +267,7 @@ public void test05UpdatePasswordUnsyncedMode() { johnRep.setRequiredActions(Collections.singletonList(UserModel.RequiredAction.UPDATE_PASSWORD.name())); john.update(johnRep); - // Check in LDAP, that johnkeycloak has pwdLastSet set attribute set in MSAD to bigger value than 0. Previous update of requiredAction did not updated LDAP + // Check in LDAP, that johnkeycloak has pwdLastSet set attribute set in MSAD to bigger value than 0. Previous update of requiredAction did not update LDAP long pwdLastSetFromLDAP = getPwdLastSetOfJohn(); assertThat(pwdLastSetFromLDAP, Matchers.greaterThan(0L)); @@ -384,6 +389,15 @@ public void test08DisabledUserUnsyncedMode() { ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.UNSYNCED.toString()); appRealm.updateComponent(ctx.getLdapModel()); + + // change MSAD mapper config "ALWAYS_READ_ENABLED_VALUE_FROM_LDAP" to false, so that local db has priority. + ComponentModel msadMapperComponent = appRealm.getComponentsStream(ctx.getLdapModel().getId(), LDAPStorageMapper.class.getName()) + .filter(c -> MSADUserAccountControlStorageMapperFactory.PROVIDER_ID.equals(c.getProviderId())) + .findFirst().orElse(null); + if (msadMapperComponent != null) { + msadMapperComponent.getConfig().putSingle(MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP, "false"); + appRealm.updateComponent(msadMapperComponent); + } }); // Disable user johnkeycloak through Keycloak admin API. Due UNSYNCED mode, this should update Keycloak DB, but not MSAD @@ -401,6 +415,7 @@ public void test08DisabledUserUnsyncedMode() { Assert.assertEquals("Account is disabled, contact your administrator.", loginPage.getError()); // Enable johnkeycloak in admin REST API + johnRep = john.toRepresentation(); johnRep.setEnabled(true); john.update(johnRep); @@ -419,9 +434,103 @@ public void test08DisabledUserUnsyncedMode() { ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString()); appRealm.updateComponent(ctx.getLdapModel()); + + // reset MSAD mapper config "ALWAYS_READ_ENABLED_VALUE_FROM_LDAP" to true. + ComponentModel msadMapperComponent = appRealm.getComponentsStream(ctx.getLdapModel().getId(), LDAPStorageMapper.class.getName()) + .filter(c -> MSADUserAccountControlStorageMapperFactory.PROVIDER_ID.equals(c.getProviderId())) + .findFirst().orElse(null); + if (msadMapperComponent != null) { + msadMapperComponent.getConfig().putSingle(MSADUserAccountControlStorageMapper.ALWAYS_READ_ENABLED_VALUE_FROM_LDAP, "true"); + appRealm.updateComponent(msadMapperComponent); + } + + }); + } + + @Test + public void test09DisableUserImportDisabled() { + testingClient.server().run(session -> { + // set import enabled to false - in this case only attributes known to LDAP (via one of the mappers) are written + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + ctx.getLdapModel().getConfig().putSingle(UserStorageProviderModel.IMPORT_ENABLED, "false"); + appRealm.updateComponent(ctx.getLdapModel()); + }); + + // check user is enabled both locally and on MSAD. + UserResource john = ApiUtil.findUserByUsernameId(adminClient.realm("test"), "johnkeycloak"); + UserRepresentation johnRep = john.toRepresentation(); + Assert.assertTrue(johnRep.isEnabled()); + Assert.assertTrue(isJohnEnabledInMSAD()); + + // disable user johnkeycloak - it should disable both locally and on MSAD. + johnRep.setEnabled(false); + john.update(johnRep); + + // Login as johnkeycloak and see the user is disabled. + loginPage.open(); + loginPage.login("johnkeycloak", "Password1"); + Assert.assertEquals("Account is disabled, contact your administrator.", loginPage.getError()); + + // check user is disabled in all places. + johnRep = john.toRepresentation(); + Assert.assertFalse(johnRep.isEnabled()); + Assert.assertFalse(isJohnEnabledInMSAD()); + + // restore john to enabled state. + johnRep.setEnabled(true); + john.update(johnRep); + + // Login again. User should be enabled. + loginPage.open(); + loginPage.login("johnkeycloak", "Password1"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + testingClient.server().run(session -> { + // restore import enabled mode in the storage provider. + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + ctx.getLdapModel().getConfig().putSingle(UserStorageProviderModel.IMPORT_ENABLED, "true"); + appRealm.updateComponent(ctx.getLdapModel()); }); } + @Test + public void test10DisabledUserSwitchedToEnabledOnMSAD() { + // disable user johnkeycloak via REST API - should be disabled in MSAD as well. + UserResource john = ApiUtil.findUserByUsernameId(adminClient.realm("test"), "johnkeycloak"); + UserRepresentation johnRep = john.toRepresentation(); + johnRep.setEnabled(false); + john.update(johnRep); + + Assert.assertFalse(isJohnEnabledInMSAD()); + + // Login as johnkeycloak and see the user is disabled. + loginPage.open(); + loginPage.login("johnkeycloak", "Password1"); + Assert.assertEquals("Account is disabled, contact your administrator.", loginPage.getError()); + + // enable user johnkeycloak in MSAD only + testingClient.server().run(session -> { + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + + LDAPObject ldapJohn = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johnkeycloak"); + String userAccountControlStr = ldapJohn.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL); + UserAccountControl control = new UserAccountControl(Long.parseLong(userAccountControlStr)); + control.remove(UserAccountControl.ACCOUNTDISABLE); + ldapJohn.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, String.valueOf(control.getValue())); + ctx.getLdapProvider().getLdapIdentityStore().update(ldapJohn); + }); + + Assert.assertTrue(isJohnEnabledInMSAD()); + + // Login again. User should be enabled. + loginPage.open(); + loginPage.login("johnkeycloak", "Password1"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + private long getPwdLastSetOfJohn() { String pwdLastSett = testingClient.server().fetchString(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestContext.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestContext.java index e724296e2d8d..a7f263fe2c65 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestContext.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestContext.java @@ -34,8 +34,12 @@ public class LDAPTestContext { private final LDAPStorageProvider ldapProvider; public static LDAPTestContext init(KeycloakSession session) { + return init(session, null); + } + + public static LDAPTestContext init(KeycloakSession session, String providerName) { RealmModel testRealm = session.realms().getRealmByName(AbstractLDAPTest.TEST_REALM_NAME); - ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(testRealm); + ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(testRealm, providerName); UserStorageProviderModel ldapModel = new UserStorageProviderModel(ldapCompModel); LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); return new LDAPTestContext(testRealm, ldapModel, ldapProvider); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserProfileTest.java index b99d2c23298b..dd0ecf15d87a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserProfileTest.java @@ -29,6 +29,8 @@ import org.junit.Test; import org.junit.runners.MethodSorters; import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.PrioritizedComponentModel; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -38,6 +40,8 @@ import org.keycloak.representations.userprofile.config.UPAttributePermissions; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.LoginUpdateProfilePage; @@ -68,7 +72,7 @@ protected LDAPRule getLDAPRule() { @Override protected void afterImportTestRealm() { testingClient.server().run(session -> { - LDAPTestContext ctx = LDAPTestContext.init(session); + LDAPTestContext ctx = LDAPTestContext.init(session, "test-ldap"); RealmModel appRealm = ctx.getRealm(); UserModel user = LDAPTestUtils.addLocalUser(session, appRealm, "marykeycloak", "mary@test.com", "Password1"); @@ -218,7 +222,20 @@ public void testUserProfileWithReadOnlyLdapLocalUser() { @Test public void testUserProfileWithoutImport() { setLDAPImportDisabled(); + UPConfig origConfig = testRealm().users().userProfile().getConfiguration(); try { + UPConfig config = testRealm().users().userProfile().getConfiguration(); + // Set postal code + UPAttribute postalCode = new UPAttribute(); + postalCode.setName("postal_code"); + postalCode.setDisplayName("Postal Code"); + + UPAttributePermissions permissions = new UPAttributePermissions(); + permissions.setView(Set.of(UPConfigUtils.ROLE_USER, UPConfigUtils.ROLE_ADMIN)); + permissions.setEdit(Set.of(UPConfigUtils.ROLE_USER, UPConfigUtils.ROLE_ADMIN)); + postalCode.setPermissions(permissions); + config.getAttributes().add(postalCode); + testRealm().users().userProfile().update(config); // Test local user is writable and has only attributes defined explicitly in user-profile // Test user profile of user johnkeycloak in admin API UserResource johnResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak2"); @@ -229,12 +246,56 @@ public void testUserProfileWithoutImport() { assertProfileAttributes(john, USER_METADATA_GROUP, true, LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN); } finally { setLDAPImportEnabled(); + testRealm().users().userProfile().update(origConfig); + } + } + + @Test + public void testMultipleLDAPProviders() { + testingClient.server().run(session -> { + RealmModel testRealm = session.realms().getRealmByName(AbstractLDAPTest.TEST_REALM_NAME); + ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(testRealm); + UserStorageProviderModel ldapModel = new UserStorageProviderModel(ldapCompModel); + ldapModel.setId(null); + ldapModel.setParentId(null); + ldapModel.setName("other-ldap"); + ldapModel.put(LDAPConstants.USERS_DN, ldapModel.getConfig().getFirst(LDAPConstants.USERS_DN).replace("People", "OtherPeople")); + ldapCompModel.put(PrioritizedComponentModel.PRIORITY, "100"); + testRealm.addComponentModel(ldapModel); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPObject john = LDAPTestUtils.addLDAPUser(ldapProvider, testRealm, "anotherjohn", "AnotherJohn", "AnotherDoe", "anotherjohn@email.org", null, "1234"); + LDAPTestUtils.updateLDAPPassword(ldapProvider, john, "Password1"); + }); + + // the provider for this user does not have postal_code mapper + UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), "anotherjohn"); + UserRepresentation userRep = userResource.toRepresentation(true); + Assert.assertNull(userRep.getAttributes().get("postal_code")); + + // the provider for this user does have postal_code mapper + userResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak"); + userRep = userResource.toRepresentation(true); + Assert.assertNotNull(userRep.getAttributes().get("postal_code")); + + setLDAPReadOnly(); + try { + // the second provider is not readonly + userResource = ApiUtil.findUserByUsernameId(testRealm(), "anotherjohn"); + userRep = userResource.toRepresentation(true); + assertProfileAttributes(userRep, null, false, "username", "email", "firstName", "lastName"); + + // the original provider is readonly + userResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak"); + userRep = userResource.toRepresentation(true); + assertProfileAttributes(userRep, null, true, "username", "email", "firstName", "lastName", "postal_code"); + } finally { + setLDAPWritable(); } } private void setLDAPReadOnly() { testingClient.server().run(session -> { - LDAPTestContext ctx = LDAPTestContext.init(session); + LDAPTestContext ctx = LDAPTestContext.init(session, "test-ldap"); RealmModel appRealm = ctx.getRealm(); ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.toString()); @@ -244,7 +305,7 @@ private void setLDAPReadOnly() { private void setLDAPWritable() { testingClient.server().run(session -> { - LDAPTestContext ctx = LDAPTestContext.init(session); + LDAPTestContext ctx = LDAPTestContext.init(session, "test-ldap"); RealmModel appRealm = ctx.getRealm(); ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString()); @@ -254,7 +315,7 @@ private void setLDAPWritable() { private void setLDAPImportDisabled() { testingClient.server().run(session -> { - LDAPTestContext ctx = LDAPTestContext.init(session); + LDAPTestContext ctx = LDAPTestContext.init(session, "test-ldap"); RealmModel appRealm = ctx.getRealm(); ctx.getLdapModel().getConfig().putSingle(IMPORT_ENABLED, "false"); @@ -264,7 +325,7 @@ private void setLDAPImportDisabled() { private void setLDAPImportEnabled() { testingClient.server().run(session -> { - LDAPTestContext ctx = LDAPTestContext.init(session); + LDAPTestContext ctx = LDAPTestContext.init(session, "test-ldap"); RealmModel appRealm = ctx.getRealm(); ctx.getLdapModel().getConfig().putSingle(IMPORT_ENABLED, "true"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java index 59dbe39550ab..38ce45b54a74 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java @@ -60,6 +60,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE; @@ -565,10 +566,8 @@ public void testAlternativeNonInteractiveExecutorInSubflow() { WebElement aHref = driver.findElement(By.tagName("a")); driver.get(aHref.getAttribute("href")); // Waiting for account redirection from app page - driver.wait(1000); + waitForPage(driver, "Account Management", true); assertThat(driver.getTitle(), containsString("Account Management")); - } catch (Throwable t) { - t.printStackTrace(); } finally { revertFlows("browser - alternative non-interactive executor"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java index 7e4715e270e8..1a9ba2bbc67c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java @@ -21,26 +21,36 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.AuthenticationFlow; +import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.Profile; import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.Constants; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -48,19 +58,27 @@ import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.account.AccountRestClient; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.DisableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; import org.keycloak.testsuite.client.KeycloakTestingClient; +import org.keycloak.testsuite.pages.DeleteCredentialPage; import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.PushTheButtonPage; +import org.keycloak.testsuite.pages.SelectAuthenticatorPage; +import org.keycloak.testsuite.pages.SetupRecoveryAuthnCodesPage; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.FlowUtil; import org.keycloak.testsuite.util.OAuthClient; @@ -70,14 +88,19 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.keycloak.common.Profile.Feature.RECOVERY_CODES; +import static org.keycloak.testsuite.actions.AppInitiatedActionDeleteCredentialTest.getKcActionParamForDeleteCredential; /** * Tests for Level Of Assurance conditions in authentication flow. * * @author Sebastian Zoescher */ +@EnableFeature(value = RECOVERY_CODES, skipRestart = true) public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { + private static final String FLOW_NAME = ""; + @Rule public AssertEvents events = new AssertEvents(this); @@ -87,6 +110,15 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { @Page protected LoginTotpPage loginTotpPage; + @Page + protected LoginConfigTotpPage totpSetupPage; + + @Page + protected SetupRecoveryAuthnCodesPage setupRecoveryAuthnCodesPage; + + @Page + protected SelectAuthenticatorPage selectAuthenticatorPage; + private TimeBasedOTP totp = new TimeBasedOTP(); @Page @@ -95,6 +127,9 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { @Page protected ErrorPage errorPage; + @Page + protected DeleteCredentialPage deleteCredentialPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { try { @@ -109,6 +144,24 @@ public void configureTestRealm(RealmRepresentation testRealm) { } } + @Before + public void beforeTest() { + UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost"); + UserRepresentation userRep = user.toRepresentation(); + user.remove(); + + userRep.setId(null); + UserBuilder.edit(userRep) + .password("password") + .totpSecret("totpSecret") + .otpEnabled(); + Response response = testRealm().users().create(userRep); + Assert.assertEquals(201, response.getStatus()); + response.close(); + + oauth.kcAction(null); + } + private String getAcrToLoaMappingForClient() throws IOException { Map acrLoaMap = new HashMap<>(); acrLoaMap.put("copper", 0); @@ -145,7 +198,7 @@ private static void configureStepUpFlow(KeycloakTestingClient testingClient, int testingClient.server(TEST_REALM_NAME) .run(session -> FlowUtil.inCurrentRealm(session).selectFlow(newFlowAlias).inForms(forms -> forms.clear() // level 1 authentication - .addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> { + .addSubFlowExecution("level1-subflow", AuthenticationFlow.BASIC_FLOW, Requirement.CONDITIONAL, subFlow -> { subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, config -> { config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1"); @@ -157,7 +210,7 @@ private static void configureStepUpFlow(KeycloakTestingClient testingClient, int }) // level 2 authentication - .addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> { + .addSubFlowExecution("level2-subflow", AuthenticationFlow.BASIC_FLOW, Requirement.CONDITIONAL, subFlow -> { subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, config -> { config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2"); @@ -169,7 +222,7 @@ private static void configureStepUpFlow(KeycloakTestingClient testingClient, int }) // level 3 authentication - .addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> { + .addSubFlowExecution("level3-subflow", AuthenticationFlow.BASIC_FLOW, Requirement.CONDITIONAL, subFlow -> { subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, config -> { config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3"); @@ -188,6 +241,21 @@ private void reconfigureStepUpFlow(int maxAge1, int maxAge2, int maxAge3) { configureStepUpFlow(testingClient, maxAge1, maxAge2, maxAge3); } + private static void configureFlowsWithRecoveryCodes(KeycloakTestingClient testingClient) { + final String newFlowAlias = "browser - Level of Authentication FLow"; + + testingClient.server(TEST_REALM_NAME) + .run(session -> { + FlowUtil.inCurrentRealm(session).selectFlow(newFlowAlias).inForms(forms -> + // Remove "OTP" required execution + forms.selectFlow("level2-subflow") + .removeExecution(1) + .addAuthenticatorExecution(Requirement.ALTERNATIVE, OTPFormAuthenticatorFactory.PROVIDER_ID) + .addAuthenticatorExecution(Requirement.ALTERNATIVE, RecoveryAuthnCodesFormAuthenticatorFactory.PROVIDER_ID) + ); + }); + } + @After public void after() { BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow"); @@ -669,6 +737,172 @@ public void testDisableStepupFeatureInNewRealm() { } } + @Test + public void testWithMultipleOTPCodes() throws Exception { + // Get regular authentication. Only level1 required. + oauth.openLoginForm(); + // Authentication without specific LOA results in level 1 authentication + authenticateWithUsernamePassword(); + TokenCtx token1 = assertLoggedInWithAcr("silver"); + + // Add "kc_action" for setup another OTP. Existing OTP authentication should be required. No offer for recovery-codes as they are different level + oauth.kcAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); + oauth.openLoginForm(); + loginTotpPage.assertCurrent(); + loginTotpPage.assertOtpCredentialSelectorAvailability(false); + + authenticateWithTotp(); + totpSetupPage.assertCurrent(); + totpSetupPage.configure(totp.generateTOTP(totpSetupPage.getTotpSecret()), "totp2-label"); + events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + TokenCtx token2 = assertLoggedInWithAcr("gold"); + + // Trying to add another OTP by "kc_action". Level 2 should be required and user can choose between 2 OTP codes + oauth.kcAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); + oauth.openLoginForm(); + loginTotpPage.assertCurrent(); + loginTotpPage.assertOtpCredentialSelectorAvailability(true); + List availableOtps = loginTotpPage.getAvailableOtpCredentials(); + Assert.assertNames(availableOtps, OTPFormAuthenticator.UNNAMED, "totp2-label"); + + // Removing 2nd OTP by account REST API with regular token. Should fail as acr=2 is required + String otpCredentialId; + try (AccountRestClient accountRestClient = AccountRestClient + .builder(suiteContext) + .accessToken(token1.accessToken) + .build()) { + otpCredentialId = accountRestClient.getCredentialByUserLabel("totp2-label").getId(); + try (SimpleHttp.Response response = accountRestClient.removeCredential(otpCredentialId)) { + Assert.assertEquals(403, response.getStatus()); + } + } + + // Removing 2nd OTP by account REST API with level2 token. Should work as acr=2 is required + try (AccountRestClient accountRestClient = AccountRestClient + .builder(suiteContext) + .accessToken(token2.accessToken) + .build()) { + otpCredentialId = accountRestClient.getCredentialByUserLabel("totp2-label").getId(); + try (SimpleHttp.Response response = accountRestClient.removeCredential(otpCredentialId)) { + Assert.assertEquals(204, response.getStatus()); + } + Assert.assertNull(accountRestClient.getCredentialByUserLabel("totp2-label")); + } + } + + @Test + public void testDeleteCredentialAction() throws Exception { + // Login level1 + oauth.openLoginForm(); + authenticateWithUsernamePassword(); + TokenCtx token1 = assertLoggedInWithAcr("silver"); + + // Setup another OTP (requires login with existing OTP) + oauth.kcAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); + oauth.openLoginForm(); + authenticateWithTotp(); + totpSetupPage.assertCurrent(); + totpSetupPage.configure(totp.generateTOTP(totpSetupPage.getTotpSecret()), "totp2-label"); + events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + TokenCtx token2 = assertLoggedInWithAcr("gold"); + + String otp2CredentialId = getCredentialIdByLabel("totp2-label"); + + // Delete OTP credential requires level2. Re-authentication is required (because of max_age=0 for level2 evaluated during re-authentication) + oauth.kcAction(getKcActionParamForDeleteCredential(otp2CredentialId)); + oauth.openLoginForm(); + loginTotpPage.assertCurrent(); + authenticateWithTotp(); + + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage("totp2-label"); + deleteCredentialPage.confirm(); + + events.expectRequiredAction(EventType.REMOVE_TOTP).assertEvent(); + assertLoggedInWithAcr("gold"); + } + + @Test + public void testWithOTPAndRecoveryCodesAtLevel2() { + configureFlowsWithRecoveryCodes(testingClient); + try { + // Get regular authentication. Only level1 required. + oauth.openLoginForm(); + authenticateWithUsernamePassword(); + TokenCtx token1 = assertLoggedInWithAcr("silver"); + + // Trying to delete existing OTP. Should require authentication with this OTP + String otpCredentialId = getCredentialIdByType(OTPCredentialModel.TYPE); + oauth.kcAction(getKcActionParamForDeleteCredential(otpCredentialId)); + oauth.openLoginForm(); + Assert.assertEquals("Strong authentication required to continue", loginPage.getInfoMessage()); + authenticateWithTotp(); + + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage("otp"); + deleteCredentialPage.confirm(); + events.expectRequiredAction(EventType.REMOVE_TOTP).assertEvent(); + assertLoggedInWithAcr("gold"); + + // Trying to add OTP. No 2nd factor should be required as user doesn't have any + oauth.kcAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); + oauth.openLoginForm(); + totpSetupPage.assertCurrent(); + String totp2Secret = totpSetupPage.getTotpSecret(); + totpSetupPage.configure(totp.generateTOTP(totp2Secret), "totp2-label"); + events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + assertLoggedInWithAcr("silver"); + + // set time offset for OTP as it is not permitted to authenticate with same OTP code multiple times + setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp); + + // Add "kc_action" for setup recovery codes. OTP should be required + oauth.kcAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name()); + oauth.openLoginForm(); + loginTotpPage.assertCurrent(); + loginTotpPage.login(totp.generateTOTP(totp2Secret)); + setupRecoveryAuthnCodesPage.assertCurrent(); + setupRecoveryAuthnCodesPage.clickSaveRecoveryAuthnCodesButton(); + events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).assertEvent(); + assertLoggedInWithAcr("gold"); + + // Removing recovery-code credential. User required to authenticate with 2nd-factor. He can choose between OTP or recovery-codes + String recoveryCodesId = getCredentialIdByType(RecoveryAuthnCodesCredentialModel.TYPE); + oauth.kcAction(getKcActionParamForDeleteCredential(recoveryCodesId)); + oauth.openLoginForm(); + loginTotpPage.assertCurrent(); + loginTotpPage.clickTryAnotherWayLink(); + selectAuthenticatorPage.assertCurrent(); + Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION, SelectAuthenticatorPage.RECOVERY_AUTHN_CODES), selectAuthenticatorPage.getAvailableLoginMethods()); + selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION); + loginTotpPage.assertCurrent(); + loginTotpPage.login(totp.generateTOTP(totp2Secret)); + + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage("Recovery codes"); + deleteCredentialPage.confirm(); + events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).assertEvent(); + assertLoggedInWithAcr("gold"); + } finally { + setOtpTimeOffset(0, totp); + } + } + + private String getCredentialIdByLabel(String credentialLabel) { + return ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost").credentials() + .stream() + .filter(credential -> "totp2-label".equals(credential.getUserLabel())) + .map(CredentialRepresentation::getId) + .findFirst().orElseThrow(() -> new IllegalStateException("Did not found credential with label " + credentialLabel)); + } + + private String getCredentialIdByType(String credentialType) { + return ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost").credentials() + .stream() + .filter(credential -> credentialType.equals(credential.getType())) + .map(CredentialRepresentation::getId) + .findFirst().orElseThrow(() -> new IllegalStateException("Did not found credential with OTP type on the user")); + } public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) { openLoginFormWithAcrClaim(oauth, essential, acrValues); @@ -707,10 +941,12 @@ private void authenticateWithButton() { pushTheButtonPage.submit(); } - private void assertLoggedInWithAcr(String acr) { + private TokenCtx assertLoggedInWithAcr(String acr) { EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); - IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); + OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); + IDToken idToken = oauth.verifyIDToken(tokenResponse.getIdToken()); Assert.assertEquals(acr, idToken.getAcr()); + return new TokenCtx(tokenResponse.getAccessToken(), idToken); } private void assertErrorPage(String expectedError) { @@ -718,4 +954,14 @@ private void assertErrorPage(String expectedError) { Assert.assertEquals(expectedError, errorPage.getError()); events.clear(); } + + private class TokenCtx { + private String accessToken; + private IDToken idToken; + + private TokenCtx(String accessToken, IDToken idToken) { + this.accessToken = accessToken; + this.idToken = idToken; + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java new file mode 100644 index 000000000000..fa1f0598849b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.forms; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.models.BrowserSecurityHeaders; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.LogoutToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * + * @author rmartinc + */ +public class RPInitiatedFrontChannelLogoutTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + // no-op + } + + @Test + public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(OAuthClient.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + + Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogoutWithoutSessionRequired() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "false"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(OAuthClient.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + + Assert.assertNull(logoutToken.getIssuer()); + Assert.assertNull(logoutToken.getSid()); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "true"); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogout() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setName("My Testing App"); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + org.keycloak.testsuite.Assert.assertNotNull(logoutToken); + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + org.keycloak.testsuite.Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + org.keycloak.testsuite.Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + Assert.assertTrue(driver.getTitle().equals("Logging out")); + Assert.assertTrue(driver.getPageSource().contains("You are logging out from following apps")); + Assert.assertTrue(driver.getPageSource().contains("My Testing App")); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogoutCustomCSP() throws Exception { + try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(adminClient.realm(oauth.getRealm())) + .setBrowserSecurityHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getKey(), + "frame-src 'keycloak.org'; frame-ancestors 'self'; object-src 'none'; style-src 'self';") + .update(); + ClientAttributeUpdater clientUpdater = ClientAttributeUpdater.forClient(adminClient, oauth.getRealm(), oauth.getClientId()) + .setName("My Testing App") + .setFrontchannelLogout(true) + .setAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout") + .update()) { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + Assert.assertTrue(driver.getTitle().equals("Logging out")); + Assert.assertTrue(driver.getPageSource().contains("You are logging out from following apps")); + Assert.assertTrue(driver.getPageSource().contains("My Testing App")); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java index 24207a2d7157..9b1a36c8538a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java @@ -28,10 +28,13 @@ import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.Test; +import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -302,6 +305,67 @@ public void identityFirstFormReauthenticationWithGithubLink() { BrowserFlowTest.revertFlows(testRealm(), "browser - identity first"); } + @Test + public void restartLoginWithNewRootAuthSession() { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password"); + + oauth.prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN); + loginPage.open(); + loginPage.clickResetLogin(); + loginPage.login("john-doh@localhost", "password"); + + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password"); + + + AccessToken accessToken1 = oauth.verifyToken(response1.getAccessToken()); + AccessToken accessToken2 = oauth.verifyToken(response2.getAccessToken()); + + Assert.assertNotEquals(accessToken1.getSubject(), accessToken2.getSubject()); + Assert.assertNotEquals(accessToken1.getSessionId(), accessToken2.getSessionId()); + } + + @Test + public void loginAfterExpiredUserSession() { + RealmRepresentation rep = testRealm().toRepresentation(); + Integer originalSsoSessionIdleTimeout = rep.getSsoSessionIdleTimeout(); + Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan(); + + rep.setSsoSessionIdleTimeout(10); + rep.setSsoSessionMaxLifespan(10); + realmsResouce().realm(rep.getRealm()).update(rep); + + loginPage.open(); + driver.navigate().refresh(); + loginPage.login("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password"); + + //set time offset after user session expiration (10s) but before accessCodeLifespanLogin (1800s) and accessCodeLifespan (60s) + setTimeOffset(20); + + loginPage.open(); + loginPage.login("john-doh@localhost", "password"); + + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password"); + + AccessToken accessToken1 = oauth.verifyToken(response1.getAccessToken()); + AccessToken accessToken2 = oauth.verifyToken(response2.getAccessToken()); + + Assert.assertNotEquals(accessToken1.getSubject(), accessToken2.getSubject()); + Assert.assertNotEquals(accessToken1.getSessionId(), accessToken2.getSessionId()); + + setTimeOffset(0); + rep.setSsoSessionIdleTimeout(originalSsoSessionIdleTimeout); + rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan); + realmsResouce().realm(rep.getRealm()).update(rep); + } + private void setupIdentityFirstFlow() { String newFlowAlias = "browser - identity first"; testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java index d0c4a84954ea..8d8ccd88579d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java @@ -442,46 +442,46 @@ public void testAttributeGrouping() { registerPage.assertCurrent(); String htmlFormId="kc-register-form"; - //assert fields and groups location in form, attributes without a group are the last + //assert fields and groups location in form, attributes without a group appear first Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(1) > label#header-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > label#description-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username") ).isDisplayed() ); + // password and password confirmation fields appear after the username field, in positions 3 and 4 Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#department") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#firstName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(1) > label#header-contact") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#email") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > label#description-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#lastName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#department") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > input#username") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(8) > div:nth-child(1) > label#header-contact") ).isDisplayed() ); - // firstname order is after username, so it will render after password and password confirmation fields Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(9) > div:nth-child(2) > input#firstName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(9) > div:nth-child(2) > input#email") ).isDisplayed() ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java index 228abc1441e5..88f403ad6430 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java @@ -18,12 +18,20 @@ package org.keycloak.testsuite.forms; import jakarta.ws.rs.core.Response; + import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; + import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.TokenCategory; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.jose.jws.JWSBuilder; @@ -32,17 +40,26 @@ import org.keycloak.keys.KeyProvider; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ParConfig; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.DefaultKeyProviders; import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.TokenUtil; import org.openqa.selenium.Cookie; +import javax.crypto.SecretKey; + +import static org.junit.Assert.assertEquals; + /** * @author Marek Posolda */ @@ -76,6 +93,16 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest { " }\n" + "}"; + public static final Set sensitiveNotes = new HashSet<>(); + static { + sensitiveNotes.add(OAuth2Constants.CLIENT_ASSERTION_TYPE); + sensitiveNotes.add(OAuth2Constants.CLIENT_ASSERTION); + sensitiveNotes.add(OAuth2Constants.CLIENT_SECRET); + sensitiveNotes.add(AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + OAuth2Constants.CLIENT_ASSERTION_TYPE); + sensitiveNotes.add(AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + OAuth2Constants.CLIENT_ASSERTION); + sensitiveNotes.add(AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + OAuth2Constants.CLIENT_SECRET); + } + @Override public void configureTestRealm(RealmRepresentation testRealm) { } @@ -99,6 +126,67 @@ protected void afterAbstractKeycloakTestRealmImport() { } } + @Test + public void testRestartCookie() { + loginPage.open(); + String restartCookie = loginPage.getDriver().manage().getCookieNamed(RestartLoginCookie.KC_RESTART).getValue(); + assertRestartCookie(restartCookie); + } + + @Test + public void testRestartCookieWithPar() { + String clientId = "par-confidential-client"; + adminClient.realm("test").clients().create(ClientBuilder.create() + .clientId("par-confidential-client") + .secret("secret") + .redirectUris(oauth.getRedirectUri() + "/*") + .attribute(ParConfig.REQUIRE_PUSHED_AUTHORIZATION_REQUESTS, "true") + .build()); + + oauth.clientId(clientId); + String requestUri = null; + try { + OAuthClient.ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, "secret"); + assertEquals(201, pResp.getStatusCode()); + requestUri = pResp.getRequestUri(); + } + catch (Exception e) { + Assert.fail(); + } + + oauth.redirectUri(null); + oauth.scope(null); + oauth.responseType(null); + oauth.requestUri(requestUri); + String state = oauth.stateParamRandom().getState(); + oauth.stateParamHardcoded(state); + + oauth.openLoginForm(); + String restartCookie = loginPage.getDriver().manage().getCookieNamed(RestartLoginCookie.KC_RESTART).getValue(); + assertRestartCookie(restartCookie); + } + + private void assertRestartCookie(String restartCookie) { + getTestingClient() + .server(TEST_REALM_NAME) + .run(session -> + { + try { + String sigAlgorithm = session.tokens().signatureAlgorithm(TokenCategory.INTERNAL); + String encAlgorithm = session.tokens().cekManagementAlgorithm(TokenCategory.INTERNAL); + SecretKey encKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.ENC, encAlgorithm).getSecretKey(); + SecretKey signKey = session.keys().getActiveKey(session.getContext().getRealm(), KeyUse.SIG, sigAlgorithm).getSecretKey(); + + byte[] contentBytes = TokenUtil.jweDirectVerifyAndDecode(encKey, signKey, restartCookie); + String jwt = new String(contentBytes, StandardCharsets.UTF_8); + RestartLoginCookie restartLoginCookie = session.tokens().decode(jwt, RestartLoginCookie.class); + Assert.assertFalse(restartLoginCookie.getNotes().keySet().stream().anyMatch(sensitiveNotes::contains)); + } catch (Exception e) { + Assert.fail(); + } + }); + } + // KEYCLOAK-5440 -- migration from Keycloak 3.1.0 @Test public void testRestartCookieBackwardsCompatible_Keycloak25() throws IOException { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java index e99342b9a202..5c60803e2655 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java @@ -220,45 +220,45 @@ public void testAttributeGrouping() { verifyProfilePage.assertCurrent(); String htmlFormId="kc-update-profile-form"; - //assert fields and groups location in form, attributes without a group are the last + //assert fields and groups location in form, attributes without a group appear first Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(1) > label#header-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > label#description-company") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#department") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(1) > label#header-contact") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#email") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#lastName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > input#username") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact") ).isDisplayed() ); Assert.assertTrue( driver.findElement( - By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#firstName") + By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email") ).isDisplayed() ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/FallbackKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/FallbackKeyProviderTest.java index 5212318c45e6..ba11ae46afec 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/FallbackKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/FallbackKeyProviderTest.java @@ -82,7 +82,7 @@ public void fallbackAfterDeletingAllKeysInRealm() { Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); providers = realmsResouce().realm("test").components().query(realmId, "org.keycloak.keys.KeyProvider"); - assertProviders(providers, "fallback-RS256", "fallback-" + Constants.INTERNAL_SIGNATURE_ALGORITHM); + assertProviders(providers, "fallback-RS256", "fallback-AES", "fallback-" + Constants.INTERNAL_SIGNATURE_ALGORITHM); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index f7f557f92f43..f2d13c788cc4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -411,6 +411,7 @@ protected void testMigrationTo24_0_0(boolean testUserProfileMigration, boolean t testHS512KeyCreated(migrationRealm); testHS512KeyCreated(migrationRealm2); testClientAttributes(migrationRealm); + testDeleteCredentialActionAvailable(migrationRealm); } if (testLdapUseTruststoreSpiMigration) { testLdapUseTruststoreSpiMigration(migrationRealm2); @@ -931,6 +932,8 @@ private void testRequiredActionsPriority(RealmResource... realms) { for (RequiredActionProviderRepresentation action : actions) { if (action.getAlias().equals("update_user_locale")) { assertEquals(1000, action.getPriority()); + } else if (action.getAlias().equals("delete_credential")) { + assertEquals(100, action.getPriority()); } else { assertEquals(priority, action.getPriority()); } @@ -1272,4 +1275,15 @@ private void testClientAttributes(RealmResource realm) { .collect(Collectors.toList()); Assert.assertEquals(Collections.singletonList(client.getClientId()), clientIds); } + + private void testDeleteCredentialActionAvailable(RealmResource realm) { + RequiredActionProviderRepresentation rep = realm.flows().getRequiredAction("delete_credential"); + assertNotNull(rep); + assertEquals("delete_credential", rep.getAlias()); + assertEquals("delete_credential", rep.getProviderId()); + assertEquals("Delete Credential", rep.getName()); + assertEquals(100, rep.getPriority()); + assertTrue(rep.isEnabled()); + assertFalse(rep.isDefaultAction()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java index 5e8a75debfed..3620e884177b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java @@ -17,11 +17,13 @@ package org.keycloak.testsuite.oauth; +import jakarta.ws.rs.core.Response.Status; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.common.Profile; @@ -65,6 +67,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; @@ -179,6 +182,7 @@ public static void setupRealm(KeycloakSession session) { directLegal.setSecret("secret"); directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); directLegal.setFullScopeAllowed(false); + directLegal.addRedirectUri(OAuthClient.APP_ROOT + "/auth"); ClientModel directPublic = realm.addClient("direct-public"); directPublic.setClientId("direct-public"); @@ -1017,6 +1021,36 @@ public void testExchangeWithDynamicScopesEnabled() throws Exception { testExchange(); } + @Test + public void testSupportedTokenTypesWhenValidatingSubjectToken() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + oauth.realm(TEST); + oauth.clientId("direct-legal"); + oauth.scope(OAuth2Constants.SCOPE_OPENID); + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, oauth.APP_ROOT + "/admin/backchannelLogout"); + getCleanup().addCleanup(() -> { + rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, ""); + clients.get(rep.getId()).update(rep); + }); + clients.get(rep.getId()).update(rep); + String logoutToken; + oauth.clientSessionState("client-session"); + oauth.doLogin("user", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + logoutToken = testingClient.testApp().getBackChannelRawLogoutToken(); + Assert.assertNotNull(logoutToken); + OAuthClient.AccessTokenResponse response = oauth.doTokenExchange(TEST, logoutToken, "target", "direct-legal", "secret"); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode()); + + } + private static void addDirectExchanger(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(TEST); RoleModel exampleRole = realm.addRole("example"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java index 86298bee50c6..528377eda97a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -40,7 +40,12 @@ import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.oidc.PkceGenerator; import org.keycloak.testsuite.runonserver.ServerVersion; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.RealmBuilder; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.net.URLEncoder; import java.util.Collections; @@ -200,8 +205,23 @@ public void checkIframeCache() throws IOException { } } + @Test + public void checkEmptyCsp() throws Exception { + try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(adminClient.realm("test")) + .setBrowserSecurityHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getKey(), "") + .update(); + Client client = AdminClientUtil.createResteasyClient(); + Response response = client.target(suiteContext.getAuthServerInfo().getContextRoot() + + "/auth/realms/test/protocol/openid-connect/login-status-iframe.html").request().get()) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNull(response.getHeaderString(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getKey())); + assertNull(response.getHeaderString(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName())); + } + } + @Override public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name("test").build()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java index 842b7b5f2f7a..313ffce5f1ff 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java @@ -351,12 +351,12 @@ public void testWildcard() throws IOException { checkRedirectUri("http://example.com/foo/../", false); checkRedirectUri("http://example.com/foo/%2E%2E/", false); // url-encoded "http://example.com/foobar/../" checkRedirectUri("http://example.com/foo%2F%2E%2E%2F", false); // url-encoded "http://example.com/foobar/../" - checkRedirectUri("http://example.com/foo/%252E%252E/", false); // double-encoded "http://example.com/foobar/../" - checkRedirectUri("http://example.com/foo/%252E%252E/?some_query_param=some_value", false); // double-encoded "http://example.com/foobar/../?some_query_param=some_value" - checkRedirectUri("http://example.com/foo/%252E%252E/?encodeTest=a%3Cb", false); // double-encoded "http://example.com/foobar/../?encodeTest=a access token without it and same refresh token + //refresh token without sending offline_access scope oauth.scope("phone"); - testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, true); + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); } @Test @@ -1289,4 +1289,26 @@ public void testClientOfflineSessionIdleTimeout() throws Exception { client.update(clientRepresentation); } } + + @Test + public void offlineTokenRefreshWithoutOfflineAccessScope() { + ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(false); + + try { + oauth.scope("openid " + OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + oauth.redirectUri(offlineClientAppUri); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "secret1"); + + oauth.scope("openid"); + response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1"); + assertEquals(200, response.getStatusCode()); + } + finally { + ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(true); + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java index a56ebeee1ccc..e5b9bfe16c93 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java @@ -28,18 +28,14 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; -import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.UriUtils; import org.keycloak.events.Details; import org.keycloak.events.Errors; -import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.representations.IDToken; -import org.keycloak.representations.LogoutToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.Assert; @@ -1032,98 +1028,6 @@ public void testIncorrectChangingParameters() throws IOException { events.expectLogoutError(Errors.LOGOUT_FAILED).assertEvent(); } - - @Test - public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) - .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - - IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); - - Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); - Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - clients.get(rep.getId()).update(rep); - } - } - - @Test - public void testFrontChannelLogoutWithoutSessionRequired() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "false"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) - .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - - Assert.assertNull(logoutToken.getIssuer()); - Assert.assertNull(logoutToken.getSid()); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "true"); - clients.get(rep.getId()).update(rep); - } - } - - @Test - public void testFrontChannelLogout() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setName("My Testing App"); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); - Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); - Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); - assertTrue(driver.getTitle().equals("Logging out")); - assertTrue(driver.getPageSource().contains("You are logging out from following apps")); - assertTrue(driver.getPageSource().contains("My Testing App")); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - clients.get(rep.getId()).update(rep); - } - } - @Test public void logoutWithIdTokenAndDisabledClientMustWork() throws Exception { OAuthClient.AccessTokenResponse tokenResponse = loginUser(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java index 9ff1a5cfa7ca..13a593b16a4f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java @@ -123,7 +123,7 @@ public void userInfoCorsInvalidSession() throws Exception { // remove the session in keycloak AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); - adminClient.realm("test").deleteSession(accessToken.getSessionState()); + adminClient.realm("test").deleteSession(accessToken.getSessionState(), false); try (ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient()) { WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java index edfd7a8087b7..961c17d8d7d9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java @@ -52,7 +52,7 @@ public void testThemeFallback() { // Fallback to default theme when requested theme don't exists Theme theme = session.theme().getTheme("address", Theme.Type.ADMIN); Assert.assertNotNull(theme); - Assert.assertEquals("keycloak.v2", theme.getName()); + Assert.assertEquals("primesign.v2", theme.getName()); } catch (IOException e) { Assert.fail(e.getMessage()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java index b090a611a4e7..574155bcef9d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java @@ -29,6 +29,7 @@ import static org.junit.Assert.fail; import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN; import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER; +import static org.keycloak.userprofile.config.UPConfigUtils.parseSystemDefaultConfig; import jakarta.ws.rs.core.Response; import java.util.ArrayList; @@ -47,6 +48,7 @@ import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.Profile.Feature; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; import org.keycloak.models.Constants; @@ -233,6 +235,48 @@ private static void testCustomAttributeInAnyContext(KeycloakSession session) { profile.validate(); } + @Test + public void testEmptyAttributeRemoved() { + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testEmptyAttributeRemoved); + } + + private static void testEmptyAttributeRemoved(KeycloakSession session) { + Map attributes = new HashMap<>(); + + attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId()); + attributes.put(UserModel.FIRST_NAME, "John"); + attributes.put(UserModel.LAST_NAME, "Doe"); + attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org"); + attributes.put("address", "foo"); + + UserProfileProvider provider = getUserProfileProvider(session); + UPConfig config = UPConfigUtils.parseSystemDefaultConfig(); + config.addOrReplaceAttribute(new UPAttribute("address", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER)))); + provider.setConfiguration(config); + + UserProfile profile = provider.create(UserProfileContext.USER_API, attributes); + UserModel user = profile.create(); + + // attribute explicitly set with an empty value so we assume it should be removed regardless the `removeAttributes` parameter being set to false + attributes.put("address", ""); + profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user); + profile.update(false); + + assertNull(user.getFirstAttribute("address")); + + attributes.put("address", "bar"); + profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user); + profile.update(); + assertEquals("bar", user.getFirstAttribute("address")); + + // attribute not provided so we assume there is no intention to remove the attribute + attributes.remove("address"); + profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user); + profile.update(false); + + assertNotNull(user.getFirstAttribute("address")); + } + @Test public void testResolveProfile() { getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResolveProfile); @@ -2244,4 +2288,19 @@ private static void testMultivalued(KeycloakSession session) { profile = provider.create(UserProfileContext.USER_API, attributes, user); profile.update(); } + + @Test + public void testDefaultConfigWhenComponentConfigIsNotSet() { + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDefaultConfigWhenComponentConfigIsNotSet); + } + + private static void testDefaultConfigWhenComponentConfigIsNotSet(KeycloakSession session) { + UserProfileProvider provider = getUserProfileProvider(session); + provider.setConfiguration(parseSystemDefaultConfig()); + RealmModel realm = session.getContext().getRealm(); + ComponentModel component = realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny().get(); + component.setConfig(new MultivaluedHashMap<>()); + realm.updateComponent(component); + provider.create(UserProfileContext.USER_API, Map.of()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java index 1859e8a5f6ec..6b00fd2a7cc1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java @@ -48,6 +48,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.clientpolicy.ClientPolicyEvent; import org.keycloak.services.clientpolicy.condition.ClientAccessTypeCondition; +import org.keycloak.services.clientpolicy.condition.ClientIdsCondition; import org.keycloak.services.clientpolicy.condition.ClientRolesCondition; import org.keycloak.services.clientpolicy.condition.ClientScopesCondition; import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextCondition; @@ -61,6 +62,7 @@ import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor; import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutor; import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutor; +import org.keycloak.services.clientpolicy.executor.RegexRedirectUriExecutor; import org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutor; import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutor; import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutor; @@ -241,6 +243,12 @@ public static SecureSigningAlgorithmExecutor.Configuration createSecureSigningAl return config; } + public static RegexRedirectUriExecutor.Configuration createRegexRedirectUriExecutorConfig(List regexPatterns){ + RegexRedirectUriExecutor.Configuration config = new RegexRedirectUriExecutor.Configuration(); + config.setRedirectUriRegexPatterns(regexPatterns); + return config; + } + public static SecureCibaAuthenticationRequestSigningAlgorithmExecutor.Configuration createSecureCibaAuthenticationRequestSigningAlgorithmExecutorConfig(String defaultAlgorithm) { SecureCibaAuthenticationRequestSigningAlgorithmExecutor.Configuration config = new SecureCibaAuthenticationRequestSigningAlgorithmExecutor.Configuration(); config.setDefaultAlgorithm(defaultAlgorithm); @@ -501,4 +509,10 @@ private static SignatureSignerContext createSignatureSignerContext(KeyWrapper ke throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType()); } } + + public static ClientIdsCondition.Configuration createClientIdsConditionConfig(List clientIds){ + ClientIdsCondition.Configuration config = new ClientIdsCondition.Configuration(); + config.setClientIds(clientIds); + return config; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/import/partial-authentication-flows-import.json b/testsuite/integration-arquillian/tests/base/src/test/resources/import/partial-authentication-flows-import.json new file mode 100644 index 000000000000..d1da7dbaacb6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/import/partial-authentication-flows-import.json @@ -0,0 +1,23 @@ +{ + "enabled": true, + "realm": "partial-authentication-flows-import", + "authenticationFlows": [ + { + "alias": "X.509 browser", + "description": "Browser-based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "requirement": "ALTERNATIVE", + "priority": 10, + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json index adf78ceabe95..a96370c969d4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json @@ -50,5 +50,10 @@ } ] }, + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port": "3025" + }, "eventsListeners": ["jboss-logging", "event-queue"] } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/ldap/users.ldif b/testsuite/integration-arquillian/tests/base/src/test/resources/ldap/users.ldif index 4df8b5e61a64..388686add060 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/ldap/users.ldif +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/ldap/users.ldif @@ -23,3 +23,8 @@ dn: ou=Groups,dc=keycloak,dc=org objectclass: top objectclass: organizationalUnit ou: Groups + +dn: ou=OtherPeople,dc=keycloak,dc=org +objectclass: top +objectclass: organizationalUnit +ou: People diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml index 4a6cdb684327..b47e9803dcf4 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-jboss diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml index 165ab3386bbf..80fe9cf9a07d 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-fuse61 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml index 012da49743bc..57604f50e6b3 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-fuse62 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml index e4eb4dc31252..db79a7fbf078 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-karaf3 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml index c11a2e159adf..c9158f4620a3 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-karaf diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index f64034d58652..74daa0e7a5c5 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml index 97a7a6f1f33a..ee461a99b593 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-was diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml index 6bb112f1325b..1ce769f028ac 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-was - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-was8 diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml index 4cdabe88b6ea..9bb61e946938 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-wls diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml index 925b66590116..1717f9bef0b4 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-wls - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-adapters-wls12 diff --git a/testsuite/integration-arquillian/tests/other/base-ui/pom.xml b/testsuite/integration-arquillian/tests/other/base-ui/pom.xml index 1ce0c878c4ba..98d44e654363 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/pom.xml +++ b/testsuite/integration-arquillian/tests/other/base-ui/pom.xml @@ -22,7 +22,7 @@ integration-arquillian-tests-other org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java index 01d623ad5543..ad6626ed3e00 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java @@ -21,6 +21,7 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.testsuite.pages.DeleteCredentialPage; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage; @@ -78,14 +79,18 @@ public static void testSetUpLink(RealmResource realmResource, SigningInPage.Cred assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false)); } - public static void testRemoveCredential(AbstractLoggedInPage accountPage, SigningInPage.UserCredential userCredential) { + public static void testRemoveCredential(AbstractLoggedInPage accountPage, DeleteCredentialPage deleteCredentialPage, SigningInPage.UserCredential userCredential) { int countBeforeRemove = userCredential.getCredentialType().getUserCredentialsCount(); + userCredential.clickRemoveBtn(); - testModalDialog(accountPage, userCredential::clickRemoveBtn, () -> { - assertThat(userCredential.isPresent(), is(true)); - assertThat(userCredential.getCredentialType().getUserCredentialsCount(), is(countBeforeRemove)); - }); - accountPage.alert().assertSuccess(); + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.cancel(); + accountPage.assertCurrent(); + assertThat(userCredential.isPresent(), is(true)); + assertThat(userCredential.getCredentialType().getUserCredentialsCount(), is(countBeforeRemove)); + userCredential.clickRemoveBtn(); + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.confirm(); assertThat(userCredential.isPresent(), is(false)); assertThat(userCredential.getCredentialType().getUserCredentialsCount(), is(countBeforeRemove - 1)); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java index b7cff99b593a..5ab8b088f67f 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java @@ -29,6 +29,7 @@ import org.keycloak.testsuite.admin.Users; import org.keycloak.testsuite.auth.page.login.OTPSetup; import org.keycloak.testsuite.auth.page.login.UpdatePassword; +import org.keycloak.testsuite.pages.DeleteCredentialPage; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage; import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; @@ -58,6 +59,9 @@ public class SigningInTest extends BaseAccountPageTest { @Page private UpdatePassword updatePasswordPage; + @Page + private DeleteCredentialPage deleteCredentialPage; + @Page private OTPSetup otpSetupPage; @@ -214,6 +218,6 @@ private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.Crede } private void testRemoveCredential(SigningInPage.UserCredential userCredential) { - SigningInPageUtils.testRemoveCredential(getAccountPage(), userCredential); + SigningInPageUtils.testRemoveCredential(getAccountPage(), deleteCredentialPage, userCredential); } } diff --git a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml index a4715a5937be..44b71021c76b 100644 --- a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml +++ b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-jpa-performance diff --git a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml index d6ce88890d05..a9aaf0d8992e 100644 --- a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml +++ b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-other-mod_auth_mellon diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml index 4e8f7f9a10de..e576f668eb81 100644 --- a/testsuite/integration-arquillian/tests/other/pom.xml +++ b/testsuite/integration-arquillian/tests/other/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests - 999.0.0-SNAPSHOT + 24.0.5-PS-2 integration-arquillian-tests-other diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml index cd3ea630c6d7..bb00ea6281b7 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-tests-other org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/tests/other/sssd/pom.xml b/testsuite/integration-arquillian/tests/other/sssd/pom.xml index abf95fde1c14..3df92b7bdb54 100644 --- a/testsuite/integration-arquillian/tests/other/sssd/pom.xml +++ b/testsuite/integration-arquillian/tests/other/sssd/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-tests-other org.keycloak.testsuite - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDUserProfileTest.java b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDUserProfileTest.java index 9d9ea49274e1..81965f78e2a4 100644 --- a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDUserProfileTest.java @@ -184,7 +184,8 @@ public void test04MixedSSSDUserProfile() throws Exception { String sssdId = getSssdProviderId(); UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), username); UserRepresentation user = userResource.toRepresentation(true); - assertUser(user, username, getEmail(username), getFirstName(username), getLastName(username), sssdId); + // first and last names are removed from the UP config (unmanaged) and are not available from the representation + assertUser(user, username, getEmail(username), null, null, sssdId); assertProfileAttributes(user, null, true, UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME); assertProfileAttributes(user, null, false, "postal_code"); diff --git a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml index 4d23a83daab8..6dd1dceb966e 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml +++ b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 jar diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java index ec85d259270e..41ebf8715723 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java @@ -36,6 +36,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.page.AbstractPatternFlyAlert; +import org.keycloak.testsuite.pages.DeleteCredentialPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage; import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; @@ -71,6 +72,9 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple @Page protected WebAuthnLoginPage webAuthnLoginPage; + @Page + private DeleteCredentialPage deleteCredentialPage; + private VirtualAuthenticatorManager webAuthnManager; protected SigningInPage.CredentialType webAuthnCredentialType; protected SigningInPage.CredentialType webAuthnPwdlessCredentialType; @@ -192,7 +196,7 @@ protected SigningInPage.UserCredential addWebAuthnCredential(String label, boole protected void testRemoveCredential(SigningInPage.UserCredential userCredential) { AbstractPatternFlyAlert.waitUntilHidden(); - SigningInPageUtils.testRemoveCredential(signingInPage, userCredential); + SigningInPageUtils.testRemoveCredential(signingInPage, deleteCredentialPage, userCredential); } protected SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) { diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 18d9ef82a558..141e6e8aec7c 100644 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian - 999.0.0-SNAPSHOT + 24.0.5-PS-2 pom diff --git a/testsuite/integration-arquillian/util/pom.xml b/testsuite/integration-arquillian/util/pom.xml index 1f762ab47abf..82d4ad580d2c 100644 --- a/testsuite/integration-arquillian/util/pom.xml +++ b/testsuite/integration-arquillian/util/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/fuse/FuseUtils.java b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/fuse/FuseUtils.java index 8a44455c5c58..cdda36d7478f 100644 --- a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/fuse/FuseUtils.java +++ b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/fuse/FuseUtils.java @@ -113,7 +113,7 @@ private static void setUpFuse7() throws IOException { assertCommand(managementUser, managementPassword, "feature:repo-add mvn:org.keycloak/keycloak-osgi-features/" + fuseAdapterVersion + "/xml/features; " + "feature:repo-add mvn:org.keycloak.testsuite/fuse-example-keycloak-features/" + projectVersion + "/xml/features; " + - "feature:install pax-http-undertow; " + + "feature:install pax-web-http-undertow; " + "feature:install keycloak-jaas keycloak-pax-http-undertow; " + "feature:install keycloak-fuse-7.0-example", Result.OK); diff --git a/testsuite/model/pom.xml b/testsuite/model/pom.xml index 7f8ae395ef98..c4bd1fd582ec 100644 --- a/testsuite/model/pom.xml +++ b/testsuite/model/pom.xml @@ -4,7 +4,7 @@ org.keycloak keycloak-testsuite-pom - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml diff --git a/testsuite/pom.xml b/testsuite/pom.xml index b8f8a84e8bf0..a91cf883eace 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml 4.0.0 diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml index 3f1f3f586ba4..dfe3aa1e76d2 100755 --- a/testsuite/utils/pom.xml +++ b/testsuite/utils/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/themes/pom.xml b/themes/pom.xml index 280ddde2115f..5f8d605c32b0 100755 --- a/themes/pom.xml +++ b/themes/pom.xml @@ -3,7 +3,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 4.0.0 diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_es.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_es.properties index 6493cd2aecf0..f41b033a6470 100644 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_es.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_es.properties @@ -41,10 +41,10 @@ requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES=Generar códigos de recuperación # for languages which have more unit plural forms depending on the value (eg. Czech and other Slavic langs) you can override unit text for some other values like described in the Java choice format which is documented here. For Czech, it would be '{0,choice,0#minut|1#minuta|2#minuty|2 Verifique su dirección de correo electrónico ingresando el siguiente código.

    {0}

    diff --git a/themes/src/main/resources/META-INF/keycloak-themes.json b/themes/src/main/resources/META-INF/keycloak-themes.json index f3599a2fbe7d..8383cb610fb1 100755 --- a/themes/src/main/resources/META-INF/keycloak-themes.json +++ b/themes/src/main/resources/META-INF/keycloak-themes.json @@ -7,6 +7,9 @@ "types": [ "login", "common", "email", "welcome" ] }, { "name" : "keycloak.v2", - "types": [ "account", "admin", "login" ] + "types": [ "account", "login" ] + }, { + "name" : "primesign.v2", + "types": [ "admin" ] }] } diff --git a/themes/src/main/resources/theme/base/login/delete-credential.ftl b/themes/src/main/resources/theme/base/login/delete-credential.ftl new file mode 100644 index 000000000000..57f30e709ea6 --- /dev/null +++ b/themes/src/main/resources/theme/base/login/delete-credential.ftl @@ -0,0 +1,15 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${msg("deleteCredentialTitle", credentialLabel)} + <#elseif section = "form"> +
    + ${msg("deleteCredentialMessage", credentialLabel)} +
    +
    + + +
    +
    + + diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 20067a14fec0..fd84ef6d27be 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -35,6 +35,7 @@ loginProfileTitle=Update Account Information loginIdpReviewProfileTitle=Update Account Information loginTimeout=Your login attempt timed out. Login will start from the beginning. reauthenticate=Please re-authenticate to continue +authenticateStrong=Strong authentication required to continue oauthGrantTitle=Grant Access to {0} oauthGrantTitleHtml={0} oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data. @@ -72,6 +73,9 @@ termsPlainText=Terms and conditions to be defined. termsAcceptanceRequired=You must agree to our terms and conditions. acceptTerms=I agree to the terms and conditions +deleteCredentialTitle=Delete {0} +deleteCredentialMessage=Do you want to delete {0}? + recaptchaFailed=Invalid Recaptcha recaptchaNotConfigured=Recaptcha is required, but not configured consentDenied=Consent denied. diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/signingin-page/SigningInPage.tsx b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/signingin-page/SigningInPage.tsx index fc9babad4a66..ef18d1c5b023 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/signingin-page/SigningInPage.tsx +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/signingin-page/SigningInPage.tsx @@ -305,6 +305,7 @@ class SigningInPage extends React.Component< removeable={removeable} updateAction={updateAIA} credRemover={this.handleRemove} + keycloak={keycloak} /> @@ -471,6 +472,7 @@ interface CredentialActionProps { removeable: boolean; updateAction: AIACommand; credRemover: CredRemover; + keycloak: KeycloakService; }; class CredentialAction extends React.Component { @@ -499,20 +501,24 @@ class CredentialAction extends React.Component { if (this.props.removeable) { const userLabel: string = this.props.credential.userLabel; + const removeAction: AIACommand = new AIACommand(this.props.keycloak, 'delete_credential:' + this.props.credential.id); return ( - this.props.credRemover(this.props.credential.id, userLabel)} - /> + ); } diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/app/util/RedirectUri.ts b/themes/src/main/resources/theme/keycloak.v2/account/src/app/util/RedirectUri.ts index c5211addbb52..55d134f88b9a 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/src/app/util/RedirectUri.ts +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/app/util/RedirectUri.ts @@ -38,5 +38,5 @@ export const createRedirect = (currentLocation: string): string => { redirectUri += "?referrer=" + referrer + "&referrer_uri=" + referrerUri.replace('#', '_hash_'); } - return encodeURIComponent(redirectUri) + encodeURIComponent("/#" + currentLocation); -} \ No newline at end of file + return encodeURIComponent(redirectUri) + encodeURIComponent("#" + currentLocation); +} diff --git a/themes/src/main/resources/theme/keycloak.v2/login/register.ftl b/themes/src/main/resources/theme/keycloak.v2/login/register.ftl index dd27958a5963..38e864654111 100755 --- a/themes/src/main/resources/theme/keycloak.v2/login/register.ftl +++ b/themes/src/main/resources/theme/keycloak.v2/login/register.ftl @@ -1,4 +1,4 @@ -<#import "pf-5-template.ftl" as layout> +<#import "template.ftl" as layout> <#import "user-profile-commons.ftl" as userProfileCommons> <#import "register-commons.ftl" as registerCommons> <@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> diff --git a/util/embedded-ldap/pom.xml b/util/embedded-ldap/pom.xml index 1e10ff4e357b..f5973f2af788 100644 --- a/util/embedded-ldap/pom.xml +++ b/util/embedded-ldap/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../../pom.xml 4.0.0 diff --git a/util/pom.xml b/util/pom.xml index 8b716dbbd2e5..00e9d8c3e874 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 999.0.0-SNAPSHOT + 24.0.5-PS-2 ../pom.xml