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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlspring-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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.keycloakkeycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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.keycloakkeycloak-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.keycloakkeycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xml4.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.keycloakkeycloak-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.keycloakkeycloak-authz-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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.keycloakkeycloak-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.keycloakkeycloak-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.keycloakkeycloak-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.bomkeycloak-bom-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2org.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.bomkeycloak-bom-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2org.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.bomkeycloak-bom-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2pom
@@ -57,7 +57,20 @@
spimisc
-
+
+
+
+ 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.bomkeycloak-bom-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2org.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Adapters 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-parentorg.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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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.keycloakfeature-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Feature 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Galleon 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.keycloakgalleon-feature-packs-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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.keycloakgalleon-feature-packs-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2SAML 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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../../pom.xmlKeycloak 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-parentorg.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-parentorg.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-parentorg.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-parentorg.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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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-parentorg.keycloak.documentation
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2org.keycloak.documentationheader-maven-plugin
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2maven-plugingithub-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak Documentation Parentorg.keycloak.documentationdocumentation-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2pom
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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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.documentationdocumentation-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].
@tmpl.guide>
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.
@tmpl.guide>
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.
@tmpl.guide>
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.
@tmpl.guide>
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::[]
+
@tmpl.guide>
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.
@tmpl.guide>
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-parentorg.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.
+
@tmpl.guide>
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.
@tmpl.guide>
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.
-@tmpl.guide>
\ 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.
+
+@tmpl.guide>
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-parentorg.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Keycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Keycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Keycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Authenticator 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Provider 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2REST 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2SAML 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2saml-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Themes 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Keycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-parentorg.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) => (
+
+
+ {t(category as TFuncKey)}
-
-
-
-
- {t(container.displayName as TFuncKey)}
-
-
- {t(container.helptext as TFuncKey)}
-
- {container.createAction && (
-
-
+ * 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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-pomorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-24.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-parentorg.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.keycloakkeycloak-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2pom
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-21.5.8
@@ -45,8 +45,8 @@
jboss-snapshots-repositoryhttps://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.2246.2.13.Final6.2.13.Final
- 14.0.25.Final
+ 14.0.27.Final2.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.keycloakkeycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xmlKeycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.0.0
@@ -247,6 +247,16 @@
+
+ org.keycloak
+ keycloak-model-legacy
+
+
+ *
+ *
+
+
+ org.keycloakkeycloak-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-parentorg.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-parentorg.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-parentorg.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-parentorg.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.keycloakkeycloak-rest-parent
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2keycloak-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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2Keycloak 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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.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-parentorg.keycloak
- 999.0.0-SNAPSHOT
+ 24.0.5-PS-2../pom.xml4.0.0
@@ -228,6 +228,16 @@
org.keycloakkeycloak-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