diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..57f92cb572c4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[{*.js,*.tsx,*.adoc}] +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +insert_final_newline = true +# Don't use class imports with an asterisk ('*') in IntelliJ +ij_java_use_single_class_imports = true +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_names_count_to_use_import_on_demand = 999 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0d1123c2fee2..73ff39055e15 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,7 +2,7 @@ # Overview # # Pattern used to match files follows most of the same rules as used in gitignore files. Order is -# important; the last matching pattern takes precendence. +# important; the last matching pattern takes precedence. # # For more info see: # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners @@ -19,13 +19,18 @@ # Testsuite ################################################################################################### -/testsuite/ @keycloak/core-maintainers @keycloak/cloud-native-maintainers @keycloak/store-maintainers @keycloak/maintainers +/testsuite/ @keycloak/core-clients-maintainers @keycloak/core-iam-maintainers @keycloak/cloud-native-maintainers @keycloak/maintainers ################################################################################################### # Core (@keycloak/core-maintainers) ################################################################################################### +################################################################################################### +# Core Clients (@keycloak/core-clients-maintainers) +################################################################################################### + + ################################################################################################### # Cloud Native (@keycloak/cloud-native-maintainers) ################################################################################################### @@ -34,20 +39,24 @@ /quarkus/ @keycloak/cloud-native-maintainers @keycloak/maintainers /docs/guides/server/ @keycloak/cloud-native-maintainers @keycloak/maintainers /docs/guides/operator/ @keycloak/cloud-native-maintainers @keycloak/maintainers +/integration/client-cli/admin-cli/ @keycloak/cloud-native-maintainers @keycloak/maintainers ################################################################################################### -# Store (@keycloak/store-maintainers) +# UI (@keycloak/ui-maintainers) ################################################################################################### -/model/ @keycloak/store-maintainers @keycloak/maintainers -/testsuite/model/ @keycloak/store-maintainers @keycloak/maintainers +/themes/ @keycloak/ui-maintainers @keycloak/maintainers +/js/ @keycloak/ui-maintainers @keycloak/maintainers +/js/**/messages_*.properties @keycloak/ui-maintainers @keycloak/maintainers +/adapters/oidc/js/ @keycloak/ui-maintainers @keycloak/maintainers +/rest/admin-ui-ext/ @keycloak/ui-maintainers @keycloak/maintainers ################################################################################################### -# UI (@keycloak/ui-maintainers) +# SRE (@keycloak/sre-maintainers) ################################################################################################### -/js/ @keycloak/ui-maintainers -/adapters/oidc/js/ @keycloak/ui-maintainers -/rest/admin-ui-ext/ @keycloak/ui-maintainers -/themes/src/main/resources/theme/keycloak.v2/account/ @keycloak/ui-maintainers -/testsuite/integration-arquillian/tests/other/base-ui/ @keycloak/ui-maintainers \ No newline at end of file +/model/infinispan/ @keycloak/sre-maintainers @keycloak/maintainers +/tests/clustering/ @keycloak/sre-maintainers @keycloak/maintainers +/test-framework/clustering/ @keycloak/sre-maintainers @keycloak/maintainers +/docs/guides/high-availability/ @keycloak/sre-maintainers @keycloak/maintainers +/docs/guides/observability/ @keycloak/sre-maintainers @keycloak/maintainers \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 328062818163..da6e38577f7b 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -33,8 +33,8 @@ body: - admin/cli - admin/fine-grained-permissions - admin/ui - - admin/client/node - - admin/client/java + - admin/client-java + - admin/client-js - authentication - authentication/webauthn - authorization-services @@ -42,20 +42,23 @@ body: - core - dependencies - dist/quarkus - - dist/wildfly - docs - identity-brokering - import-export - infinispan - ldap + - login/ui - oidc + - oid4vc - operator + - organizations - saml - storage - testsuite - token-exchange - translations - user-profile + - welcome/ui validations: required: true - type: textarea @@ -72,6 +75,13 @@ body: description: What version of Keycloak are you running? validations: required: true + - type: checkboxes + id: regression + attributes: + label: Regression + description: Was the issue introduced only after upgrading Keycloak, and it worked as expected in the past? + options: + - label: The issue is a regression - type: textarea id: behaviorExpected attributes: diff --git a/.github/actions/archive-surefire-reports/action.yml b/.github/actions/archive-surefire-reports/action.yml index a9104e66bcef..ddc0acb89d94 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","refs/heads/release/26.0"]' keep-days: description: 'For how many days to store the particular artifact.' required: false @@ -37,7 +37,7 @@ runs: - id: upload-surefire-linux name: Upload Surefire reports if: (!cancelled() && contains(fromJSON(inputs.release-branches), github.ref) && contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name)) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: surefire-${{ inputs.job-id }} path: | diff --git a/.github/actions/aurora-create-database/action.yml b/.github/actions/aurora-create-database/action.yml new file mode 100644 index 000000000000..ae4625111ae3 --- /dev/null +++ b/.github/actions/aurora-create-database/action.yml @@ -0,0 +1,32 @@ +name: Create Aurora Database +description: Create AWS Aurora Database + +inputs: + name: + description: 'The name of the Aurora DB cluster to deploy' + required: true + region: + description: 'The AWS region used to host the Aurora DB' + required: true + password: + description: 'The master password of the Aurora DB cluster' + required: false + +outputs: + endpoint: + description: 'The Endpoint URL for Aurora clients to connect to' + value: ${{ steps.create.outputs.endpoint }} + +runs: + using: "composite" + steps: + - id: create + shell: bash + run: | + source ./aurora_create.sh + echo "endpoint=${AURORA_ENDPOINT}" >> $GITHUB_OUTPUT + working-directory: .github/scripts/aws/rds + env: + AURORA_CLUSTER: ${{ inputs.name }} + AURORA_PASSWORD: ${{ inputs.password }} + AURORA_REGION: ${{ inputs.region }} diff --git a/.github/actions/build-keycloak/action.yml b/.github/actions/build-keycloak/action.yml index 827351e5142f..481a1f34537d 100644 --- a/.github/actions/build-keycloak/action.yml +++ b/.github/actions/build-keycloak/action.yml @@ -24,45 +24,18 @@ runs: with: create-cache-if-it-doesnt-exist: true - - id: phantomjs-cache - name: PhantomJS cache - uses: ./.github/actions/phantomjs-cache - - - id: frontend-plugin-cache - name: Frontend Plugin Cache - uses: ./.github/actions/frontend-plugin-cache - - # Remove once https://github.com/keycloak/keycloak/issues/19299 is solved - ######################################################################################################## - - id: check-adapter-changes - if: github.event_name == 'pull_request' - name: Check changes for WildFly adapters - shell: bash - # If there are no changes for WildFly adapters, we use adapters built in the latest nightly build - run: | - WF_ADAPTERS_REGEX="^adapters/oidc/wildfly|^adapters/saml/wildfly" - - git fetch origin --tags --force - - echo "GIT_WF_ADAPTERS_DIFF=$(git diff origin/main --name-only | egrep -ic -e "$WF_ADAPTERS_REGEX")" >> $GITHUB_ENV - echo "NIGHTLY_DIFF=$(git diff nightly --name-only | egrep -ic -e "$WF_ADAPTERS_REGEX")" >> $GITHUB_ENV - - - id: set-maven-profile - if: ${{ github.event_name != 'pull_request' || env.GIT_WF_ADAPTERS_DIFF != 0 || env.NIGHTLY_DIFF != 0}} - name: Set profile for building distribution - shell: bash - run: | - echo "MVN_PROFILES=-Pdistribution" >> $GITHUB_ENV - echo "WildFly adapters will be built in our codebase" - ######################################################################################################## + - id: pnpm-store-cache + name: PNPM store cache + uses: ./.github/actions/pnpm-store-cache - id: build-keycloak name: Build Keycloak shell: bash - # By using "dependency:resolve", it will download all dependencies used in later stages for running the tests run: | - MVN_HTTP_CONFIG="-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" - ./mvnw install dependency:resolve -nsu -V -B -e -DskipTests -DskipExamples $MVN_HTTP_CONFIG ${{ env.MVN_PROFILES}} + # Ensure this plugin is built first to avoid warnings in the build + ./mvnw install -Pdistribution -am -pl distribution/maven-plugins/licenses-processor + # By using "dependency:resolve", it will download all dependencies used in later stages for running the tests + ./mvnw install dependency:resolve -V -e -DskipTests -DskipExamples -DexcludeGroupIds=org.keycloak -Dsilent=true -DcommitProtoLockChanges=true - id: compress-keycloak-maven-repository name: Compress Keycloak Maven artifacts @@ -76,7 +49,7 @@ runs: - id: upload-keycloak-maven-repository name: Upload Keycloak Maven artifacts if: inputs.upload-m2-repo == 'true' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: m2-keycloak.tzts path: m2-keycloak.tzts @@ -85,7 +58,7 @@ runs: - id: upload-keycloak-dist name: Upload Keycloak dist if: inputs.upload-dist == 'true' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: keycloak-dist path: quarkus/dist/target/keycloak*.tar.gz diff --git a/.github/actions/conditional/action.yml b/.github/actions/conditional/action.yml index b323a25a90fa..63b2913880fe 100644 --- a/.github/actions/conditional/action.yml +++ b/.github/actions/conditional/action.yml @@ -10,12 +10,18 @@ outputs: ci: description: Should "ci.yml" execute value: ${{ steps.changes.outputs.ci }} + ci-quarkus: + description: Should "ci.yml" execute (Quarkus) + value: ${{ steps.changes.outputs.ci-quarkus }} ci-store: - description: Should "ci.yml" execute + description: Should "ci.yml" execute (Store) value: ${{ steps.changes.outputs.ci-store }} ci-sssd: - description: Should "ci.yml" execute + description: Should "ci.yml" execute (SSSD) value: ${{ steps.changes.outputs.ci-sssd }} + ci-webauthn: + description: Should "ci.yml" execute (WebAuthn) + value: ${{ steps.changes.outputs.ci-webauthn }} operator: description: Should "operator-ci.yml" execute value: ${{ steps.changes.outputs.operator }} @@ -25,9 +31,12 @@ outputs: codeql-java: description: Should "codeql-analysis.yml / java" execute value: ${{ steps.changes.outputs.codeql-java }} - codeql-themes: - description: Should "codeql-analysis.yml / themes" execute - value: ${{ steps.changes.outputs.codeql-themes }} + codeql-javascript: + description: Should "codeql-analysis.yml / javascript" execute + value: ${{ steps.changes.outputs.codeql-javascript }} + codeql-typescript: + description: Should "codeql-analysis.yml / typescript" execute + value: ${{ steps.changes.outputs.codeql-typescript }} guides: description: Should "guides.yml" execute value: ${{ steps.changes.outputs.guides }} diff --git a/.github/actions/conditional/conditions b/.github/actions/conditional/conditions index 1b29f19af822..88e1b2b7b6d4 100644 --- a/.github/actions/conditional/conditions +++ b/.github/actions/conditional/conditions @@ -2,33 +2,56 @@ # # To test a pattern run '.github/actions/conditional/conditional.sh ' -.github/actions/ ci ci-store ci-sssd operator js codeql-java codeql-themes guides documentation -.github/scripts/ ci ci-sssd +.github/actions/ ci ci-quarkus ci-store ci-sssd operator js codeql-java codeql-javascript codeql-typescript guides documentation +.github/fake_fips/ ci +.github/scripts/ ci ci-quarkus ci-sssd +.github/scripts/ansible/ ci-store +.github/scripts/aws/ ci-store -.github/workflows/ci.yml ci ci-store ci-sssd +.github/workflows/ci.yml ci ci-quarkus ci-store ci-sssd ci-webauthn .github/workflows/operator-ci.yml operator .github/workflows/js-ci.yml js -.github/workflows/codeql-analysis.yml codeql-java codeql-themes +.github/workflows/codeql-analysis.yml codeql-java codeql-javascript codeql-typescript .github/workflows/guides.yml guides .github/workflows/documentation.yml documentation -.mvn/ ci ci-store ci-sssd operator js codeql-java codeql-themes guides documentation -mvnw ci ci-store ci-sssd operator js codeql-java codeql-themes guides documentation -mvnw.cmd ci ci-store ci-sssd operator js codeql-java codeql-themes guides documentation +.mvn/ ci ci-quarkus ci-store ci-sssd ci-webauthn operator js codeql-java codeql-javascript codeql-typescript guides documentation +mvnw ci ci-quarkus ci-store ci-sssd ci-webauthn operator js codeql-java codeql-javascript codeql-typescript guides documentation +mvnw.cmd ci ci-quarkus ci-store ci-sssd ci-webauthn operator js codeql-java codeql-javascript codeql-typescript guides documentation -*/src/main/ ci operator -*/src/test/ ci operator -pom.xml ci ci-store operator +*/src/main/ ci ci-webauthn operator +*/src/test/ ci ci-webauthn operator +pom.xml ci ci-quarkus ci-store ci-webauthn operator federation/sssd/ ci ci-sssd +quarkus/ ci-quarkus guides + model/ ci-store +testsuite/model/ ci-store +operator/ operator docs/guides/ guides docs/documentation/ documentation js/ js +rest/admin-ui-ext/ js +services/ js +themes/ js +js/apps/account-ui/ ci ci-webauthn +js/libs/ui-shared/ ci ci-webauthn + +# The sections below contain a sub-set of files existing in the project which are supported languages by CodeQL. +# See: https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/ +## CodeQL Java *.java codeql-java -themes/ codeql-themes + +## CodeQL JavaScript +*.js codeql-javascript +*.html codeql-javascript + +## CodeQL TypeScript +*.ts codeql-typescript +*.tsx codeql-typescript testsuite::database-suite ci-store diff --git a/.github/actions/frontend-plugin-cache/action.yml b/.github/actions/frontend-plugin-cache/action.yml deleted file mode 100644 index 9d2f0e5a3045..000000000000 --- a/.github/actions/frontend-plugin-cache/action.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Frontend Plugin Cache -description: Caches NPM dependencies for the frontend-maven-plugin to speed up builds - -runs: - using: composite - steps: - - name: Get PNPM version - id: pnpm-version - shell: bash - run: | - echo "version=$(mvn help:evaluate -Dexpression=pnpm.version -q -DforceStdout)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - name: Cache PNPM store - with: - # See: https://pnpm.io/npmrc#store-dir - path: | - ~/.local/share/pnpm/store - ~/AppData/Local/pnpm/store - ~/Library/pnpm/store - key: ${{ runner.os }}-frontend-plugin-pnpm-store-${{ steps.pnpm-version.outputs.version }}-${{ hashFiles('pnpm-lock.yaml') }} diff --git a/.github/actions/install-chrome/action.yml b/.github/actions/install-chrome/action.yml index bb05679251be..c880cddf9345 100644 --- a/.github/actions/install-chrome/action.yml +++ b/.github/actions/install-chrome/action.yml @@ -1,38 +1,39 @@ name: Install Chrome browser and driver for Testing -description: Download and install the latest available Chrome for Testing and Chromedriver +description: Download and install the compatible Chrome and Chromedriver + +inputs: + version: + description: The version of Chrome and Chromedriver to install. By default none is installed. + required: false + default: default # E.g. 135.0.7049.84 (fixed version), default (chrome provided by GHA box) runs: using: composite steps: - - id: cache-chrome-browser - name: Chrome browser cache - uses: actions/cache@v3 - with: - path: ./chrome - key: chrome - - - id: cache-chromedriver - name: Chrome driver cache - uses: actions/cache@v3 - with: - path: ./chromedriver - key: chromedriver - id: install-chrome name: Install Chrome + if: inputs.version != 'default' shell: bash run: | sudo apt-get remove google-chrome-stable - npx @puppeteer/browsers install chrome - npx @puppeteer/browsers install chromedriver - # In case there's more than one version of each package, let's use only the latest - LATEST_CHROME=$(ls -td $PWD/chrome/*/ | head -1) - LATEST_CHROMEDRIVER=$(ls -td $PWD/chromedriver/*/ | head -1) - sudo ln -s -f "${LATEST_CHROME}chrome-linux64/chrome" /usr/bin/google-chrome-stable - sudo cp -u "${LATEST_CHROMEDRIVER}chromedriver-linux64/chromedriver" $CHROMEWEBDRIVER/ - # Remove any older version of browser or driver so we don't keep it in the cache - cd chrome - rm -R $(ls -lt | grep '^d' | tail -1 | tr " " "\n" | tail -1) - cd ../chromedriver - rm -R $(ls -lt | grep '^d' | tail -1 | tr " " "\n" | tail -1) - cd .. + wget http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${{ inputs.version }}-1_amd64.deb -O /tmp/google-chrome-stable.deb --no-verbose + sudo apt-get install -y /tmp/google-chrome-stable.deb + + - id: install-chromedriver + name: Install Chromedriver + if: inputs.version != 'default' + shell: bash + run: | + wget https://storage.googleapis.com/chrome-for-testing-public/${{ inputs.version }}/linux64/chromedriver-linux64.zip -O /tmp/chromedriver.zip --no-verbose + unzip -j /tmp/chromedriver.zip -d /tmp + sudo mv -f /tmp/chromedriver $CHROMEWEBDRIVER/chromedriver + sudo chmod +x $CHROMEWEBDRIVER/chromedriver + + - id: show-version + name: Show Version + if: inputs.version == 'default' + shell: bash + run: | + google-chrome --version + $CHROMEWEBDRIVER/chromedriver --version diff --git a/.github/actions/integration-test-setup/action.yml b/.github/actions/integration-test-setup/action.yml index 3a50e4224f07..1a4a15218303 100644 --- a/.github/actions/integration-test-setup/action.yml +++ b/.github/actions/integration-test-setup/action.yml @@ -9,11 +9,15 @@ inputs: jdk-version: description: JDK version required: false - default: "17" + default: "21" runs: using: composite steps: + - id: update-hosts + name: Update /etc/hosts + uses: ./.github/actions/update-hosts + - id: setup-java name: Setup Java uses: ./.github/actions/java-setup @@ -25,17 +29,13 @@ runs: name: Maven cache uses: ./.github/actions/maven-cache - - id: phantomjs-cache - name: PhantomJS cache - uses: ./.github/actions/phantomjs-cache - - - id: frontend-plugin-cache - name: Frontend Plugin Cache - uses: ./.github/actions/frontend-plugin-cache + - id: pnpm-store-cache + name: PNPM store cache + uses: ./.github/actions/pnpm-store-cache - id: download-keycloak name: Download Keycloak Maven artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: m2-keycloak.tzts @@ -44,6 +44,7 @@ runs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - choco install zstandard + # zstd binary might be missing in older versions, install only when necessary + which zstd > /dev/null || choco install zstandard fi tar -C ~/ --use-compress-program="zstd -d" -xf m2-keycloak.tzts diff --git a/.github/actions/java-setup/action.yml b/.github/actions/java-setup/action.yml index ffafcf752b7f..8f7f43e3c045 100644 --- a/.github/actions/java-setup/action.yml +++ b/.github/actions/java-setup/action.yml @@ -9,14 +9,14 @@ inputs: java-version: description: The Java version that is going to be set up. required: false - default: "17" + default: "21" runs: using: composite steps: - id: setup-java name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: ${{ inputs.distribution }} java-version: ${{ inputs.java-version }} diff --git a/.github/actions/maven-cache/action.yml b/.github/actions/maven-cache/action.yml index ea695b4356c8..f5a9a2873d34 100644 --- a/.github/actions/maven-cache/action.yml +++ b/.github/actions/maven-cache/action.yml @@ -19,7 +19,7 @@ runs: - id: cache-maven-repository name: Maven cache - uses: actions/cache@v3 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 if: inputs.create-cache-if-it-doesnt-exist == 'true' with: # Two asterisks are needed to make the follow-up exclusion work @@ -28,24 +28,30 @@ runs: ~/.m2/repository/*/* !~/.m2/repository/org/keycloak key: ${{ steps.weekly-cache-key.outputs.key }} + # Enable cross-os archive use the cache on both Linux and Windows + enableCrossOsArchive: true - id: restore-maven-repository name: Maven cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 if: inputs.create-cache-if-it-doesnt-exist == 'false' with: - # Two asterisks are needed to make the follow-up exclusion work - # see https://github.com/actions/toolkit/issues/713 for the upstream issue + # This needs to repeat the same path pattern as above to find the matching cache path: | ~/.m2/repository/*/* !~/.m2/repository/org/keycloak key: ${{ steps.weekly-cache-key.outputs.key }} + enableCrossOsArchive: true - - name: Cache Maven Wrapper - uses: actions/cache@v3 - with: - path: .mvn/wrapper/maven-wrapper.jar - key: ${{ runner.os }}-maven-wrapper-${{ hashFiles('**/maven-wrapper.properties') }} - # use a previously cached JAR file as something else besides the version could have changed in the file - restore-keys: | - ${{ runner.os }}-maven-wrapper- \ No newline at end of file + - shell: bash + name: Copy restored maven repo to home folder in Windows + if: (steps.cache-maven-repository.outputs.cache-hit == 'true' || steps.restore-maven-repository.outputs.cache-hit == 'true') && runner.os == 'Windows' + run: | + if [ -d ../../../.m2/repository ]; then + cp -r ../../../.m2/repository ~/.m2 + rm -r ../../../.m2/repository + fi + + - id: node-cache + name: Node cache + uses: ./.github/actions/node-cache diff --git a/.github/actions/node-cache/action.yml b/.github/actions/node-cache/action.yml new file mode 100644 index 000000000000..6c2421294cc4 --- /dev/null +++ b/.github/actions/node-cache/action.yml @@ -0,0 +1,27 @@ +name: Node Cache +description: Caches Node and PNPM binaries + +runs: + using: composite + steps: + - name: Get Node.js and PNPM versions + id: tooling-versions + shell: bash + run: | + echo "node=$(cat js/pom.xml | grep '' | cut -d '>' -f 2 | cut -d '<' -f 1 | cut -c 2-)" >> $GITHUB_OUTPUT + echo "pnpm=$(cat js/pom.xml | grep '' | cut -d '>' -f 2 | cut -d '<' -f 1 | cut -c 1-)" >> $GITHUB_OUTPUT + + # Downloading Node.js often fails due to network issues, therefore we cache the artifacts downloaded by the frontend plugin. + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: cache-binaries + name: Cache Node.js and PNPM binaries + with: + path: | + ~/.m2/repository/com/github/eirslett/node + ~/.m2/repository/com/github/eirslett/pnpm + key: ${{ runner.os }}-frontend-plugin-artifacts-${{ steps.tooling-versions.outputs.node }}-${{ steps.tooling-versions.outputs.pnpm }} + + - name: Download Node.js and PNPM + if: steps.cache-binaries.outputs.cache-hit != 'true' + shell: bash + run: ./.github/scripts/download-node-tooling.sh ${{ steps.tooling-versions.outputs.node }} ${{ steps.tooling-versions.outputs.pnpm }} diff --git a/.github/actions/phantomjs-cache/action.yml b/.github/actions/phantomjs-cache/action.yml deleted file mode 100644 index 6b17be10c18d..000000000000 --- a/.github/actions/phantomjs-cache/action.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: PhantomJS Cache -description: Caches PhantomJS driver - -inputs: - version: - description: PhantomJS Driver version - required: false - default: 2.1.1 - -runs: - using: composite - steps: - - id: cache-phantomjs-driver - name: PhantomJS Driver cache - uses: actions/cache@v3 - with: - path: ~/.arquillian/drone - key: phantomjs-${{ inputs.version }} - - - id: download-phantomjs-driver - name: Download PhantomJS Driver - if: steps.cache-phantomjs-driver.outputs.cache-hit != 'true' - shell: bash - run: | - mkdir -p ~/.arquillian/drone/phantomjs/${{ inputs.version }}/ - curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-${{ inputs.version }}-linux-x86_64.tar.bz2 --output ~/.arquillian/drone/phantomjs/${{ inputs.version }}/phantomjs-${{ inputs.version }}-linux-x86_64.tar.bz2 diff --git a/.github/actions/pnpm-setup/action.yml b/.github/actions/pnpm-setup/action.yml index 1f11a317bd83..6eb1853a97cf 100644 --- a/.github/actions/pnpm-setup/action.yml +++ b/.github/actions/pnpm-setup/action.yml @@ -5,18 +5,13 @@ inputs: node-version: description: Node.js version required: false - default: "18" - - working-directory: - description: The working directory where the `pnpm-lock.yaml` file is located. - required: false - default: "" + default: "22" runs: using: composite steps: - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ inputs.node-version }} check-latest: true @@ -25,24 +20,10 @@ runs: shell: bash run: corepack enable - - name: Get PNPM store directory - id: pnpm-cache - shell: bash - run: | - echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - name: Setup PNPM cache - with: - # Also cache Cypress binary. - path: | - ~/.cache/Cypress - ${{ steps.pnpm-cache.outputs.store-path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{ inputs.working-directory }}/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + - name: PNPM store cache + uses: ./.github/actions/pnpm-store-cache - name: Install dependencies - working-directory: ${{ inputs.working-directory }} shell: bash run: pnpm install --prefer-offline --frozen-lockfile + working-directory: js diff --git a/.github/actions/pnpm-store-cache/action.yml b/.github/actions/pnpm-store-cache/action.yml new file mode 100644 index 000000000000..374cfcac2be2 --- /dev/null +++ b/.github/actions/pnpm-store-cache/action.yml @@ -0,0 +1,20 @@ +name: Cache PNPM store +description: Caches the PNPM store to speed up the build. + +runs: + using: composite + steps: + - id: weekly-cache-key + name: Key for weekly rotation of cache + shell: bash + run: echo "key=pnpm-store-`date -u "+%Y-%U"`" >> $GITHUB_OUTPUT + + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + name: Cache PNPM store + with: + # See: https://pnpm.io/npmrc#store-dir + path: | + ~/.local/share/pnpm/store + ~/AppData/Local/pnpm/store + ~/Library/pnpm/store + key: ${{ runner.os }}-${{ steps.weekly-cache-key.outputs.key }} diff --git a/.github/actions/unit-test-setup/action.yml b/.github/actions/unit-test-setup/action.yml index e18b0cbaaa97..f03eb91fe742 100644 --- a/.github/actions/unit-test-setup/action.yml +++ b/.github/actions/unit-test-setup/action.yml @@ -11,6 +11,6 @@ runs: name: Maven cache uses: ./.github/actions/maven-cache - - id: frontend-plugin-cache + - id: pnpm-store-cache name: Frontend Plugin Cache - uses: ./.github/actions/frontend-plugin-cache + uses: ./.github/actions/pnpm-store-cache diff --git a/.github/actions/update-hosts/action.yml b/.github/actions/update-hosts/action.yml new file mode 100644 index 000000000000..d2c7f4289719 --- /dev/null +++ b/.github/actions/update-hosts/action.yml @@ -0,0 +1,21 @@ +name: Update /etc/hosts +description: Update /etc/hosts file to hardcode known nip.io hostnames. This is to avoid test instability due to DNS resolution issues. + +runs: + using: composite + steps: + + - id: update-hosts-linux + name: Update /etc/hosts + if: runner.os == 'Linux' + shell: bash + run: | + printf "\n\n$(cat .github/actions/update-hosts/nipio-hosts)" | sudo tee -a /etc/hosts + + - id: update-hosts-windows + name: Update C:\Windows\System32\drivers\etc\hosts + if: runner.os == 'Windows' + shell: powershell + run: | + "`n`n" | Add-Content C:\Windows\System32\drivers\etc\hosts + Get-Content .github/actions/update-hosts/nipio-hosts | Add-Content C:\Windows\System32\drivers\etc\hosts \ No newline at end of file diff --git a/.github/actions/update-hosts/nipio-hosts b/.github/actions/update-hosts/nipio-hosts new file mode 100644 index 000000000000..d7f1a601cf0f --- /dev/null +++ b/.github/actions/update-hosts/nipio-hosts @@ -0,0 +1,2 @@ +127.0.0.1 localtest.me 127.0.0.1.nip.io admin.127.0.0.1.nip.io localhost-myapp.127.0.0.1.nip.io localhost-sso.127.0.0.1.nip.io realmFrontend.127.0.0.1.nip.io proxy.kc.127.0.0.1.nip.io +::1 localtest.me 127.0.0.1.nip.io admin.127.0.0.1.nip.io localhost-myapp.127.0.0.1.nip.io localhost-sso.127.0.0.1.nip.io realmFrontend.127.0.0.1.nip.io proxy.kc.127.0.0.1.nip.io \ No newline at end of file diff --git a/.github/actions/upload-flaky-tests/action.yml b/.github/actions/upload-flaky-tests/action.yml index ec3963968362..b69de078072e 100644 --- a/.github/actions/upload-flaky-tests/action.yml +++ b/.github/actions/upload-flaky-tests/action.yml @@ -32,7 +32,7 @@ runs: JOB_NAME="$JOB_NAME ($MATRIX)" fi - JOB_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs --jq ".jobs | map(select(.name == \"$JOB_NAME\")) | .[].html_url") + JOB_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs --paginate --jq ".jobs | map(select(.name == \"$JOB_NAME\")) | .[].html_url") echo "job_name=$JOB_NAME" >> job-summary.properties echo "job_url=$JOB_URL" >> job-summary.properties @@ -47,9 +47,9 @@ runs: echo "EOF" >> $GITHUB_OUTPUT fi - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ steps.flaky-tests.outputs.flakes }} with: name: flaky-tests-${{ github.job }}-${{ join(matrix.*, '-') }} path: ${{ steps.flaky-tests.outputs.flakes }} - if-no-files-found: error \ No newline at end of file + if-no-files-found: error diff --git a/.github/actions/upload-heapdumps/action.yml b/.github/actions/upload-heapdumps/action.yml index d9c71b4d6f90..c305f3d42c05 100644 --- a/.github/actions/upload-heapdumps/action.yml +++ b/.github/actions/upload-heapdumps/action.yml @@ -8,8 +8,10 @@ runs: name: Upload JVM Heapdumps # Windows runners are running into https://github.com/actions/upload-artifact/issues/240 if: runner.os != 'Windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: jvm-heap-dumps - path: '**/java_pid*.hprof' + path: | + '**/java_pid*.hprof' + !distribution/** if-no-files-found: ignore diff --git a/.github/check-area-labels.sh b/.github/check-area-labels.sh new file mode 100755 index 000000000000..32a3c22d2179 --- /dev/null +++ b/.github/check-area-labels.sh @@ -0,0 +1,57 @@ +#!/bin/bash -e + +TEAMS="teams.yml" +BUG="ISSUE_TEMPLATE/bug.yml" + +GH_AREAS=( $(gh label list --search 'area/' --limit 100 --json name | jq -r .[].name | sort) ) +LOCAL_AREAS=( $(cat teams.yml | grep 'area/' | sed 's/.*- //') ) +BUG_TEMPLATE_AREAS=( $(yq '.body.[] | select(.id | test("area")) | .attributes.options.[]' ISSUE_TEMPLATE/bug.yml | grep -v '^$') ) + +TEAMS_VALID=true +BUG_VALID=true + +echo "Checking: $TEAMS" +for AREA in "${GH_AREAS[@]}"; do + if ! ( echo "${LOCAL_AREAS[@]}" | grep -q -F -w "$AREA" ); then + echo "[$AREA] missing in $TEAMS" + TEAMS_VALID=false + fi +done + +for AREA in "${LOCAL_AREAS[@]}"; do + if ! ( echo "${GH_AREAS[@]}" | grep -q -F -w "$AREA" ); then + echo "[$AREA] missing in GitHub" + TEAMS_VALID=false + fi +done + +if [ "$TEAMS_VALID" = true ]; then + echo "[OK]" +fi + +echo "" + +echo "Checking: $BUG" +for AREA in "${GH_AREAS[@]}"; do + AREA_SHORT=$(echo $AREA | sed 's|area/||g') + if ! ( echo "${BUG_TEMPLATE_AREAS[@]}" | grep -q -F -w "$AREA_SHORT" ); then + echo "[$AREA] missing in $BUG" + BUG_VALID=false + fi +done + +for AREA in "${BUG_TEMPLATE_AREAS[@]}"; do + AREA_LONG="area/$AREA" + if ! ( echo "${GH_AREAS[@]}" | grep -q -F -w "$AREA_LONG" ); then + echo "[$AREA_LONG] missing in GitHub" + BUG_VALID=false + fi +done + +if [ "$BUG_VALID" = true ]; then + echo "[OK]" +fi + +if [ "$TEAMS_VALID" != true ] || [ "$BUG_VALID" != true ]; then + exit 1 +fi diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a8597c199ed5..9fea7a95316b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,7 +12,7 @@ updates: - area/dependencies - area/ci - package-ecosystem: npm - directory: /themes/src/main/resources/theme/keycloak/common/resources + directory: js schedule: interval: daily time: "00:00" @@ -22,18 +22,16 @@ updates: 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 ignore: - - dependency-name: react-error-boundary + - dependency-name: "@patternfly/*" + update-types: ["version-update:semver-major"] + - dependency-name: react + update-types: ["version-update:semver-major"] + - dependency-name: react-dom + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/react-dom" + update-types: ["version-update:semver-major"] + - dependency-name: "react-router-dom" update-types: ["version-update:semver-major"] diff --git a/.github/fake_fips/fake_fips.c b/.github/fake_fips/fake_fips.c index 207a4a5c7dbf..6ea9dda2a777 100644 --- a/.github/fake_fips/fake_fips.c +++ b/.github/fake_fips/fake_fips.c @@ -22,6 +22,7 @@ #include #include +#include int fips_enabled = 1; @@ -33,22 +34,32 @@ static struct ctl_table crypto_sysctl_table[] = { .mode = 0444, .proc_handler = proc_dointvec }, - {} +#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 11, 0)) + {} +#endif }; static struct ctl_table crypto_dir_table[] = { { .procname = "crypto", .mode = 0555, +#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)) .child = crypto_sysctl_table +#endif }, +#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 11, 0)) {} +#endif }; static struct ctl_table_header *crypto_sysctls; static void crypto_proc_fips_init(void) { +#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)) crypto_sysctls = register_sysctl_table(crypto_dir_table); +#else + crypto_sysctls = register_sysctl(crypto_dir_table->procname, crypto_sysctl_table); +#endif } static void crypto_proc_fips_exit(void) diff --git a/.github/mvn-rel-settings.xml b/.github/mvn-rel-settings.xml deleted file mode 100644 index 79949b291e93..000000000000 --- a/.github/mvn-rel-settings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - keycloak-rel - - - - keycloak-rel - - - ${env.MAVEN_ID} - ${env.MAVEN_URL} - - true - - - true - - - - - - - - ${env.MAVEN_ID} - ${env.MAVEN_USERNAME} - ${env.MAVEN_PASSWORD} - - - gpg.passphrase - ${env.MAVEN_GPG_PASSPHRASE} - - - - diff --git a/.github/scripts/ansible/.gitignore b/.github/scripts/ansible/.gitignore new file mode 100644 index 000000000000..2afb69fe4b50 --- /dev/null +++ b/.github/scripts/ansible/.gitignore @@ -0,0 +1,7 @@ +# Ansible +########### +*_inventory.yml +*.pem +ansible.log +files/ +env.yml \ No newline at end of file diff --git a/.github/scripts/ansible/ansible.cfg b/.github/scripts/ansible/ansible.cfg new file mode 100644 index 000000000000..0304b4c91ef5 --- /dev/null +++ b/.github/scripts/ansible/ansible.cfg @@ -0,0 +1,8 @@ +[defaults] +#log_path = ./ansible.log +host_key_checking=False +transport = ssh +forks = 50 + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes -o StrictHostKeyChecking=no -o IdentitiesOnly=yes diff --git a/.github/scripts/ansible/aws_ec2.sh b/.github/scripts/ansible/aws_ec2.sh new file mode 100755 index 000000000000..4754e2141da9 --- /dev/null +++ b/.github/scripts/ansible/aws_ec2.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e +cd $(dirname "${BASH_SOURCE[0]}") + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +OPERATION=$1 +REGION=$2 +CLUSTER_NAME=$3 + +case $OPERATION in + requirements) + ansible-galaxy collection install -r requirements.yml + pip3 install ansible boto3 botocore + ;; + create|delete|start|stop) + if [ -f "env.yml" ]; then ANSIBLE_CUSTOM_VARS_ARG="-e @env.yml"; fi + ansible-playbook aws_ec2.yml -v -e "region=$REGION" -e "operation=$OPERATION" -e "cluster_name=$CLUSTER_NAME" $ANSIBLE_CUSTOM_VARS_ARG "${@:4}" + ;; + *) + echo "Invalid option!" + echo "Available operations: requirements, create, delete, start, stop." + ;; +esac diff --git a/.github/scripts/ansible/aws_ec2.yml b/.github/scripts/ansible/aws_ec2.yml new file mode 100644 index 000000000000..803528445522 --- /dev/null +++ b/.github/scripts/ansible/aws_ec2.yml @@ -0,0 +1,3 @@ +- hosts: localhost + connection: local + roles: [aws_ec2] diff --git a/.github/scripts/ansible/keycloak.yml b/.github/scripts/ansible/keycloak.yml new file mode 100644 index 000000000000..571234dafd9d --- /dev/null +++ b/.github/scripts/ansible/keycloak.yml @@ -0,0 +1,2 @@ +- hosts: keycloak + roles: [keycloak_ec2_installer] diff --git a/.github/scripts/ansible/keycloak_ec2_installer.sh b/.github/scripts/ansible/keycloak_ec2_installer.sh new file mode 100755 index 000000000000..5d04ed1c4e74 --- /dev/null +++ b/.github/scripts/ansible/keycloak_ec2_installer.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e +cd $(dirname "${BASH_SOURCE[0]}") + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +REGION=$1 +CLUSTER_NAME=$2 +KEYCLOAK_SRC=$3 +MAVEN_ARCHIVE=$4 + +ansible-playbook -i ${CLUSTER_NAME}_${REGION}_inventory.yml keycloak.yml \ + -e "keycloak_src=\"${KEYCLOAK_SRC}\"" \ + -e "maven_archive=\"${MAVEN_ARCHIVE}\"" diff --git a/.github/scripts/ansible/mvn.yml b/.github/scripts/ansible/mvn.yml new file mode 100644 index 000000000000..6454cd02ce91 --- /dev/null +++ b/.github/scripts/ansible/mvn.yml @@ -0,0 +1,2 @@ +- hosts: keycloak + roles: [mvn_ec2_runner] diff --git a/.github/scripts/ansible/mvn_ec2_runner.sh b/.github/scripts/ansible/mvn_ec2_runner.sh new file mode 100755 index 000000000000..d5a608aea4b2 --- /dev/null +++ b/.github/scripts/ansible/mvn_ec2_runner.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e +cd $(dirname "${BASH_SOURCE[0]}") + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +REGION=$1 +CLUSTER_NAME=$2 +MVN_PARAMS=${@:3} + +echo "mvn_params=\"${MVN_PARAMS}\"" + +ansible-playbook -i ${CLUSTER_NAME}_${REGION}_inventory.yml mvn.yml \ + -e "mvn_params=\"${MVN_PARAMS}\"" diff --git a/.github/scripts/ansible/requirements.yml b/.github/scripts/ansible/requirements.yml new file mode 100644 index 000000000000..19ec5c3293ae --- /dev/null +++ b/.github/scripts/ansible/requirements.yml @@ -0,0 +1,3 @@ +collections: + - name: amazon.aws + version: 6.0.0 diff --git a/.github/scripts/ansible/roles/aws_ec2/README.md b/.github/scripts/ansible/roles/aws_ec2/README.md new file mode 100644 index 000000000000..1b4f2d1f7ab1 --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/README.md @@ -0,0 +1,91 @@ +# Ansible Role `aws_ec2` + +Ansible role for creating, deleting, stopping and starting AWS EC2 instances +for running keycloak tests. + +## Prerequisities + +Role requires Ansible Collection `amazon.aws` version `6.0.0` or higher. + +Role assumes that user is authenticated to use AWS CLI, ie. that authentication +variables `AWS_ACCESS_KEY` and `AWS_SECRET_KEY` are set in the environment. + + +## Parameters +- `region`: AWS region for the resources to be created in. +- `cluster_name`: Unique name of the instance cluster within the region. Defaults to `keycloak_{{ cluster_identifier }}`. +- `cluster_identifier`: Identifier to distingish multiple clusters within the region. Defaults to `${USER}`. +- `cluster_size`: Number of EC2 instances to be created. +- `ami_name`: Name of the AMI image to be used for spawning instances. +- `instance_type`: [AWS instance type](https://aws.amazon.com/ec2/instance-types/). +- `instance_volume_size`: Size of instance storage device. +- `instance_device`: Path to Linux storage device. + +For defaults see `defaults/main.yml`. + + +## Example Playbook + +Example playbook `aws_ec2.yml`: +``` +- hosts: localhost + connection: local + roles: [aws_ec2_client] +``` + +## Create Instances + +Using the example playbook run: +``` +ansible-playbook aws_ec2.yml -e region= -e operation=create +``` + +Replace with actual value, e.g. `us-west-1`. + +Optionally you can override other parameters by `-e PARAMETER=VALUE` or `-e @PARAMS.yml`. + +This operation will create the following 2 files: +- `{{ cluster_name }}_{{ region }}.pem` - private SSH key. +- `{{ cluster_name }}_{{ region }}_inventory.yml` - an Ansible host inventory file. + +``` +keycloak: + children: + "{{ cluster_name }}_{{ region }}": + vars: + ansible_user: ec2-user + ansible_become: yes + ansible_ssh_private_key_file: "{{ cluster_name }}_{{ region }}.pem" + hosts: + host-1-ip-address: + host-2-ip-address: + ... +``` + +Notice that the created hosts will be included in Ansible group `keycloak` +and subgroup `{{ cluster_name }}_{{ region }}`. + + +## Stop and Start instances + +Using the example playbook run: +``` +ansible-playbook aws_ec2.yml -e region= -e operation=stop +``` + +After the instances are stopped their public IP addresses will be de-allocated. + +``` +ansible-playbook aws_ec2.yml -e region= -e operation=start +``` + +After the instances are started again the role will re-create the host inventory file with updated public IP addresses. + + +## Delete Instances +Using the example playbook run: +``` +ansible-playbook aws_ec2.yml -e region= -e operation=delete +``` + +This will remove created AWS resources and delete the host inventory file and private key. diff --git a/.github/scripts/ansible/roles/aws_ec2/defaults/main.yml b/.github/scripts/ansible/roles/aws_ec2/defaults/main.yml new file mode 100644 index 000000000000..acb1d92cd029 --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/defaults/main.yml @@ -0,0 +1,14 @@ +cluster_identifier: "{{ lookup('env', 'USER') }}" +cluster_name: "keycloak_{{ cluster_identifier }}" +cluster_size: 1 + +cidr_ip: "{{ control_host_ip.stdout }}/32" + +# aws ec2 describe-images --owners 309956199498 --filters "Name=architecture,Values=x86_64" "Name=virtualization-type,Values=hvm" --region eu-west-1 --no-include-deprecated --query 'Images[] | sort_by(@, &CreationDate)[].Name' +ami_name: RHEL-9.4_HVM_GA-20240827-x86_64-0-Hourly2-GP3 + +instance_type: t3.large +instance_volume_size: 20 +instance_device: /dev/sda1 + +no_log_sensitive: true diff --git a/.github/scripts/ansible/roles/aws_ec2/tasks/create-resources.yml b/.github/scripts/ansible/roles/aws_ec2/tasks/create-resources.yml new file mode 100644 index 000000000000..62666e843850 --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/tasks/create-resources.yml @@ -0,0 +1,75 @@ +- name: Get Ansible Control Host's public IP + shell: curl -fks --ipv4 https://ifconfig.me + register: control_host_ip + no_log: "{{ no_log_sensitive }}" + +- debug: var=cidr_ip + +- name: Create Security Group + amazon.aws.ec2_group: + state: present + region: '{{ region }}' + name: '{{ cluster_name }}' + description: '{{ cluster_name }}' + rules: + - proto: tcp + from_port: 22 + to_port: 22 + cidr_ip: '{{cidr_ip}}' + register: group + no_log: "{{ no_log_sensitive }}" + +- name: Delete existing key pair if it exists + amazon.aws.ec2_key: + region: '{{ region }}' + name: '{{ cluster_name }}' + state: absent + ignore_errors: true + +- name: Create Key + amazon.aws.ec2_key: + state: present + region: '{{ region }}' + name: '{{ cluster_name }}' + register: key + no_log: "{{ no_log_sensitive }}" + +- name: Save Private Key on Ansible Control Machine + when: key.changed + copy: + content: '{{ key.key.private_key }}' + dest: '{{ cluster_name }}_{{ region }}.pem' + mode: 0600 + no_log: "{{ no_log_sensitive }}" + +- name: Look up AMI '{{ ami_name }}' + amazon.aws.ec2_ami_info: + region: '{{ region}}' + filters: + name: '{{ ami_name }}' + register: ami_info + +- name: Create {{ cluster_size }} EC2 Instances + amazon.aws.ec2_instance: + state: started + region: '{{ region }}' + name: "{{ cluster_name }}" + exact_count: "{{ cluster_size }}" + instance_type: '{{ instance_type }}' + image_id: '{{ ami_info.images[0].image_id }}' + key_name: '{{ cluster_name }}' + security_group: '{{ group.group_id }}' + network: + assign_public_ip: yes + volumes: + - device_name: '{{ instance_device }}' + ebs: + volume_size: '{{ instance_volume_size }}' + delete_on_termination: true + register: instances + no_log: "{{ no_log_sensitive }}" + +- name: Create Inventory File + template: + src: inventory.yml.j2 + dest: '{{ cluster_name }}_{{ region }}_inventory.yml' diff --git a/.github/scripts/ansible/roles/aws_ec2/tasks/delete-resources.yml b/.github/scripts/ansible/roles/aws_ec2/tasks/delete-resources.yml new file mode 100644 index 000000000000..b52c97ad2025 --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/tasks/delete-resources.yml @@ -0,0 +1,26 @@ +- name: 'Delete EC2 instances' + amazon.aws.ec2_instance: + state: absent + region: '{{ region }}' + filters: + "tag:Name": '{{ cluster_name }}*' + +- name: 'Delete EC2 security group' + amazon.aws.ec2_group: + state: absent + region: '{{ region }}' + name: '{{ cluster_name }}' + +- name: 'Delete Key' + amazon.aws.ec2_key: + state: absent + region: '{{ region }}' + name: '{{ cluster_name }}' + +- name: 'Delete inventory, key, and log' + file: + state: absent + path: '{{ item }}' + with_items: + - '{{ cluster_name }}_{{ region }}_inventory.yml' + - '{{ cluster_name }}_{{ region }}.pem' diff --git a/.github/scripts/ansible/roles/aws_ec2/tasks/main.yml b/.github/scripts/ansible/roles/aws_ec2/tasks/main.yml new file mode 100644 index 000000000000..f03fa1e8360c --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/tasks/main.yml @@ -0,0 +1,12 @@ +- debug: var=cluster_identifier +- debug: var=region +- debug: var=cluster_name + +- include_tasks: create-resources.yml + when: operation == "create" + +- include_tasks: manage-instances.yml + when: operation == "start" or operation == "stop" + +- include_tasks: delete-resources.yml + when: operation == "delete" diff --git a/.github/scripts/ansible/roles/aws_ec2/tasks/manage-instances.yml b/.github/scripts/ansible/roles/aws_ec2/tasks/manage-instances.yml new file mode 100644 index 000000000000..2068ddbe85fa --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/tasks/manage-instances.yml @@ -0,0 +1,26 @@ +# Start or Stop Instances +- name: "{{ operation[0]|upper }}{{ operation[1:] }} Instances" + amazon.aws.ec2_instance: + state: '{{ "stopped" if operation == "stop" else "started" }}' + region: '{{ region }}' + filters: + "tag:Name": '{{ cluster_name }}*' + instance-state-name: ['running', 'stopped', 'stopping'] + no_log: "{{ no_log_sensitive }}" + +- when: operation == "start" + block: + # When starting instances via `ec2_instance` module sometimes the `public_ip_address` is missing in the result. + # Added additional `ec2_instance_info` step to work around the issue. + - name: Get Instance Information + amazon.aws.ec2_instance_info: + region: '{{ region }}' + filters: + "tag:Name": '{{ cluster_name }}*' + instance-state-name: ['running'] + register: instances + no_log: "{{ no_log_sensitive }}" + - name: Recreate Inventory File + template: + src: inventory.yml.j2 + dest: '{{ cluster_name }}_{{ region }}_inventory.yml' diff --git a/.github/scripts/ansible/roles/aws_ec2/templates/inventory.yml.j2 b/.github/scripts/ansible/roles/aws_ec2/templates/inventory.yml.j2 new file mode 100644 index 000000000000..60d03e275328 --- /dev/null +++ b/.github/scripts/ansible/roles/aws_ec2/templates/inventory.yml.j2 @@ -0,0 +1,11 @@ +keycloak: + children: + {{ cluster_name }}_{{ region | replace('-','_') }}: + vars: + ansible_user: ec2-user + ansible_become: yes + ansible_ssh_private_key_file: {{ cluster_name }}_{{ region }}.pem + hosts: +{% for instance in instances.instances %} + {{ instance.public_ip_address }}: +{% endfor %} \ No newline at end of file diff --git a/.github/scripts/ansible/roles/keycloak_ec2_installer/README.md b/.github/scripts/ansible/roles/keycloak_ec2_installer/README.md new file mode 100644 index 000000000000..f683bb51d713 --- /dev/null +++ b/.github/scripts/ansible/roles/keycloak_ec2_installer/README.md @@ -0,0 +1,35 @@ +# Ansible Role `keycloak_ec2_installer` + +Ansible role for installing Keycloak sources and build dependencies on remote nodes. + +Role assumes presence of host inventory file and a matching SSH key for "sudoer" access to the hosts. +The hosts are expected to be included in `keycloak` group. + +## Parameters + +See `defaults/main.yml` for default values. + +### Execution +- `keycloak_src`: Path to a local `*.zip` file containing the Keycloak src + +### Other +- `update_system_packages`: Whether to update the system packages. Defaults to `no`. +- `install_java`: Whether to install OpenJDK on the system. Defaults to `yes`. +- `java_version`: Version of OpenJDK to be installed. Defaults to `21`. + + +## Example Playbook + +An example playbook `keycloak.yml` that applies the role to hosts in the `keycloak` group: +``` +- hosts: keycloak + roles: [keycloak] +``` + +## Run keycloak-benchmark + +Run: +``` +ansible-playbook -i ${CLUSTER_NAME}_${REGION}_inventory.yml keycloak.yml \ + -e "keycloak_src=\"/tmp/keycloak.zip\"" +``` diff --git a/.github/scripts/ansible/roles/keycloak_ec2_installer/defaults/main.yml b/.github/scripts/ansible/roles/keycloak_ec2_installer/defaults/main.yml new file mode 100644 index 000000000000..b0f7a3190f96 --- /dev/null +++ b/.github/scripts/ansible/roles/keycloak_ec2_installer/defaults/main.yml @@ -0,0 +1,8 @@ +# This should match the user in the *_inventory.yml +ansible_ssh_user: ec2-user +# Workspace on the remote hosts +kc_home: /opt/keycloak +m2_home: ~/.m2 +update_system_packages: no +install_java: yes +java_version: 21 diff --git a/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/install.yml b/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/install.yml new file mode 100644 index 000000000000..0c19fbacf8ac --- /dev/null +++ b/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/install.yml @@ -0,0 +1,36 @@ +- name: Update system packages on the remote hosts + when: update_system_packages + package: + name: "*" + state: latest + +- name: Install Java {{ java_version }} packages on the remote hosts + when: install_java + package: + name: + - "java-{{ java_version }}-openjdk" + - "java-{{ java_version }}-openjdk-devel" + state: present + +- name: Install dependencies on the remote hosts + package: name={{item}} state=present + with_items: + - unzip + +- name: Create Keycloak src dir + file: + path: "{{ kc_home }}" + state: directory + +- name: Install Keycloak src on the remote hosts + unarchive: + src: "{{ keycloak_src }}" + dest: "{{ kc_home }}" + owner: "{{ ansible_ssh_user }}" + +- name: Install Maven repository on the remote hosts + unarchive: + src: "{{ maven_archive }}" + creates: "{{ kc_home }}" + dest: "{{ kc_home }}" + owner: "{{ ansible_ssh_user }}" diff --git a/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/main.yml b/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/main.yml new file mode 100644 index 000000000000..6222f2384947 --- /dev/null +++ b/.github/scripts/ansible/roles/keycloak_ec2_installer/tasks/main.yml @@ -0,0 +1,3 @@ +- include_tasks: install.yml + vars: + ansible_become: yes diff --git a/.github/scripts/ansible/roles/mvn_ec2_runner/README.md b/.github/scripts/ansible/roles/mvn_ec2_runner/README.md new file mode 100644 index 000000000000..2113d9a1f164 --- /dev/null +++ b/.github/scripts/ansible/roles/mvn_ec2_runner/README.md @@ -0,0 +1,33 @@ +# Ansible Role `mvn_ec2_runner` + +Ansible role for executing `mvn` commands against a Keycloak src on a remote node. + +Role assumes presence of host inventory file and a matching SSH key for "sudoer" access to the hosts. +The hosts are expected to be included in `keycloak` group. + +## Parameters + +See `defaults/main.yml` for default values. + +### Execution +- `mvn_params`: The `mvn` command to execute on the remote nodes. + +### Other +- `kc_home`: Location of the Keycloak src on the remote node. + + +## Example Playbook + +An example playbook `keycloak.yml` that applies the role to hosts in the `keycloak` group: +``` +- hosts: keycloak + roles: [mvn] +``` + +## Run keycloak-benchmark + +Run: +``` +ansible-playbook -i ${CLUSTER_NAME}_${REGION}_inventory.yml mvn.yml \ + -e "mvn_params=\"mvn clean install\"" +``` diff --git a/.github/scripts/ansible/roles/mvn_ec2_runner/defaults/main.yml b/.github/scripts/ansible/roles/mvn_ec2_runner/defaults/main.yml new file mode 100644 index 000000000000..1217e5bc966c --- /dev/null +++ b/.github/scripts/ansible/roles/mvn_ec2_runner/defaults/main.yml @@ -0,0 +1,5 @@ +# Workspace on the localhost +local_workspace: files/keycloak + +# Workspace on the remote hosts +kc_home: /opt/keycloak diff --git a/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/main.yml b/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/main.yml new file mode 100644 index 000000000000..996354c88605 --- /dev/null +++ b/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/main.yml @@ -0,0 +1 @@ +- include_tasks: run.yml diff --git a/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/run.yml b/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/run.yml new file mode 100644 index 000000000000..514fa65a3cd3 --- /dev/null +++ b/.github/scripts/ansible/roles/mvn_ec2_runner/tasks/run.yml @@ -0,0 +1,54 @@ +- name: Initialization + run_once: yes + block: + - debug: msg="Variable `mvn_params` must be set." + failed_when: mvn_params == "" + - set_fact: local_results_dir="{{ local_workspace }}/results/{{ '%Y%m%d%H%M%S' | strftime }}" + - debug: var=local_results_dir + +- name: Cleanup Previous Runs + # Kill any currently running Java process from a previous (possibly aborted) run before starting the next. + shell: | + killall java + ignore_errors: yes + +- name: Run mvn command on the remote hosts + ansible.builtin.shell: | + set -o pipefail + cd {{ kc_home }} + echo "{{ mvn_params }}" + ./mvnw {{ mvn_params }} + args: + executable: /usr/bin/bash + # Tests can run for hours. To prevent the test from failing when the SSH connection breaks, use asynchronous polling. + async: 86400 + poll: 10 + register: result + +- debug: var=result + +- name: Recursively find surefire-report directories + ansible.builtin.find: + file_type: directory + paths: "{{ kc_home }}" + patterns: "surefire-reports*" + recurse: true + register: output + +- debug: var=output + +- name: Create local results directories + delegate_to: localhost + file: + path: "{{ local_results_dir }}/{{ item.path | replace(kc_home, '') }}" + state: directory + with_items: + - "{{ output.files }}" + +- name: Copy surefire-report directories to localhost + synchronize: + src: "{{ item.path }}/" + dest: "{{ local_results_dir }}/{{ item.path | replace(kc_home, '') }}" + mode: pull + with_items: + - "{{ output.files }}" diff --git a/.github/scripts/aws/rds/aurora_common.sh b/.github/scripts/aws/rds/aurora_common.sh new file mode 100755 index 000000000000..2b32c0d3b3ec --- /dev/null +++ b/.github/scripts/aws/rds/aurora_common.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +function requiredEnv() { + for ENV in $@; do + if [ -z "${!ENV}" ]; then + echo "${ENV} variable must be set" + exit 1 + fi + done +} + +requiredEnv AURORA_CLUSTER AURORA_REGION + +SCRIPT_DIR=${SCRIPT_DIR:-$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )} +export AURORA_ENGINE=${AURORA_ENGINE:-"aurora-postgresql"} +export AURORA_ENGINE_VERSION=${AURORA_ENGINE_VERSION:-"$(${SCRIPT_DIR}/../../../../mvnw help:evaluate -f ${SCRIPT_DIR}/../../../../pom.xml -Dexpression=aurora-postgresql.version -q -DforceStdout)"} +export AURORA_INSTANCES=${AURORA_INSTANCES:-"2"} +export AURORA_INSTANCE_CLASS=${AURORA_INSTANCE_CLASS:-"db.t4g.large"} +export AURORA_PASSWORD=${AURORA_PASSWORD:-"secret99"} +export AURORA_SECURITY_GROUP_NAME=${AURORA_SECURITY_GROUP_NAME:-"${AURORA_CLUSTER}-security-group"} +export AURORA_SUBNET_A_CIDR=${AURORA_SUBNET_A_CIDR:-"192.168.0.0/19"} +export AURORA_SUBNET_B_CIDR=${AURORA_SUBNET_B_CIDR:-"192.168.32.0/19"} +export AURORA_SUBNET_GROUP_NAME=${AURORA_SUBNET_GROUP_NAME:-"${AURORA_CLUSTER}-subnet-group"} +export AURORA_VPC_CIDR=${AURORA_VPC_CIDR:-"192.168.0.0/16"} +export AURORA_USERNAME=${AURORA_USERNAME:-"keycloak"} +export AWS_REGION=${AWS_REGION:-${AURORA_REGION}} +export AWS_PAGER="" diff --git a/.github/scripts/aws/rds/aurora_create.sh b/.github/scripts/aws/rds/aurora_create.sh new file mode 100755 index 000000000000..d24975254b87 --- /dev/null +++ b/.github/scripts/aws/rds/aurora_create.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -e + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source ${SCRIPT_DIR}/aurora_common.sh + +EXISTING_INSTANCES=$(aws rds describe-db-instances \ + --query "DBInstances[?starts_with(DBInstanceIdentifier, '${AURORA_CLUSTER}')].DBInstanceIdentifier" \ + --output text +) +if [ -n "${EXISTING_INSTANCES}" ]; then + echo "Aurora instances '${EXISTING_INSTANCES}' already exist in the '${AWS_REGION}' region" + exit 1 +fi + +# Create the Aurora VPC +AURORA_VPC=$(aws ec2 create-vpc \ + --cidr-block ${AURORA_VPC_CIDR} \ + --tag-specifications "ResourceType=vpc, Tags=[{Key=AuroraCluster,Value=${AURORA_CLUSTER}},{Key=Name,Value=Aurora Cluster ${AURORA_CLUSTER}}]" \ + --query "Vpc.VpcId" \ + --output text +) + +# Each region may have different availability-zones, so we need to ensure that we use an az that exists +IFS=' ' read -a AZS <<< "$(aws ec2 describe-availability-zones --region ${AURORA_REGION} --query "AvailabilityZones[].ZoneName" --output text)" + +# Create the Aurora Subnets +SUBNET_A=$(aws ec2 create-subnet \ + --availability-zone "${AZS[0]}" \ + --vpc-id ${AURORA_VPC} \ + --cidr-block ${AURORA_SUBNET_A_CIDR} \ + --query "Subnet.SubnetId" \ + --output text +) + +SUBNET_B=$(aws ec2 create-subnet \ + --availability-zone "${AZS[1]}" \ + --vpc-id ${AURORA_VPC} \ + --cidr-block ${AURORA_SUBNET_B_CIDR} \ + --query "Subnet.SubnetId" \ + --output text +) + +AURORA_PUBLIC_ROUTE_TABLE_ID=$(aws ec2 describe-route-tables \ + --filters Name=vpc-id,Values=${AURORA_VPC} \ + --query "RouteTables[0].RouteTableId" \ + --output text +) + +aws ec2 associate-route-table \ + --route-table-id ${AURORA_PUBLIC_ROUTE_TABLE_ID} \ + --subnet-id ${SUBNET_A} + +aws ec2 associate-route-table \ + --route-table-id ${AURORA_PUBLIC_ROUTE_TABLE_ID} \ + --subnet-id ${SUBNET_B} + +# Create Aurora Subnet Group +aws rds create-db-subnet-group \ + --db-subnet-group-name ${AURORA_SUBNET_GROUP_NAME} \ + --db-subnet-group-description "Aurora DB Subnet Group" \ + --subnet-ids ${SUBNET_A} ${SUBNET_B} + +# Create an Aurora VPC Security Group +AURORA_SECURITY_GROUP_ID=$(aws ec2 create-security-group \ + --group-name ${AURORA_SECURITY_GROUP_NAME} \ + --description "Aurora DB Security Group" \ + --vpc-id ${AURORA_VPC} \ + --query "GroupId" \ + --output text +) + +# Make the Aurora endpoint accessible outside the VPC +## Create Internet gateway +INTERNET_GATEWAY=$(aws ec2 create-internet-gateway \ + --tag-specifications "ResourceType=internet-gateway, Tags=[{Key=AuroraCluster,Value=${AURORA_CLUSTER}},{Key=Name,Value=Aurora Cluster ${AURORA_CLUSTER}}]" \ + --query "InternetGateway.InternetGatewayId" \ + --output text +) + +aws ec2 attach-internet-gateway \ + --internet-gateway-id ${INTERNET_GATEWAY} \ + --vpc-id ${AURORA_VPC} + +aws ec2 create-route \ + --route-table-id ${AURORA_PUBLIC_ROUTE_TABLE_ID} \ + --destination-cidr-block 0.0.0.0/0 \ + --gateway-id ${INTERNET_GATEWAY} + +## Enable DNS hostnames required for publicly accessible Aurora instances +aws ec2 modify-vpc-attribute \ + --vpc-id ${AURORA_VPC} \ + --enable-dns-hostnames + +## Ensure the Postgres port is accessible outside the VPC +aws ec2 authorize-security-group-ingress \ + --group-id ${AURORA_SECURITY_GROUP_ID} \ + --ip-permissions "FromPort=5432,ToPort=5432,IpProtocol=tcp,IpRanges=[{CidrIp=0.0.0.0/0}]" + +# Create the Aurora DB cluster and instance +aws rds create-db-cluster \ + --db-cluster-identifier ${AURORA_CLUSTER} \ + --database-name keycloak \ + --engine ${AURORA_ENGINE} \ + --engine-version ${AURORA_ENGINE_VERSION} \ + --master-username ${AURORA_USERNAME} \ + --master-user-password ${AURORA_PASSWORD} \ + --vpc-security-group-ids ${AURORA_SECURITY_GROUP_ID} \ + --db-subnet-group-name ${AURORA_SUBNET_GROUP_NAME} \ + --tags "Key=keepalive" # Add keepalive tag to prevent keycloak-benchmark reaper from removing DB during nightly runs + +# For now only two AZs in each region are supported due to the two subnets created above +for i in $( seq ${AURORA_INSTANCES} ); do + aws rds create-db-instance \ + --no-auto-minor-version-upgrade \ + --db-cluster-identifier ${AURORA_CLUSTER} \ + --db-instance-identifier "${AURORA_CLUSTER}-instance-${i}" \ + --db-instance-class ${AURORA_INSTANCE_CLASS} \ + --engine ${AURORA_ENGINE} \ + --availability-zone "${AZS[$(((i - 1) % ${#AZS[@]}))]}" \ + --publicly-accessible +done + +for i in $( seq ${AURORA_INSTANCES} ); do + aws rds wait db-instance-available --db-instance-identifier "${AURORA_CLUSTER}-instance-${i}" +done + +export AURORA_ENDPOINT=$(aws rds describe-db-clusters \ + --db-cluster-identifier ${AURORA_CLUSTER} \ + --query "DBClusters[*].Endpoint" \ + --output text +) diff --git a/.github/scripts/aws/rds/aurora_delete.sh b/.github/scripts/aws/rds/aurora_delete.sh new file mode 100755 index 000000000000..1e65863701f9 --- /dev/null +++ b/.github/scripts/aws/rds/aurora_delete.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -e + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source ${SCRIPT_DIR}/aurora_common.sh + +AURORA_VPC=$(aws ec2 describe-vpcs \ + --filters "Name=tag:AuroraCluster,Values=${AURORA_CLUSTER}" \ + --query "Vpcs[*].VpcId" \ + --output text +) + +# Delete the Aurora DB cluster and instances +for i in $( aws rds describe-db-clusters --db-cluster-identifier ${AURORA_CLUSTER} --query "DBClusters[0].DBClusterMembers[].DBInstanceIdentifier" --output text ); do + echo "Deleting Aurora DB instance ${i}" + aws rds delete-db-instance --db-instance-identifier "${i}" --skip-final-snapshot || true +done + +aws rds delete-db-cluster \ + --db-cluster-identifier ${AURORA_CLUSTER} \ + --skip-final-snapshot \ + || true + +for i in $( aws rds describe-db-clusters --db-cluster-identifier ${AURORA_CLUSTER} --query "DBClusters[0].DBClusterMembers[].DBInstanceIdentifier" --output text ); do + aws rds wait db-instance-deleted --db-instance-identifier "${i}" +done + +aws rds wait db-cluster-deleted --db-cluster-identifier ${AURORA_CLUSTER} || true + +# Delete the Aurora subnet group +aws rds delete-db-subnet-group --db-subnet-group-name ${AURORA_SUBNET_GROUP_NAME} || true + +# Delete the Aurora subnets +AURORA_SUBNETS=$(aws ec2 describe-subnets \ + --filters "Name=vpc-id,Values=${AURORA_VPC}" \ + --query "Subnets[*].SubnetId" \ + --output text +) +for AURORA_SUBNET in ${AURORA_SUBNETS}; do + aws ec2 delete-subnet --subnet-id ${AURORA_SUBNET} +done + +# Delete the Aurora VPC Security Group +AURORA_SECURITY_GROUP_ID=$(aws ec2 describe-security-groups \ + --filters "Name=vpc-id,Values=${AURORA_VPC}" "Name=group-name,Values=${AURORA_SECURITY_GROUP_NAME}" \ + --query "SecurityGroups[*].GroupId" \ + --output text +) +if [ -n "${AURORA_SECURITY_GROUP_ID}" ]; then + aws ec2 delete-security-group --group-id ${AURORA_SECURITY_GROUP_ID} --region ${AURORA_REGION} +fi + +# Detach the internet gateway from the VPC and remove +INTERNET_GATEWAY=$(aws ec2 describe-internet-gateways \ + --filters "Name=tag:AuroraCluster,Values=${AURORA_CLUSTER}" \ + --query "InternetGateways[*].InternetGatewayId" \ + --output text +) + +aws ec2 detach-internet-gateway \ + --internet-gateway-id ${INTERNET_GATEWAY} \ + --vpc-id ${AURORA_VPC} \ + || true + +aws ec2 delete-internet-gateway --internet-gateway-id ${INTERNET_GATEWAY} || true + +# Delete the Aurora VPC, retrying 5 times in case that dependencies are not removed instantly +n=0 +until [ "$n" -ge 20 ] +do + aws ec2 delete-vpc --vpc-id ${AURORA_VPC} && break + n=$((n+1)) + echo "Unable to remove VPC ${AURORA_VPC}. Attempt ${n}" + sleep 10 +done diff --git a/.github/scripts/download-node-tooling.sh b/.github/scripts/download-node-tooling.sh new file mode 100755 index 000000000000..afee184ae886 --- /dev/null +++ b/.github/scripts/download-node-tooling.sh @@ -0,0 +1,52 @@ +#!/bin/bash -e + +abort() { + echo $1 + exit 1 +} + +download_file() { + local url=$1 + local target=$2 + + echo "Downloading $(basename "$url")..." + mkdir -p "$(dirname "$target")" + curl --silent --fail --show-error --retry 3 --retry-delay 30 --output "$target" "$url" +} + +node_url() { + local version=$1 + local platform=$2 + local root_url="https://nodejs.org/dist/v$version" + + if [ "$platform" == "windows" ]; then + echo "$root_url/win-x64/node.exe" + elif [ "$platform" == "linux" ]; then + echo "$root_url/node-v$version-linux-x64.tar.gz" + else + abort "Unsupported platform: $platform" + fi +} + +pnpm_url() { + local version=$1 + + echo "https://registry.npmjs.org/pnpm/-/pnpm-$version.tgz" +} + +main() { + local node_version=$1 + local pnpm_version=$2 + + if [ "$node_version" == "" ] || [ "$pnpm_version" == "" ]; then + abort "Usage: download-node-tooling.sh " + fi + + local target_directory=~/.m2/repository/com/github/eirslett + + download_file "$(node_url "$node_version" "linux")" "$target_directory/node/$node_version/node-$node_version-linux-x64.tar.gz" + download_file "$(node_url "$node_version" "windows")" "$target_directory/node/$node_version/node-$node_version-win-x64.exe" + download_file "$(pnpm_url "$pnpm_version")" "$target_directory/pnpm/$pnpm_version/pnpm-$pnpm_version.tar.gz" +} + +main "$1" "$2" diff --git a/.github/scripts/pr-backport.sh b/.github/scripts/pr-backport.sh new file mode 100755 index 000000000000..4dcf5608c0f2 --- /dev/null +++ b/.github/scripts/pr-backport.sh @@ -0,0 +1,91 @@ +#!/bin/bash -e + +TARGET_REMOTE=upstream +KEYCLOAK_REPO=https://github.com/keycloak/keycloak +WORK_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +PR=$1 +TARGET=$2 + +function echo_header() { + echo "" + echo "=======================================================================" + echo "$1" + echo "-----------------------------------------------------------------------" +} + +function error() { + echo "=======================================================================" + echo "Error" + echo "-----------------------------------------------------------------------" + echo "$1" + echo "" + exit 1 +} + +if ! [ -x "$(command -v gh)" ]; then + error "The GitHub CLI is not installed. See: https://github.com/cli/cli#installation" +fi + +gh auth status + +if ! [ -x "$(command -v jq)" ]; then + error "The jq CLI is not installed. See: https://jqlang.github.io/jq/download/" +fi + +if [ "$PR" == "" ] || [ "$TARGET" == "" ]; then + error "Usage: gh-backport-pr.sh " +fi + +TARGET_BRANCH=release/$TARGET + +echo_header "Fetching '$TARGET_REMOTE' remote." +git fetch $TARGET_REMOTE + +PR_STATE=$(gh pr view $PR --json state 2>/dev/null | jq -r .state) + +if [ "$PR_STATE" == "" ]; then + error "PR #$PR not found. Make sure the PR exists, and that it's been merged, and your gh repo is set to keycloak/keycloak" +elif [ "$PR_STATE" != "MERGED" ]; then + error "PR #$PR not merged yet. Only merged PRs can be backported." +fi + +MERGE_COMMIT=$(gh pr view $PR --json mergeCommit | jq -r .mergeCommit.oid) + +if [ "$MERGE_COMMIT" == "" ]; then + error "Could not resolve merge commit for PR #$PR" +fi + +PR_BRANCH=backport-$PR-$TARGET +PR_BODY=$(gh pr view $PR --json body | jq -r .body) + +echo_header "Details" +echo "PR Body: $PR_BODY" +echo "" +echo "PR: $KEYCLOAK_REPO/pull/$PR" +echo "Commit: $KEYCLOAK_REPO/commit/$MERGE_COMMIT" +echo "" +echo "PR branch: $PR_BRANCH" +echo "Target branch: $KEYCLOAK_REPO/tree/$TARGET_BRANCH" +echo "" +echo -n "Continue (y/n): " +read PROMPT + +if [ "$PROMPT" != "y" ]; then + exit 1 +fi + +echo_header "Checkout '$TARGET_REMOTE/$TARGET_BRANCH' to '$PR_BRANCH'" +git checkout $TARGET_REMOTE/$TARGET_BRANCH -B $PR_BRANCH + +echo_header "Cherry-pick $MERGE_COMMIT" +git cherry-pick -x $MERGE_COMMIT + +echo_header "Push '$PR_BRANCH' to 'origin' remote" +git push origin $PR_BRANCH:$PR_BRANCH --set-upstream + +echo_header "Opening web browser to create pull request" +gh pr create -B $TARGET_BRANCH -f -w + +echo_header "Checkout to $WORK_BRANCH branch" +git checkout $WORK_BRANCH diff --git a/.github/scripts/pr-find-issues-test.sh b/.github/scripts/pr-find-issues-test.sh new file mode 100755 index 000000000000..03de462421b9 --- /dev/null +++ b/.github/scripts/pr-find-issues-test.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e +# Use this script to test different variants of a PR body. + +source ./pr-find-issues.sh + +function testParsing() { + echo -n "$1 -> $2 " + if [ "$(parse_issues "$1")" != "$2" ]; then + echo "(failure)" + return 1 + fi + echo "(success)" + return 0 +} + +function testFailed() { + echo "Test Failed!" +} + +trap 'testFailed' ERR + +testParsing "Closes #123" "123" +testParsing "Fixes #123" "123" +testParsing "Fixes: #123" "123" +testParsing "Fixes https://github.com/keycloak/keycloak/issues/123" "123" \ No newline at end of file diff --git a/.github/scripts/pr-find-issues.sh b/.github/scripts/pr-find-issues.sh new file mode 100755 index 000000000000..e9bd720ea29b --- /dev/null +++ b/.github/scripts/pr-find-issues.sh @@ -0,0 +1,34 @@ +#!/bin/bash -e + +PR="$1" +REPO="$2" + +if [ "$REPO" == "" ]; then + REPO="keycloak/keycloak" +fi + +function parse_issues() { + echo "$1" | \ + grep -i -P -o "(close|closes|closed|resolve|resolves|resolved|fixes|fixed):? (#|https://github.com/keycloak/keycloak/issues/)[[:digit:]]*" | \ + sed -e 's|https://github.com/keycloak/keycloak/issues/|#|g' | \ + sed -e 's|keycloak/keycloak/issues/|#|g' | \ + cut -d '#' -f 2 | sort -n +} + +if [ "$PR" != "" ]; then + PR_JSON=$(gh api "/repos/$REPO/pulls/$PR") + PR_BODY=$(echo "$PR_JSON" | jq -r .body) + PR_MERGE_COMMIT_SHA=$(echo "$PR_JSON" | jq -r .merge_commit_sha) + + ISSUES=$(parse_issues "$PR_BODY") + if [ "$ISSUES" == "" ]; then + COMMIT_JSON=$(gh api "/repos/$REPO/commits/$PR_MERGE_COMMIT_SHA") + COMMIT_MESSAGE=$(echo "$COMMIT_JSON" | jq -r .commit.message) + + ISSUES=$(parse_issues "$COMMIT_MESSAGE") + fi + + for i in $ISSUES; do + echo "$i" + done +fi diff --git a/.github/scripts/prepare-quarkus-next.sh b/.github/scripts/prepare-quarkus-next.sh new file mode 100755 index 000000000000..60d6c1acd5d8 --- /dev/null +++ b/.github/scripts/prepare-quarkus-next.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -xeuo pipefail + +add_repository() { + local file=$1 + local element=$2 + + local id="sonatype-snapshots" + local name="Sonatype Snapshots" + local url="https://central.sonatype.com/repository/maven-snapshots/" + + # Decide the tag based on the element + local tag + if [ "$element" = "repository" ]; then + tag="repositories" + elif [ "$element" = "pluginRepository" ]; then + tag="pluginRepositories" + fi + + # Repository to be inserted + local repository="<$element> \ + $id \ + $name \ + $url \ + \ + true \ + daily \ + \ + \ + false \ + \ + " + + # Check if the tag exists in the file + if grep -q "<$tag>" "$file"; then + # Insert the element before the closing tag + sed -i "/<\/$tag>/i $repository" "$file" + else + # If the tag doesn't exist, create it and insert the element + sed -i "/<\/project>/i \ + <$tag> \ + $repository \ + " "$file" + fi +} + +git checkout -b new-quarkus-next origin/main + +add_repository "pom.xml" "repository" +add_repository "quarkus/pom.xml" "pluginRepository" +add_repository "operator/pom.xml" "pluginRepository" + +./quarkus/set-quarkus-version.sh +git commit -am "Set quarkus version to 999-SNAPSHOT" + +snapshot_version_hash=$(git log origin/quarkus-next --grep="Set quarkus version to 999-SNAPSHOT" --format="%H" -n 1) +commits_to_cherry_pick=$(git rev-list --right-only --no-merges --reverse new-quarkus-next...origin/quarkus-next | grep -vE "$snapshot_version_hash" || echo "") + +if [ -z "$commits_to_cherry_pick" ]; then + echo "Nothing to cherry-pick." +else + for commit in $commits_to_cherry_pick + do + if git cherry-pick "$commit"; then + echo "Successfully cherry-picked $commit" + else + echo "Failed to cherry-pick $commit" + exit 1 + fi + done +fi \ No newline at end of file diff --git a/.github/scripts/run-fips-it.sh b/.github/scripts/run-fips-it.sh index bd19dd2981dd..64b40498568c 100755 --- a/.github/scripts/run-fips-it.sh +++ b/.github/scripts/run-fips-it.sh @@ -1,6 +1,7 @@ -#!/bin/bash +#!/bin/bash -x -dnf install -y java-17-openjdk-devel +rm -f /etc/system-fips +dnf install -y java-21-openjdk-devel fips-mode-setup --enable --no-bootcfg fips-mode-setup --is-enabled if [ $? -ne 0 ]; then @@ -13,8 +14,26 @@ fi echo "STRICT_OPTIONS: $STRICT_OPTIONS" TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh fips` echo "Tests: $TESTS" -export JAVA_HOME=/etc/alternatives/java_sdk_17 +export JAVA_HOME=/etc/alternatives/java_sdk_21 set -o pipefail +# Build adapter distributions +./mvnw install -DskipTests -f distribution/pom.xml +if [ $? -ne 0 ]; then + exit 1 +fi + +# Build app servers +./mvnw install -DskipTests -Pbuild-app-servers -f testsuite/integration-arquillian/servers/app-server/pom.xml +if [ $? -ne 0 ]; then + exit 1 +fi + +# Prepare Quarkus distribution with BCFIPS +./mvnw install -e -pl testsuite/integration-arquillian/servers/auth-server/quarkus -Pauth-server-quarkus,auth-server-fips140-2 +if [ $? -ne 0 ]; then + exit 1 +fi + # Profile app-server-wildfly needs to be explicitly set for FIPS tests -./mvnw test -Dsurefire.rerunFailingTestsCount=$SUREFIRE_RERUN_FAILING_COUNT -nsu -B -Pauth-server-quarkus,auth-server-fips140-2,app-server-wildfly -Dcom.redhat.fips=false $STRICT_OPTIONS -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base | misc/log/trimmer.sh +./mvnw test -Dsurefire.rerunFailingTestsCount=$SUREFIRE_RERUN_FAILING_COUNT -nsu -B -Pauth-server-quarkus,auth-server-fips140-2,app-server-wildfly -Dcom.redhat.fips=false $STRICT_OPTIONS -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh diff --git a/.github/scripts/run-fips-ut.sh b/.github/scripts/run-fips-ut.sh index 0579caebe9a4..0ebcb002a236 100755 --- a/.github/scripts/run-fips-ut.sh +++ b/.github/scripts/run-fips-ut.sh @@ -1,13 +1,13 @@ #!/bin/bash -dnf install -y java-17-openjdk-devel crypto-policies-scripts +rm -f /etc/system-fips +dnf install -y java-21-openjdk-devel crypto-policies-scripts fips-mode-setup --enable --no-bootcfg fips-mode-setup --is-enabled if [ $? -ne 0 ]; then exit 1 fi -echo "fips.provider.7=XMLDSig" >>/etc/alternatives/java_sdk_17/conf/security/java.security -export JAVA_HOME=/etc/alternatives/java_sdk_17 +export JAVA_HOME=/etc/alternatives/java_sdk_21 # Build all dependent modules ./mvnw install -nsu -B -am -pl crypto/default,crypto/fips1402 -DskipTests diff --git a/.github/scripts/run-ipa-tests.sh b/.github/scripts/run-ipa-tests.sh index 34c784bb7f16..0b3bd4d462b0 100755 --- a/.github/scripts/run-ipa-tests.sh +++ b/.github/scripts/run-ipa-tests.sh @@ -39,9 +39,9 @@ EOF kdestroy fi -echo "Installing jdk-17 in the container" -dnf install -y java-17-openjdk-devel -export JAVA_HOME=/etc/alternatives/java_sdk_17 +echo "Installing jdk-21 in the container" +dnf install -y java-21-openjdk-devel +export JAVA_HOME=/etc/alternatives/java_sdk_21 echo "Building quarkus keyclok server with SSSD integration" ./mvnw install -nsu -B -e -pl testsuite/integration-arquillian/servers/auth-server/quarkus -Pauth-server-quarkus diff --git a/.github/scripts/version-compatibility.sh b/.github/scripts/version-compatibility.sh new file mode 100755 index 000000000000..fa42ccf1a9b9 --- /dev/null +++ b/.github/scripts/version-compatibility.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +TARGET_BRANCH="$1" +REPO="${2:-keycloak}" +ORG="${3:-keycloak}" + +if [[ "${TARGET_BRANCH}" != "release/"* ]]; then + exit 0 +fi + +ALL_RELEASES=$(gh release list \ + --repo "${ORG}/${REPO}" \ + --exclude-drafts \ + --exclude-pre-releases \ + --json name \ + --template '{{range .}}{{.name}}{{"\n"}}{{end}}' +) +MAJOR_MINOR=${TARGET_BRANCH#"release/"} +MAJOR_MINOR_RELEASES=$(echo "${ALL_RELEASES}" | grep "${MAJOR_MINOR}") + +echo "${MAJOR_MINOR_RELEASES}" | jq -cnR '[inputs] | map({version: .})' \ No newline at end of file diff --git a/.github/snyk/.snyk b/.github/snyk/.snyk index a665816c2e0f..d4327350ba4b 100644 --- a/.github/snyk/.snyk +++ b/.github/snyk/.snyk @@ -15,19 +15,6 @@ ignore: The Keycloak services module is not affected by CVE-2021-3461 anymore, the issue was fixed on Keycloak 14.0.0 last year. More details: - https://issues.redhat.com/browse/KEYCLOAK-17495 - SNYK-JAVA-IONETTY-1042268: - - "*": - reason: > - There is no fixed version for io.netty:netty-handler. More details: - - https://github.com/netty/netty/issues/10806 - - https://github.com/netty/netty/issues/8537 - - https://github.com/netty/netty/issues/9930 - - https://github.com/netty/netty/issues/10362 - Netty Handler is a transitive dependency coming from Quarkus, - according to the Netty team, the fix should be available on Netty 5. - The expiry date was set as a reminder for us to upgrade, once they - provide the fix. - expires: 2024-06-31T00:00:00.000Z SNYK-JAVA-ORGKEYCLOAK-1658295: - "*": reason: > diff --git a/.github/teams.yml b/.github/teams.yml index e0793a339487..85b00787318d 100644 --- a/.github/teams.yml +++ b/.github/teams.yml @@ -1,6 +1,5 @@ team/cloud-native: - area/admin/cli - - area/dependencies - area/dist/quarkus - area/operator @@ -8,40 +7,49 @@ team/continuous-testing: - area/ci - area/testsuite -team/core: - - area/admin/api - - area/admin/fine-grained-permissions +team/core-clients: + - area/adapter/fuse + - area/adapter/java-cli + - area/adapter/jee + - area/adapter/jee-saml + - area/adapter/spring + - area/adapter/javascript - area/authentication - area/authentication/webauthn - - area/authorization-services - - area/core - - area/identity-brokering + - area/login/ui - area/oidc + - area/oid4vc - area/saml - area/token-exchange + +team/core-iam: + - area/admin/fine-grained-permissions + - area/authorization-services + - area/identity-brokering + - area/ldap + - area/organizations - area/user-profile -team/store: +team/core-shared: + - area/account/api + - area/admin/api + - area/admin/client-java + - area/core - area/import-export - area/infinispan - - area/ldap - area/storage +team/sre: + team/ui: - area/account/ui - - area/adapter/javascript - - area/admin/client/node + - area/admin/client-js - area/admin/ui - -team/other: - - area/account/api - - area/adapter/fuse - - area/adapter/java-cli - - area/adapter/jee - - area/adapter/jee-saml - - area/adapter/spring - - area/dist/wildfly - - area/docs + - area/welcome/ui team/community: - area/translations + +no-team: + - area/docs + - area/dependencies diff --git a/.github/workflows/aurora-delete.yml b/.github/workflows/aurora-delete.yml new file mode 100644 index 000000000000..34fa4808ff5b --- /dev/null +++ b/.github/workflows/aurora-delete.yml @@ -0,0 +1,37 @@ +name: Aurora Delete + +on: + workflow_dispatch: + inputs: + name: + description: 'The name of the Aurora DB cluster' + type: string + required: true + region: + description: 'The AWS region used to host the Aurora DB' + type: string + required: true + +permissions: + contents: read + +jobs: + delete: + name: Delete Aurora DB + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Initialize AWS client + run: | + aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} + aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws configure set region ${{ inputs.region }} + + - id: delete + shell: bash + run: ./aurora_delete.sh + working-directory: .github/scripts/aws/rds + env: + AURORA_CLUSTER: ${{ inputs.name }} + AURORA_REGION: ${{ inputs.region }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be19c95767e7..08b64439cdf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ defaults: run: shell: bash +permissions: + contents: read + jobs: conditional: @@ -29,34 +32,68 @@ jobs: runs-on: ubuntu-latest outputs: ci: ${{ steps.conditional.outputs.ci }} + ci-quarkus: ${{ steps.conditional.outputs.ci-quarkus }} ci-store: ${{ steps.conditional.outputs.ci-store }} ci-sssd: ${{ steps.conditional.outputs.ci-sssd }} + ci-webauthn: ${{ steps.conditional.outputs.ci-webauthn }} + ci-aurora: ${{ steps.auroradb-tests.outputs.run-aurora-tests }} + ci-compatibility-matrix: ${{ steps.version-compatibility.outputs.matrix }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional with: token: ${{ secrets.GITHUB_TOKEN }} + - name: AuroraDB conditional check + id: auroradb-tests + run: | + RUN_AURORADB_TESTS=false + if [[ $GITHUB_EVENT_NAME != "pull_request" && -n "${{ secrets.AWS_SECRET_ACCESS_KEY }}" ]]; then + RUN_AURORADB_TESTS=true + fi + echo "run-aurora-tests=$RUN_AURORADB_TESTS" >> $GITHUB_OUTPUT + + - name: Version Compatibility Matrix + id: version-compatibility + env: + GH_TOKEN: ${{ github.token }} + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BRANCH="${{ github.base_ref }}" + else + BRANCH="${{ github.ref_name }}" + fi + MATRIX_JSON=$(./.github/scripts/version-compatibility.sh "${BRANCH}") + echo "${MATRIX_JSON}" + echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT + build: name: Build if: needs.conditional.outputs.ci == 'true' runs-on: ubuntu-latest needs: conditional steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Keycloak uses: ./.github/actions/build-keycloak + - name: Check for unstaged proto.lock files + if: github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') + run: git diff --name-only --exit-code -- "**/proto.lock" + unit-tests: name: Base UT runs-on: ubuntu-latest needs: build timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: unit-test-setup name: Unit test setup @@ -66,7 +103,7 @@ jobs: run: | SEP="" PROJECTS="" - for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do + for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs|tests|test-framework)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do PROJECTS="$PROJECTS$SEP$i" SEP="," done @@ -93,7 +130,7 @@ jobs: group: [1, 2, 3, 4, 5, 6] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -103,7 +140,7 @@ jobs: run: | TESTS=`testsuite/integration-arquillian/tests/base/testsuites/base-suite.sh ${{ matrix.group }}` echo "Tests: $TESTS" - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base | misc/log/trimmer.sh + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -122,16 +159,99 @@ jobs: with: job-id: base-integration-tests-${{ matrix.group }} + adapter-integration-tests: + name: Adapter IT + needs: build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Build adapter distributions + run: ./mvnw install -DskipTests -f distribution/pom.xml + + - name: Build app servers + run: ./mvnw install -DskipTests -Pbuild-app-servers -f testsuite/integration-arquillian/servers/app-server/pom.xml + + - name: Run adapter tests + run: | + TESTS="org.keycloak.testsuite.adapter.**" + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Papp-server-wildfly -Dtest=$TESTS -Dapp.server.ssl.required=true -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Base IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: adapter-integration-tests + + adapter-integration-tests-strict-cookies: + name: Adapter IT Strict Cookies + needs: build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Build adapter distributions + run: ./mvnw install -DskipTests -f distribution/pom.xml + + - name: Build app servers + run: ./mvnw install -DskipTests -Pbuild-app-servers -f testsuite/integration-arquillian/servers/app-server/pom.xml + + - name: Run adapter tests + run: | + TESTS="org.keycloak.testsuite.adapter.**" + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pfirefox-strict-cookies -Pauth-server-quarkus -Papp-server-wildfly -Dtest=$TESTS -Dapp.server.ssl.required=true "-Dwebdriver.gecko.driver=$GECKOWEBDRIVER/geckodriver" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Base IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: adapter-integration-tests-strict-cookies + quarkus-unit-tests: name: Quarkus UT - needs: build + needs: [build, conditional] + if: needs.conditional.outputs.ci-quarkus == 'true' timeout-minutes: 15 strategy: matrix: os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # We want to download Keycloak artifacts - id: integration-test-setup @@ -150,45 +270,62 @@ jobs: if: always() uses: ./.github/actions/archive-surefire-reports with: - job-id: quarkus-unit-tests + job-id: quarkus-unit-tests-${{ matrix.os }} quarkus-integration-tests: name: Quarkus IT - needs: build + needs: [build, conditional] timeout-minutes: 115 strategy: matrix: - os: [ubuntu-latest, windows-latest] - server: [sanity-check-zip, zip, container, storage] - exclude: - - os: windows-latest - server: zip + os: [ubuntu-latest] + suite: [zip-slow, zip-fast, container, storage, smoke] + full-testsuite: + - ${{ needs.conditional.outputs.ci-quarkus == 'true' }} + # Win runs always as includes are evaluated after excludes + include: - os: windows-latest - server: container - - os: windows-latest - server: storage + suite: win + # Either run smoke tests, or full testsuite + exclude: + - full-testsuite: false + suite: zip-slow + - full-testsuite: false + suite: zip-fast + - full-testsuite: false + suite: container + - full-testsuite: false + suite: storage + - full-testsuite: true + suite: smoke fail-fast: false runs-on: ${{ matrix.os }} env: - MAVEN_OPTS: -Xmx1024m + MAVEN_OPTS: -Xmx1536m steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - id: unit-test-setup - name: Unit test setup - uses: ./.github/actions/unit-test-setup + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup - # Not sure why, but needs to re-build otherwise there's some failures starting up + # Smoke tests should cover scenarios that could be broken by changes in other modules that quarkus + # test classes and even individual tests are included in the following suites by junit tags + # kc.quarkus.tests.groups acts as the tag filter - name: Run Quarkus integration Tests run: | declare -A PARAMS - PARAMS["sanity-check-zip"]="-Dtest=StartCommandDistTest,StartDevCommandDistTest,BuildAndStartDistTest,ImportAtStartupDistTest" - PARAMS["zip"]="" + PARAMS["win"]="-Dkc.quarkus.tests.groups=win" + PARAMS["zip-slow"]="-Dkc.quarkus.tests.groups=slow" + PARAMS["zip-fast"]='-Dkc.quarkus.tests.groups=!slow' PARAMS["container"]="-Dkc.quarkus.tests.dist=docker" - PARAMS["storage"]="-Ptest-database -Dtest=PostgreSQLDistTest,MariaDBDistTest#testSuccessful,MySQLDistTest#testSuccessful,DatabaseOptionsDistTest,JPAStoreDistTest,HotRodStoreDistTest,MixedStoreDistTest,TransactionConfigurationDistTest" + PARAMS["storage"]="-Ptest-database" + PARAMS["smoke"]="-Dkc.quarkus.tests.groups=smoke" - ./mvnw install -pl quarkus/tests/integration -am -DskipTests - ./mvnw test -pl quarkus/tests/integration ${PARAMS["${{ matrix.server }}"]} | misc/log/trimmer.sh + if [ "${{ matrix.suite }}" == "container" ]; then + ./mvnw install -pl quarkus/tests/integration -am -DskipTests + fi + ./mvnw test -pl quarkus/tests/integration ${PARAMS["${{ matrix.suite }}"]} 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -198,7 +335,7 @@ jobs: if: always() uses: ./.github/actions/archive-surefire-reports with: - job-id: quarkus-integration-tests-${{ matrix.os }}-${{ matrix.server }} + job-id: quarkus-integration-tests-${{ matrix.os }}-${{ matrix.suite }} jdk-integration-tests: name: Java Distribution IT @@ -208,11 +345,11 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] dist: [temurin] - version: [19] + version: [17, 24] fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -224,15 +361,57 @@ jobs: - name: Prepare Quarkus distribution with current JDK run: ./mvnw install -e -pl testsuite/integration-arquillian/servers/auth-server/quarkus + - name: Run new base tests + run: ./mvnw package -f tests/pom.xml -Dtest=JDKTestSuite + - name: Run base tests run: | TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh jdk` echo "Tests: $TESTS" - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base | misc/log/trimmer.sh + if [ "$OSTYPE" == "msys" ]; then + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS "-Dwebdriver.chrome.driver=$ChromeWebDriver/chromedriver.exe" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + else + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + fi - name: Build with JDK run: - ./mvnw install -e -DskipTests -DskipExamples + ./mvnw install -e -DskipTests -DskipExamples -DskipProtoLock=true + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Java Distribution IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: jdk-integration-tests-${{ matrix.os }}-${{ matrix.dist }}-${{ matrix.version }} + + login-v1-tests: + name: Login Theme v1 tests + needs: build + timeout-minutes: 100 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run base tests + run: | + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh login` + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -251,28 +430,333 @@ jobs: with: job-id: jdk-integration-tests-${{ matrix.os }}-${{ matrix.dist }}-${{ matrix.version }} - legacy-store-integration-tests: - name: Legacy Store IT + volatile-sessions-tests: + name: Volatile Sessions IT needs: [build, conditional] if: needs.conditional.outputs.ci-store == 'true' runs-on: ubuntu-latest + timeout-minutes: 150 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run base tests + run: | + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh volatile-sessions` + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dauth.server.feature.disable=persistent-user-sessions -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Store IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: store-integration-tests-${{ matrix.variant }} + + - name: EC2 Maven Logs + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: store-it-mvn-logs + path: .github/scripts/ansible/files + + external-infinispan-tests: + name: External Infinispan IT + needs: [ build, conditional ] + if: needs.conditional.outputs.ci-store == 'true' + runs-on: ubuntu-latest + timeout-minutes: 150 + strategy: + matrix: + variant: [ "clusterless,multi-site" ] + fail-fast: false + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run base tests without cache + run: | + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh clusterless` + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Pinfinispan-server -Dauth.server.feature=${{ matrix.variant }} -Dauth.server.feature.disable=persistent-user-sessions -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Remote Infinispan IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: remote-infinispan-integration-tests + + auroradb-integration-tests: + name: AuroraDB IT + needs: conditional + if: needs.conditional.outputs.ci-aurora == 'true' + runs-on: ubuntu-latest + timeout-minutes: 150 + permissions: + contents: read + actions: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: node-cache + name: Node cache + uses: ./.github/actions/node-cache + + - id: aurora-init + name: Initialize Aurora environment + run: | + AWS_REGION=us-east-1 + echo "AWS Region: ${AWS_REGION}" + + aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} + aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws configure set region ${AWS_REGION} + + AURORA_CLUSTER_NAME="gh-action-$(git rev-parse --short HEAD)-${{ github.run_id }}-${{ github.run_attempt }}" + PASS=$(tr -dc A-Za-z0-9 > $GITHUB_OUTPUT + echo "aurora-cluster-password=${PASS}" >> $GITHUB_OUTPUT + echo "region=${AWS_REGION}" >> $GITHUB_OUTPUT + curl --fail-with-body https://truststore.pki.rds.amazonaws.com/${AWS_REGION}/${AWS_REGION}-bundle.pem -o aws.pem + PROPS+=' -Dkeycloak.connectionsJpa.jdbcParameters=\"?ssl=true&sslmode=verify-ca&sslrootcert=/opt/keycloak/aws.pem\"' + + echo "maven_properties=${PROPS}" >> $GITHUB_OUTPUT + + - id: aurora-create + name: Create Aurora DB + uses: ./.github/actions/aurora-create-database + with: + name: ${{ steps.aurora-init.outputs.aurora-cluster-name }} + password: ${{ steps.aurora-init.outputs.aurora-cluster-password }} + region: ${{ steps.aurora-init.outputs.region }} + + - id: ec2-create + name: Create EC2 runner instance + run: | + AWS_REGION=${{ steps.aurora-init.outputs.region }} + EC2_CLUSTER_NAME=keycloak_$(git rev-parse --short HEAD) + echo "ec2_cluster=${EC2_CLUSTER_NAME}" >> $GITHUB_OUTPUT + + git archive --format=zip --output /tmp/keycloak.zip $GITHUB_REF + zip -u /tmp/keycloak.zip aws.pem + tar -C ~/ -czvf m2.tar.gz .m2 + + cd .github/scripts/ansible + python3 -m venv .venv + source .venv/bin/activate + ./aws_ec2.sh requirements + pipx inject ansible-core boto3 botocore + ./aws_ec2.sh create ${AWS_REGION} ${EC2_CLUSTER_NAME} + ./keycloak_ec2_installer.sh ${AWS_REGION} ${EC2_CLUSTER_NAME} /tmp/keycloak.zip m2.tar.gz + ./mvn_ec2_runner.sh ${AWS_REGION} ${EC2_CLUSTER_NAME} "clean install -B -DskipTests -Pdistribution -DskipProtoLock=true" + ./mvn_ec2_runner.sh ${AWS_REGION} ${EC2_CLUSTER_NAME} "clean install -B -DskipTests -pl testsuite/integration-arquillian/servers/auth-server/quarkus -Pauth-server-quarkus -Pdb-aurora-postgres -Dmaven.build.cache.enabled=true" + + - name: Run Aurora migration tests on EC2 + id: aurora-migration-tests + env: + old-version: 24.0.4 + run: | + EC2_CLUSTER_NAME=${{ steps.ec2-create.outputs.ec2_cluster }} + AWS_REGION=${{ steps.aurora-init.outputs.region }} + PROPS='${{ steps.aurora-init.outputs.maven_properties }}' + + PROPS+=" -Dauth.server.db.host=${{ steps.aurora-create.outputs.endpoint }} -Dkeycloak.connectionsJpa.password=${{ steps.aurora-init.outputs.aurora-cluster-password }}" + PROPS+=" -Djdbc.mvn.groupId=software.amazon.jdbc -Djdbc.mvn.artifactId=aws-advanced-jdbc-wrapper -Djdbc.mvn.version=2.3.1 -Djdbc.driver.tmp.dir=target/unpacked/keycloak-${{ env.old-version }}/providers" + + cd .github/scripts/ansible + ./mvn_ec2_runner.sh ${AWS_REGION} ${EC2_CLUSTER_NAME} "clean install -B ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Pdb-aurora-postgres -Pauth-server-migration $PROPS -Dtest=MigrationTest -Dmigration.mode=auto -Dmigrated.auth.server.version=${{ env.old-version }} -Dmigration.import.file.name=migration-realm-${{ env.old-version }}.json -Dauth.server.ssl.required=false -f testsuite/integration-arquillian/pom.xml 2>&1 | misc/log/trimmer.sh" + + # Copy returned surefire-report directories to workspace root to ensure they're discovered + results=(files/keycloak/results/*) + rsync -a $results/* ../../../ + + rm -rf $results + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: AuroraDB IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: migration-tests-${{ env.old-version }}-aurora-postgres + + - name: EC2 Maven Logs + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: auroraDB-migration-tests-mvn-logs + path: .github/scripts/ansible/files + + - name: Run Aurora integration tests on EC2 + id: aurora-integration-tests + run: | + EC2_CLUSTER_NAME=${{ steps.ec2-create.outputs.ec2_cluster }} + AWS_REGION=${{ steps.aurora-init.outputs.region }} + PROPS='${{ steps.aurora-init.outputs.maven_properties }}' + PROPS+=" -Dauth.server.db.host=${{ steps.aurora-create.outputs.endpoint }} -Dkeycloak.connectionsJpa.password=${{ steps.aurora-init.outputs.aurora-cluster-password }}" + + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh database` + echo "Tests: $TESTS" + + cd .github/scripts/ansible + ./mvn_ec2_runner.sh ${AWS_REGION} ${EC2_CLUSTER_NAME} "test -B ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Pdb-aurora-postgres $PROPS -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh" + + # Copy returned surefire-report directories to workspace root to ensure they're discovered + results=(files/keycloak/results/*) + rsync -a $results/* ../../../ + rm -rf $results + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: AuroraDB IT + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: aurora-integration-tests + + - name: EC2 Maven Logs + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: aurora-integration-tests-mvn-logs + path: .github/scripts/ansible/files + + - name: Delete EC2 Instance + if: always() + working-directory: .github/scripts/ansible + run: | + source .venv/bin/activate + ./aws_ec2.sh delete ${{ steps.aurora-init.outputs.region }} ${{ steps.ec2-create.outputs.ec2_cluster }} + + - name: Delete Aurora DB + if: always() + run: | + gh workflow run aurora-delete.yml \ + -f name=${{ steps.aurora-init.outputs.aurora-cluster-name }} \ + -f region=${{ steps.aurora-init.outputs.region }} \ + --repo ${{ github.repository }} \ + --ref ${{ github.ref_name }} + env: + GH_TOKEN: ${{ github.token }} + + store-integration-tests: + name: Store IT + needs: build + runs-on: ubuntu-latest timeout-minutes: 75 strategy: matrix: db: [postgres, mysql, oracle, mssql, mariadb] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup uses: ./.github/actions/integration-test-setup + - name: Run new base tests + run: | + KC_TEST_DATABASE=${{ matrix.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=DatabaseTestSuite -Dkeycloak.distribution.start.timeout=360 + + - name: Database container port + run: | + # The Ryuk container process exists temporarily after the JVM terminates, wait for only the database container to remain + while [ "$(docker ps -q | wc -l)" -ne 1 ]; do + docker ps + sleep 10 + done + DATABASE_PORT=$(docker ps -l --format '{{ .ID }}' | xargs docker port | cut -d ':' -f 2) + echo "DATABASE_PORT=$DATABASE_PORT" >> $GITHUB_ENV + - name: Run base tests run: | TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh database` echo "Tests: $TESTS" - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Pdb-${{ matrix.db }} -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base | misc/log/trimmer.sh + ./mvnw test ${{ env.SUREFIRE_RETRY }} \ + -Pauth-server-quarkus -Pdb-${{ matrix.db }} \ + "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" \ + -Dtest=$TESTS \ + -Ddocker.database.skip=true \ + -Ddocker.database.port=$DATABASE_PORT \ + -Ddocker.container.testdb.ip=localhost \ + -Dkeycloak.distribution.start.timeout=360 \ + -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + + - name: Run cluster JDBC_PING2 UDP smoke test + run: | + ./mvnw test ${{ env.SUREFIRE_RETRY }} \ + -Pauth-server-cluster-quarkus \ + -Pdb-${{ matrix.db }} \ + -Dtest=RealmInvalidationClusterTest \ + -Dsession.cache.owners=2 \ + -Dauth.server.quarkus.cluster.stack=jdbc-ping-udp \ + -Ddocker.database.skip=true \ + -Ddocker.database.port=$DATABASE_PORT \ + -Ddocker.container.testdb.ip=localhost \ + -pl testsuite/integration-arquillian/tests/base \ + 2>&1 | misc/log/trimmer.sh + + - name: Run cluster JDBC_PING2 TCP smoke test + run: | + ./mvnw test ${{ env.SUREFIRE_RETRY }} \ + -Pauth-server-cluster-quarkus \ + -Pdb-${{ matrix.db }} \ + -Dtest=RealmInvalidationClusterTest \ + -Dsession.cache.owners=2 \ + -Dauth.server.quarkus.cluster.stack=jdbc-ping \ + -Ddocker.database.skip=true \ + -Ddocker.database.port=$DATABASE_PORT \ + -Ddocker.container.testdb.ip=localhost \ + -pl testsuite/integration-arquillian/tests/base \ + 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -283,13 +767,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} with: - job-name: Legacy Store IT + job-name: Store IT - name: Surefire reports if: always() uses: ./.github/actions/archive-surefire-reports with: - job-id: legacy-store-integration-tests-${{ matrix.db }} + job-id: store-integration-tests-${{ matrix.db }} store-model-tests: name: Store Model Tests @@ -298,7 +782,7 @@ jobs: if: needs.conditional.outputs.ci-store == 'true' timeout-minutes: 75 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -325,14 +809,14 @@ jobs: job-id: store-model-tests clustering-integration-tests: - name: Legacy Clustering IT + name: Clustering IT needs: build runs-on: ubuntu-latest timeout-minutes: 35 env: - MAVEN_OPTS: -Xmx1024m + MAVEN_OPTS: -Xmx1536m steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -340,7 +824,7 @@ jobs: - name: Run cluster tests run: | - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-cluster-quarkus -Dsession.cache.owners=2 -Dtest=**.cluster.** -pl testsuite/integration-arquillian/tests/base | misc/log/trimmer.sh + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-cluster-quarkus,db-postgres "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dsession.cache.owners=2 -Dtest=**.cluster.** -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -351,7 +835,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} with: - job-name: Legacy Clustering IT + job-name: Clustering IT - name: Surefire reports if: always() @@ -365,7 +849,11 @@ jobs: needs: build timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: unit-test-setup + name: Unit test setup + uses: ./.github/actions/unit-test-setup - name: Fake fips run: | @@ -373,10 +861,6 @@ jobs: make sudo insmod fake_fips.ko - - id: unit-test-setup - name: Unit test setup - uses: ./.github/actions/unit-test-setup - - name: Run crypto tests run: docker run --rm --workdir /github/workspace -v "${{ github.workspace }}":"/github/workspace" -v "$HOME/.m2":"/root/.m2" registry.access.redhat.com/ubi8/ubi:latest .github/scripts/run-fips-ut.sh @@ -400,22 +884,19 @@ jobs: mode: [non-strict, strict] fail-fast: false steps: - - uses: actions/checkout@v4 - - - name: Fake fips - run: | - cd .github/fake_fips - make - sudo insmod fake_fips.ko + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup uses: ./.github/actions/integration-test-setup with: - jdk-version: 17 + jdk-version: 21 - - name: Prepare Quarkus distribution with BCFIPS - run: ./mvnw install -e -pl testsuite/integration-arquillian/servers/auth-server/quarkus -Pauth-server-quarkus,auth-server-fips140-2 + - name: Fake fips + run: | + cd .github/fake_fips + make + sudo insmod fake_fips.ko - name: Run base tests run: docker run --rm --workdir /github/workspace -e "SUREFIRE_RERUN_FAILING_COUNT" -v "${{ github.workspace }}":"/github/workspace" -v "$HOME/.m2":"/root/.m2" registry.access.redhat.com/ubi8/ubi:latest .github/scripts/run-fips-it.sh ${{ matrix.mode }} @@ -437,42 +918,6 @@ jobs: with: job-id: fips-integration-tests-${{ matrix.mode }} - account-console-integration-tests: - name: Account Console IT - runs-on: ubuntu-latest - needs: build - timeout-minutes: 75 - strategy: - matrix: - browser: [chrome] - fail-fast: false - steps: - - uses: actions/checkout@v4 - - - id: integration-test-setup - name: Integration test setup - uses: ./.github/actions/integration-test-setup - - - name: Run Account Console IT - run: ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=**.account2.**,!SigningInTest#passwordlessWebAuthnTest,!SigningInTest#twoFactorWebAuthnTest -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -f testsuite/integration-arquillian/tests/other/base-ui/pom.xml | misc/log/trimmer.sh - - - name: Upload JVM Heapdumps - if: always() - uses: ./.github/actions/upload-heapdumps - - - uses: ./.github/actions/upload-flaky-tests - name: Upload flaky tests - env: - GH_TOKEN: ${{ github.token }} - with: - job-name: Account Console IT - - - name: Surefire reports - if: always() - uses: ./.github/actions/archive-surefire-reports - with: - job-id: account-console-integration-tests-${{ matrix.browser }} - forms-integration-tests: name: Forms IT runs-on: ubuntu-latest @@ -483,17 +928,20 @@ jobs: browser: [chrome, firefox] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup uses: ./.github/actions/integration-test-setup + - uses: ./.github/actions/install-chrome + if: matrix.browser == 'chrome' + - name: Run Forms IT run: | TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh forms` echo "Tests: $TESTS" - ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" "-Dwebdriver.gecko.driver=$GECKOWEBDRIVER/geckodriver" -f testsuite/integration-arquillian/tests/base/pom.xml | misc/log/trimmer.sh + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -Dbrowser=${{ matrix.browser }} -f testsuite/integration-arquillian/tests/base/pom.xml 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -514,6 +962,7 @@ jobs: webauthn-integration-tests: name: WebAuthn IT + if: needs.conditional.outputs.ci-webauthn == 'true' runs-on: ubuntu-latest needs: build timeout-minutes: 45 @@ -521,23 +970,23 @@ jobs: matrix: browser: - chrome - # - firefox disabled until https://github.com/keycloak/keycloak/issues/20777 is resolved + - firefox fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup uses: ./.github/actions/integration-test-setup - # Don't use Chrome for testing (just regular Chrome) until https://github.com/keycloak/keycloak/issues/22214 is resolved - #- id: install-chrome - # name: Install Chrome browser - # uses: ./.github/actions/install-chrome - # if: matrix.browser == 'chrome' + - uses: ./.github/actions/install-chrome + if: matrix.browser == 'chrome' - name: Run WebAuthn IT - run: ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=org.keycloak.testsuite.webauthn.**.*Test -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" "-Dwebdriver.gecko.driver=$GECKOWEBDRIVER/geckodriver" -Pwebauthn -f testsuite/integration-arquillian/tests/other/pom.xml | misc/log/trimmer.sh + run: | + TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh webauthn` + echo "Tests: $TESTS" + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" "-Dwebdriver.gecko.driver=$GECKOWEBDRIVER/geckodriver" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -566,7 +1015,7 @@ jobs: timeout-minutes: 30 steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -579,7 +1028,7 @@ jobs: - id: cache-maven-repository name: ipa-data cache - uses: actions/cache@v3 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/ipa-data.tar key: ${{ steps.weekly-cache-key.outputs.key }} @@ -600,11 +1049,11 @@ jobs: timeout-minutes: 45 strategy: matrix: - old-version: [19.0.3] + old-version: [24.0.4] database: [postgres, mysql, oracle, mssql, mariadb] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: integration-test-setup name: Integration test setup @@ -620,7 +1069,8 @@ jobs: -Dmigration.import.file.name=migration-realm-${{ matrix.old-version }}.json \ -Dauth.server.ssl.required=false \ -Dauth.server.db.host=localhost \ - -f testsuite/integration-arquillian/pom.xml | misc/log/trimmer.sh + "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" \ + -f testsuite/integration-arquillian/pom.xml 2>&1 | misc/log/trimmer.sh - name: Upload JVM Heapdumps if: always() @@ -632,36 +1082,109 @@ jobs: GH_TOKEN: ${{ github.token }} with: job-name: Migration Tests - + - name: Surefire reports if: always() uses: ./.github/actions/archive-surefire-reports with: job-id: migration-tests-${{ matrix.old-version }}-${{ matrix.database }} + test-framework: + name: Test Framework + runs-on: ubuntu-latest + needs: build + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run tests + run: ./mvnw package -f test-framework/pom.xml + + base-new-integration-tests: + name: Base IT (new) + runs-on: ubuntu-latest + needs: + - build + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + # This step is necessary because test/clustering requires building a new Keycloak image built from tar.gz + # file that is not part of m2-keycloak.tzts archive + - name: Build tar keycloak-quarkus-dist + run: ./mvnw package -pl quarkus/server/,quarkus/dist/ + + - name: Run tests + run: ./mvnw package -f tests/pom.xml + + mixed-cluster-compatibility-tests: + name: Cluster Compatibility Tests + if: needs.conditional.outputs.ci-compatibility-matrix != '' + runs-on: ubuntu-latest + needs: + - build + - conditional + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.conditional.outputs.ci-compatibility-matrix) }} + timeout-minutes: 10 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + # This step is necessary because test/clustering requires building a new Keycloak image built from tar.gz + # file that is not part of m2-keycloak.tzts archive + - name: Build tar keycloak-quarkus-dist + run: ./mvnw package -pl quarkus/server/,quarkus/dist/ + + - name: Run tests + run: ./mvnw verify -pl tests/clustering + env: + KC_TEST_SERVER_IMAGES: "quay.io/keycloak/keycloak:${{ matrix.version }},-" + check: name: Status Check - Keycloak CI if: always() needs: - conditional + - build - unit-tests - base-integration-tests + - adapter-integration-tests + - adapter-integration-tests-strict-cookies - quarkus-unit-tests - quarkus-integration-tests - jdk-integration-tests - - legacy-store-integration-tests + - store-integration-tests + - volatile-sessions-tests - store-model-tests - clustering-integration-tests - fips-unit-tests - fips-integration-tests - - account-console-integration-tests - forms-integration-tests - webauthn-integration-tests - sssd-unit-tests - migration-tests + - external-infinispan-tests + - test-framework + - base-new-integration-tests + - mixed-cluster-compatibility-tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2aab95b6542e..afbd4e885440 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,10 +5,14 @@ on: branches-ignore: - main - dependabot/** + - quarkus-next pull_request: branches: [main] workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + concurrency: # Only cancel jobs for PR updates group: codeql-analysis-${{ github.ref }} @@ -18,16 +22,22 @@ defaults: run: shell: bash -jobs: +permissions: + contents: read +jobs: conditional: name: Check conditional workflows and jobs runs-on: ubuntu-latest outputs: java: ${{ steps.conditional.outputs.codeql-java }} - themes: ${{ steps.conditional.outputs.codeql-themes }} + javascript: ${{ steps.conditional.outputs.codeql-javascript }} + typescript: ${{ steps.conditional.outputs.codeql-typescript }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional @@ -38,15 +48,17 @@ jobs: name: CodeQL Java needs: conditional runs-on: ubuntu-latest + permissions: + security-events: write # Required for SARIF upload if: needs.conditional.outputs.java == 'true' outputs: conclusion: ${{ steps.check.outputs.conclusion }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: java @@ -54,33 +66,61 @@ jobs: uses: ./.github/actions/build-keycloak - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: wait-for-processing: true env: CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"interpret-results":["--max-paths",0]}}' - themes: - name: CodeQL Themes + javascript: + name: CodeQL JavaScript needs: conditional runs-on: ubuntu-latest - if: needs.conditional.outputs.themes == 'true' + permissions: + security-events: write # Required for SARIF upload + if: needs.conditional.outputs.javascript == 'true' outputs: conclusion: ${{ steps.check.outputs.conclusion }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 env: CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"finalize":["--no-run-unnecessary-builds"]}}' with: languages: javascript - source-root: themes/src/main/ - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + with: + wait-for-processing: true + env: + CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"interpret-results":["--max-paths",0]}}' + + typescript: + name: CodeQL TypeScript + needs: conditional + runs-on: ubuntu-latest + permissions: + security-events: write # Required for SARIF upload + if: needs.conditional.outputs.typescript == 'true' + outputs: + conclusion: ${{ steps.check.outputs.conclusion }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + env: + CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"finalize":["--no-run-unnecessary-builds"]}}' + with: + languages: typescript + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: wait-for-processing: true env: @@ -92,10 +132,11 @@ jobs: needs: - conditional - java - - themes + - javascript + - typescript runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 25f19458e188..58aa4ab1e236 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -5,9 +5,13 @@ on: branches-ignore: - main - dependabot/** + - quarkus-next pull_request: workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + concurrency: # Only cancel jobs for PR updates group: documentation-${{ github.ref }} @@ -17,6 +21,9 @@ defaults: run: shell: bash +permissions: + contents: read + jobs: conditional: @@ -24,8 +31,11 @@ jobs: runs-on: ubuntu-latest outputs: documentation: ${{ steps.conditional.outputs.documentation }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional @@ -38,7 +48,7 @@ jobs: runs-on: ubuntu-latest needs: conditional steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: setup-java name: Setup Java @@ -52,12 +62,11 @@ jobs: name: Build and verify Keycloak documentation shell: bash run: | - MVN_HTTP_CONFIG="-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" - ./mvnw install -Dtest=!ExternalLinksTest -am -pl docs/documentation/tests,docs/documentation/dist -nsu -B -e $MVN_HTTP_CONFIG -Pdocumentation + ./mvnw install -Dtest=!ExternalLinksTest -am -pl docs/documentation/tests,docs/documentation/dist -e -Pdocumentation - id: upload-keycloak-documentation name: Upload Keycloak documentation - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: keycloak-documentation path: docs/documentation/dist/target/*.zip @@ -69,7 +78,7 @@ jobs: runs-on: ubuntu-latest needs: conditional steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: setup-java name: Setup Java @@ -83,8 +92,7 @@ jobs: name: Build and verify Keycloak documentation shell: bash run: | - MVN_HTTP_CONFIG="-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" - ./mvnw install -Dtest=ExternalLinksTest -am -pl docs/documentation/tests -nsu -B -e -Pdocumentation $MVN_HTTP_CONFIG + ./mvnw install -Dtest=ExternalLinksTest -am -pl docs/documentation/tests -e -Pdocumentation check: name: Status Check - Keycloak Documentation @@ -94,7 +102,7 @@ jobs: - build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: - jobs: ${{ toJSON(needs) }} \ No newline at end of file + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/guides.yml b/.github/workflows/guides.yml index 4bb752d41331..6a521254d6a3 100644 --- a/.github/workflows/guides.yml +++ b/.github/workflows/guides.yml @@ -5,9 +5,13 @@ on: branches-ignore: - main - dependabot/** + - quarkus-next pull_request: workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + concurrency: # Only cancel jobs for PR updates group: guides-${{ github.ref }} @@ -17,6 +21,9 @@ defaults: run: shell: bash +permissions: + contents: read + jobs: conditional: @@ -25,8 +32,11 @@ jobs: outputs: guides: ${{ steps.conditional.outputs.guides }} ci: ${{ steps.conditional.outputs.ci }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional @@ -40,7 +50,7 @@ jobs: runs-on: ubuntu-latest needs: conditional steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Keycloak uses: ./.github/actions/build-keycloak @@ -53,7 +63,7 @@ jobs: - build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 62f3d28b5068..1a60b211986e 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -5,9 +5,14 @@ on: branches-ignore: - main - dependabot/** + - quarkus-next pull_request: workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + RETRY_COUNT: 3 + concurrency: # Only cancel jobs for PR updates group: js-ci-${{ github.ref }} @@ -17,14 +22,20 @@ defaults: run: shell: bash +permissions: + contents: read + jobs: conditional: name: Check conditional workflows and jobs runs-on: ubuntu-latest outputs: js-ci: ${{ steps.conditional.outputs.js }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional @@ -37,23 +48,17 @@ jobs: if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 17 - check-latest: true - cache: maven + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Keycloak + uses: ./.github/actions/build-keycloak + + - name: Prepare archive for upload run: | - ./mvnw clean install --batch-mode --errors -DskipTests -DskipTestsuite -DskipExamples -Pdistribution mv ./quarkus/dist/target/keycloak-999.0.0-SNAPSHOT.tar.gz ./keycloak-999.0.0-SNAPSHOT.tar.gz - name: Upload Keycloak dist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: keycloak path: keycloak-999.0.0-SNAPSHOT.tar.gz @@ -66,53 +71,14 @@ jobs: env: WORKSPACE: "@keycloak/keycloak-admin-client" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - - run: pnpm --filter ${{ env.WORKSPACE }} run lint - working-directory: js - - - run: pnpm --filter ${{ env.WORKSPACE }} run build - working-directory: js - - keycloak-js: - name: Keycloak JS - needs: conditional - if: needs.conditional.outputs.js-ci == 'true' - runs-on: ubuntu-latest - env: - WORKSPACE: keycloak-js - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - - run: pnpm --filter ${{ env.WORKSPACE }} run build - working-directory: js - - keycloak-masthead: - name: Keycloak Masthead - needs: conditional - if: needs.conditional.outputs.js-ci == 'true' - runs-on: ubuntu-latest - env: - WORKSPACE: keycloak-masthead - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run lint + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} lint working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run build + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} build working-directory: js ui-shared: @@ -121,18 +87,16 @@ jobs: if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest env: - WORKSPACE: ui-shared + WORKSPACE: "@keycloak/keycloak-ui-shared" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run lint + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} lint working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run build + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} build working-directory: js account-ui: @@ -141,18 +105,16 @@ jobs: if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest env: - WORKSPACE: account-ui + WORKSPACE: "@keycloak/keycloak-account-ui" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run lint + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} lint working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run build + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} build working-directory: js admin-ui: @@ -161,24 +123,19 @@ jobs: if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest env: - WORKSPACE: admin-ui + WORKSPACE: keycloak-admin-ui steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - - - run: pnpm --filter ${{ env.WORKSPACE }} run lint - working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run test + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} lint working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run build + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} test working-directory: js - - run: pnpm --filter ${{ env.WORKSPACE }} run cy:check-types + - run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} build working-directory: js account-ui-e2e: @@ -189,141 +146,132 @@ jobs: if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest env: - WORKSPACE: account-ui + WORKSPACE: "@keycloak/keycloak-account-ui" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - name: Download Keycloak server - uses: actions/download-artifact@v3 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keycloak - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 17 + uses: ./.github/actions/java-setup - 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=declarative-user-profile,transient-users &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=transient-users,oid4vc-vci &> ~/server.log & env: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin - name: Install Playwright browsers - run: pnpm --filter ${{ env.WORKSPACE }} exec playwright install --with-deps + run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} exec playwright install --with-deps working-directory: js - name: Run Playwright tests - run: pnpm --filter ${{ env.WORKSPACE }} run test + run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} test working-directory: js - env: - KEYCLOAK_SERVER: http://localhost:8080 - - uses: actions/upload-artifact@v3 + - name: Upload Playwright report + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: account-ui-playwright-report - path: js/apps/${{ env.WORKSPACE }}/playwright-report + path: js/apps/account-ui/playwright-report retention-days: 30 - name: Upload server logs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: account-ui-server-log path: ~/server.log + generate-test-seed: + name: Generate Test Seed + needs: + - conditional + if: needs.conditional.outputs.js-ci == 'true' + runs-on: ubuntu-latest + outputs: + seed: ${{ steps.generate-random-number.outputs.value }} + steps: + - name: Generate random number + id: generate-random-number + shell: bash + run: | + echo "value=$(shuf -i 1-100 -n 1)" >> $GITHUB_OUTPUT + admin-ui-e2e: name: Admin UI E2E needs: - conditional - build-keycloak - if: needs.conditional.outputs.js-ci == 'true' && github.repository == 'keycloak/keycloak' + - generate-test-seed + if: needs.conditional.outputs.js-ci == 'true' runs-on: ubuntu-latest env: - WORKSPACE: admin-ui + WORKSPACE: keycloak-admin-ui strategy: matrix: - container: [1, 2, 3, 4, 5] - browser: [chrome, firefox] + browser: [chromium, firefox] exclude: # Only test with Firefox on scheduled runs - browser: ${{ github.event_name != 'workflow_dispatch' && 'firefox' || '' }} + fail-fast: false steps: - - uses: actions/checkout@v4 - - - name: Install Google Chrome - if: matrix.browser == 'chrome' - uses: browser-actions/setup-chrome@v1 - with: - chrome-version: stable - - - name: Install Firefox - if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1 - with: - firefox-version: latest + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/pnpm-setup - with: - working-directory: js - name: Compile Admin Client - run: pnpm --filter @keycloak/keycloak-admin-client run build + run: pnpm --fail-if-no-match --filter @keycloak/keycloak-admin-client build working-directory: js - name: Download Keycloak server - uses: actions/download-artifact@v3 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keycloak - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 17 + uses: ./.github/actions/java-setup - 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,declarative-user-profile,transient-users &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users &> ~/server.log & env: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_CLIENT_ID: temporary-admin-service + KC_BOOTSTRAP_ADMIN_CLIENT_SECRET: temporary-admin-service - - name: Start LDAP server - run: pnpm --filter ${{ env.WORKSPACE }} run cy:ldap-server & + - name: Install Playwright browsers + run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} exec playwright install --with-deps working-directory: js - - name: Run Cypress - uses: cypress-io/github-action@v6 + - name: Run Playwright tests + run: pnpm --fail-if-no-match --filter ${{ env.WORKSPACE }} test:integration --project=${{ matrix.browser }} + working-directory: js + + - name: Upload Playwright report + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() with: - install: false - record: true - parallel: true - group: ${{ matrix.browser }} - browser: ${{ matrix.browser }} - wait-on: http://localhost:8080 - working-directory: js/apps/admin-ui - env: - CYPRESS_BASE_URL: http://localhost:8080/admin/ - CYPRESS_KEYCLOAK_SERVER: http://localhost:8080 - CYPRESS_RECORD_KEY: b8f1d15e-eab8-4ee7-8e44-c6d7cd8fc0eb - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + name: admin-ui-playwright-report-${{ matrix.browser }} + path: js/apps/admin-ui/playwright-report + retention-days: 30 - name: Upload server logs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: admin-ui-server-log-${{ matrix.container }}-${{ matrix.browser }} + name: admin-ui-server-log-${{ matrix.browser }} path: ~/server.log check: @@ -331,9 +279,8 @@ jobs: if: always() needs: - conditional + - build-keycloak - admin-client - - keycloak-js - - keycloak-masthead - ui-shared - account-ui - account-ui-e2e @@ -341,7 +288,7 @@ jobs: - admin-ui-e2e runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml new file mode 100644 index 000000000000..864874900f54 --- /dev/null +++ b/.github/workflows/label.yml @@ -0,0 +1,60 @@ +name: Labeller +on: + pull_request_target: + types: closed + +permissions: + contents: read + +jobs: + label: + + runs-on: ubuntu-latest + permissions: + issues: write # Required to add labels to Issues + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: .github/scripts + - name: Add release labels on merge + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + + echo "Base REF: $GITHUB_BASE_REF" + echo "**Branch:** [$GITHUB_BASE_REF](https://github.com/$GITHUB_REPOSITORY/tree/$GITHUB_BASE_REF)" >> $GITHUB_STEP_SUMMARY + echo "PR: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER" + echo "**PR:** [$PR_NUMBER](https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER)" >> $GITHUB_STEP_SUMMARY + + if [ "$GITHUB_BASE_REF" == "main" ]; then + LAST_RELEASE="$(gh api /repos/$GITHUB_REPOSITORY/branches --paginate --jq .[].name | grep '^release/' | cut -d '/' -f 2 | sort -n -r | head -n 1)" + LAST_MINOR=$(echo $LAST_RELEASE | cut -d '.' -f 2) + NEXT_MAJOR=$(echo $LAST_RELEASE | cut -d '.' -f 1) + NEXT_MINOR="$(($LAST_MINOR + 1))" + LABEL="release/$NEXT_MAJOR.$NEXT_MINOR.0" + 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 -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" + fi + + echo "Label: $LABEL" + echo "**Label:** [$LABEL](https://github.com/$GITHUB_REPOSITORY/labels/$LABEL)" >> $GITHUB_STEP_SUMMARY + + gh api "/repos/$GITHUB_REPOSITORY/labels/$LABEL" --silent 2>/dev/null || gh label create -R "$GITHUB_REPOSITORY" "$LABEL" -c "0E8A16" + + echo "" + echo "" >> $GITHUB_STEP_SUMMARY + echo "Updating issues:" + echo "**Updating issues:**" >> $GITHUB_STEP_SUMMARY + + ISSUES=$(.github/scripts/pr-find-issues.sh "$PR_NUMBER" "$GITHUB_REPOSITORY") + for ISSUE in $ISSUES; do + gh issue edit "$ISSUE" -R "$GITHUB_REPOSITORY" --add-label "$LABEL" --remove-label "$BACKPORT_LABEL" + echo "* [$ISSUE](https://github.com/$GITHUB_REPOSITORY/issues/$ISSUE)" >> $GITHUB_STEP_SUMMARY + done + if: github.repository == 'keycloak/keycloak' && github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml index 81c7f908655f..0b813498fb34 100644 --- a/.github/workflows/operator-ci.yml +++ b/.github/workflows/operator-ci.yml @@ -9,8 +9,10 @@ on: workflow_dispatch: env: - MINIKUBE_VERSION: v1.31.2 - KUBERNETES_VERSION: v1.24.17 # OCP 4.11 + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + 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: @@ -21,6 +23,9 @@ concurrency: group: operator-ci-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: conditional: @@ -28,8 +33,11 @@ jobs: runs-on: ubuntu-latest outputs: operator: ${{ steps.conditional.outputs.operator }} + permissions: + contents: read + pull-requests: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: conditional uses: ./.github/actions/conditional @@ -42,7 +50,7 @@ jobs: runs-on: ubuntu-latest needs: conditional steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Keycloak uses: ./.github/actions/build-keycloak @@ -50,57 +58,29 @@ jobs: upload-m2-repo: false upload-dist: true - test-local: - name: Test local + test-local-apiserver: + name: Test local apiserver runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/checkout@v4 - - - name: Set version - id: vars - run: echo "version_local=0.0.1-${GITHUB_SHA::6}" >> $GITHUB_ENV + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java uses: ./.github/actions/java-setup - - name: Setup Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.9.0 - with: - minikube version: ${{ env.MINIKUBE_VERSION }} - kubernetes version: ${{ env.KUBERNETES_VERSION }} - github token: ${{ secrets.GITHUB_TOKEN }} - driver: docker - start args: --addons=ingress - - - name: Download keycloak distribution - id: download-keycloak-dist - uses: actions/download-artifact@v3 - with: - name: keycloak-dist - path: quarkus/container - - - name: Build Keycloak Docker images - run: | - eval $(minikube -p minikube docker-env) - (cd quarkus/container && docker build --build-arg KEYCLOAK_DIST=$(ls keycloak-*.tar.gz) . -t keycloak:${{ env.version_local }}) - (cd operator && ./scripts/build-testing-docker-images.sh ${{ env.version_local }} keycloak custom-keycloak) - - name: Test operator running locally run: | - ./mvnw install --batch-mode -Poperator -pl :keycloak-operator -am \ - -Dquarkus.kubernetes.image-pull-policy=IfNotPresent \ - -Doperator.keycloak.image=keycloak:${{ env.version_local }} \ - -Dtest.operator.custom.image=custom-keycloak:${{ env.version_local }} \ - -Doperator.keycloak.image-pull-policy=Never \ - -Dtest.operator.kubernetes.ip=$(minikube ip) + ./mvnw install -Poperator -pl :keycloak-operator -am test-remote: name: Test remote runs-on: ubuntu-latest needs: [build] + strategy: + matrix: + suite: [slow, fast] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set version id: vars @@ -110,17 +90,17 @@ jobs: uses: ./.github/actions/java-setup - name: Setup Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.9.0 + uses: manusa/actions-setup-minikube@b589f2d61bf96695c546929c72b38563e856059d # v2.14.0 with: minikube version: ${{ env.MINIKUBE_VERSION }} kubernetes version: ${{ env.KUBERNETES_VERSION }} github token: ${{ secrets.GITHUB_TOKEN }} driver: docker - start args: --addons=ingress + start args: --addons=ingress --memory=${{ env.MINIKUBE_MEMORY }} --cni cilium --cpus=max - name: Download keycloak distribution id: download-keycloak-dist - uses: actions/download-artifact@v3 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keycloak-dist path: quarkus/container @@ -133,36 +113,40 @@ jobs: - name: Test operator running in cluster run: | + declare -A PARAMS + PARAMS["slow"]="-Dkc.quarkus.tests.groups=slow" + PARAMS["fast"]='-Dkc.quarkus.tests.groups=!slow' + eval $(minikube -p minikube docker-env) - ./mvnw install --batch-mode -Poperator -pl :keycloak-operator -am \ + ./mvnw install -Poperator -pl :keycloak-operator -am \ -Dquarkus.container-image.build=true \ -Dquarkus.kubernetes.image-pull-policy=IfNotPresent \ - -Doperator.keycloak.image=keycloak:${{ env.version_remote }} \ - -Dquarkus.kubernetes.env.vars.operator-keycloak-image-pull-policy=Never \ + -Dkc.operator.keycloak.image=keycloak:${{ env.version_remote }} \ + -Dquarkus.kubernetes.env.vars.kc-operator-keycloak-image-pull-policy=Never \ -Dtest.operator.custom.image=custom-keycloak:${{ env.version_remote }} \ - --no-transfer-progress -Dtest.operator.deployment=remote \ - -Dtest.operator.kubernetes.ip=$(minikube ip) + --no-transfer-progress -Dtest.operator.deployment=remote ${PARAMS["${{ matrix.suite }}"]} test-olm: name: Test OLM installation runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java uses: ./.github/actions/java-setup - name: Setup Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.9.0 + uses: manusa/actions-setup-minikube@b589f2d61bf96695c546929c72b38563e856059d # v2.14.0 with: minikube version: ${{ env.MINIKUBE_VERSION }} kubernetes version: ${{ env.KUBERNETES_VERSION }} github token: ${{ secrets.GITHUB_TOKEN }} driver: docker + start args: --memory=${{ env.MINIKUBE_MEMORY }} --addons=registry --insecure-registry=192.168.49.0/24 - name: Install OPM - uses: redhat-actions/openshift-tools-installer@v1 + uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1.13.1 with: source: github opm: 1.21.0 @@ -176,7 +160,7 @@ jobs: - name: Download keycloak distribution id: download-keycloak-dist - uses: actions/download-artifact@v3 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keycloak-dist path: quarkus/container @@ -184,8 +168,9 @@ jobs: - name: Arrange OLM test installation working-directory: operator run: | + echo "Minikube IP $(minikube ip)" eval $(minikube -p minikube docker-env) - ./scripts/olm-testing.sh ${GITHUB_SHA::6} + REGISTRY=$(minikube ip):5000 ./scripts/olm-testing.sh ${GITHUB_SHA::6} - name: Deploy an example Keycloak and wait for it to be ready working-directory: operator @@ -231,12 +216,13 @@ jobs: if: always() needs: - conditional - - test-local + - build + - test-local-apiserver - test-remote - test-olm runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/status-check with: jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/quarkus-next.yml b/.github/workflows/quarkus-next.yml new file mode 100644 index 000000000000..532e662b83ba --- /dev/null +++ b/.github/workflows/quarkus-next.yml @@ -0,0 +1,64 @@ +name: Quarkus Next + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +defaults: + run: + shell: bash + +concurrency: + # Only cancel jobs for PR updates + group: quarkus-next-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + update-quarkus-next-branch: + name: Update quarkus-next branch + if: github.event_name != 'schedule' || github.repository == 'keycloak/keycloak' + runs-on: ubuntu-latest + permissions: + contents: write # Required to push changes to the repository + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + fetch-depth: 0 + + - name: Configure Git + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Cherry-pick additional commits in quarkus-next + run: | + ${GITHUB_WORKSPACE}/.github/scripts/prepare-quarkus-next.sh + + - name: Push changes + run: | + git push -f origin HEAD:quarkus-next + + run-matrix-with-quarkus-next: + name: Run workflow matrix with the quarkus-next branch + runs-on: ubuntu-latest + permissions: + actions: write # Required to trigger workflows using gh + needs: + - update-quarkus-next-branch + + strategy: + matrix: + workflow: + - ci.yml + - operator-ci.yml + + steps: + - name: Run workflow with the nightly Quarkus release + run: gh workflow run -R ${{ github.repository }} ${{ matrix.workflow }} -r quarkus-next + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/schedule-nightly.yml b/.github/workflows/schedule-nightly.yml index f40d1b5969a7..47f9396bf932 100644 --- a/.github/workflows/schedule-nightly.yml +++ b/.github/workflows/schedule-nightly.yml @@ -5,11 +5,15 @@ on: - cron: '0 0 * * *' workflow_dispatch: -jobs: +permissions: + contents: read +jobs: setup: if: github.event_name != 'schedule' || github.repository == 'keycloak/keycloak' runs-on: ubuntu-latest + permissions: + actions: write # Required to trigger workflows using gh outputs: latest-release-branch: ${{ steps.latest-release.outputs.branch }} steps: @@ -24,8 +28,9 @@ jobs: run-default-branch: name: Run default branch runs-on: ubuntu-latest + permissions: + actions: write # Required to trigger workflows using gh needs: setup - strategy: matrix: workflow: @@ -33,6 +38,7 @@ jobs: - documentation.yml - js-ci.yml - operator-ci.yml + - codeql-analysis.yml - snyk-analysis.yml - trivy-analysis.yml @@ -46,7 +52,8 @@ jobs: name: Run latest release branch needs: setup runs-on: ubuntu-latest - + permissions: + actions: write # Required to trigger workflows using gh strategy: matrix: workflow: diff --git a/.github/workflows/snyk-analysis.yml b/.github/workflows/snyk-analysis.yml index 44b416274bba..761b69512325 100644 --- a/.github/workflows/snyk-analysis.yml +++ b/.github/workflows/snyk-analysis.yml @@ -3,22 +3,30 @@ name: Snyk on: workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + defaults: run: shell: bash +permissions: + contents: read + jobs: analysis: name: Analysis of Quarkus and Operator runs-on: ubuntu-latest if: github.repository == 'keycloak/keycloak' + permissions: + security-events: write # Required for SARIF uploads steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Keycloak uses: ./.github/actions/build-keycloak - - uses: snyk/actions/setup@master + - uses: snyk/actions/setup@28606799782bc8e809f4076e9f8293bc4212d05e # master - name: Check for vulnerabilities in Quarkus run: snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --sarif-file-output=quarkus-report.sarif quarkus/deployment @@ -27,21 +35,22 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: Upload Quarkus scanner results to GitHub - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + continue-on-error: true with: sarif_file: quarkus-report.sarif category: snyk-quarkus-report - name: Check for vulnerabilities in Operator run: | - ./mvnw -Poperator -pl operator -am -DskipTests clean install --batch-mode + ./mvnw -Poperator -pl operator -am -DskipTests clean install snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --sarif-file-output=operator-report.sarif operator continue-on-error: true env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: Upload Operator scanner results to GitHub - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: operator-report.sarif category: snyk-operator-report diff --git a/.github/workflows/trivy-analysis.yml b/.github/workflows/trivy-analysis.yml index f160f9cd19c7..ad8e25b9e3cb 100644 --- a/.github/workflows/trivy-analysis.yml +++ b/.github/workflows/trivy-analysis.yml @@ -7,6 +7,9 @@ defaults: run: shell: bash +permissions: + contents: read + jobs: analysis: @@ -17,20 +20,28 @@ jobs: matrix: container: [keycloak, keycloak-operator] fail-fast: false + permissions: + security-events: write # Required for SARIF uploads steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@2b6a709cf9c4025c5438138008beaddbb02086f0 + uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # 0.32.0 with: - image-ref: quay.io/keycloak/${{ matrix.container}}:nightly - format: template - template: '@/contrib/sarif.tpl' + image-ref: quay.io/keycloak/${{ matrix.container }}:nightly + format: sarif output: trivy-results.sarif severity: MEDIUM,CRITICAL,HIGH ignore-unfixed: true - security-checks: vuln + version: v0.57.1 timeout: 15m + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db + TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: trivy-results.sarif + category: ${{ matrix.container }} diff --git a/.github/workflows/weblate.yml b/.github/workflows/weblate.yml new file mode 100644 index 000000000000..ba569f04e72f --- /dev/null +++ b/.github/workflows/weblate.yml @@ -0,0 +1,38 @@ +name: Weblate Sync + +on: + # Update Weblate once a day, and if a translation file (source or target) changes. + # Using this workflow prevents Weblate to rebase its PRs on every commit in Keycloak's main branch. + schedule: + - cron: '0 0 * * *' + workflow_dispatch: {} + push: + branches: + - main + paths: + - 'themes/**/messages_*.properties' + - 'js/**/messages_*.properties' + +defaults: + run: + shell: bash + +concurrency: + # Only cancel jobs for PR updates + group: weblate-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + update-weblate: + name: Trigger Weblate to pull the latest changes + runs-on: ubuntu-latest + + steps: + # language=bash + - run: | + if [ '${{ secrets.WEBLATE_TOKEN }}' != '' ]; then + curl --fail-with-body -d operation=pull -H "Authorization: Token ${{ secrets.WEBLATE_TOKEN }}" https://hosted.weblate.org/api/projects/keycloak/repository/ + fi diff --git a/.gitignore b/.gitignore index c684a680212c..b68f39bdeaef 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ nbproject # VS Code # ########### *.code-workspace +*.vscode # Compiled source # ################### @@ -64,6 +65,7 @@ nbproject # Maven # ######### target +bin # Maven shade ############# @@ -79,22 +81,19 @@ quarkus/data/*.db # Jakarta transformed sources # ############################### -/integration/admin-client/src/ -/adapters/saml/jakarta-servlet-filter/src/ -/adapters/oidc/jakarta-servlet-filter/src/ -/adapters/saml/wildfly-elytron-jakarta/src/ -/adapters/saml/core-jakarta/src/ -/adapters/saml/wildfly/wildfly-jakarta-subsystem/src/ /.metadata/ # Git ephemeral files *.versionsBackup -# Node.js for frontend-maven-plugin # -node - -# NPM -node_modules +!/quarkus/dist +!/quarkus/**/src/**/dist # SDKMAM environment file .sdkmanrc + +# JENV +.java-version + +.env +.env.test diff --git a/.gitleaks.toml b/.gitleaks.toml index 16528252804e..90684a458896 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,10 +1,9 @@ -# -# GitLeaks Repo Specific Configuration -# -# This allowlist is used to help Red Hat ignore false positives during its code -# scans. - [allowlist] paths = [ - '''saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java''' + '''saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java''', + '''services/src/test/java/org/keycloak/protocol/oidc/utils/RedirectUtilsTest.java''' + ] + + regexes = [ + '''H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw''' ] diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 4bd06e01bcf9..8e03165c241a 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -20,6 +20,6 @@ org.apache.maven.extensions maven-build-cache-extension - 1.0.1 + 1.2.0 \ No newline at end of file diff --git a/.mvn/maven-build-cache-config.xml b/.mvn/maven-build-cache-config.xml index 9840b2446c75..fc667dca1ade 100644 --- a/.mvn/maven-build-cache-config.xml +++ b/.mvn/maven-build-cache-config.xml @@ -41,6 +41,8 @@ + + diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index a36fc474ef88..f95f1ee80715 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -5,14 +5,15 @@ # to you 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. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip diff --git a/ADOPTERS.md b/ADOPTERS.md index 03f356cf9c31..00f9e50dd23e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -14,6 +14,7 @@ List of organization names below is based on information collected using Keycloa * Associazione Rousseau * BISPRO * Bluestem Brands, Inc +* Bundesagentur für Arbeit * Bundesversicherungsamt * Capgemini * CERN (European Organisation for Nuclear Research) @@ -38,6 +39,7 @@ List of organization names below is based on information collected using Keycloa * ITROI Solutions * Kindly Ops, LLC * [Microcks](https://landscape.cncf.io/?selected=microcks) +* [Minder](https://github.com/mindersec/minder) * msg systems ag * Netdava International * Ohio Supercomputer Center @@ -48,7 +50,10 @@ List of organization names below is based on information collected using Keycloa * Prodesan * Quest Software * Research Industrial Software Engineering (RISE) +* [SICK AG](https://www.sick.com) +* [SMF](https://www.smf.de) * Sportsbet.com.au +* [Stacklok](https://stacklok.com/) * Stack Labs * Storebrand * Synekus @@ -58,4 +63,5 @@ List of organization names below is based on information collected using Keycloa * TRT9 - Brasil * UnitedHealthcare * Wayfair LLC +* [Xata](https://xata.io) * ...More individuals diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca6ff8bd71ab..e04582353783 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,17 +13,20 @@ Keycloak is an Open Source community-driven project and we welcome contributions Firstly, if you want to contribute a larger change to Keycloak we ask that you open a discussion first. For minor changes you can skip this part and go straight ahead to sending a contribution. Bear in mind that if you open a discussion first you can identify if the change will be accepted, as well as getting early feedback. +Each PR, no matter how small, should have a GitHub issue associated with it. +Issues are important for administrative purposes such as generating a changelog and handling backports. + Here's a quick checklist for a good PR, more details below: 1. A discussion around the change (https://github.com/keycloak/keycloak/discussions/categories/ideas) -2. A GitHub Issue with a good description associated with the PR -3. One feature/change per PR -4. One commit per PR -5. PR rebased on main (`git rebase`, not `git pull`) -5. [Good descriptive commit message, with link to issue](#commit-messages-and-issue-linking) -6. No changes to code not directly related to your PR -7. Includes functional/integration test -8. Includes documentation +1. A GitHub Issue with a good description associated with the PR +1. One feature/change per PR +1. One commit per PR +1. PR rebased on main (`git rebase`, not `git pull`) +1. [Good descriptive commit message, with link to issue](#commit-messages-and-issue-linking) +1. No changes to code not directly related to your PR +1. Includes functional/integration test +1. Includes documentation Once you have submitted your PR please monitor it for comments/feedback. We reserve the right to close inactive PRs if you do not respond within 2 weeks (bear in mind you can always open a new PR if it is closed due to inactivity). @@ -144,3 +147,7 @@ $ git commit -m "Summary ``` For more information linking PRs to issues refer to the [GitHub Documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). + +### Contributing Translations + +In order to provide translations for Keycloak, kindly follow the instructions provided in [Translation Docs](./docs/translation.md). diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 4cd0dbb39a92..d72c7beff1c2 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -72,7 +72,14 @@ For the nominee to be accepted as a maintainer at least 2/3 of existing maintain ### Changes in Maintainership -Maintainers can be removed if at least 2/3 of existing maintainers agree. +Every 6 months (March and September) active maintainers fill in a survey covering what they have been doing as a maintainer +for the last period, and what they are planning on focusing on in the next period. The survey is not made publicly +available, but is shared with existing maintainers. This serves two purposes; firstly for the maintainers group to better +understand what each maintainer is focusing on, and secondly to identify maintainers that are no longer active. + +In the majority of cases a maintainer is removed by the maintainer stepping down themselves (by sending an email to +keycloak-maintainers(at)googlegroups.com). However, in exceptional circumstances a maintainer can also be removed with +2/3 of votes from existing maintainers. ## Contributing Changes diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f7dad3c722fb..a9aa88dc22ed 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,16 +1,19 @@ -Maintainers -=========== +# Active maintainers -* [Stian Thorgersen](https://github.com/stianst) (project lead) * [Alexander Schwartz](https://github.com/ahus1) * [Bruno Oliveira da Silva](https://github.com/abstractj) -* [Hynek Mlnařík](https://github.com/hmlnarik) * [Marek Posolda](https://github.com/mposolda) -* [Michal Hajas](https://github.com/mhajas) -* [Pavel Drozd](https://github.com/pdrozd) * [Pedro Igor](https://github.com/pedroigor) * [Sebastian Schuster](https://github.com/sschu) * [Stan Silvert](https://github.com/ssilvert) +* [Stian Thorgersen](https://github.com/stianst) (project lead) * [Takashi Norimatsu](https://github.com/tnorimat) * [Thomas Darimont](https://github.com/thomasdarimont) * [Václav Muzikář](https://github.com/vmuzikar) + +# Emeritus maintainers + +* [Hynek Mlnařík](https://github.com/hmlnarik) +* [Michal Hajas](https://github.com/mhajas) +* [Pavel Drozd](https://github.com/pdrozd) + diff --git a/README.md b/README.md index 6e90f2490292..c9856046d433 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,26 @@ -# Keycloak +![Keycloak](https://github.com/keycloak/keycloak-misc/blob/main/logo/logo.svg) -Keycloak is an Open Source Identity and Access Management solution for modern Applications and Services. +![GitHub Release](https://img.shields.io/github/v/release/keycloak/keycloak?label=latest%20release) +[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/6818/badge)](https://bestpractices.coreinfrastructure.org/projects/6818) +[![CLOMonitor](https://img.shields.io/endpoint?url=https://clomonitor.io/api/projects/cncf/keycloak/badge)](https://clomonitor.io/projects/cncf/keycloak) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/keycloak/keycloak/badge)](https://securityscorecards.dev/viewer/?uri=github.com/keycloak/keycloak) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/keycloak-operator)](https://artifacthub.io/packages/olm/community-operators/keycloak-operator) +![GitHub Repo stars](https://img.shields.io/github/stars/keycloak/keycloak?style=flat) +![GitHub commit activity](https://img.shields.io/github/commit-activity/m/keycloak/keycloak) +[![Translation status](https://hosted.weblate.org/widget/keycloak/svg-badge.svg)](docs/translation.md) -This repository contains the source code for the Keycloak Server, Java adapters and the JavaScript adapter. +# Open Source Identity and Access Management + +Add authentication to applications and secure services with minimum effort. No need to deal with storing users or authenticating users. + +Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more. ## Help and Documentation * [Documentation](https://www.keycloak.org/documentation.html) * [User Mailing List](https://groups.google.com/d/forum/keycloak-user) - Mailing list for help and general questions about Keycloak +* Join [#keycloak](https://cloud-native.slack.com/archives/C056HC17KK9) for general questions, or [#keycloak-dev](https://cloud-native.slack.com/archives/C056XU905S6) on Slack for design and development discussions, by creating an account at [https://slack.cncf.io/](https://slack.cncf.io/). ## Reporting Security Vulnerabilities @@ -52,8 +64,9 @@ To write tests, refer to the [writing tests](docs/tests-development.md) guide. ## Contributing -Before contributing to Keycloak, please read our [contributing guidelines](CONTRIBUTING.md). +Before contributing to Keycloak, please read our [contributing guidelines](CONTRIBUTING.md). Participation in the Keycloak project is governed by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). +Joining a [community meeting](https://www.keycloak.org/community) is a great way to get involved and help shape the future of Keycloak. ## Other Keycloak Projects diff --git a/SECURITY-INSIGHTS.yml b/SECURITY-INSIGHTS.yml new file mode 100644 index 000000000000..d5882e991a34 --- /dev/null +++ b/SECURITY-INSIGHTS.yml @@ -0,0 +1,77 @@ +header: + schema-version: 1.0.0 + expiration-date: '2025-02-14T01:00:00.000Z' + last-updated: '2024-02-14' + last-reviewed: '2024-02-14' + project-url: 'https://github.com/keycloak/keycloak' + license: 'https://github.com/keycloak/keycloak/blob/main/LICENSE.txt' +project-lifecycle: + bug-fixes-only: false + core-maintainers: + - https://github.com/keycloak/keycloak/blob/main/MAINTAINERS.md + status: Active +contribution-policy: + accepts-pull-requests: true + accepts-automated-pull-requests: true + automated-tools-list: + - automated-tool: dependabot + action: allowed + path: + - / + contributing-policy: 'https://github.com/keycloak/keycloak/blob/main/CONTRIBUTING.md' + code-of-conduct: + - 'https://github.com/keycloak/keycloak?tab=coc-ov-file' +documentation: + - 'https://www.keycloak.org/documentation' +distribution-points: + - 'https://www.keycloak.org/downloads' + - 'https://github.com/keycloak/keycloak/releases' + - 'https://quay.io/repository/keycloak/keycloak' +security-testing: +- tool-type: sca + tool-name: Dependabot + tool-version: "2" + tool-url: https://github.com/dependabot + integration: + ad-hoc: false + ci: true + before-release: false +- tool-type: sca + tool-name: Snyk + tool-version: latest + integration: + ad-hoc: false + ci: true + before-release: false +- tool-type: sca + tool-name: CodeQL + tool-version: latest + integration: + ad-hoc: false + ci: true + before-release: false +- tool-type: sca + tool-name: Trivy + tool-version: latest + integration: + ad-hoc: false + ci: true + before-release: false +security-contacts: +- type: email + value: keycloak-security@googlegroups.com + primary: true +vulnerability-reporting: + accepts-vulnerability-reports: true + email-contact: keycloak-security@googlegroups.com + security-policy: 'https://www.keycloak.org/security' + bug-bounty-available: false + bug-bounty-url: '' +dependencies: + third-party-packages: true + dependencies-lists: + - 'https://github.com/keycloak/keycloak/blob/main/pom.xml' + dependencies-lifecycle: + policy-url: 'https://www.keycloak.org/security' + env-dependencies-policy: + policy-url: '' diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml deleted file mode 100755 index 2baddd35d190..000000000000 --- a/adapters/oidc/adapter-core/pom.xml +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-adapter-core - Keycloak Adapter Core - - - - - org.keycloak.adapters.* - - - org.keycloak.*;version="${project.version}", - org.apache.http.auth.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.client.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.conn.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.cookie.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.auth.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.client.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.conn.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.cookie.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.execchain.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.*;version=${apache.httpcomponents.httpcore.fuse.version}, - org.apache.karaf.jaas.boot.principal;resolution:=optional, - org.apache.karaf.jaas.modules;resolution:=optional, - *;resolution:=optional - - - - - - org.bouncycastle - bcprov-jdk18on - provided - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-adapter-spi - provided - - - org.keycloak - keycloak-core - provided - - - org.keycloak - ${keycloak.crypto.artifactId} - - - org.keycloak - keycloak-authz-client - provided - - - org.keycloak - keycloak-policy-enforcer - provided - - - com.fasterxml.jackson.core - jackson-core - provided - - - com.fasterxml.jackson.core - jackson-databind - provided - - - com.fasterxml.jackson.core - jackson-annotations - provided - - - junit - junit - test - - - org.apache.httpcomponents - httpclient - provided - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)", osgi.serviceloader; filter:="(osgi.serviceloader=org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory)";cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)" - - - osgi.serviceloader; osgi.serviceloader=org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory - - - - - - - - diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java deleted file mode 100755 index 50c57fc2aecf..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ /dev/null @@ -1,550 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.jboss.logging.Logger; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.rotation.PublicKeyLocator; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.common.enums.RelativeUrlsUsed; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.enums.TokenStore; -import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import java.io.IOException; -import java.net.URI; -import java.util.Map; -import java.util.concurrent.Callable; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AdapterDeploymentContext { - private static final Logger log = Logger.getLogger(AdapterDeploymentContext.class); - protected KeycloakDeployment deployment; - protected KeycloakConfigResolver configResolver; - - public AdapterDeploymentContext() { - } - - /** - * For single-tenant deployments, this constructor is to be used, as a - * full KeycloakDeployment is known at deployment time and won't change - * during the application deployment's life cycle. - * - * @param deployment A KeycloakConfigResolver, possibly missing the Auth - * Server URL - */ - public AdapterDeploymentContext(KeycloakDeployment deployment) { - this.deployment = deployment; - } - - /** - * For multi-tenant deployments, this constructor is to be used, as a - * KeycloakDeployment is not known at deployment time. It defers the - * resolution of a KeycloakDeployment to a KeycloakConfigResolver, - * to be implemented by the target application. - * - * @param configResolver A KeycloakConfigResolver that will be used - * to resolve a KeycloakDeployment - */ - public AdapterDeploymentContext(KeycloakConfigResolver configResolver) { - this.configResolver = configResolver; - } - - /** - * For single-tenant deployments, it complements KeycloakDeployment - * by resolving a relative Auth Server's URL based on the current request - * - * For multi-tenant deployments, defers the resolution of KeycloakDeployment - * to the KeycloakConfigResolver . - * - * @param facade the Request/Response Façade , used to either determine - * the Auth Server URL (single tenant) or pass thru to the - * KeycloakConfigResolver. - * @return - */ - public KeycloakDeployment resolveDeployment(HttpFacade facade) { - if (null != configResolver) { - return configResolver.resolve(facade.getRequest()); - } - - if (deployment == null) return null; - if (deployment.getAuthServerBaseUrl() == null) return deployment; - - KeycloakDeployment resolvedDeployment = resolveUrls(deployment, facade); - if (resolvedDeployment.getPublicKeyLocator() == null) { - throw new RuntimeException("KeycloakDeployment was never initialized through appropriate SPIs"); - } - return resolvedDeployment; - } - - protected KeycloakDeployment resolveUrls(KeycloakDeployment deployment, HttpFacade facade) { - if (deployment.relativeUrls == RelativeUrlsUsed.NEVER) { - // Absolute URI are already set to everything - return deployment; - } else { - DeploymentDelegate delegate = new DeploymentDelegate(this.deployment); - delegate.setAuthServerBaseUrl(getBaseBuilder(facade, this.deployment.getAuthServerBaseUrl()).build().toString()); - return delegate; - } - } - - /** - * This delegate is used to store temporary, per-request metadata like request resolved URLs. - * Ever method is delegated except URL get methods and isConfigured() - * - */ - protected static class DeploymentDelegate extends KeycloakDeployment { - protected KeycloakDeployment delegate; - - public DeploymentDelegate(KeycloakDeployment delegate) { - this.delegate = delegate; - } - - public void setAuthServerBaseUrl(String authServerBaseUrl) { - this.authServerBaseUrl = authServerBaseUrl; - KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(authServerBaseUrl); - resolveUrls(serverBuilder); - } - - @Override - public RelativeUrlsUsed getRelativeUrls() { - return delegate.getRelativeUrls(); - } - - @Override - public String getRealmInfoUrl() { - return (this.realmInfoUrl != null) ? this.realmInfoUrl : delegate.getRealmInfoUrl(); - } - - @Override - public String getTokenUrl() { - return (this.tokenUrl != null) ? this.tokenUrl : delegate.getTokenUrl(); - } - - @Override - public KeycloakUriBuilder getLogoutUrl() { - return (this.logoutUrl != null) ? this.logoutUrl : delegate.getLogoutUrl(); - } - - @Override - public String getAccountUrl() { - return (this.accountUrl != null) ? this.accountUrl : delegate.getAccountUrl(); - } - - @Override - public String getRegisterNodeUrl() { - return (this.registerNodeUrl != null) ? this.registerNodeUrl : delegate.getRegisterNodeUrl(); - } - - @Override - public String getUnregisterNodeUrl() { - return (this.unregisterNodeUrl != null) ? this.unregisterNodeUrl : delegate.getUnregisterNodeUrl(); - } - - @Override - public String getJwksUrl() { - return (this.jwksUrl != null) ? this.jwksUrl : delegate.getJwksUrl(); - } - - @Override - public String getResourceName() { - return delegate.getResourceName(); - } - - @Override - public String getRealm() { - return delegate.getRealm(); - } - - @Override - public void setRealm(String realm) { - delegate.setRealm(realm); - } - - @Override - public void setPublicKeyLocator(PublicKeyLocator publicKeyLocator) { - delegate.setPublicKeyLocator(publicKeyLocator); - } - - @Override - public PublicKeyLocator getPublicKeyLocator() { - return delegate.getPublicKeyLocator(); - } - - @Override - public void setResourceName(String resourceName) { - delegate.setResourceName(resourceName); - } - - @Override - public boolean isBearerOnly() { - return delegate.isBearerOnly(); - } - - @Override - public void setBearerOnly(boolean bearerOnly) { - delegate.setBearerOnly(bearerOnly); - } - - @Override - public boolean isAutodetectBearerOnly() { - return delegate.isAutodetectBearerOnly(); - } - - @Override - public void setAutodetectBearerOnly(boolean autodetectBearerOnly) { - delegate.setAutodetectBearerOnly(autodetectBearerOnly); - } - - @Override - public boolean isEnableBasicAuth() { - return delegate.isEnableBasicAuth(); - } - - @Override - public void setEnableBasicAuth(boolean enableBasicAuth) { - delegate.setEnableBasicAuth(enableBasicAuth); - } - - @Override - public boolean isPublicClient() { - return delegate.isPublicClient(); - } - - @Override - public void setPublicClient(boolean publicClient) { - delegate.setPublicClient(publicClient); - } - - @Override - public Map getResourceCredentials() { - return delegate.getResourceCredentials(); - } - - @Override - public void setResourceCredentials(Map resourceCredentials) { - delegate.setResourceCredentials(resourceCredentials); - } - - @Override - public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) { - delegate.setClientAuthenticator(clientAuthenticator); - } - - @Override - public ClientCredentialsProvider getClientAuthenticator() { - return delegate.getClientAuthenticator(); - } - - @Override - public HttpClient getClient() { - return delegate.getClient(); - } - - @Override - public void setClient(HttpClient client) { - delegate.setClient(client); - } - - @Override - public String getScope() { - return delegate.getScope(); - } - - @Override - public void setScope(String scope) { - delegate.setScope(scope); - } - - @Override - public SslRequired getSslRequired() { - return delegate.getSslRequired(); - } - - @Override - public void setSslRequired(SslRequired sslRequired) { - delegate.setSslRequired(sslRequired); - } - - @Override - public int getConfidentialPort() { - return delegate.getConfidentialPort(); - } - - @Override - public void setConfidentialPort(int confidentialPort) { - delegate.setConfidentialPort(confidentialPort); - } - - @Override - public TokenStore getTokenStore() { - return delegate.getTokenStore(); - } - - @Override - public void setTokenStore(TokenStore tokenStore) { - delegate.setTokenStore(tokenStore); - } - - @Override - public String getAdapterStateCookiePath() { - return delegate.getAdapterStateCookiePath(); - } - - @Override - public void setAdapterStateCookiePath(String adapterStateCookiePath) { - delegate.setAdapterStateCookiePath(adapterStateCookiePath); - } - - @Override - public String getStateCookieName() { - return delegate.getStateCookieName(); - } - - @Override - public void setStateCookieName(String stateCookieName) { - delegate.setStateCookieName(stateCookieName); - } - - @Override - public boolean isUseResourceRoleMappings() { - return delegate.isUseResourceRoleMappings(); - } - - @Override - public void setUseResourceRoleMappings(boolean useResourceRoleMappings) { - delegate.setUseResourceRoleMappings(useResourceRoleMappings); - } - - @Override - public boolean isCors() { - return delegate.isCors(); - } - - @Override - public void setCors(boolean cors) { - delegate.setCors(cors); - } - - @Override - public int getCorsMaxAge() { - return delegate.getCorsMaxAge(); - } - - @Override - public void setCorsMaxAge(int corsMaxAge) { - delegate.setCorsMaxAge(corsMaxAge); - } - - @Override - public String getCorsAllowedHeaders() { - return delegate.getCorsAllowedHeaders(); - } - - @Override - public void setNotBefore(int notBefore) { - delegate.setNotBefore(notBefore); - } - - @Override - public int getNotBefore() { - return delegate.getNotBefore(); - } - - @Override - public void updateNotBefore(int notBefore) { - delegate.setNotBefore(notBefore); - getPublicKeyLocator().reset(this); - } - - @Override - public void setExposeToken(boolean exposeToken) { - delegate.setExposeToken(exposeToken); - } - - @Override - public boolean isExposeToken() { - return delegate.isExposeToken(); - } - - @Override - public void setCorsAllowedMethods(String corsAllowedMethods) { - delegate.setCorsAllowedMethods(corsAllowedMethods); - } - - @Override - public String getCorsAllowedMethods() { - return delegate.getCorsAllowedMethods(); - } - - @Override - public void setCorsAllowedHeaders(String corsAllowedHeaders) { - delegate.setCorsAllowedHeaders(corsAllowedHeaders); - } - - @Override - public boolean isAlwaysRefreshToken() { - return delegate.isAlwaysRefreshToken(); - } - - @Override - public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { - delegate.setAlwaysRefreshToken(alwaysRefreshToken); - } - - @Override - public int getRegisterNodePeriod() { - return delegate.getRegisterNodePeriod(); - } - - @Override - public void setRegisterNodePeriod(int registerNodePeriod) { - delegate.setRegisterNodePeriod(registerNodePeriod); - } - - @Override - public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) { - delegate.setRegisterNodeAtStartup(registerNodeAtStartup); - } - - @Override - public boolean isRegisterNodeAtStartup() { - return delegate.isRegisterNodeAtStartup(); - } - - @Override - public String getPrincipalAttribute() { - return delegate.getPrincipalAttribute(); - } - - @Override - public void setPrincipalAttribute(String principalAttribute) { - delegate.setPrincipalAttribute(principalAttribute); - } - - @Override - public boolean isTurnOffChangeSessionIdOnLogin() { - return delegate.isTurnOffChangeSessionIdOnLogin(); - } - - @Override - public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) { - delegate.setTurnOffChangeSessionIdOnLogin(turnOffChangeSessionIdOnLogin); - } - - @Override - public int getTokenMinimumTimeToLive() { - return delegate.getTokenMinimumTimeToLive(); - } - - @Override - public void setTokenMinimumTimeToLive(final int tokenMinimumTimeToLive) { - delegate.setTokenMinimumTimeToLive(tokenMinimumTimeToLive); - } - - @Override - public PolicyEnforcer getPolicyEnforcer() { - return delegate.getPolicyEnforcer(); - } - - @Override - public void setPolicyEnforcer(Callable policyEnforcer) { - delegate.setPolicyEnforcer(policyEnforcer); - } - - @Override - public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) { - delegate.setMinTimeBetweenJwksRequests(minTimeBetweenJwksRequests); - } - - @Override - public int getMinTimeBetweenJwksRequests() { - return delegate.getMinTimeBetweenJwksRequests(); - } - - @Override - public int getPublicKeyCacheTtl() { - return delegate.getPublicKeyCacheTtl(); - } - - @Override - public void setPublicKeyCacheTtl(int publicKeyCacheTtl) { - delegate.setPublicKeyCacheTtl(publicKeyCacheTtl); - } - - @Override - public boolean isVerifyTokenAudience() { - return delegate.isVerifyTokenAudience(); - } - - @Override - public void setVerifyTokenAudience(boolean verifyTokenAudience) { - delegate.setVerifyTokenAudience(verifyTokenAudience); - } - - @Override - public AdapterConfig getAdapterConfig() { - return delegate.getAdapterConfig(); - } - } - - protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) { - KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(base); - URI request = URI.create(facade.getRequest().getURI()); - String scheme = request.getScheme(); - if (deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - scheme = "https"; - if (!request.getScheme().equals(scheme) && request.getPort() != -1) { - log.error("request scheme: " + request.getScheme() + " ssl required"); - throw new RuntimeException("Can't resolve relative url from adapter config."); - } - } - builder.scheme(scheme); - builder.host(request.getHost()); - if (request.getPort() != -1) { - builder.port(request.getPort()); - } - return builder; - } - - - - protected void close(HttpResponse response) { - if (response.getEntity() != null) { - try { - response.getEntity().getContent().close(); - } catch (IOException e) { - - } - } - } - - public void updateDeployment(AdapterConfig config) { - if (null != configResolver) { - throw new IllegalStateException("Cannot parse an adapter config and build an updated deployment when on a multi-tenant scenario."); - } - deployment = KeycloakDeploymentBuilder.build(config); - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java deleted file mode 100755 index daf9754685a6..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.keycloak.adapters.spi.AdapterSessionStore; - -/** - * Abstraction for storing token info on adapter side. Intended to be per-request object - * - * @author Marek Posolda - */ -public interface AdapterTokenStore extends AdapterSessionStore { - - /** - * Impl can validate if current token exists and perform refreshing if it exists and is expired - */ - void checkCurrentToken(); - - /** - * Check if we are logged already (we have already valid and successfully refreshed accessToken). Establish security context if yes - * - * @param authenticator used for actual request authentication - * @return true if we are logged-in already - */ - boolean isCached(RequestAuthenticator authenticator); - - /** - * Finish successful OAuth2 login and store validated account - * - * @param account - */ - void saveAccountInfo(OidcKeycloakAccount account); - - /** - * Handle logout on store side and possibly propagate logout call to Keycloak - */ - void logout(); - - /** - * Callback invoked after successful token refresh - * - * @param securityContext context where refresh was performed - */ - void refreshCallback(RefreshableKeycloakSecurityContext securityContext); - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java deleted file mode 100755 index cda16359313f..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.NameValuePair; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils; -import org.keycloak.representations.AccessToken; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** - * @author Marek Posolda - */ -public class AdapterUtils { - - private static Logger log = Logger.getLogger(AdapterUtils.class); - - public static String generateId() { - return UUID.randomUUID().toString(); - } - - public static Set getRolesFromSecurityContext(RefreshableKeycloakSecurityContext session) { - Set roles = null; - AccessToken accessToken = session.getToken(); - if (session.getDeployment().isUseResourceRoleMappings()) { - if (log.isTraceEnabled()) { - log.trace("useResourceRoleMappings"); - } - AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName()); - if (access != null) roles = access.getRoles(); - } else { - if (log.isTraceEnabled()) { - log.trace("use realm role mappings"); - } - AccessToken.Access access = accessToken.getRealmAccess(); - if (access != null) roles = access.getRoles(); - } - if (roles == null) roles = Collections.emptySet(); - if (log.isTraceEnabled()) { - log.trace("Setting roles: "); - for (String role : roles) { - log.trace(" role: " + role); - } - } - return roles; - } - - public static String getPrincipalName(KeycloakDeployment deployment, AccessToken token) { - String attr = "sub"; - if (deployment.getPrincipalAttribute() != null) attr = deployment.getPrincipalAttribute(); - String name = null; - - if ("sub".equals(attr)) { - name = token.getSubject(); - } else if ("email".equals(attr)) { - name = token.getEmail(); - } else if ("preferred_username".equals(attr)) { - name = token.getPreferredUsername(); - } else if ("name".equals(attr)) { - name = token.getName(); - } else if ("given_name".equals(attr)) { - name = token.getGivenName(); - } else if ("family_name".equals(attr)) { - name = token.getFamilyName(); - } else if ("nickname".equals(attr)) { - name = token.getNickName(); - } - if (name == null) name = token.getSubject(); - return name; - } - - public static KeycloakPrincipal createPrincipal(KeycloakDeployment deployment, RefreshableKeycloakSecurityContext securityContext) { - return new KeycloakPrincipal<>(getPrincipalName(deployment, securityContext.getToken()), securityContext); - } - - /** - * Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)} - */ - public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List formparams) { - Map reqHeaders = new HashMap<>(); - Map reqParams = new HashMap<>(); - ClientCredentialsProviderUtils.setClientCredentials(deployment.getAdapterConfig(), deployment.getClientAuthenticator(), reqHeaders, reqParams); - - for (Map.Entry header : reqHeaders.entrySet()) { - post.setHeader(header.getKey(), header.getValue()); - } - - for (Map.Entry param : reqParams.entrySet()) { - formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java deleted file mode 100755 index c0d19c16512d..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.AuthorizationContext; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.pep.HttpAuthzRequest; -import org.keycloak.adapters.pep.HttpAuthzResponse; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.common.util.UriUtils; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.representations.AccessToken; - -import java.io.IOException; -import java.util.Set; - -/** - * Pre-installed actions that must be authenticated - * - * Actions include: - * - * CORS Origin Check and Response headers - * k_query_bearer_token: Get bearer token from server for Javascripts CORS requests - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AuthenticatedActionsHandler { - private static final Logger log = Logger.getLogger(AuthenticatedActionsHandler.class); - protected KeycloakDeployment deployment; - protected OIDCHttpFacade facade; - - public AuthenticatedActionsHandler(KeycloakDeployment deployment, OIDCHttpFacade facade) { - this.deployment = deployment; - this.facade = facade; - } - - public boolean handledRequest() { - log.debugv("AuthenticatedActionsValve.invoke {0}", facade.getRequest().getURI()); - if (corsRequest()) return true; - String requestUri = facade.getRequest().getURI(); - if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) { - queryBearerToken(); - return true; - } - if (!isAuthorized()) { - return true; - } - return false; - } - - protected void queryBearerToken() { - log.debugv("queryBearerToken {0}",facade.getRequest().getURI()); - if (abortTokenResponse()) return; - facade.getResponse().setStatus(200); - facade.getResponse().setHeader("Content-Type", "text/plain"); - try { - facade.getResponse().getOutputStream().write(facade.getSecurityContext().getTokenString().getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - facade.getResponse().end(); - } - - protected boolean abortTokenResponse() { - if (facade.getSecurityContext() == null) { - log.debugv("Not logged in, sending back 401: {0}",facade.getRequest().getURI()); - facade.getResponse().sendError(401); - facade.getResponse().end(); - return true; - } - if (!deployment.isExposeToken()) { - facade.getResponse().setStatus(200); - facade.getResponse().end(); - return true; - } - // Don't allow a CORS request if we're not validating CORS requests. - String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN); - if (!deployment.isCors() && origin != null && !origin.equals("null")) { - facade.getResponse().setStatus(200); - facade.getResponse().end(); - return true; - } - return false; - } - - protected boolean corsRequest() { - if (!deployment.isCors()) return false; - KeycloakSecurityContext securityContext = facade.getSecurityContext(); - String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN); - origin = "null".equals(origin) ? null : origin; - String exposeHeaders = deployment.getCorsExposedHeaders(); - - if (deployment.getPolicyEnforcer() != null) { - if (exposeHeaders != null) { - exposeHeaders += ","; - } else { - exposeHeaders = ""; - } - - exposeHeaders += "WWW-Authenticate"; - } - - String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI()); - log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI()); - if (securityContext != null && origin != null && !origin.equals(requestOrigin)) { - AccessToken token = securityContext.getToken(); - Set allowedOrigins = token.getAllowedOrigins(); - - log.debugf("Allowed origins in token: %s", allowedOrigins); - - if (allowedOrigins == null || (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin))) { - if (allowedOrigins == null) { - log.debugv("allowedOrigins was null in token"); - } else { - log.debugv("allowedOrigins did not contain origin"); - } - facade.getResponse().sendError(403); - facade.getResponse().end(); - return true; - } - log.debugv("returning origin: {0}", origin); - facade.getResponse().setStatus(200); - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - if (exposeHeaders != null) { - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, exposeHeaders); - } - } else { - log.debugv("cors validation not needed as we are not a secure session or origin header was null: {0}", facade.getRequest().getURI()); - } - return false; - } - - private boolean isAuthorized() { - PolicyEnforcer policyEnforcer = this.deployment.getPolicyEnforcer(); - - if (policyEnforcer == null) { - log.debugv("Policy enforcement is disabled."); - return true; - } - try { - OIDCHttpFacade facade = (OIDCHttpFacade) this.facade; - AuthorizationContext authorizationContext = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade)); - RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext(); - - if (session != null) { - session.setAuthorizationContext(authorizationContext); - } - - return authorizationContext.isGranted(); - } catch (Exception e) { - throw new RuntimeException("Failed to enforce policy decisions.", e); - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java deleted file mode 100755 index ebdece45a753..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; -import org.jboss.logging.Logger; -import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.common.util.Base64; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.util.JsonSerialization; - -import java.util.List; - -/** - * Basic auth request authenticator. - */ -public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticator { - protected Logger log = Logger.getLogger(BasicAuthRequestAuthenticator.class); - - public BasicAuthRequestAuthenticator(KeycloakDeployment deployment) { - super(deployment); - } - - public AuthOutcome authenticate(HttpFacade exchange) { - List authHeaders = exchange.getRequest().getHeaders("Authorization"); - if (authHeaders == null || authHeaders.isEmpty()) { - log.debug("Authorization header not present"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_AUTHORIZATION_HEADER, null, null); - return AuthOutcome.NOT_ATTEMPTED; - } - - tokenString = null; - for (String authHeader : authHeaders) { - String[] split = authHeader.trim().split("\\s+"); - if (split.length != 2) continue; - if (!split[0].equalsIgnoreCase("Basic")) continue; - tokenString = split[1]; - } - - if (tokenString == null) { - log.debug("Token is not present in Authorization header"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, null, null); - return AuthOutcome.NOT_ATTEMPTED; - } - - AccessTokenResponse atr=null; - try { - String userpw=new String(Base64.decode(tokenString)); - int seperatorIndex = userpw.indexOf(":"); - String user = userpw.substring(0, seperatorIndex); - String pw = userpw.substring(seperatorIndex + 1); - atr = getToken(user, pw); - tokenString = atr.getToken(); - } catch (Exception e) { - log.debug("Failed to obtain token", e); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "no_token", e.getMessage()); - return AuthOutcome.FAILED; - } - - return authenticateToken(exchange, atr.getToken()); - } - - protected AccessTokenResponse getToken(String username, String password) throws Exception { - AccessTokenResponse tokenResponse=null; - HttpClient client = deployment.getClient(); - - HttpPost post = new HttpPost(deployment.getTokenUrl()); - java.util.List formparams = new java.util.ArrayList (); - formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); - formparams.add(new BasicNameValuePair("username", username)); - formparams.add(new BasicNameValuePair("password", password)); - - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - - HttpResponse response = client.execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 200) { - EntityUtils.consumeQuietly(entity); - throw new java.io.IOException("Bad status: " + status); - } - if (entity == null) { - throw new java.io.IOException("No Entity"); - } - java.io.InputStream is = entity.getContent(); - try { - tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class); - } finally { - try { - is.close(); - } catch (java.io.IOException ignored) { } - } - - return (tokenResponse); - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java deleted file mode 100755 index d34c3fa817cc..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.adapters.pep.HttpAuthzRequest; -import org.keycloak.adapters.pep.HttpAuthzResponse; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.common.VerificationException; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.representations.AccessToken; - -import java.util.List; -import javax.security.cert.X509Certificate; -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class BearerTokenRequestAuthenticator { - protected Logger log = Logger.getLogger(BearerTokenRequestAuthenticator.class); - protected String tokenString; - protected AccessToken token; - protected String surrogate; - protected AuthChallenge challenge; - protected KeycloakDeployment deployment; - - public BearerTokenRequestAuthenticator(KeycloakDeployment deployment) { - this.deployment = deployment; - } - - public AuthChallenge getChallenge() { - return challenge; - } - - public String getTokenString() { - return tokenString; - } - - public AccessToken getToken() { - return token; - } - - public String getSurrogate() { - return surrogate; - } - - public AuthOutcome authenticate(HttpFacade exchange) { - List authHeaders = exchange.getRequest().getHeaders("Authorization"); - if (authHeaders == null || authHeaders.isEmpty()) { - log.debug("Authorization header not present"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, null, null); - return AuthOutcome.NOT_ATTEMPTED; - } - - tokenString = null; - for (String authHeader : authHeaders) { - String[] split = authHeader.trim().split("\\s+"); - if (split.length != 2) continue; - if (split[0].equalsIgnoreCase("Bearer")) { - tokenString = split[1]; - - log.debugf("Found [%d] values in authorization header, selecting the first value for Bearer.", (Integer) authHeaders.size()); - break; - } - } - - if (tokenString == null) { - log.debug("Token is not present in Authorization header"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, null, null); - return AuthOutcome.NOT_ATTEMPTED; - } - - return (authenticateToken(exchange, tokenString)); - } - - protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) { - log.debug("Verifying access_token"); - if (log.isTraceEnabled()) { - try { - JWSInput jwsInput = new JWSInput(tokenString); - String wireString = jwsInput.getWireString(); - log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature"); - } catch (JWSInputException e) { - log.errorf(e, "Failed to parse access_token: %s", tokenString); - } - } - try { - token = AdapterTokenVerifier.verifyToken(tokenString, deployment); - } catch (VerificationException e) { - log.debugf("Failed to verify token: %s", e.getMessage()); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage()); - return AuthOutcome.FAILED; - } - if (token.getIssuedAt() < deployment.getNotBefore()) { - log.debug("Stale token"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token"); - return AuthOutcome.FAILED; - } - boolean verifyCaller = false; - if (deployment.isUseResourceRoleMappings()) { - verifyCaller = token.isVerifyCaller(deployment.getResourceName()); - } else { - verifyCaller = token.isVerifyCaller(); - } - surrogate = null; - if (verifyCaller) { - if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) { - log.warn("No trusted certificates in token"); - challenge = clientCertChallenge(); - return AuthOutcome.FAILED; - } - - // for now, we just make sure Undertow did two-way SSL - // assume JBoss Web verifies the client cert - X509Certificate[] chain = new X509Certificate[0]; - try { - chain = exchange.getCertificateChain(); - } catch (Exception ignore) { - - } - if (chain == null || chain.length == 0) { - log.warn("No certificates provided by undertow to verify the caller"); - challenge = clientCertChallenge(); - return AuthOutcome.FAILED; - } - surrogate = chain[0].getSubjectDN().getName(); - } - log.debug("successful authorized"); - return AuthOutcome.AUTHENTICATED; - } - - protected AuthChallenge clientCertChallenge() { - return new AuthChallenge() { - @Override - public int getResponseCode() { - return 0; - } - - @Override - public boolean challenge(HttpFacade exchange) { - // do the same thing as client cert auth - return false; - } - }; - } - - - protected AuthChallenge challengeResponse(HttpFacade facade, final OIDCAuthenticationError.Reason reason, final String error, final String description) { - StringBuilder header = new StringBuilder("Bearer realm=\""); - header.append(deployment.getRealm()).append("\""); - if (error != null) { - header.append(", error=\"").append(error).append("\""); - } - if (description != null) { - header.append(", error_description=\"").append(description).append("\""); - } - final String challenge = header.toString(); - return new AuthChallenge() { - @Override - public int getResponseCode() { - return 401; - } - - @Override - public boolean challenge(HttpFacade facade) { - OIDCHttpFacade oidcFacade = (OIDCHttpFacade) facade; - if (deployment.getPolicyEnforcer() != null) { - deployment.getPolicyEnforcer().enforce(new HttpAuthzRequest(oidcFacade), new HttpAuthzResponse(oidcFacade)); - return true; - } - OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description); - facade.getRequest().setError(error); - facade.getResponse().addHeader("WWW-Authenticate", challenge); - if(deployment.isDelegateBearerErrorResponseSending()){ - facade.getResponse().setStatus(401); - } - else { - facade.getResponse().sendError(401); - } - return true; - } - }; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java deleted file mode 100755 index de837c4a8ab7..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.TokenVerifier; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.IDToken; - -/** - * @author Marek Posolda - */ -public class CookieTokenStore { - - private static final Logger log = Logger.getLogger(CookieTokenStore.class); - private static final String DELIM = "___"; - - public static void setTokenCookie(KeycloakDeployment deployment, HttpFacade facade, RefreshableKeycloakSecurityContext session) { - log.debugf("Set new %s cookie now", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE); - String accessToken = session.getTokenString(); - String idToken = session.getIdTokenString(); - String refreshToken = session.getRefreshToken(); - String cookie = new StringBuilder(accessToken).append(DELIM) - .append(idToken).append(DELIM) - .append(refreshToken).toString(); - - String cookiePath = getCookiePath(deployment, facade); - facade.getResponse().setCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookie, cookiePath, null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true); - } - - public static KeycloakPrincipal getPrincipalFromCookie(KeycloakDeployment deployment, HttpFacade facade, AdapterTokenStore tokenStore) { - OIDCHttpFacade.Cookie cookie = facade.getRequest().getCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE); - if (cookie == null) { - log.debug("Not found adapter state cookie in current request"); - return null; - } - - String cookieVal = cookie.getValue(); - - String[] tokens = cookieVal.split(DELIM); - if (tokens.length != 3) { - log.warnf("Invalid format of %s cookie. Count of tokens: %s, expected 3", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, tokens.length); - return null; - } - - String accessTokenString = tokens[0]; - String idTokenString = tokens[1]; - String refreshTokenString = tokens[2]; - - try { - // Skip check if token is active now. It's supposed to be done later by the caller - TokenVerifier tokenVerifier = AdapterTokenVerifier.createVerifier(accessTokenString, deployment, true, AccessToken.class) - .checkActive(false) - .verify(); - AccessToken accessToken = tokenVerifier.getToken(); - - IDToken idToken; - if (idTokenString != null && idTokenString.length() > 0) { - try { - JWSInput input = new JWSInput(idTokenString); - idToken = input.readJsonContent(IDToken.class); - } catch (JWSInputException e) { - throw new VerificationException(e); - } - } else { - idToken = null; - } - - log.debug("Token Verification succeeded!"); - RefreshableKeycloakSecurityContext secContext = new RefreshableKeycloakSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); - return new KeycloakPrincipal<>(AdapterUtils.getPrincipalName(deployment, accessToken), secContext); - } catch (VerificationException ve) { - log.warn("Failed verify token", ve); - return null; - } - } - - public static void removeCookie(KeycloakDeployment deployment, HttpFacade facade) { - String cookiePath = getCookiePath(deployment, facade); - facade.getResponse().resetCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookiePath); - } - - static String getCookiePath(KeycloakDeployment deployment, HttpFacade facade) { - String path = deployment.getAdapterStateCookiePath() == null ? "" : deployment.getAdapterStateCookiePath().trim(); - if (path.startsWith("/")) { - return path; - } - String contextPath = getContextPath(facade); - StringBuilder cookiePath = new StringBuilder(contextPath); - if (!contextPath.endsWith("/") && !path.isEmpty()) { - cookiePath.append("/"); - } - return cookiePath.append(path).toString(); - } - - static String getContextPath(HttpFacade facade) { - String uri = facade.getRequest().getURI(); - String path = KeycloakUriBuilder.fromUri(uri).getPath(); - if (path == null || path.isEmpty()) { - return "/"; - } - int index = path.indexOf("/", 1); - return index == -1 ? path : path.substring(0, index); - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java deleted file mode 100755 index 416c3923f810..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface CorsHeaders { - String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - String ORIGIN = "Origin"; - String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; - String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java deleted file mode 100644 index 6a3176a3b92c..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpRequestBase; -import org.keycloak.util.JsonSerialization; - -import java.io.IOException; -import java.io.InputStream; - -/** - * @author Marek Posolda - */ -public class HttpAdapterUtils { - - - public static T sendJsonHttpRequest(KeycloakDeployment deployment, HttpRequestBase httpRequest, Class clazz) throws HttpClientAdapterException { - try { - HttpResponse response = deployment.getClient().execute(httpRequest); - int status = response.getStatusLine().getStatusCode(); - if (status != 200) { - close(response); - throw new HttpClientAdapterException("Unexpected status = " + status); - } - HttpEntity entity = response.getEntity(); - if (entity == null) { - throw new HttpClientAdapterException("There was no entity."); - } - InputStream is = entity.getContent(); - try { - return JsonSerialization.readValue(is, clazz); - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } catch (IOException e) { - throw new HttpClientAdapterException("IO error", e); - } - } - - - private static void close(HttpResponse response) { - if (response.getEntity() != null) { - try { - response.getEntity().getContent().close(); - } catch (IOException e) { - - } - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java deleted file mode 100644 index 7d303e290db3..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -/** - * @author Marek Posolda - */ -public class HttpClientAdapterException extends Exception { - - public HttpClientAdapterException(String message) { - super(message); - } - - public HttpClientAdapterException(String message, Throwable t) { - super(message, t); - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java deleted file mode 100755 index 7338f220a688..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthSchemeProvider; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.Credentials; -import org.apache.http.client.CookieStore; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.AuthSchemes; -import org.apache.http.client.config.CookieSpecs; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.ConnectionConfig; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.config.SocketConfig; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.AllowAllHostnameVerifier; -import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import org.apache.http.conn.ssl.X509HostnameVerifier; -import org.apache.http.cookie.Cookie; -import org.apache.http.cookie.CookieSpecProvider; -import org.apache.http.impl.auth.SPNegoSchemeFactory; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.impl.client.CookieSpecRegistries; -import org.apache.http.impl.conn.BasicHttpClientConnectionManager; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.impl.cookie.DefaultCookieSpecProvider; -import org.keycloak.common.util.EnvUtil; -import org.keycloak.common.util.KeystoreUtil; -import org.keycloak.representations.adapters.config.AdapterHttpClientConfig; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.URI; -import java.security.KeyStore; -import java.security.Principal; -import java.security.SecureRandom; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Abstraction for creating HttpClients. Allows SSL configuration. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class HttpClientBuilder { - - public static enum HostnameVerificationPolicy { - /** - * Hostname verification is not done on the server's certificate - */ - ANY, - /** - * Allows wildcards in subdomain names i.e. *.foo.com - */ - WILDCARD, - /** - * CN must match hostname connecting to - */ - STRICT - } - - - /** - * @author Bill Burke - * @version $Revision: 1 $ - */ - private static class PassthroughTrustManager implements X509TrustManager { - public void checkClientTrusted(X509Certificate[] chain, - String authType) throws CertificateException { - } - - public void checkServerTrusted(X509Certificate[] chain, - String authType) throws CertificateException { - } - - public X509Certificate[] getAcceptedIssuers() { - return null; - } - } - - protected KeyStore truststore; - protected KeyStore clientKeyStore; - protected String clientPrivateKeyPassword; - protected boolean disableTrustManager; - protected boolean disableCookieCache = true; - protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD; - protected SSLContext sslContext; - protected int connectionPoolSize = 100; - protected int maxPooledPerRoute = 0; - protected long connectionTTL = -1; - protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS; - protected HostnameVerifier verifier = null; - protected long socketTimeout = -1; - protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS; - protected long establishConnectionTimeout = -1; - protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS; - protected HttpHost proxyHost; - private SPNegoSchemeFactory spNegoSchemeFactory; - private boolean useSpNego; - - /** - * Socket inactivity timeout - * - * @param timeout - * @param unit - * @return - */ - public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit) { - this.socketTimeout = timeout; - this.socketTimeoutUnits = unit; - return this; - } - - /** - * When trying to make an initial socket connection, what is the timeout? - * - * @param timeout - * @param unit - * @return - */ - public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) { - this.establishConnectionTimeout = timeout; - this.establishConnectionTimeoutUnits = unit; - return this; - } - - public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) { - this.connectionTTL = ttl; - this.connectionTTLUnit = unit; - return this; - } - - public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) { - this.maxPooledPerRoute = maxPooledPerRoute; - return this; - } - - public HttpClientBuilder connectionPoolSize(int connectionPoolSize) { - this.connectionPoolSize = connectionPoolSize; - return this; - } - - /** - * Disable trust management and hostname verification. NOTE this is a security - * hole, so only set this option if you cannot or do not want to verify the identity of the - * host you are communicating with. - */ - public HttpClientBuilder disableTrustManager() { - this.disableTrustManager = true; - return this; - } - - public HttpClientBuilder disableCookieCache(boolean disable) { - this.disableCookieCache = disable; - return this; - } - - /** - * SSL policy used to verify hostnames - * - * @param policy - * @return - */ - public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) { - this.policy = policy; - return this; - } - - - public HttpClientBuilder sslContext(SSLContext sslContext) { - this.sslContext = sslContext; - return this; - } - - public HttpClientBuilder trustStore(KeyStore truststore) { - this.truststore = truststore; - return this; - } - - public HttpClientBuilder keyStore(KeyStore keyStore, String password) { - this.clientKeyStore = keyStore; - this.clientPrivateKeyPassword = password; - return this; - } - - public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) { - this.clientKeyStore = keyStore; - this.clientPrivateKeyPassword = new String(password); - return this; - } - - - static class VerifierWrapper implements X509HostnameVerifier { - protected HostnameVerifier verifier; - - VerifierWrapper(HostnameVerifier verifier) { - this.verifier = verifier; - } - - @Override - public void verify(String host, SSLSocket ssl) throws IOException { - if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure"); - } - - @Override - public void verify(String host, X509Certificate cert) throws SSLException { - throw new SSLException("This verification path not implemented"); - } - - @Override - public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { - throw new SSLException("This verification path not implemented"); - } - - @Override - public boolean verify(String s, SSLSession sslSession) { - return verifier.verify(s, sslSession); - } - } - - public HttpClientBuilder spNegoSchemeFactory(SPNegoSchemeFactory spnegoSchemeFactory) { - this.spNegoSchemeFactory = spnegoSchemeFactory; - return this; - } - - public HttpClientBuilder useSPNego(boolean useSpnego) { - this.useSpNego = useSpnego; - return this; - } - - public HttpClient build() { - X509HostnameVerifier verifier = null; - if (this.verifier != null) verifier = new VerifierWrapper(this.verifier); - else { - switch (policy) { - case ANY: - verifier = new AllowAllHostnameVerifier(); - break; - case WILDCARD: - verifier = new BrowserCompatHostnameVerifier(); - break; - case STRICT: - verifier = new StrictHostnameVerifier(); - break; - } - } - try { - ConnectionSocketFactory sslsf; - SSLContext theContext = sslContext; - if (disableTrustManager) { - theContext = SSLContext.getInstance("SSL"); - theContext.init(null, new TrustManager[]{new PassthroughTrustManager()}, - new SecureRandom()); - verifier = new AllowAllHostnameVerifier(); - sslsf = new SniSSLSocketFactory(theContext, verifier); - } else if (theContext != null) { - sslsf = new SniSSLSocketFactory(theContext, verifier); - } else if (clientKeyStore != null || truststore != null) { - sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier); - } else { - final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS); - tlsContext.init(null, null, null); - sslsf = new SniSSLSocketFactory(tlsContext, verifier); - } - - RegistryBuilder sf = RegistryBuilder.create(); - - sf.register("http", PlainConnectionSocketFactory.getSocketFactory()); - sf.register("https", sslsf); - - HttpClientConnectionManager cm; - - if (connectionPoolSize > 0) { - PoolingHttpClientConnectionManager tcm = new PoolingHttpClientConnectionManager(sf.build(), null, null, null, connectionTTL, connectionTTLUnit); - tcm.setMaxTotal(connectionPoolSize); - if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize; - tcm.setDefaultMaxPerRoute(maxPooledPerRoute); - cm = tcm; - - } else { - cm = new BasicHttpClientConnectionManager(sf.build()); - } - - SocketConfig.Builder socketConfig = SocketConfig.copy(SocketConfig.DEFAULT); - ConnectionConfig.Builder connConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT); - RequestConfig.Builder requestConfig = RequestConfig.copy(RequestConfig.DEFAULT); - - if (proxyHost != null) { - requestConfig.setProxy(new HttpHost(proxyHost)); - } - - if (socketTimeout > -1) { - requestConfig.setSocketTimeout((int) socketTimeoutUnits.toMillis(socketTimeout)); - - } - if (establishConnectionTimeout > -1) { - requestConfig.setConnectTimeout((int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout)); - } - - Registry cookieSpecs = CookieSpecRegistries.createDefaultBuilder() - .register(CookieSpecs.DEFAULT, new DefaultCookieSpecProvider()).build(); - - if (useSpNego) { - requestConfig.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.SPNEGO)); - } - - org.apache.http.impl.client.HttpClientBuilder clientBuilder = org.apache.http.impl.client.HttpClientBuilder.create() - .setDefaultSocketConfig(socketConfig.build()) - .setDefaultConnectionConfig(connConfig.build()) - .setDefaultRequestConfig(requestConfig.build()) - .setDefaultCookieSpecRegistry(cookieSpecs) - .setConnectionManager(cm); - - if (spNegoSchemeFactory != null) { - RegistryBuilder authSchemes = RegistryBuilder.create(); - - authSchemes.register(AuthSchemes.SPNEGO, spNegoSchemeFactory); - - clientBuilder.setDefaultAuthSchemeRegistry(authSchemes.build()); - } - - if (useSpNego) { - Credentials fake = new Credentials() { - - @Override - public String getPassword() { - return null; - } - - @Override - public Principal getUserPrincipal() { - return null; - } - - }; - - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, fake); - clientBuilder.setDefaultCredentialsProvider(credentialsProvider); - } - - if (disableCookieCache) { - clientBuilder.setDefaultCookieStore(new CookieStore() { - @Override - public void addCookie(Cookie cookie) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public List getCookies() { - return Collections.emptyList(); - } - - @Override - public boolean clearExpired(Date date) { - return false; //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void clear() { - //To change body of implemented methods use File | Settings | File Templates. - } - }); - - } - return clientBuilder.build(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public HttpClient build(AdapterHttpClientConfig adapterConfig) { - disableCookieCache(true); // disable cookie cache as we don't want sticky sessions for load balancing - - String truststorePath = adapterConfig.getTruststore(); - if (truststorePath != null) { - truststorePath = EnvUtil.replace(truststorePath); - String truststorePassword = adapterConfig.getTruststorePassword(); - try { - this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword); - } catch (Exception e) { - throw new RuntimeException("Failed to load truststore", e); - } - } - String clientKeystore = adapterConfig.getClientKeystore(); - if (clientKeystore != null) { - clientKeystore = EnvUtil.replace(clientKeystore); - String clientKeystorePassword = adapterConfig.getClientKeystorePassword(); - try { - KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); - keyStore(clientCertKeystore, clientKeystorePassword); - } catch (Exception e) { - throw new RuntimeException("Failed to load keystore", e); - } - } - - HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD; - if (adapterConfig.isAllowAnyHostname()) - policy = HttpClientBuilder.HostnameVerificationPolicy.ANY; - connectionPoolSize(adapterConfig.getConnectionPoolSize()); - hostnameVerification(policy); - if (adapterConfig.isDisableTrustManager()) { - disableTrustManager(); - } else { - trustStore(truststore); - } - - configureProxyForAuthServerIfProvided(adapterConfig); - - if (socketTimeout == -1 && adapterConfig.getSocketTimeout() > 0) { - socketTimeout(adapterConfig.getSocketTimeout(), TimeUnit.MILLISECONDS); - } - - if (establishConnectionTimeout == -1 && adapterConfig.getConnectionTimeout() > 0) { - establishConnectionTimeout(adapterConfig.getConnectionTimeout(), TimeUnit.MILLISECONDS); - } - - if (connectionTTL == -1 && adapterConfig.getConnectionTTL() > 0) { - connectionTTL(adapterConfig.getConnectionTTL(), TimeUnit.MILLISECONDS); - } - - return build(); - } - - /** - * Configures a the proxy to use for auth-server requests if provided. - *

- * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the - * given URL as a proxy server, otherwise the proxy configuration is ignored. - *

- * - * @param adapterConfig - */ - private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) { - - if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) { - return; - } - - URI uri = URI.create(adapterConfig.getProxyUrl()); - this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); - } -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java deleted file mode 100644 index 987adaecef74..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.keycloak.adapters.spi.HttpFacade.Request; - -/** - * On multi-tenant scenarios, Keycloak will defer the resolution of a - * KeycloakDeployment to the target application at the request-phase. - * - * A Request object is passed to the resolver and callers expect a complete - * KeycloakDeployment. Based on this KeycloakDeployment, Keycloak will resume - * authenticating and authorizing the request. - * - * The easiest way to build a KeycloakDeployment is to use - * KeycloakDeploymentBuilder , passing the InputStream of an existing - * keycloak.json to the build() method. - * - * The resolved KeycloakDeployment may not be null - * - * @see KeycloakDeploymentBuilder - * @author Juraci Paixão Kröhling - */ -public interface KeycloakConfigResolver { - - /** - * Resolves the KeycloakDeployment based on the Request - * - * @param facade The request - * @return KeycloakDeployment, may never be null - */ - public KeycloakDeployment resolve(Request facade); - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java deleted file mode 100755 index 590ac4b39265..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ /dev/null @@ -1,608 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.util.EntityUtils; -import org.jboss.logging.Logger; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.rotation.PublicKeyLocator; -import org.keycloak.common.enums.RelativeUrlsUsed; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.constants.ServiceUrlConstants; -import org.keycloak.enums.TokenStore; -import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; -import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; -import org.keycloak.representations.adapters.config.AdapterConfig; -import org.keycloak.util.JsonSerialization; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Callable; - -/** - * @author Bill Burke - * @author Brad Culley - * @author John D. Ament - * @version $Revision: 1 $ - */ -public class KeycloakDeployment { - - private static final Logger log = Logger.getLogger(KeycloakDeployment.class); - - protected RelativeUrlsUsed relativeUrls; - protected String realm; - protected PublicKeyLocator publicKeyLocator; - protected String authServerBaseUrl; - protected String realmInfoUrl; - protected KeycloakUriBuilder authUrl; - protected String tokenUrl; - protected KeycloakUriBuilder logoutUrl; - protected String accountUrl; - protected String registerNodeUrl; - protected String unregisterNodeUrl; - protected String jwksUrl; - protected String principalAttribute = "sub"; - - protected String resourceName; - protected boolean bearerOnly; - protected boolean autodetectBearerOnly; - protected boolean enableBasicAuth; - protected boolean publicClient; - protected Map resourceCredentials = new HashMap<>(); - protected ClientCredentialsProvider clientAuthenticator; - protected Callable client; - - protected String scope; - protected SslRequired sslRequired = SslRequired.ALL; - protected int confidentialPort = -1; - protected TokenStore tokenStore = TokenStore.SESSION; - protected String adapterStateCookiePath = ""; - protected String stateCookieName = "OAuth_Token_Request_State"; - protected boolean useResourceRoleMappings; - protected boolean cors; - protected int corsMaxAge = -1; - protected String corsAllowedHeaders; - protected String corsAllowedMethods; - protected String corsExposedHeaders; - protected boolean exposeToken; - protected boolean alwaysRefreshToken; - protected boolean registerNodeAtStartup; - protected int registerNodePeriod; - protected boolean turnOffChangeSessionIdOnLogin; - - protected volatile int notBefore; - protected int tokenMinimumTimeToLive; - protected int minTimeBetweenJwksRequests; - protected int publicKeyCacheTtl; - protected Callable policyEnforcer; - - // https://tools.ietf.org/html/rfc7636 - protected boolean pkce = false; - protected boolean ignoreOAuthQueryParameter; - - protected Map redirectRewriteRules; - - protected boolean delegateBearerErrorResponseSending = false; - protected boolean verifyTokenAudience = false; - private AdapterConfig adapterConfig; - - public KeycloakDeployment() { - } - - public boolean isConfigured() { - return getRealm() != null && getPublicKeyLocator() != null && (isBearerOnly() || getAuthServerBaseUrl() != null); - } - - public String getResourceName() { - return resourceName; - } - - public String getRealm() { - return realm; - } - - public void setRealm(String realm) { - this.realm = realm; - } - - public PublicKeyLocator getPublicKeyLocator() { - return publicKeyLocator; - } - - public void setPublicKeyLocator(PublicKeyLocator publicKeyLocator) { - this.publicKeyLocator = publicKeyLocator; - } - - public String getAuthServerBaseUrl() { - return authServerBaseUrl; - } - - public void setAuthServerBaseUrl(AdapterConfig config) { - this.authServerBaseUrl = config.getAuthServerUrl(); - if (authServerBaseUrl == null) return; - - authServerBaseUrl = KeycloakUriBuilder.fromUri(authServerBaseUrl).build().toString(); - - authUrl = null; - realmInfoUrl = null; - tokenUrl = null; - logoutUrl = null; - accountUrl = null; - registerNodeUrl = null; - unregisterNodeUrl = null; - jwksUrl = null; - - URI authServerUri = URI.create(authServerBaseUrl); - - if (authServerUri.getHost() == null) { - relativeUrls = RelativeUrlsUsed.ALWAYS; - } else { - // We have absolute URI in config - relativeUrls = RelativeUrlsUsed.NEVER; - } - - this.adapterConfig = config; - } - - /** - * URLs are loaded lazily when used. This allows adapter to be deployed prior to Keycloak server starting, and will - * also allow the adapter to retry loading config for each request until the Keycloak server is ready. - * - * In the future we may want to support reloading config at a configurable interval. - */ - protected void resolveUrls() { - if (realmInfoUrl == null) { - synchronized (this) { - if (realmInfoUrl == null) { - KeycloakUriBuilder authUrlBuilder = KeycloakUriBuilder - .fromUri(authServerBaseUrl); - - String discoveryUrl = authUrlBuilder.clone() - .path(ServiceUrlConstants.DISCOVERY_URL).build(getRealm()).toString(); - try { - log.debugv("Resolving URLs from {0}", discoveryUrl); - - OIDCConfigurationRepresentation config = getOidcConfiguration(discoveryUrl); - - authUrl = KeycloakUriBuilder.fromUri(config.getAuthorizationEndpoint()); - realmInfoUrl = config.getIssuer(); - - tokenUrl = config.getTokenEndpoint(); - logoutUrl = KeycloakUriBuilder.fromUri(config.getLogoutEndpoint()); - accountUrl = KeycloakUriBuilder.fromUri(config.getIssuer()).path("/account") - .build().toString(); - registerNodeUrl = authUrlBuilder.clone() - .path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH) - .build(getRealm()).toString(); - unregisterNodeUrl = authUrlBuilder.clone() - .path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH) - .build(getRealm()).toString(); - jwksUrl = config.getJwksUri(); - - log.infov("Loaded URLs from {0}", discoveryUrl); - } catch (Exception e) { - log.warnv(e, "Failed to load URLs from {0}", discoveryUrl); - } - } - } - } - } - - protected void resolveUrls(KeycloakUriBuilder authUrlBuilder) { - if (log.isDebugEnabled()) { - log.debug("resolveUrls"); - } - - String login = authUrlBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(getRealm()).toString(); - authUrl = KeycloakUriBuilder.fromUri(login); - realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString(); - - tokenUrl = authUrlBuilder.clone().path(ServiceUrlConstants.TOKEN_PATH).build(getRealm()).toString(); - logoutUrl = KeycloakUriBuilder.fromUri(authUrlBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH).build(getRealm()).toString()); - accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString(); - registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString(); - unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString(); - jwksUrl = authUrlBuilder.clone().path(ServiceUrlConstants.JWKS_URL).build(getRealm()).toString(); - } - - protected OIDCConfigurationRepresentation getOidcConfiguration(String discoveryUrl) throws Exception { - HttpGet request = new HttpGet(discoveryUrl); - request.addHeader("accept", "application/json"); - - try { - HttpResponse response = getClient().execute(request); - if (response.getStatusLine().getStatusCode() != 200) { - EntityUtils.consumeQuietly(response.getEntity()); - throw new Exception(response.getStatusLine().getReasonPhrase()); - } - return JsonSerialization.readValue(response.getEntity().getContent(), OIDCConfigurationRepresentation.class); - } finally { - request.releaseConnection(); - } - } - - public RelativeUrlsUsed getRelativeUrls() { - return relativeUrls; - } - - public String getRealmInfoUrl() { - resolveUrls(); - return realmInfoUrl; - } - - public KeycloakUriBuilder getAuthUrl() { - resolveUrls(); - return authUrl; - } - - public String getTokenUrl() { - resolveUrls(); - return tokenUrl; - } - - public KeycloakUriBuilder getLogoutUrl() { - resolveUrls(); - return logoutUrl; - } - - public String getAccountUrl() { - resolveUrls(); - return accountUrl; - } - - public String getRegisterNodeUrl() { - resolveUrls(); - return registerNodeUrl; - } - - public String getUnregisterNodeUrl() { - resolveUrls(); - return unregisterNodeUrl; - } - - public String getJwksUrl() { - resolveUrls(); - return jwksUrl; - } - - public void setResourceName(String resourceName) { - this.resourceName = resourceName; - } - - public boolean isBearerOnly() { - return bearerOnly; - } - - public void setBearerOnly(boolean bearerOnly) { - this.bearerOnly = bearerOnly; - } - - public boolean isAutodetectBearerOnly() { - return autodetectBearerOnly; - } - - public void setAutodetectBearerOnly(boolean autodetectBearerOnly) { - this.autodetectBearerOnly = autodetectBearerOnly; - } - - public boolean isEnableBasicAuth() { - return enableBasicAuth; - } - - public void setEnableBasicAuth(boolean enableBasicAuth) { - this.enableBasicAuth = enableBasicAuth; - } - - public boolean isPublicClient() { - return publicClient; - } - - public void setPublicClient(boolean publicClient) { - this.publicClient = publicClient; - } - - public Map getResourceCredentials() { - return resourceCredentials; - } - - public void setResourceCredentials(Map resourceCredentials) { - this.resourceCredentials = resourceCredentials; - } - - public ClientCredentialsProvider getClientAuthenticator() { - return clientAuthenticator; - } - - public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) { - this.clientAuthenticator = clientAuthenticator; - } - - public HttpClient getClient() { - try { - return client.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public void setClient(final HttpClient client) { - this.client = new Callable() { - @Override - public HttpClient call() { - return client; - } - }; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public SslRequired getSslRequired() { - return sslRequired; - } - - public void setSslRequired(SslRequired sslRequired) { - this.sslRequired = sslRequired; - } - - public boolean isSSLEnabled() { - if (SslRequired.NONE == sslRequired) { - return false; - } - return true; - } - - public int getConfidentialPort() { - return confidentialPort; - } - - public void setConfidentialPort(int confidentialPort) { - this.confidentialPort = confidentialPort; - } - - public TokenStore getTokenStore() { - return tokenStore; - } - - public void setTokenStore(TokenStore tokenStore) { - this.tokenStore = tokenStore; - } - - public String getAdapterStateCookiePath() { - return adapterStateCookiePath; - } - - public void setAdapterStateCookiePath(String adapterStateCookiePath) { - this.adapterStateCookiePath = adapterStateCookiePath; - } - - public String getStateCookieName() { - return stateCookieName; - } - - public void setStateCookieName(String stateCookieName) { - this.stateCookieName = stateCookieName; - } - - public boolean isUseResourceRoleMappings() { - return useResourceRoleMappings; - } - - public void setUseResourceRoleMappings(boolean useResourceRoleMappings) { - this.useResourceRoleMappings = useResourceRoleMappings; - } - - public boolean isCors() { - return cors; - } - - public void setCors(boolean cors) { - this.cors = cors; - } - - public int getCorsMaxAge() { - return corsMaxAge; - } - - public void setCorsMaxAge(int corsMaxAge) { - this.corsMaxAge = corsMaxAge; - } - - public String getCorsAllowedHeaders() { - return corsAllowedHeaders; - } - - public void setCorsAllowedHeaders(String corsAllowedHeaders) { - this.corsAllowedHeaders = corsAllowedHeaders; - } - - public String getCorsAllowedMethods() { - return corsAllowedMethods; - } - - public void setCorsAllowedMethods(String corsAllowedMethods) { - this.corsAllowedMethods = corsAllowedMethods; - } - - public String getCorsExposedHeaders() { - return corsExposedHeaders; - } - - public void setCorsExposedHeaders(String corsExposedHeaders) { - this.corsExposedHeaders = corsExposedHeaders; - } - - public boolean isExposeToken() { - return exposeToken; - } - - public void setExposeToken(boolean exposeToken) { - this.exposeToken = exposeToken; - } - - public int getNotBefore() { - return notBefore; - } - - public void setNotBefore(int notBefore) { - this.notBefore = notBefore; - } - - public void updateNotBefore(int notBefore) { - this.notBefore = notBefore; - getPublicKeyLocator().reset(this); - } - - public boolean isAlwaysRefreshToken() { - return alwaysRefreshToken; - } - - public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { - this.alwaysRefreshToken = alwaysRefreshToken; - } - - public boolean isRegisterNodeAtStartup() { - return registerNodeAtStartup; - } - - public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) { - this.registerNodeAtStartup = registerNodeAtStartup; - } - - public int getRegisterNodePeriod() { - return registerNodePeriod; - } - - public void setRegisterNodePeriod(int registerNodePeriod) { - this.registerNodePeriod = registerNodePeriod; - } - - public String getPrincipalAttribute() { - return principalAttribute; - } - - public void setPrincipalAttribute(String principalAttribute) { - this.principalAttribute = principalAttribute; - } - - public boolean isTurnOffChangeSessionIdOnLogin() { - return turnOffChangeSessionIdOnLogin; - } - - public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) { - this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin; - } - - public int getTokenMinimumTimeToLive() { - return tokenMinimumTimeToLive; - } - - public void setTokenMinimumTimeToLive(final int tokenMinimumTimeToLive) { - this.tokenMinimumTimeToLive = tokenMinimumTimeToLive; - } - - public int getMinTimeBetweenJwksRequests() { - return minTimeBetweenJwksRequests; - } - - public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) { - this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests; - } - - public int getPublicKeyCacheTtl() { - return publicKeyCacheTtl; - } - - public void setPublicKeyCacheTtl(int publicKeyCacheTtl) { - this.publicKeyCacheTtl = publicKeyCacheTtl; - } - - public void setPolicyEnforcer(Callable policyEnforcer) { - this.policyEnforcer = policyEnforcer; - } - - public PolicyEnforcer getPolicyEnforcer() { - if (policyEnforcer == null) { - return null; - } - try { - return policyEnforcer.call(); - } catch (Exception cause) { - throw new RuntimeException("Failed to obtain policy enforcer", cause); - } - } - - // https://tools.ietf.org/html/rfc7636 - public boolean isPkce() { - return pkce; - } - - public void setPkce(boolean pkce) { - this.pkce = pkce; - } - - public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) { - this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter; - } - - public boolean isOAuthQueryParameterEnabled() { - return !this.ignoreOAuthQueryParameter; - } - - public Map getRedirectRewriteRules() { - return redirectRewriteRules; - } - - public void setRewriteRedirectRules(Map redirectRewriteRules) { - this.redirectRewriteRules = redirectRewriteRules; - } - - public boolean isDelegateBearerErrorResponseSending() { - return delegateBearerErrorResponseSending; - } - - public void setDelegateBearerErrorResponseSending(boolean delegateBearerErrorResponseSending) { - this.delegateBearerErrorResponseSending = delegateBearerErrorResponseSending; - } - - public boolean isVerifyTokenAudience() { - return verifyTokenAudience; - } - - public void setVerifyTokenAudience(boolean verifyTokenAudience) { - this.verifyTokenAudience = verifyTokenAudience; - } - - public void setClient(Callable callable) { - client = callable; - } - - public AdapterConfig getAdapterConfig() { - return adapterConfig; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java deleted file mode 100755 index 3626362de577..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import static org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils.bootstrapClientAuthenticator; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.client.HttpClient; -import org.jboss.logging.Logger; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator; -import org.keycloak.adapters.rotation.JWKPublicKeyLocator; -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.PemUtils; -import org.keycloak.enums.TokenStore; -import org.keycloak.representations.adapters.config.AdapterConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.keycloak.util.SystemPropertiesJsonParserFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.security.PublicKey; -import java.util.concurrent.Callable; - -/** - * @author Bill Burke - * @author Brad Culley - * @author John D. Ament - * @version $Revision: 1 $ - */ -public class KeycloakDeploymentBuilder { - - private static final Logger log = Logger.getLogger(KeycloakDeploymentBuilder.class); - - protected KeycloakDeployment deployment = new KeycloakDeployment(); - - protected KeycloakDeploymentBuilder() { - } - - - protected KeycloakDeployment internalBuild(final AdapterConfig adapterConfig) { - if (adapterConfig.getRealm() == null) throw new RuntimeException("Must set 'realm' in config"); - deployment.setRealm(adapterConfig.getRealm()); - String resource = adapterConfig.getResource(); - if (resource == null) throw new RuntimeException("Must set 'resource' in config"); - deployment.setResourceName(resource); - - String realmKeyPem = adapterConfig.getRealmKey(); - if (realmKeyPem != null) { - PublicKey realmKey; - try { - realmKey = PemUtils.decodePublicKey(realmKeyPem); - HardcodedPublicKeyLocator pkLocator = new HardcodedPublicKeyLocator(realmKey); - deployment.setPublicKeyLocator(pkLocator); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - JWKPublicKeyLocator pkLocator = new JWKPublicKeyLocator(); - deployment.setPublicKeyLocator(pkLocator); - } - - if (adapterConfig.getSslRequired() != null) { - deployment.setSslRequired(SslRequired.valueOf(adapterConfig.getSslRequired().toUpperCase())); - } else { - deployment.setSslRequired(SslRequired.EXTERNAL); - } - - if (adapterConfig.getConfidentialPort() != -1) { - deployment.setConfidentialPort(adapterConfig.getConfidentialPort()); - } - - if (adapterConfig.getTokenStore() != null) { - deployment.setTokenStore(TokenStore.valueOf(adapterConfig.getTokenStore().toUpperCase())); - } else { - deployment.setTokenStore(TokenStore.SESSION); - } - if (adapterConfig.getTokenCookiePath() != null) { - deployment.setAdapterStateCookiePath(adapterConfig.getTokenCookiePath()); - } - if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute()); - - deployment.setResourceCredentials(adapterConfig.getCredentials()); - deployment.setClientAuthenticator(bootstrapClientAuthenticator(adapterConfig)); - - deployment.setPublicClient(adapterConfig.isPublicClient()); - deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings()); - - deployment.setExposeToken(adapterConfig.isExposeToken()); - - if (adapterConfig.isCors()) { - deployment.setCors(true); - deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge()); - deployment.setCorsAllowedHeaders(adapterConfig.getCorsAllowedHeaders()); - deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods()); - deployment.setCorsExposedHeaders(adapterConfig.getCorsExposedHeaders()); - } - - // https://tools.ietf.org/html/rfc7636 - if (adapterConfig.isPkce()) { - deployment.setPkce(true); - } - - deployment.setBearerOnly(adapterConfig.isBearerOnly()); - deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly()); - deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth()); - deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); - deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup()); - deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod()); - deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive()); - deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests()); - deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl()); - deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter()); - deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules()); - deployment.setVerifyTokenAudience(adapterConfig.isVerifyTokenAudience()); - - if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { - throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); - } - if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) { - throw new RuntimeException("You must specify auth-server-url"); - } - deployment.setClient(createHttpClientProducer(adapterConfig)); - deployment.setAuthServerBaseUrl(adapterConfig); - if (adapterConfig.getTurnOffChangeSessionIdOnLogin() != null) { - deployment.setTurnOffChangeSessionIdOnLogin(adapterConfig.getTurnOffChangeSessionIdOnLogin()); - } - - final PolicyEnforcerConfig policyEnforcerConfig = adapterConfig.getPolicyEnforcerConfig(); - - if (policyEnforcerConfig != null) { - deployment.setPolicyEnforcer(new Callable() { - PolicyEnforcer policyEnforcer; - @Override - public PolicyEnforcer call() { - if (policyEnforcer == null) { - synchronized (deployment) { - if (policyEnforcer == null) { - policyEnforcer = PolicyEnforcer.builder() - .authServerUrl(adapterConfig.getAuthServerUrl()) - .realm(adapterConfig.getRealm()) - .clientId(adapterConfig.getResource()) - .bearerOnly(adapterConfig.isBearerOnly()) - .credentialProvider(deployment.getClientAuthenticator()) - .enforcerConfig(policyEnforcerConfig) - .httpClient(deployment.getClient()).build(); - } - } - } - return policyEnforcer; - } - }); - } - - return deployment; - } - - private Callable createHttpClientProducer(final AdapterConfig adapterConfig) { - return new Callable() { - private HttpClient client; - @Override - public HttpClient call() { - if (client == null) { - synchronized (deployment) { - if (client == null) { - client = new HttpClientBuilder().build(adapterConfig); - } - } - } - return client; - } - }; - } - - public static KeycloakDeployment build(InputStream is) { - CryptoIntegration.init(KeycloakDeploymentBuilder.class.getClassLoader()); - AdapterConfig adapterConfig = loadAdapterConfig(is); - return new KeycloakDeploymentBuilder().internalBuild(adapterConfig); - } - - public static AdapterConfig loadAdapterConfig(InputStream is) { - ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); - mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); - AdapterConfig adapterConfig; - try { - adapterConfig = mapper.readValue(is, AdapterConfig.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - return adapterConfig; - } - - - public static KeycloakDeployment build(AdapterConfig adapterConfig) { - CryptoIntegration.init(KeycloakDeploymentBuilder.class.getClassLoader()); - return new KeycloakDeploymentBuilder().internalBuild(adapterConfig); - } - - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationManagement.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationManagement.java deleted file mode 100755 index f32fa0dce161..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationManagement.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.common.util.HostUtils; -import org.keycloak.common.util.Time; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * @author Marek Posolda - */ -public class NodesRegistrationManagement { - - private static final Logger log = Logger.getLogger(NodesRegistrationManagement.class); - - private final Map nodeRegistrations = new ConcurrentHashMap(); - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - - // Sending registration event during first request to application or if re-registration is needed - public void tryRegister(final KeycloakDeployment resolvedDeployment) { - if (resolvedDeployment.isRegisterNodeAtStartup()) { - final String registrationUri = resolvedDeployment.getRegisterNodeUrl(); - if (needRefreshRegistration(registrationUri, resolvedDeployment)) { - Runnable runnable = new Runnable() { - - @Override - public void run() { - // Need to check it again in case that executor triggered by other thread already finished computation in the meantime - if (needRefreshRegistration(registrationUri, resolvedDeployment)) { - sendRegistrationEvent(resolvedDeployment); - } - } - }; - executor.execute(runnable); - } - } - } - - private boolean needRefreshRegistration(String registrationUri, KeycloakDeployment resolvedDeployment) { - NodeRegistrationContext currentRegistration = nodeRegistrations.get(registrationUri); - /// We don't yet have any registration for this node - if (currentRegistration == null) { - return true; - } - - return currentRegistration.lastRegistrationTime + resolvedDeployment.getRegisterNodePeriod() < Time.currentTime(); - } - - /** - * Called during undeployment or server stop. De-register from all previously registered deployments - */ - public void stop() { - executor.shutdownNow(); - - Collection allRegistrations = nodeRegistrations.values(); - for (NodeRegistrationContext registration : allRegistrations) { - sendUnregistrationEvent(registration.resolvedDeployment); - } - } - - protected void sendRegistrationEvent(KeycloakDeployment deployment) { - // This method is invoked from single-thread executor, so no synchronization is needed - // However, it could happen that the same deployment was submitted more than once to that executor - // Hence we need to recheck that the registration is really needed - final String registrationUri = deployment.getRegisterNodeUrl(); - if (! needRefreshRegistration(registrationUri, deployment)) { - return; - } - if (Thread.currentThread().isInterrupted()) { - return; - } - - log.debug("Sending registration event right now"); - - String host = HostUtils.getHostName(); - try { - ServerRequest.invokeRegisterNode(deployment, host); - NodeRegistrationContext regContext = new NodeRegistrationContext(Time.currentTime(), deployment); - nodeRegistrations.put(deployment.getRegisterNodeUrl(), regContext); - log.debugf("Node '%s' successfully registered in Keycloak", host); - } catch (ServerRequest.HttpFailure failure) { - log.error("failed to register node to keycloak"); - log.error("status from server: " + failure.getStatus()); - if (failure.getError() != null) { - log.error(" " + failure.getError()); - } - } catch (IOException e) { - log.error("failed to register node to keycloak", e); - } - } - - protected boolean sendUnregistrationEvent(KeycloakDeployment deployment) { - log.debug("Sending Unregistration event right now"); - - String host = HostUtils.getHostName(); - try { - ServerRequest.invokeUnregisterNode(deployment, host); - log.debugf("Node '%s' successfully unregistered from Keycloak", host); - return true; - } catch (ServerRequest.HttpFailure failure) { - log.error("failed to unregister node from keycloak"); - log.error("status from server: " + failure.getStatus()); - if (failure.getError() != null) { - log.error(" " + failure.getError()); - } - return false; - } catch (IOException e) { - log.error("failed to unregister node from keycloak", e); - return false; - } - } - - public static class NodeRegistrationContext { - - private final Integer lastRegistrationTime; - // deployment instance used for registration request - private final KeycloakDeployment resolvedDeployment; - - public NodeRegistrationContext(Integer lastRegTime, KeycloakDeployment deployment) { - this.lastRegistrationTime = lastRegTime; - this.resolvedDeployment = deployment; - } - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java deleted file mode 100755 index 68acc1118fb2..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.common.util.UriUtils; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.enums.TokenStore; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.IDToken; -import org.keycloak.util.TokenUtil; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Map; - - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OAuthRequestAuthenticator { - private static final Logger log = Logger.getLogger(OAuthRequestAuthenticator.class); - protected KeycloakDeployment deployment; - protected RequestAuthenticator reqAuthenticator; - protected int sslRedirectPort; - protected AdapterSessionStore tokenStore; - protected String tokenString; - protected String idTokenString; - protected IDToken idToken; - protected AccessToken token; - protected HttpFacade facade; - protected AuthChallenge challenge; - protected String refreshToken; - protected String strippedOauthParametersRequestUri; - - public OAuthRequestAuthenticator(RequestAuthenticator requestAuthenticator, HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, AdapterSessionStore tokenStore) { - this.reqAuthenticator = requestAuthenticator; - this.facade = facade; - this.deployment = deployment; - this.sslRedirectPort = deployment.getConfidentialPort() != -1 ? deployment.getConfidentialPort() : sslRedirectPort; - this.tokenStore = tokenStore; - } - - public AuthChallenge getChallenge() { - return challenge; - } - - public String getTokenString() { - return tokenString; - } - - public AccessToken getToken() { - return token; - } - - public String getRefreshToken() { - return refreshToken; - } - - public String getIdTokenString() { - return idTokenString; - } - - public void setIdTokenString(String idTokenString) { - this.idTokenString = idTokenString; - } - - public IDToken getIdToken() { - return idToken; - } - - public void setIdToken(IDToken idToken) { - this.idToken = idToken; - } - - public String getStrippedOauthParametersRequestUri() { - return strippedOauthParametersRequestUri; - } - - public void setStrippedOauthParametersRequestUri(String strippedOauthParametersRequestUri) { - this.strippedOauthParametersRequestUri = strippedOauthParametersRequestUri; - } - - protected String getRequestUrl() { - return facade.getRequest().getURI(); - } - - protected boolean isRequestSecure() { - return facade.getRequest().isSecure(); - } - - protected OIDCHttpFacade.Cookie getCookie(String cookieName) { - return facade.getRequest().getCookie(cookieName); - } - - protected String getCookieValue(String cookieName) { - OIDCHttpFacade.Cookie cookie = getCookie(cookieName); - if (cookie == null) return null; - return cookie.getValue(); - } - - protected String getQueryParamValue(String paramName) { - return facade.getRequest().getQueryParamValue(paramName); - } - - protected String getError() { - return getQueryParamValue(OAuth2Constants.ERROR); - } - - protected String getCode() { - return getQueryParamValue(OAuth2Constants.CODE); - } - - protected String getRedirectUri(String state) { - String url = getRequestUrl(); - log.debugf("callback uri: %s", url); - - if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - int port = sslRedirectPort(); - if (port < 0) { - // disabled? - return null; - } - KeycloakUriBuilder secureUrl = KeycloakUriBuilder.fromUri(url).scheme("https").port(-1); - if (port != 443) secureUrl.port(port); - url = secureUrl.buildAsString(); - } - - String loginHint = getQueryParamValue("login_hint"); - url = UriUtils.stripQueryParam(url,"login_hint"); - - String idpHint = getQueryParamValue(AdapterConstants.KC_IDP_HINT); - url = UriUtils.stripQueryParam(url, AdapterConstants.KC_IDP_HINT); - - String scope = getQueryParamValue(OAuth2Constants.SCOPE); - url = UriUtils.stripQueryParam(url, OAuth2Constants.SCOPE); - - String prompt = getQueryParamValue(OAuth2Constants.PROMPT); - url = UriUtils.stripQueryParam(url, OAuth2Constants.PROMPT); - - String maxAge = getQueryParamValue(OAuth2Constants.MAX_AGE); - url = UriUtils.stripQueryParam(url, OAuth2Constants.MAX_AGE); - - String uiLocales = getQueryParamValue(OAuth2Constants.UI_LOCALES_PARAM); - url = UriUtils.stripQueryParam(url, OAuth2Constants.UI_LOCALES_PARAM); - - KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone() - .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) - .queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url)) - .queryParam(OAuth2Constants.STATE, state) - .queryParam("login", "true"); - if(loginHint != null && loginHint.length() > 0){ - redirectUriBuilder.queryParam("login_hint",loginHint); - } - if (idpHint != null && idpHint.length() > 0) { - redirectUriBuilder.queryParam(AdapterConstants.KC_IDP_HINT,idpHint); - } - if (prompt != null && prompt.length() > 0) { - redirectUriBuilder.queryParam(OAuth2Constants.PROMPT, prompt); - } - if (maxAge != null && maxAge.length() > 0) { - redirectUriBuilder.queryParam(OAuth2Constants.MAX_AGE, maxAge); - } - if (uiLocales != null && uiLocales.length() > 0) { - redirectUriBuilder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales); - } - - scope = TokenUtil.attachOIDCScope(scope); - redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, scope); - - return redirectUriBuilder.buildAsString(); - } - - protected int sslRedirectPort() { - return sslRedirectPort; - } - - protected String getStateCode() { - return AdapterUtils.generateId(); - } - - protected AuthChallenge loginRedirect() { - final String state = getStateCode(); - final String redirect = getRedirectUri(state); - if (redirect == null) { - return challenge(403, OIDCAuthenticationError.Reason.NO_REDIRECT_URI, null); - } - return new AuthChallenge() { - - @Override - public int getResponseCode() { - return 0; - } - - @Override - public boolean challenge(HttpFacade exchange) { - tokenStore.saveRequest(); - log.debug("Sending redirect to login page: " + redirect); - exchange.getResponse().setStatus(302); - exchange.getResponse().setCookie(deployment.getStateCookieName(), state, "/", null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true); - exchange.getResponse().setHeader("Location", redirect); - return true; - } - }; - } - - protected AuthChallenge checkStateCookie() { - OIDCHttpFacade.Cookie stateCookie = getCookie(deployment.getStateCookieName()); - - if (stateCookie == null) { - log.warn("No state cookie"); - return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null); - } - // reset the cookie - log.debug("** reseting application state cookie"); - facade.getResponse().resetCookie(deployment.getStateCookieName(), stateCookie.getPath()); - String stateCookieValue = getCookieValue(deployment.getStateCookieName()); - - String state = getQueryParamValue(OAuth2Constants.STATE); - if (state == null) { - log.warn("state parameter was null"); - return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null); - } - if (!state.equals(stateCookieValue)) { - log.warn("state parameter invalid"); - log.warn("cookie: " + stateCookieValue); - log.warn("queryParam: " + state); - return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null); - } - return null; - - } - - public AuthOutcome authenticate() { - String code = getCode(); - if (code == null) { - log.debug("there was no code"); - String error = getError(); - if (error != null) { - // todo how do we send a response? - log.warn("There was an error: " + error); - challenge = challenge(400, OIDCAuthenticationError.Reason.OAUTH_ERROR, error); - return AuthOutcome.FAILED; - } else { - log.debug("redirecting to auth server"); - challenge = loginRedirect(); - return AuthOutcome.NOT_ATTEMPTED; - } - } else { - log.debug("there was a code, resolving"); - challenge = resolveCode(code); - if (challenge != null) { - return AuthOutcome.FAILED; - } - return AuthOutcome.AUTHENTICATED; - } - - } - - protected AuthChallenge challenge(final int code, final OIDCAuthenticationError.Reason reason, final String description) { - return new AuthChallenge() { - @Override - public int getResponseCode() { - return code; - } - - @Override - public boolean challenge(HttpFacade exchange) { - OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description); - exchange.getRequest().setError(error); - exchange.getResponse().sendError(code); - return true; - } - }; - } - - /** - * Start or continue the oauth login process. - *

- * if code query parameter is not present, then browser is redirected to authUrl. The redirect URL will be - * the URL of the current request. - *

- * If code query parameter is present, then an access token is obtained by invoking a secure request to the codeUrl. - * If the access token is obtained, the browser is again redirected to the current request URL, but any OAuth - * protocol specific query parameters are removed. - * - * @return null if an access token was obtained, otherwise a challenge is returned - */ - protected AuthChallenge resolveCode(String code) { - // abort if not HTTPS - if (!isRequestSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - log.error("Adapter requires SSL. Request: " + facade.getRequest().getURI()); - return challenge(403, OIDCAuthenticationError.Reason.SSL_REQUIRED, null); - } - - log.debug("checking state cookie for after code"); - AuthChallenge challenge = checkStateCookie(); - if (challenge != null) return challenge; - - AccessTokenResponse tokenResponse = null; - strippedOauthParametersRequestUri = rewrittenRedirectUri(stripOauthParametersFromRedirect()); - - try { - // For COOKIE store we don't have httpSessionId and single sign-out won't be available - String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null; - tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId); - } catch (ServerRequest.HttpFailure failure) { - log.error("failed to turn code into token"); - log.error("status from server: " + failure.getStatus()); - if (failure.getError() != null && !failure.getError().trim().isEmpty()) { - log.error(" " + failure.getError()); - } - return challenge(403, OIDCAuthenticationError.Reason.CODE_TO_TOKEN_FAILURE, null); - - } catch (IOException e) { - log.error("failed to turn code into token", e); - return challenge(403, OIDCAuthenticationError.Reason.CODE_TO_TOKEN_FAILURE, null); - } - - tokenString = tokenResponse.getToken(); - refreshToken = tokenResponse.getRefreshToken(); - idTokenString = tokenResponse.getIdToken(); - - log.debug("Verifying tokens"); - if (log.isTraceEnabled()) { - logToken("\taccess_token", tokenString); - logToken("\tid_token", idTokenString); - logToken("\trefresh_token", refreshToken); - } - - try { - AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment); - token = tokens.getAccessToken(); - idToken = tokens.getIdToken(); - log.debug("Token Verification succeeded!"); - } catch (VerificationException e) { - log.error("failed verification of token: " + e.getMessage()); - return challenge(403, OIDCAuthenticationError.Reason.INVALID_TOKEN, null); - } - if (tokenResponse.getNotBeforePolicy() > deployment.getNotBefore()) { - deployment.updateNotBefore(tokenResponse.getNotBeforePolicy()); - } - if (token.getIssuedAt() < deployment.getNotBefore()) { - log.error("Stale token"); - return challenge(403, OIDCAuthenticationError.Reason.STALE_TOKEN, null); - } - log.debug("successful authenticated"); - return null; - } - - /** - * strip out unwanted query parameters and redirect so bookmarks don't retain oauth protocol bits - */ - protected String stripOauthParametersFromRedirect() { - KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(facade.getRequest().getURI()) - .replaceQueryParam(OAuth2Constants.CODE, null) - .replaceQueryParam(OAuth2Constants.STATE, null) - .replaceQueryParam(OAuth2Constants.SESSION_STATE, null) - .replaceQueryParam(OAuth2Constants.ISSUER, null); - return builder.buildAsString(); - } - - private String rewrittenRedirectUri(String originalUri) { - Map rewriteRules = deployment.getRedirectRewriteRules(); - if(rewriteRules != null && !rewriteRules.isEmpty()) { - try { - URL url = new URL(originalUri); - Map.Entry rule = rewriteRules.entrySet().iterator().next(); - StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol()); - redirectUriBuilder.append("://"+ url.getAuthority()); - redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue())); - return redirectUriBuilder.toString(); - } catch (MalformedURLException ex) { - log.error("Not a valid request url"); - throw new RuntimeException(ex); - } - } - return originalUri; - } - - private void logToken(String name, String token) { - try { - JWSInput jwsInput = new JWSInput(token); - String wireString = jwsInput.getWireString(); - log.tracef("\t%s: %s", name, wireString.substring(0, wireString.lastIndexOf(".")) + ".signature"); - } catch (JWSInputException e) { - log.errorf(e, "Failed to parse %s: %s", name, token); - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java deleted file mode 100755 index a58a05a87bce..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.keycloak.adapters.spi.AuthenticationError; - -/** - * Object that describes the OIDC error that happened. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCAuthenticationError implements AuthenticationError { - public static enum Reason { - NO_BEARER_TOKEN, - NO_REDIRECT_URI, - INVALID_STATE_COOKIE, - OAUTH_ERROR, - SSL_REQUIRED, - CODE_TO_TOKEN_FAILURE, - INVALID_TOKEN, - STALE_TOKEN, - NO_AUTHORIZATION_HEADER, - NO_QUERY_PARAMETER_ACCESS_TOKEN - } - - private Reason reason; - private String description; - - public OIDCAuthenticationError(Reason reason, String description) { - this.reason = reason; - this.description = description; - } - - public Reason getReason() { - return reason; - } - - public String getDescription() { - return description; - } - - @Override - public String toString() { - return "OIDCAuthenticationError [reason=" + reason + ", description=" + description + "]"; - } - - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCHttpFacade.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCHttpFacade.java deleted file mode 100755 index 6b9aa78fe769..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCHttpFacade.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * Bridge between core adapter and HTTP Engine - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface OIDCHttpFacade extends HttpFacade { - - KeycloakSecurityContext getSecurityContext(); -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OidcKeycloakAccount.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OidcKeycloakAccount.java deleted file mode 100755 index 24dcae2af505..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OidcKeycloakAccount.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.spi.KeycloakAccount; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface OidcKeycloakAccount extends KeycloakAccount { - KeycloakSecurityContext getKeycloakSecurityContext(); -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java deleted file mode 100755 index a4dc295dd058..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import java.security.PublicKey; - -import org.jboss.logging.Logger; -import org.keycloak.TokenVerifier; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.StreamUtil; -import org.keycloak.jose.jwk.JSONWebKeySet; -import org.keycloak.jose.jwk.JWK; -import org.keycloak.jose.jwk.JWKBuilder; -import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; -import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider; -import org.keycloak.representations.JsonWebToken; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.representations.adapters.action.AdminAction; -import org.keycloak.representations.adapters.action.LogoutAction; -import org.keycloak.representations.adapters.action.PushNotBeforeAction; -import org.keycloak.representations.adapters.action.TestAvailabilityAction; -import org.keycloak.util.JsonSerialization; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class PreAuthActionsHandler { - - private static final Logger log = Logger.getLogger(PreAuthActionsHandler.class); - - protected UserSessionManagement userSessionManagement; - protected AdapterDeploymentContext deploymentContext; - protected KeycloakDeployment deployment; - protected HttpFacade facade; - - public PreAuthActionsHandler(UserSessionManagement userSessionManagement, AdapterDeploymentContext deploymentContext, HttpFacade facade) { - this.userSessionManagement = userSessionManagement; - this.deploymentContext = deploymentContext; - this.facade = facade; - } - - protected boolean resolveDeployment() { - deployment = deploymentContext.resolveDeployment(facade); - if (!deployment.isConfigured()) { - log.warn("can't take request, adapter not configured"); - facade.getResponse().sendError(403, "adapter not configured"); - return false; - } - return true; - } - - public boolean handleRequest() { - String requestUri = facade.getRequest().getURI(); - log.debugv("adminRequest {0}", requestUri); - if (preflightCors()) { - return true; - } - if (requestUri.endsWith(AdapterConstants.K_LOGOUT)) { - if (!resolveDeployment()) return true; - handleLogout(); - return true; - } else if (requestUri.endsWith(AdapterConstants.K_PUSH_NOT_BEFORE)) { - if (!resolveDeployment()) return true; - handlePushNotBefore(); - return true; - } else if (requestUri.endsWith(AdapterConstants.K_TEST_AVAILABLE)) { - if (!resolveDeployment()) return true; - handleTestAvailable(); - return true; - } else if (requestUri.endsWith(AdapterConstants.K_JWKS)) { - if (!resolveDeployment()) return true; - handleJwksRequest(); - return true; - } - return false; - } - - public boolean preflightCors() { - // don't need to resolve deployment on cors requests. Just need to know local cors config. - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (!deployment.isCors()) return false; - log.debugv("checkCorsPreflight {0}", facade.getRequest().getURI()); - if (!facade.getRequest().getMethod().equalsIgnoreCase("OPTIONS")) { - return false; - } - String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN); - if (origin == null) { - log.debug("checkCorsPreflight: no origin header"); - return false; - } - log.debug("Preflight request returning"); - facade.getResponse().setStatus(200); - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - String requestMethods = facade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_METHOD); - if (requestMethods != null) { - if (deployment.getCorsAllowedMethods() != null) { - requestMethods = deployment.getCorsAllowedMethods(); - } - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethods); - } - String allowHeaders = facade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_HEADERS); - if (allowHeaders != null) { - if (deployment.getCorsAllowedHeaders() != null) { - allowHeaders = deployment.getCorsAllowedHeaders(); - } - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders); - } - if (deployment.getCorsMaxAge() > -1) { - facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_MAX_AGE, Integer.toString(deployment.getCorsMaxAge())); - } - return true; - } - - protected void handleLogout() { - if (log.isTraceEnabled()) { - log.trace("K_LOGOUT sent"); - } - try { - JWSInput token = verifyAdminRequest(); - if (token == null) { - return; - } - LogoutAction action = JsonSerialization.readValue(token.getContent(), LogoutAction.class); - if (!validateAction(action)) return; - if (action.getAdapterSessionIds() != null) { - userSessionManagement.logoutHttpSessions(action.getAdapterSessionIds()); - } else { - log.debugf("logout of all sessions for application '%s'", action.getResource()); - if (action.getNotBefore() > deployment.getNotBefore()) { - deployment.updateNotBefore(action.getNotBefore()); - } - userSessionManagement.logoutAll(); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - - protected void handlePushNotBefore() { - if (log.isTraceEnabled()) { - log.trace("K_PUSH_NOT_BEFORE sent"); - } - try { - JWSInput token = verifyAdminRequest(); - if (token == null) { - return; - } - PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class); - if (!validateAction(action)) return; - deployment.updateNotBefore(action.getNotBefore()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - protected void handleTestAvailable() { - if (log.isTraceEnabled()) { - log.trace("K_TEST_AVAILABLE sent"); - } - try { - JWSInput token = verifyAdminRequest(); - if (token == null) { - return; - } - TestAvailabilityAction action = JsonSerialization.readValue(token.getContent(), TestAvailabilityAction.class); - validateAction(action); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - protected JWSInput verifyAdminRequest() throws Exception { - if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - log.warn("SSL is required for adapter admin action"); - facade.getResponse().sendError(403, "ssl required"); - return null; - } - String token = StreamUtil.readString(facade.getRequest().getInputStream()); - if (token == null) { - log.warn("admin request failed, no token"); - facade.getResponse().sendError(403, "no token"); - return null; - } - - try { - // Check just signature. Other things checked in validateAction - TokenVerifier tokenVerifier = AdapterTokenVerifier.createVerifier(token, deployment, false, JsonWebToken.class); - tokenVerifier.verify(); - return new JWSInput(token); - } catch (VerificationException ignore) { - log.warn("admin request failed, unable to verify token: " + ignore.getMessage()); - if (log.isDebugEnabled()) { - log.debug(ignore.getMessage(), ignore); - } - - facade.getResponse().sendError(403, "token failed verification"); - return null; - } - } - - - protected boolean validateAction(AdminAction action) { - if (!action.validate()) { - log.warn("admin request failed, not validated" + action.getAction()); - facade.getResponse().sendError(400, "Not validated"); - return false; - } - if (action.isExpired()) { - log.warn("admin request failed, expired token"); - facade.getResponse().sendError(400, "Expired token"); - return false; - } - if (!deployment.getResourceName().equals(action.getResource())) { - log.warn("Resource name does not match"); - facade.getResponse().sendError(400, "Resource name does not match"); - return false; - - } - return true; - } - - protected void handleJwksRequest() { - try { - JSONWebKeySet jwks = new JSONWebKeySet(); - ClientCredentialsProvider clientCredentialsProvider = deployment.getClientAuthenticator(); - - // For now, just get signature key from JWT provider. We can add more if we support encryption etc. - if (clientCredentialsProvider instanceof JWTClientCredentialsProvider) { - PublicKey publicKey = ((JWTClientCredentialsProvider) clientCredentialsProvider).getPublicKey(); - JWK jwk = JWKBuilder.create().rs256(publicKey); - jwks.setKeys(new JWK[] { jwk }); - } else { - jwks.setKeys(new JWK[] {}); - } - - facade.getResponse().setStatus(200); - facade.getResponse().setHeader("Content-Type", "application/json"); - JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), jwks); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParameterTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParameterTokenRequestAuthenticator.java deleted file mode 100644 index c7446c6f9c6e..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParameterTokenRequestAuthenticator.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Christian Froehlich - * @author Brad Culley - * @author John D. Ament - * @version $Revision: 1 $ - */ -public class QueryParameterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator { - public static final String ACCESS_TOKEN = "access_token"; - protected Logger log = Logger.getLogger(QueryParameterTokenRequestAuthenticator.class); - - public QueryParameterTokenRequestAuthenticator(KeycloakDeployment deployment) { - super(deployment); - } - - public AuthOutcome authenticate(HttpFacade exchange) { - if(!deployment.isOAuthQueryParameterEnabled()) { - return AuthOutcome.NOT_ATTEMPTED; - } - tokenString = null; - tokenString = getAccessTokenFromQueryParameter(exchange); - if (tokenString == null || tokenString.trim().isEmpty()) { - log.debug("Token is not present in query"); - challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_QUERY_PARAMETER_ACCESS_TOKEN, null, null); - return AuthOutcome.NOT_ATTEMPTED; - } - return (authenticateToken(exchange, tokenString)); - } - - String getAccessTokenFromQueryParameter(HttpFacade exchange) { - try { - if (exchange != null && exchange.getRequest() != null) { - return exchange.getRequest().getQueryParamValue(ACCESS_TOKEN); - } - } catch (Exception ignore) { - } - return null; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java deleted file mode 100755 index c3b2e7155b9d..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.jboss.logging.Logger; -import org.keycloak.AuthorizationContext; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.Time; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.IDToken; - -import java.io.IOException; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext { - - protected static Logger log = Logger.getLogger(RefreshableKeycloakSecurityContext.class); - - protected transient KeycloakDeployment deployment; - protected transient AdapterTokenStore tokenStore; - protected String refreshToken; - - public RefreshableKeycloakSecurityContext() { - } - - public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, AdapterTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) { - super(tokenString, token, idTokenString, idToken); - this.deployment = deployment; - this.tokenStore = tokenStore; - this.refreshToken = refreshToken; - } - - @Override - public AccessToken getToken() { - refreshExpiredToken(true); - return super.getToken(); - } - - @Override - public String getTokenString() { - refreshExpiredToken(true); - return super.getTokenString(); - } - - @Override - public IDToken getIdToken() { - refreshExpiredToken(true); - return super.getIdToken(); - } - - @Override - public String getIdTokenString() { - refreshExpiredToken(true); - return super.getIdTokenString(); - } - - public String getRefreshToken() { - return refreshToken; - } - - public void logout(KeycloakDeployment deployment) { - try { - ServerRequest.invokeLogout(deployment, refreshToken); - } catch (Exception e) { - log.error("failed to invoke remote logout", e); - } - } - - public boolean isActive() { - return token != null && this.token.isActive() && deployment!=null && this.token.getIssuedAt() >= deployment.getNotBefore(); - } - - public boolean isTokenTimeToLiveSufficient(AccessToken token) { - return token != null && (token.getExpiration() - this.deployment.getTokenMinimumTimeToLive()) > Time.currentTime(); - } - - public KeycloakDeployment getDeployment() { - return deployment; - } - - public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - this.deployment = deployment; - this.tokenStore = tokenStore; - } - - /** - * @param checkActive if true, then we won't send refresh request if current accessToken is still active. - * @return true if accessToken is active or was successfully refreshed - */ - public boolean refreshExpiredToken(boolean checkActive) { - if (checkActive) { - if (log.isTraceEnabled()) { - log.trace("checking whether to refresh."); - } - if (isActive() && isTokenTimeToLiveSufficient(this.token)) return true; - } - - if (this.deployment == null || refreshToken == null) return false; // Might be serialized in HttpSession? - - if (!this.getRealm().equals(this.deployment.getRealm())) { - // this should not happen, but let's check it anyway - return false; - } - - if (log.isTraceEnabled()) { - log.trace("Doing refresh"); - } - - // block requests if the refresh token herein stored is already being used to refresh the token so that subsequent requests - // can use the last refresh token issued by the server. Note that this will only work for deployments using the session store - // and, when running in a cluster, sticky sessions must be used. - // - synchronized (this) { - if (checkActive) { - log.trace("Checking whether token has been refreshed in another thread already."); - if (isActive() && isTokenTimeToLiveSufficient(this.token)) return true; - } - AccessTokenResponse response; - try { - response = ServerRequest.invokeRefresh(deployment, refreshToken); - } catch (IOException e) { - log.error("Refresh token failure", e); - return false; - } catch (ServerRequest.HttpFailure httpFailure) { - final Logger.Level logLevel = httpFailure.getError().contains("Refresh token expired") ? Logger.Level.WARN : Logger.Level.ERROR; - log.log(logLevel, "Refresh token failure status: " + httpFailure.getStatus() + " " + httpFailure.getError()); - return false; - } - if (log.isTraceEnabled()) { - log.trace("received refresh response"); - } - String tokenString = response.getToken(); - AccessToken token = null; - IDToken idToken = null; - try { - AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), deployment); - token = tokens.getAccessToken(); - idToken = tokens.getIdToken(); - log.debug("Token Verification succeeded!"); - } catch (VerificationException e) { - log.error("failed verification of token"); - return false; - } - // If the TTL is greater-or-equal to the expire time on the refreshed token, have to abort or go into an infinite refresh loop - if (!isTokenTimeToLiveSufficient(token)) { - log.error("failed to refresh the token with a longer time-to-live than the minimum"); - return false; - } - if (response.getNotBeforePolicy() > deployment.getNotBefore()) { - deployment.updateNotBefore(response.getNotBeforePolicy()); - } - if (idToken != null) { - this.idToken = idToken; - this.idTokenString = response.getIdToken(); - } - this.token = token; - if (response.getRefreshToken() != null) { - if (log.isTraceEnabled()) { - log.trace("Setup new refresh token to the security context"); - } - this.refreshToken = response.getRefreshToken(); - } - this.tokenString = tokenString; - if (tokenStore != null) { - tokenStore.refreshCallback(this); - } - } - - return true; - } - - public void setAuthorizationContext(AuthorizationContext authorizationContext) { - this.authorizationContext = authorizationContext; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java deleted file mode 100755 index ae71fae43893..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import java.util.Collections; -import java.util.List; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class RequestAuthenticator { - protected static Logger log = Logger.getLogger(RequestAuthenticator.class); - protected HttpFacade facade; - protected AuthChallenge challenge; - - protected KeycloakDeployment deployment; - protected AdapterTokenStore tokenStore; - protected int sslRedirectPort; - - public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) { - this.facade = facade; - this.deployment = deployment; - this.tokenStore = tokenStore; - this.sslRedirectPort = sslRedirectPort; - } - - public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment) { - this.facade = facade; - this.deployment = deployment; - } - - public AuthChallenge getChallenge() { - return challenge; - } - - public AuthOutcome authenticate() { - if (log.isTraceEnabled()) { - log.trace("--> authenticate()"); - } - - BearerTokenRequestAuthenticator bearer = createBearerTokenAuthenticator(); - if (log.isTraceEnabled()) { - log.trace("try bearer"); - } - - AuthOutcome outcome = bearer.authenticate(facade); - if (outcome == AuthOutcome.FAILED) { - challenge = bearer.getChallenge(); - log.debug("Bearer FAILED"); - return AuthOutcome.FAILED; - } else if (outcome == AuthOutcome.AUTHENTICATED) { - if (verifySSL()) return AuthOutcome.FAILED; - completeAuthentication(bearer, "KEYCLOAK"); - log.debug("Bearer AUTHENTICATED"); - return AuthOutcome.AUTHENTICATED; - } - - QueryParameterTokenRequestAuthenticator queryParamAuth = createQueryParameterTokenRequestAuthenticator(); - if (log.isTraceEnabled()) { - log.trace("try query parameter auth"); - } - - outcome = queryParamAuth.authenticate(facade); - if (outcome == AuthOutcome.FAILED) { - challenge = queryParamAuth.getChallenge(); - log.debug("QueryParamAuth auth FAILED"); - return AuthOutcome.FAILED; - } else if (outcome == AuthOutcome.AUTHENTICATED) { - if (verifySSL()) return AuthOutcome.FAILED; - log.debug("QueryParamAuth AUTHENTICATED"); - completeAuthentication(queryParamAuth, "KEYCLOAK"); - return AuthOutcome.AUTHENTICATED; - } - - if (deployment.isEnableBasicAuth()) { - BasicAuthRequestAuthenticator basicAuth = createBasicAuthAuthenticator(); - if (log.isTraceEnabled()) { - log.trace("try basic auth"); - } - - outcome = basicAuth.authenticate(facade); - if (outcome == AuthOutcome.FAILED) { - challenge = basicAuth.getChallenge(); - log.debug("BasicAuth FAILED"); - return AuthOutcome.FAILED; - } else if (outcome == AuthOutcome.AUTHENTICATED) { - if (verifySSL()) return AuthOutcome.FAILED; - log.debug("BasicAuth AUTHENTICATED"); - completeAuthentication(basicAuth, "BASIC"); - return AuthOutcome.AUTHENTICATED; - } - } - - if (deployment.isBearerOnly()) { - challenge = bearer.getChallenge(); - log.debug("NOT_ATTEMPTED: bearer only"); - return AuthOutcome.NOT_ATTEMPTED; - } - - if (isAutodetectedBearerOnly(facade.getRequest())) { - challenge = bearer.getChallenge(); - log.debug("NOT_ATTEMPTED: Treating as bearer only"); - return AuthOutcome.NOT_ATTEMPTED; - } - - if (log.isTraceEnabled()) { - log.trace("try oauth"); - } - - if (tokenStore.isCached(this)) { - if (verifySSL()) return AuthOutcome.FAILED; - log.debug("AUTHENTICATED: was cached"); - return AuthOutcome.AUTHENTICATED; - } - - OAuthRequestAuthenticator oauth = createOAuthAuthenticator(); - outcome = oauth.authenticate(); - if (outcome == AuthOutcome.FAILED) { - challenge = oauth.getChallenge(); - return AuthOutcome.FAILED; - } else if (outcome == AuthOutcome.NOT_ATTEMPTED) { - challenge = oauth.getChallenge(); - return AuthOutcome.NOT_ATTEMPTED; - - } - - if (verifySSL()) return AuthOutcome.FAILED; - - completeAuthentication(oauth); - - // redirect to strip out access code and state query parameters - facade.getResponse().setHeader("Location", oauth.getStrippedOauthParametersRequestUri()); - facade.getResponse().setStatus(302); - facade.getResponse().end(); - - log.debug("AUTHENTICATED"); - return AuthOutcome.AUTHENTICATED; - } - - protected boolean verifySSL() { - if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - log.warnf("SSL is required to authenticate. Remote address %s is secure: %s, SSL required for: %s .", - facade.getRequest().getRemoteAddr(), facade.getRequest().isSecure(), deployment.getSslRequired().name()); - return true; - } - return false; - } - - protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) { - if (!deployment.isAutodetectBearerOnly()) return false; - - String headerValue = facade.getRequest().getHeader("X-Requested-With"); - if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) { - return true; - } - - headerValue = facade.getRequest().getHeader("Faces-Request"); - if (headerValue != null && headerValue.startsWith("partial/")) { - return true; - } - - headerValue = facade.getRequest().getHeader("SOAPAction"); - if (headerValue != null) { - return true; - } - - List accepts = facade.getRequest().getHeaders("Accept"); - if (accepts == null) accepts = Collections.emptyList(); - - for (String accept : accepts) { - if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) { - return false; - } - } - - return true; - } - - protected abstract OAuthRequestAuthenticator createOAuthAuthenticator(); - - protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() { - return new BearerTokenRequestAuthenticator(deployment); - } - - protected BasicAuthRequestAuthenticator createBasicAuthAuthenticator() { - return new BasicAuthRequestAuthenticator(deployment); - } - - protected QueryParameterTokenRequestAuthenticator createQueryParameterTokenRequestAuthenticator() { - return new QueryParameterTokenRequestAuthenticator(deployment); - } - - protected void completeAuthentication(OAuthRequestAuthenticator oauth) { - RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, tokenStore, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken()); - final KeycloakPrincipal principal = new KeycloakPrincipal<>(AdapterUtils.getPrincipalName(deployment, oauth.getToken()), session); - completeOAuthAuthentication(principal); - log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); - } - - protected abstract void completeOAuthAuthentication(KeycloakPrincipal principal); - - protected abstract void completeBearerAuthentication(KeycloakPrincipal principal, String method); - - /** - * After code is received, we change the session id if possible to guard against https://www.owasp.org/index.php/Session_Fixation - * - * @param create - * @return - */ - protected abstract String changeHttpSessionId(boolean create); - - protected void completeAuthentication(BearerTokenRequestAuthenticator bearer, String method) { - RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); - final KeycloakPrincipal principal = new KeycloakPrincipal<>(AdapterUtils.getPrincipalName(deployment, bearer.getToken()), session); - completeBearerAuthentication(principal, method); - log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java deleted file mode 100755 index 24f7aa73f90f..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; -import org.keycloak.OAuth2Constants; -import org.keycloak.common.util.HostUtils; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.common.util.StreamUtil; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.util.JsonSerialization; - -import org.jboss.logging.Logger; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServerRequest { - - private static Logger logger = Logger.getLogger(ServerRequest.class); - - public static class HttpFailure extends Exception { - private int status; - private String error; - - public HttpFailure(int status, String error) { - this.status = status; - this.error = error; - } - - public int getStatus() { - return status; - } - - public String getError() { - return error; - } - } - - public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { - HttpClient client = deployment.getClient(); - URI uri = deployment.getLogoutUrl().clone().build(); - List formparams = new ArrayList<>(); - - formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); - HttpPost post = new HttpPost(uri); - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - HttpResponse response = client.execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 204) { - error(status, entity); - } - if (entity == null) { - return; - } - InputStream is = entity.getContent(); - if (is != null) is.close(); - } - - public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws IOException, HttpFailure { - List formparams = new ArrayList<>(); - redirectUri = stripOauthParametersFromRedirect(redirectUri); - formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code")); - formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); - formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); - if (sessionId != null) { - formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId)); - formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName())); - } - - HttpPost post = new HttpPost(deployment.getTokenUrl()); - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - HttpResponse response = deployment.getClient().execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 200) { - error(status, entity); - } - if (entity == null) { - throw new HttpFailure(status, null); - } - InputStream is = entity.getContent(); - try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int c; - while ((c = is.read()) != -1) { - os.write(c); - } - byte[] bytes = os.toByteArray(); - String json = new String(bytes); - try { - return JsonSerialization.readValue(json, AccessTokenResponse.class); - } catch (IOException e) { - throw new IOException(json, e); - } - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } - - // https://tools.ietf.org/html/rfc7636#section-4 - public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId, String codeVerifier) throws IOException, HttpFailure { - List formparams = new ArrayList<>(); - redirectUri = stripOauthParametersFromRedirect(redirectUri); - formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code")); - formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); - formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); - if (sessionId != null) { - formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId)); - formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName())); - } - // https://tools.ietf.org/html/rfc7636#section-4 - if (codeVerifier != null) { - logger.debugf("add to POST parameters of Token Request, codeVerifier = %s", codeVerifier); - formparams.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier)); - } else { - logger.debug("add to POST parameters of Token Request without codeVerifier"); - } - - HttpPost post = new HttpPost(deployment.getTokenUrl()); - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - HttpResponse response = deployment.getClient().execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 200) { - error(status, entity); - } - if (entity == null) { - throw new HttpFailure(status, null); - } - InputStream is = entity.getContent(); - try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int c; - while ((c = is.read()) != -1) { - os.write(c); - } - byte[] bytes = os.toByteArray(); - String json = new String(bytes); - try { - return JsonSerialization.readValue(json, AccessTokenResponse.class); - } catch (IOException e) { - throw new IOException(json, e); - } - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } - - public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure { - List formparams = new ArrayList(); - formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); - formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); - - HttpPost post = new HttpPost(deployment.getTokenUrl()); - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - HttpResponse response = deployment.getClient().execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 200) { - error(status, entity); - } - if (entity == null) { - throw new HttpFailure(status, null); - } - InputStream is = entity.getContent(); - try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int c; - while ((c = is.read()) != -1) { - os.write(c); - } - byte[] bytes = os.toByteArray(); - String json = new String(bytes); - try { - return JsonSerialization.readValue(json, AccessTokenResponse.class); - } catch (IOException e) { - throw new IOException(json, e); - } - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } - - public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { - String registerNodeUrl = deployment.getRegisterNodeUrl(); - invokeClientManagementRequest(deployment, host, registerNodeUrl); - } - - public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { - String unregisterNodeUrl = deployment.getUnregisterNodeUrl(); - invokeClientManagementRequest(deployment, host, unregisterNodeUrl); - } - - public static void invokeClientManagementRequest(KeycloakDeployment deployment, String host, String endpointUrl) throws HttpFailure, IOException { - if (endpointUrl == null) { - throw new IOException("You need to configure URI for register/unregister node for application " + deployment.getResourceName()); - } - - List formparams = new ArrayList<>(); - formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host)); - - HttpPost post = new HttpPost(endpointUrl); - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - HttpResponse response = deployment.getClient().execute(post); - int status = response.getStatusLine().getStatusCode(); - if (status != 204) { - HttpEntity entity = response.getEntity(); - error(status, entity); - } - } - - public static void error(int status, HttpEntity entity) throws HttpFailure, IOException { - String body = null; - if (entity != null) { - InputStream is = entity.getContent(); - try { - body = StreamUtil.readString(is); - } catch (IOException e) { - - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } - throw new HttpFailure(status, body); - } - - protected static String stripOauthParametersFromRedirect(String uri) { - KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(uri) - .replaceQueryParam(OAuth2Constants.CODE, null) - .replaceQueryParam(OAuth2Constants.STATE, null) - .replaceQueryParam(OAuth2Constants.ISSUER, null); - return builder.buildAsString(); - } - - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/SniSSLSocketFactory.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/SniSSLSocketFactory.java deleted file mode 100644 index 89b952e7e99a..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/SniSSLSocketFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.HttpHost; -import org.apache.http.conn.scheme.HostNameResolver; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.conn.ssl.X509HostnameVerifier; -import org.apache.http.protocol.HttpContext; -import org.keycloak.common.util.Environment; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.security.AccessController; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.security.SecureRandom; -import java.security.UnrecoverableKeyException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author Marko Strukelj - */ -public class SniSSLSocketFactory extends SSLSocketFactory { - - private static final Logger LOG = Logger.getLogger(SniSSLSocketFactory.class.getName()); - private static final AtomicBoolean skipSNIApplication = new AtomicBoolean(false); - - public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, HostNameResolver nameResolver) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(algorithm, keystore, keyPassword, truststore, random, nameResolver); - } - - public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(algorithm, keystore, keyPassword, truststore, random, trustStrategy, hostnameVerifier); - } - - public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(algorithm, keystore, keyPassword, truststore, random, hostnameVerifier); - } - - public SniSSLSocketFactory(KeyStore keystore, String keystorePassword, KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(keystore, keystorePassword, truststore); - } - - public SniSSLSocketFactory(KeyStore keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(keystore, keystorePassword); - } - - public SniSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(truststore); - } - - public SniSSLSocketFactory(TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(trustStrategy, hostnameVerifier); - } - - public SniSSLSocketFactory(TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - super(trustStrategy); - } - - public SniSSLSocketFactory(SSLContext sslContext) { - super(sslContext); - } - - public SniSSLSocketFactory(SSLContext sslContext, HostNameResolver nameResolver) { - super(sslContext, nameResolver); - } - - public SniSSLSocketFactory(SSLContext sslContext, X509HostnameVerifier hostnameVerifier) { - super(sslContext, hostnameVerifier); - } - - public SniSSLSocketFactory(SSLContext sslContext, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) { - super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); - } - - public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) { - super(socketfactory, hostnameVerifier); - } - - public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) { - super(socketfactory, supportedProtocols, supportedCipherSuites, hostnameVerifier); - } - - @Override - public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException { - return super.connectSocket(connectTimeout, applySNI(socket, host.getHostName()), host, remoteAddress, localAddress, context); - } - - @Override - public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException { - return super.createLayeredSocket(applySNI(socket, target), target, port, context); - } - - private Socket applySNI(final Socket socket, String hostname) { - if (skipSNIApplication.get()) { - LOG.log(Level.FINE, "Skipping application of SNI because JDK is missing setHost() method."); - return socket; - } - - if (socket instanceof SSLSocket) { - try { - Method setHostMethod = AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Method run() throws NoSuchMethodException { - return socket.getClass().getMethod("setHost", String.class); - } - }); - - setHostMethod.invoke(socket, hostname); - LOG.log(Level.FINE, "Applied SNI to socket for host {0}", hostname); - } catch (PrivilegedActionException e) { - if (e.getCause() instanceof NoSuchMethodException) { - // For IBM java there is no method with name setHost(), however we don't need to applySNI - // because IBM java is doing it automatically, so we can set lower level of this message - // See: KEYCLOAK-6817 - Level logLevel = Environment.IS_IBM_JAVA ? Level.FINE : Level.WARNING; - LOG.log(logLevel, "Failed to apply SNI to SSLSocket", e); - skipSNIApplication.set(true); - } else { - LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e); - } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e); - } - } - return socket; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java deleted file mode 100755 index 2a8dc151637e..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2016 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.adapters.jaas; - -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.FindFile; -import org.keycloak.common.util.reflections.Reflections; -import org.keycloak.representations.AccessToken; - -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.LoginException; -import javax.security.auth.spi.LoginModule; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.Principal; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * @author Marek Posolda - */ -public abstract class AbstractKeycloakLoginModule implements LoginModule { - - public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file"; - public static final String ROLE_PRINCIPAL_CLASS_OPTION = "role-principal-class"; - public static final String PROFILE_RESOURCE = "profile:"; - protected Subject subject; - protected CallbackHandler callbackHandler; - protected Auth auth; - protected KeycloakDeployment deployment; - protected String rolePrincipalClass; - - // This is to avoid parsing keycloak.json file in each request. Key is file location, Value is parsed keycloak deployment - private static ConcurrentMap deployments = new ConcurrentHashMap<>(); - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { - this.subject = subject; - this.callbackHandler = callbackHandler; - - String configFile = (String)options.get(KEYCLOAK_CONFIG_FILE_OPTION); - rolePrincipalClass = (String)options.get(ROLE_PRINCIPAL_CLASS_OPTION); - getLogger().debug("Declared options: " + KEYCLOAK_CONFIG_FILE_OPTION + "=" + configFile + ", " + ROLE_PRINCIPAL_CLASS_OPTION + "=" + rolePrincipalClass); - - if (configFile != null) { - deployment = deployments.get(configFile); - if (deployment == null) { - // lazy init of our deployment - deployment = resolveDeployment(configFile); - deployments.putIfAbsent(configFile, deployment); - } - } - } - - protected KeycloakDeployment resolveDeployment(String keycloakConfigFile) { - try { - InputStream is = null; - if (keycloakConfigFile.startsWith(PROFILE_RESOURCE)) { - try { - is = new URL(keycloakConfigFile).openStream(); - } catch (MalformedURLException mfue) { - throw new RuntimeException(mfue); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } else { - is = FindFile.findFile(keycloakConfigFile); - } - KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is); - return kd; - - } catch (RuntimeException e) { - getLogger().debug("Unable to find or parse file " + keycloakConfigFile + " due to " + e.getMessage(), e); - throw e; - } - } - - - @Override - public boolean login() throws LoginException { - // get username and password - Callback[] callbacks = new Callback[2]; - callbacks[0] = new NameCallback("username"); - callbacks[1] = new PasswordCallback("password", false); - - try { - callbackHandler.handle(callbacks); - String username = ((NameCallback) callbacks[0]).getName(); - char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword(); - String password = new String(tmpPassword); - ((PasswordCallback) callbacks[1]).clearPassword(); - - Auth auth = doAuth(username, password); - if (auth != null) { - this.auth = auth; - return true; - } else { - return false; - } - } catch (UnsupportedCallbackException uce) { - getLogger().warn("Error: " + uce.getCallback().toString() - + " not available to gather authentication information from the user"); - return false; - } catch (Exception e) { - LoginException le = new LoginException(e.toString()); - le.initCause(e); - throw le; - } - } - - - @Override - public boolean commit() throws LoginException { - if (auth == null) { - return false; - } - - this.subject.getPrincipals().add(auth.getPrincipal()); - this.subject.getPrivateCredentials().add(auth.getTokenString()); - if (auth.getRoles() != null) { - for (String roleName : auth.getRoles()) { - Principal rolePrinc = createRolePrincipal(roleName); - this.subject.getPrincipals().add(rolePrinc); - } - } - - return true; - } - - - protected Principal createRolePrincipal(String roleName) { - if (rolePrincipalClass != null && rolePrincipalClass.length() > 0) { - try { - Class clazz = Reflections.classForName(rolePrincipalClass, getClass().getClassLoader()); - Constructor constructor = clazz.getDeclaredConstructor(String.class); - return constructor.newInstance(roleName); - } catch (Exception e) { - getLogger().warn("Unable to create declared roleClass " + rolePrincipalClass + " due to " + e.getMessage()); - } - } - - // Fallback to default rolePrincipal class - return new RolePrincipal(roleName); - } - - - @Override - public boolean abort() throws LoginException { - return true; - } - - @Override - public boolean logout() throws LoginException { - Set principals = new HashSet(subject.getPrincipals()); - for (Principal principal : principals) { - if (principal.getClass().equals(KeycloakPrincipal.class) || principal.getClass().equals(RolePrincipal.class)) { - subject.getPrincipals().remove(principal); - } - } - Set creds = subject.getPrivateCredentials(); - for (Object cred : creds) { - subject.getPrivateCredentials().remove(cred); - } - subject = null; - callbackHandler = null; - return true; - } - - - protected Auth bearerAuth(String tokenString) throws VerificationException { - AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment); - return postTokenVerification(tokenString, token); - } - - - /** - * Called after accessToken was verified (including signature, expiration etc) - * - */ - protected Auth postTokenVerification(String tokenString, AccessToken token) { - boolean verifyCaller; - if (deployment.isUseResourceRoleMappings()) { - verifyCaller = token.isVerifyCaller(deployment.getResourceName()); - } else { - verifyCaller = token.isVerifyCaller(); - } - if (verifyCaller) { - throw new IllegalStateException("VerifyCaller not supported yet in login module"); - } - - RefreshableKeycloakSecurityContext skSession = new RefreshableKeycloakSecurityContext(deployment, null, tokenString, token, null, null, null); - String principalName = AdapterUtils.getPrincipalName(deployment, token); - final KeycloakPrincipal principal = new KeycloakPrincipal(principalName, skSession); - final Set roles = AdapterUtils.getRolesFromSecurityContext(skSession); - return new Auth(principal, roles, tokenString); - } - - - protected abstract Auth doAuth(String username, String password) throws Exception; - - protected abstract Logger getLogger(); - - - public static class Auth { - private final KeycloakPrincipal principal; - private final Set roles; - private final String tokenString; - - public Auth(KeycloakPrincipal principal, Set roles, String accessToken) { - this.principal = principal; - this.roles = roles; - this.tokenString = accessToken; - } - - public KeycloakPrincipal getPrincipal() { - return principal; - } - - public Set getRoles() { - return roles; - } - - public String getTokenString() { - return tokenString; - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/BearerTokenLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/BearerTokenLoginModule.java deleted file mode 100755 index f9a72f299358..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/BearerTokenLoginModule.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.adapters.jaas; - -import org.jboss.logging.Logger; -import org.keycloak.common.VerificationException; - -/** - * Login module, which allows to authenticate Keycloak access token in environments, which rely on JAAS - *

- * It expects login based on username and password where username doesn't matter and password is keycloak access token. - * - * @author Marek Posolda - */ -public class BearerTokenLoginModule extends AbstractKeycloakLoginModule { - - private static final Logger log = Logger.getLogger(BearerTokenLoginModule.class); - - @Override - protected Auth doAuth(String username, String password) throws VerificationException { - // Should do some checking of authenticated username if it's equivalent to passed value? - return bearerAuth(password); - } - - @Override - protected Logger getLogger() { - return log; - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java deleted file mode 100755 index 945a2606d43b..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2016 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.adapters.jaas; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; -import org.jboss.logging.Logger; -import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.common.VerificationException; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.idm.OAuth2ErrorRepresentation; -import org.keycloak.util.JsonSerialization; - -import javax.security.auth.Subject; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.login.LoginException; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.net.URI; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Login module based on Resource Owner password credentials grant from OAuth2 specs. It's supposed to be used in environments. which - * can't rely on HTTP (like SSH authentication for instance). It needs that Direct Grant is enabled on particular realm in Keycloak. - * - * @author Marek Posolda - */ -public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule { - - private static final Logger log = Logger.getLogger(DirectAccessGrantsLoginModule.class); - - public static final String SCOPE_OPTION = "scope"; - - private String refreshToken; - private String scope; - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { - super.initialize(subject, callbackHandler, sharedState, options); - this.scope = (String)options.get(SCOPE_OPTION); - - // This is used just for logout - Iterator iterator = subject.getPrivateCredentials(RefreshTokenHolder.class).iterator(); - if (iterator.hasNext()) { - refreshToken = iterator.next().refreshToken; - } - } - - @Override - protected Auth doAuth(String username, String password) throws IOException, VerificationException { - return directGrantAuth(username, password); - } - - @Override - protected Logger getLogger() { - return log; - } - - protected Auth directGrantAuth(String username, String password) throws IOException, VerificationException { - String authServerBaseUrl = deployment.getAuthServerBaseUrl(); - HttpPost post = new HttpPost(deployment.getTokenUrl()); - List formparams = new ArrayList(); - formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); - formparams.add(new BasicNameValuePair("username", username)); - formparams.add(new BasicNameValuePair("password", password)); - - if (scope != null) { - formparams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope)); - } - - AdapterUtils.setClientCredentials(deployment, post, formparams); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - - HttpClient client = deployment.getClient(); - HttpResponse response = client.execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 200) { - StringBuilder errorBuilder = new StringBuilder("Login failed. Invalid status: " + status); - if (entity != null) { - InputStream is = entity.getContent(); - OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class); - errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError()) - .append(", Error description: " + errorRep.getErrorDescription()); - } - String error = errorBuilder.toString(); - log.warn(error); - throw new IOException(error); - } - - if (entity == null) { - throw new IOException("No Entity"); - } - - InputStream is = entity.getContent(); - AccessTokenResponse tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class); - - // refreshToken will be saved to privateCreds of Subject for now - refreshToken = tokenResponse.getRefreshToken(); - - AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenResponse.getToken(), tokenResponse.getIdToken(), deployment); - return postTokenVerification(tokenResponse.getToken(), tokens.getAccessToken()); - } - - @Override - public boolean commit() throws LoginException { - boolean superCommit = super.commit(); - - // refreshToken will be saved to privateCreds of Subject for now - if (refreshToken != null) { - RefreshTokenHolder refreshTokenHolder = new RefreshTokenHolder(); - refreshTokenHolder.refreshToken = refreshToken; - subject.getPrivateCredentials().add(refreshTokenHolder); - } - - return superCommit; - } - - @Override - public boolean logout() throws LoginException { - if (refreshToken != null) { - try { - URI logoutUri = deployment.getLogoutUrl().clone().build(); - HttpPost post = new HttpPost(logoutUri); - - List formparams = new ArrayList<>(); - AdapterUtils.setClientCredentials(deployment, post, formparams); - formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); - - UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(form); - - HttpClient client = deployment.getClient(); - HttpResponse response = client.execute(post); - int status = response.getStatusLine().getStatusCode(); - HttpEntity entity = response.getEntity(); - if (status != 204) { - StringBuilder errorBuilder = new StringBuilder("Logout of refreshToken failed. Invalid status: " + status); - if (entity != null) { - InputStream is = entity.getContent(); - if (status == 400) { - OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class); - errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError()) - .append(", Error description: " + errorRep.getErrorDescription()); - - } else { - if (is != null) is.close(); - } - } - - // Should do something better than warn if logout failed? Perhaps update of refresh tokens on existing subject might be supported too... - log.warn(errorBuilder.toString()); - } - } catch (IOException ioe) { - log.warn(ioe); - } - } - - return super.logout(); - } - - private static class RefreshTokenHolder implements Serializable { - private String refreshToken; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/RolePrincipal.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/RolePrincipal.java deleted file mode 100755 index 60890ffb9a45..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/RolePrincipal.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016 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.adapters.jaas; - -import java.io.Serializable; -import java.security.Principal; - -/** - * @author Marek Posolda - */ -public class RolePrincipal implements Principal, Serializable { - - private String roleName = null; - - public RolePrincipal(String roleName) { - this.roleName = roleName; - } - - public boolean equals (Object p) { - if (! (p instanceof RolePrincipal)) - return false; - return getName().equals(((RolePrincipal)p).getName()); - } - - public int hashCode () { - return getName().hashCode(); - } - - public String getName () { - return this.roleName; - } - - public String toString () - { - return getName(); - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java deleted file mode 100644 index 54c9d119df95..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.adapters.pep; - -import java.io.InputStream; -import java.util.List; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.authorization.TokenPrincipal; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.adapters.spi.HttpFacade.Cookie; -import org.keycloak.representations.AccessToken; - -/** - * @author Pedro Igor - */ -public class HttpAuthzRequest implements HttpRequest { - - private final TokenPrincipal tokenPrincipal; - private final OIDCHttpFacade oidcFacade; - - public HttpAuthzRequest(OIDCHttpFacade oidcFacade) { - this.oidcFacade = oidcFacade; - tokenPrincipal = new TokenPrincipal() { - @Override - public String getRawToken() { - KeycloakSecurityContext securityContext = oidcFacade.getSecurityContext(); - - if (securityContext == null) { - return null; - } - - return oidcFacade.getSecurityContext().getTokenString(); - } - - @Override - public AccessToken getToken() { - KeycloakSecurityContext securityContext = oidcFacade.getSecurityContext(); - - if (securityContext == null) { - return null; - } - - return securityContext.getToken(); - } - }; - } - - @Override - public String getRelativePath() { - return oidcFacade.getRequest().getRelativePath(); - } - - @Override - public String getMethod() { - return oidcFacade.getRequest().getMethod(); - } - - @Override - public String getURI() { - return oidcFacade.getRequest().getURI(); - } - - @Override - public List getHeaders(String name) { - return oidcFacade.getRequest().getHeaders(name); - } - - @Override - public String getFirstParam(String name) { - String queryParamValue = oidcFacade.getRequest().getQueryParamValue(name); - - if (queryParamValue != null) { - return queryParamValue; - } - - return oidcFacade.getRequest().getFirstParam(name); - } - - @Override - public String getCookieValue(String name) { - Cookie cookie = oidcFacade.getRequest().getCookie(name); - - if (cookie == null) { - return null; - } - - return cookie.getValue(); - } - - @Override - public String getRemoteAddr() { - return oidcFacade.getRequest().getRemoteAddr(); - } - - @Override - public boolean isSecure() { - return oidcFacade.getRequest().isSecure(); - } - - @Override - public String getHeader(String name) { - return oidcFacade.getRequest().getHeader(name); - } - - @Override - public InputStream getInputStream(boolean buffered) { - return oidcFacade.getRequest().getInputStream(buffered); - } - - @Override - public TokenPrincipal getPrincipal() { - return tokenPrincipal; - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java deleted file mode 100644 index e14d74bc0884..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.adapters.pep; - -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.authorization.spi.HttpResponse; - -/** - * @author Pedro Igor - */ -public class HttpAuthzResponse implements HttpResponse { - - private OIDCHttpFacade oidcFacade; - - public HttpAuthzResponse(OIDCHttpFacade oidcFacade) { - this.oidcFacade = oidcFacade; - } - - @Override - public void sendError(int statusCode) { - oidcFacade.getResponse().setStatus(statusCode); - } - - @Override - public void sendError(int code, String reason) { - oidcFacade.getResponse().sendError(code, reason); - } - - @Override - public void setHeader(String name, String value) { - oidcFacade.getResponse().setHeader(name, value); - } - -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java deleted file mode 100644 index d989492f81ef..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterTokenVerifier.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016 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.adapters.rotation; - -import org.jboss.logging.Logger; -import org.keycloak.TokenVerifier; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.common.VerificationException; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.IDToken; -import org.keycloak.representations.JsonWebToken; - -import java.security.PublicKey; - -/** - * @author Marek Posolda - */ -public class AdapterTokenVerifier { - - private static final Logger log = Logger.getLogger(AdapterTokenVerifier.class); - - - /** - * Verifies bearer token. Typically called when bearer token (access token) is sent to the service, which wants to verify it. Hence it also checks the audience in the token. - * - * @param tokenString - * @param deployment - * @return - * @throws VerificationException - */ - public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment) throws VerificationException { - TokenVerifier tokenVerifier = createVerifier(tokenString, deployment, true, AccessToken.class); - - // Verify audience of bearer-token - if (deployment.isVerifyTokenAudience()) { - tokenVerifier.audience(deployment.getResourceName()); - } - - return tokenVerifier.verify().getToken(); - } - - - /** - * Verify access token and ID token. Typically called after successful tokenResponse is received from Keycloak - * - * @param accessTokenString - * @param idTokenString - * @param deployment - * @return verified and parsed accessToken and idToken - * @throws VerificationException - */ - public static VerifiedTokens verifyTokens(String accessTokenString, String idTokenString, KeycloakDeployment deployment) throws VerificationException { - // Adapters currently do most of the checks including signature etc on the access token - TokenVerifier tokenVerifier = createVerifier(accessTokenString, deployment, true, AccessToken.class); - AccessToken accessToken = tokenVerifier.verify().getToken(); - - if (idTokenString != null) { - // Don't verify signature again on IDToken - IDToken idToken = TokenVerifier.create(idTokenString, IDToken.class).getToken(); - TokenVerifier idTokenVerifier = TokenVerifier.createWithoutSignature(idToken); - - // Always verify audience and azp on IDToken - idTokenVerifier.audience(deployment.getResourceName()); - idTokenVerifier.issuedFor(deployment.getResourceName()); - - idTokenVerifier.verify(); - return new VerifiedTokens(accessToken, idToken); - } else { - return new VerifiedTokens(accessToken, null); - } - } - - - /** - * Creates verifier, initializes it from the KeycloakDeployment and adds the publicKey and some default basic checks (activeness and tokenType). Useful if caller wants to add/remove/update - * some checks - * - * @param tokenString - * @param deployment - * @param withDefaultChecks - * @param tokenClass - * @param - * @return tokenVerifier - * @throws VerificationException - */ - public static TokenVerifier createVerifier(String tokenString, KeycloakDeployment deployment, boolean withDefaultChecks, Class tokenClass) throws VerificationException { - TokenVerifier tokenVerifier = TokenVerifier.create(tokenString, tokenClass); - - if (withDefaultChecks) { - tokenVerifier - .withDefaultChecks() - .realmUrl(deployment.getRealmInfoUrl()); - } - - String kid = tokenVerifier.getHeader().getKeyId(); - PublicKey publicKey = getPublicKey(kid, deployment); - tokenVerifier.publicKey(publicKey); - - return tokenVerifier; - } - - - private static PublicKey getPublicKey(String kid, KeycloakDeployment deployment) throws VerificationException { - PublicKeyLocator pkLocator = deployment.getPublicKeyLocator(); - - PublicKey publicKey = pkLocator.getPublicKey(kid, deployment); - if (publicKey == null) { - log.errorf("Didn't find publicKey for kid: %s", kid); - throw new VerificationException("Didn't find publicKey for specified kid"); - } - - return publicKey; - } - - - public static class VerifiedTokens { - - private final AccessToken accessToken; - private final IDToken idToken; - - public VerifiedTokens(AccessToken accessToken, IDToken idToken) { - this.accessToken = accessToken; - this.idToken = idToken; - } - - - public AccessToken getAccessToken() { - return accessToken; - } - - public IDToken getIdToken() { - return idToken; - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java deleted file mode 100644 index feb6e48449a8..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.adapters.rotation; - -import org.keycloak.adapters.KeycloakDeployment; - -import java.security.PublicKey; - -/** - * @author Marek Posolda - */ -public class HardcodedPublicKeyLocator implements PublicKeyLocator { - - private PublicKey publicKey; - - public HardcodedPublicKeyLocator(PublicKey publicKey) { - this.publicKey = publicKey; - } - - @Override - public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) { - return publicKey; - } - - @Override - public void reset(KeycloakDeployment deployment) { - - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java deleted file mode 100644 index b94ed2787f94..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016 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.adapters.rotation; - -import org.apache.http.client.methods.HttpGet; -import org.jboss.logging.Logger; -import org.keycloak.adapters.HttpAdapterUtils; -import org.keycloak.adapters.HttpClientAdapterException; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.common.util.Time; -import org.keycloak.jose.jwk.JSONWebKeySet; -import org.keycloak.jose.jwk.JWK; -import org.keycloak.util.JWKSUtils; - -import java.security.PublicKey; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * When needed, publicKeys are downloaded by sending request to realm's jwks_url - * - * @author Marek Posolda - */ -public class JWKPublicKeyLocator implements PublicKeyLocator { - - private static final Logger log = Logger.getLogger(JWKPublicKeyLocator.class); - - private Map currentKeys = new ConcurrentHashMap<>(); - - private volatile int lastRequestTime = 0; - - @Override - public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) { - int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests(); - int publicKeyCacheTtl = deployment.getPublicKeyCacheTtl(); - int currentTime = Time.currentTime(); - - // Check if key is in cache. - PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime, kid); - if (publicKey != null) { - return publicKey; - } - - // Check if we are allowed to send request - synchronized (this) { - currentTime = Time.currentTime(); - if (currentTime > lastRequestTime + minTimeBetweenRequests) { - sendRequest(deployment); - lastRequestTime = currentTime; - } else { - log.debugf("Won't send request to realm jwks url. Last request time was %d. Current time is %d.", lastRequestTime, currentTime); - } - - return lookupCachedKey(publicKeyCacheTtl, currentTime, kid); - } - } - - - @Override - public void reset(KeycloakDeployment deployment) { - synchronized (this) { - sendRequest(deployment); - lastRequestTime = Time.currentTime(); - log.debugf("Reset time offset to %d.", lastRequestTime); - } - } - - - private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime, String kid) { - if (lastRequestTime + publicKeyCacheTtl > currentTime && kid != null) { - return currentKeys.get(kid); - } else { - return null; - } - } - - - private void sendRequest(KeycloakDeployment deployment) { - if (log.isTraceEnabled()) { - log.trace("Going to send request to retrieve new set of realm public keys for client " + deployment.getResourceName()); - } - - HttpGet getMethod = new HttpGet(deployment.getJwksUrl()); - try { - JSONWebKeySet jwks = HttpAdapterUtils.sendJsonHttpRequest(deployment, getMethod, JSONWebKeySet.class); - - Map publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG); - - if (log.isDebugEnabled()) { - log.debug("Realm public keys successfully retrieved for client " + deployment.getResourceName() + ". New kids: " + publicKeys.keySet().toString()); - } - - // Update current keys - currentKeys.clear(); - currentKeys.putAll(publicKeys); - - } catch (HttpClientAdapterException e) { - log.error("Error when sending request to retrieve realm keys", e); - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java deleted file mode 100644 index ca0e7c6a85ff..000000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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.adapters.rotation; - -import org.keycloak.adapters.KeycloakDeployment; - -import java.security.PublicKey; - -/** - * @author Marek Posolda - */ -public interface PublicKeyLocator { - - /** - * @param kid - * @param deployment - * @return publicKey, which should be used for verify signature on given "input" - */ - PublicKey getPublicKey(String kid, KeycloakDeployment deployment); - - /** - * Reset the state of locator (eg. clear the cached keys) - * - * @param deployment - */ - void reset(KeycloakDeployment deployment); - -} diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java deleted file mode 100644 index c607e7bfe445..000000000000 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.Configurable; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.hamcrest.CoreMatchers; -import org.junit.Test; -import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator; -import org.keycloak.adapters.rotation.JWKPublicKeyLocator; -import org.keycloak.common.enums.RelativeUrlsUsed; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.PemUtils; -import org.keycloak.enums.TokenStore; -import org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider; -import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider; -import org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.lang.reflect.Field; - -/** - * @author Stian Thorgersen - * @author Brad Culley - * @author John D. Ament - */ -public class KeycloakDeploymentBuilderTest { - - @Test - public void load() { - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak.json")); - assertEquals("demo", deployment.getRealm()); - assertEquals("customer-portal", deployment.getResourceName()); - - assertTrue(deployment.getPublicKeyLocator() instanceof HardcodedPublicKeyLocator); - assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), - deployment.getPublicKeyLocator().getPublicKey(null, deployment)); - - assertEquals("https://localhost:8443/auth", deployment.getAuthServerBaseUrl()); - assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired()); - assertTrue(deployment.isUseResourceRoleMappings()); - assertTrue(deployment.isCors()); - assertEquals(1000, deployment.getCorsMaxAge()); - assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods()); - assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders()); - assertEquals("X-Custom3, X-Custom4", deployment.getCorsExposedHeaders()); - assertTrue(deployment.isBearerOnly()); - assertTrue(deployment.isPublicClient()); - assertTrue(deployment.isEnableBasicAuth()); - assertTrue(deployment.isExposeToken()); - assertFalse(deployment.isOAuthQueryParameterEnabled()); - assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret")); - assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); - HttpClient client = deployment.getClient(); - int maxPoolConnections = -1; - Field connManager = null; - - try { - connManager = client.getClass().getDeclaredField("connManager"); - connManager.setAccessible(true); - maxPoolConnections = ((PoolingHttpClientConnectionManager) connManager.get(client)).getMaxTotal(); - } catch (Exception cause) { - throw new RuntimeException("Failed to get max pool connections", cause); - } finally { - connManager.setAccessible(false); - } - - - assertEquals(20, maxPoolConnections); - assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls()); - assertTrue(deployment.isAlwaysRefreshToken()); - assertTrue(deployment.isRegisterNodeAtStartup()); - assertEquals(1000, deployment.getRegisterNodePeriod()); - assertEquals(TokenStore.COOKIE, deployment.getTokenStore()); - assertEquals("email", deployment.getPrincipalAttribute()); - assertEquals(10, deployment.getTokenMinimumTimeToLive()); - assertEquals(20, deployment.getMinTimeBetweenJwksRequests()); - assertEquals(120, deployment.getPublicKeyCacheTtl()); - assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$")); - assertTrue(deployment.isVerifyTokenAudience()); - } - - @Test - public void loadNoClientCredentials() { - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-no-credentials.json")); - assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); - - assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator); - assertEquals(10, deployment.getMinTimeBetweenJwksRequests()); - assertEquals(86400, deployment.getPublicKeyCacheTtl()); - } - - @Test - public void loadJwtCredentials() { - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-jwt.json")); - assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); - } - - @Test - public void loadSecretJwtCredentials() { - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json")); - assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); - } - - @Test - public void loadHttpClientTimeoutConfiguration() { - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-http-client.json")); - assertThat(deployment, CoreMatchers.notNullValue()); - - HttpClient client = deployment.getClient(); - assertThat(client, CoreMatchers.notNullValue()); - - long socketTimeout = ((Configurable) client).getConfig().getSocketTimeout(); - long connectionTimeout = ((Configurable) client).getConfig().getConnectTimeout(); - - assertThat(socketTimeout, CoreMatchers.is(2000L)); - assertThat(connectionTimeout, CoreMatchers.is(6000L)); - } -} diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java deleted file mode 100644 index 2d2c32e46bca..000000000000 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2016 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.adapters; - -import org.junit.Test; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author Brad Culley - * @author John D. Ament - */ -public class KeycloakDeploymentTest { - @Test - public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() { - KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); - keycloakDeployment.setIgnoreOAuthQueryParameter(true); - assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled()); - } - - @Test - public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() { - KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); - keycloakDeployment.setIgnoreOAuthQueryParameter(false); - assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled()); - } - - @Test - public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() { - KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); - - assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled()); - } - - @Test - public void stripDefaultPorts() { - KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); - keycloakDeployment.setRealm("test"); - AdapterConfig config = new AdapterConfig(); - config.setAuthServerUrl("http://localhost:80/auth"); - - keycloakDeployment.setAuthServerBaseUrl(config); - - assertEquals("http://localhost/auth", keycloakDeployment.getAuthServerBaseUrl()); - - config.setAuthServerUrl("https://localhost:443/auth"); - keycloakDeployment.setAuthServerBaseUrl(config); - - assertEquals("https://localhost/auth", keycloakDeployment.getAuthServerBaseUrl()); - } - - class KeycloakDeploymentMock extends KeycloakDeployment { - - @Override - protected OIDCConfigurationRepresentation getOidcConfiguration(String discoveryUrl) throws Exception { - String base = KeycloakUriBuilder.fromUri(discoveryUrl).replacePath("/auth").build().toString(); - - OIDCConfigurationRepresentation rep = new OIDCConfigurationRepresentation(); - rep.setAuthorizationEndpoint(base + "/realms/test/authz"); - rep.setTokenEndpoint(base + "/realms/test/tokens"); - rep.setIssuer(base + "/realms/test"); - rep.setJwksUri(base + "/realms/test/jwks"); - rep.setLogoutEndpoint(base + "/realms/test/logout"); - return rep; - } - } -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/RefreshableKeycloakSecurityContextTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/RefreshableKeycloakSecurityContextTest.java deleted file mode 100644 index 1a3b0101e9a7..000000000000 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/RefreshableKeycloakSecurityContextTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.keycloak.adapters; - -import org.junit.Assert; -import org.junit.Test; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.IDToken; -import org.keycloak.representations.oidc.TokenMetadataRepresentation; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.security.KeyPair; -import java.security.KeyPairGenerator; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author github.com/tubbynl - * - */ -public class RefreshableKeycloakSecurityContextTest { - - @Test - public void isActive() { - TokenMetadataRepresentation token = new TokenMetadataRepresentation(); - token.setActive(true); - token.issuedNow(); - RefreshableKeycloakSecurityContext sut = new RefreshableKeycloakSecurityContext(null,null,null,token,null, null, null); - - // verify false if null deployment (KEYCLOAK-3050; yielded a npe) - assertFalse(sut.isActive()); - } - - @Test - public void sameIssuedAtAsNotBeforeIsActiveKEYCLOAK10013() { - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); - keycloakDeployment.setNotBefore(5000); - - TokenMetadataRepresentation token = new TokenMetadataRepresentation(); - token.setActive(true); - token.issuedAt(4999); - - RefreshableKeycloakSecurityContext sut = new RefreshableKeycloakSecurityContext(keycloakDeployment,null,null,token,null, null, null); - - assertFalse(sut.isActive()); - - token.issuedAt(5000); - assertTrue(sut.isActive()); - } - - private AccessToken createSimpleToken() { - AccessToken token = new AccessToken(); - token.id("111"); - token.issuer("http://localhost:8080/auth/acme"); - token.addAccess("foo").addRole("admin"); - token.addAccess("bar").addRole("user"); - return token; - } - - @Test - public void testSerialization() throws Exception { - AccessToken token = createSimpleToken(); - IDToken idToken = new IDToken(); - - idToken.setEmail("joe@email.cz"); - - KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - - String encoded = new JWSBuilder() - .jsonContent(token) - .rsa256(keyPair.getPrivate()); - String encodedIdToken = new JWSBuilder() - .jsonContent(idToken) - .rsa256(keyPair.getPrivate()); - - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); - keycloakDeployment.setNotBefore(5000); - - KeycloakSecurityContext ctx = new RefreshableKeycloakSecurityContext(keycloakDeployment,null, encoded, token,encodedIdToken, null, null); - KeycloakPrincipal principal = new KeycloakPrincipal("joe", ctx); - - // Serialize - ByteArrayOutputStream bso = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(bso); - oos.writeObject(principal); - oos.close(); - - // Deserialize - byte[] bytes = bso.toByteArray(); - ByteArrayInputStream bis = new ByteArrayInputStream(bytes); - ObjectInputStream ois = new ObjectInputStream(bis); - principal = (KeycloakPrincipal)ois.readObject(); - ctx = principal.getKeycloakSecurityContext(); - token = ctx.getToken(); - idToken = ctx.getIdToken(); - - System.out.println("Size of serialized principal: " + bytes.length); - - Assert.assertEquals(encoded, ctx.getTokenString()); - Assert.assertEquals(encodedIdToken, ctx.getIdTokenString()); - Assert.assertEquals("111", token.getId()); - Assert.assertEquals("111", token.getId()); - Assert.assertTrue(token.getResourceAccess("foo").isUserInRole("admin")); - Assert.assertTrue(token.getResourceAccess("bar").isUserInRole("user")); - Assert.assertEquals("joe@email.cz", idToken.getEmail()); - Assert.assertEquals("acme", ctx.getRealm()); - ois.close(); - } -} diff --git a/adapters/oidc/adapter-core/src/test/resources/cacerts.jks b/adapters/oidc/adapter-core/src/test/resources/cacerts.jks deleted file mode 100644 index f8ae5a39a090..000000000000 Binary files a/adapters/oidc/adapter-core/src/test/resources/cacerts.jks and /dev/null differ diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-http-client.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-http-client.json deleted file mode 100644 index 12b2d543f547..000000000000 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak-http-client.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "realm": "demo", - "resource": "customer-portal", - "auth-server-url": "https://localhost:8443/auth", - "public-client": true, - "socket-timeout-millis": 2000, - "connection-timeout-millis": 6000 -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-jwt.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-jwt.json deleted file mode 100644 index 6e46f33237b7..000000000000 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak-jwt.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "realm": "demo", - "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "https://localhost:8443/auth", - "ssl-required": "external", - "credentials": { - "jwt": { - "client-keystore-file": "classpath:keystore.jks", - "client-keystore-password": "storepass" - } - } -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json deleted file mode 100644 index a3c4026c3407..000000000000 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "realm": "demo", - "resource": "customer-portal", - "auth-server-url": "https://localhost:8443/auth", - "public-client": true, - "expose-token": true -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json deleted file mode 100644 index 9832429addf3..000000000000 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "realm": "demo", - "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "https://localhost:8443/auth", - "ssl-required": "external", - "credentials": { - "secret-jwt": { - "secret": "234234-234234-234234" - } - } -} diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json deleted file mode 100644 index 9a7dd22c49cd..000000000000 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "realm": "demo", - "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "https://localhost:8443/auth", - "ssl-required": "external", - "use-resource-role-mappings": true, - "enable-cors": true, - "cors-max-age": 1000, - "cors-allowed-methods": "POST, PUT, DELETE, GET", - "cors-allowed-headers": "X-Custom, X-Custom2", - "cors-exposed-headers": "X-Custom3, X-Custom4", - "bearer-only": true, - "public-client": true, - "enable-basic-auth": true, - "expose-token": true, - "credentials": { - "secret": "234234-234234-234234" - }, - "connection-pool-size": 20, - "disable-trust-manager": true, - "allow-any-hostname": true, - "truststore": "classpath:/cacerts.jks", - "truststore-password": "changeit", - "client-keystore": "classpath:/keystore.jks", - "client-keystore-password": "storepass", - "client-key-password": "keypass", - "always-refresh-token": true, - "register-node-at-startup": true, - "register-node-period": 1000, - "token-store": "cookie", - "principal-attribute": "email", - "token-minimum-time-to-live": 10, - "min-time-between-jwks-requests": 20, - "public-key-cache-ttl": 120, - "ignore-oauth-query-parameter": true, - "verify-token-audience": true, - "redirect-rewrite-rules" : { - "^/wsmaster/api/(.*)$" : "/api/$1" - } -} \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/test/resources/keystore.jks b/adapters/oidc/adapter-core/src/test/resources/keystore.jks deleted file mode 100644 index 1d62fb22368d..000000000000 Binary files a/adapters/oidc/adapter-core/src/test/resources/keystore.jks and /dev/null differ diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml deleted file mode 100755 index 985b81d485f2..000000000000 --- a/adapters/oidc/installed/pom.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-installed-adapter - Keycloak Installed Application - - - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.bouncycastle - bcprov-jdk18on - - - org.apache.httpcomponents - httpclient - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - org.jboss.logging - jboss-logging - - - org.jboss.resteasy - resteasy-client - - - org.jboss.spec.javax.ws.rs - jboss-jaxrs-api_2.1_spec - - - io.undertow - undertow-core - - - - - diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java deleted file mode 100644 index 685b55275be3..000000000000 --- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ /dev/null @@ -1,531 +0,0 @@ -/* -* Copyright 2016 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.adapters.installed; - -import java.awt.Desktop; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.Reader; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Deque; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; - -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; -import org.keycloak.OAuth2Constants; -import org.keycloak.OAuthErrorException; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.ServerRequest; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.common.VerificationException; -import org.keycloak.common.util.Base64Url; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.common.util.SecretGenerator; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.IDToken; - -import io.undertow.Handlers; -import io.undertow.Undertow; -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.AllowedMethodsHandler; -import io.undertow.server.handlers.GracefulShutdownHandler; -import io.undertow.server.handlers.PathHandler; -import io.undertow.util.Headers; -import io.undertow.util.Methods; -import io.undertow.util.StatusCodes; - -/** - * @author Stian Thorgersen - */ -public class KeycloakInstalled { - private static final String KEYCLOAK_JSON = "META-INF/keycloak.json"; - - private KeycloakDeployment deployment; - - private enum Status { - LOGGED_MANUAL, LOGGED_DESKTOP - } - - /** - * local port to listen for callbacks. The value {@code 0} will choose a random port. - */ - private int listenPort = 0; - - /** - * local hostname to listen for callbacks. - */ - private String listenHostname = "localhost"; - - private AccessTokenResponse tokenResponse; - private String tokenString; - private String idTokenString; - private IDToken idToken; - private AccessToken token; - private String refreshToken; - private Status status; - private Locale locale; - private ResteasyClient resteasyClient; - Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\""); - Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)"); - Pattern codePattern = Pattern.compile("code=([^&]+)"); - private CallbackListener callback; - private DesktopProvider desktopProvider = new DesktopProvider(); - - - public KeycloakInstalled() { - InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON); - deployment = KeycloakDeploymentBuilder.build(config); - } - - public KeycloakInstalled(InputStream config) { - deployment = KeycloakDeploymentBuilder.build(config); - } - - public KeycloakInstalled(KeycloakDeployment deployment) { - this.deployment = deployment; - } - - public void setResteasyClient(ResteasyClient resteasyClient) { - this.resteasyClient = resteasyClient; - } - - public Locale getLocale() { - return locale; - } - - public void setLocale(Locale locale) { - this.locale = locale; - } - - public int getListenPort() { - return listenPort; - } - - /** - * Configures the local port to listen for callbacks. The value {@code 0} will choose a random port. Defaults to {@code 0}. - * @param listenPort a valid port number - */ - public void setListenPort(int listenPort) { - if (listenPort < 0 || listenPort > 65535) { - throw new IllegalArgumentException("localPort"); - } - this.listenPort = listenPort; - } - - public String getListenHostname() { - return listenHostname; - } - - /** - * Configures the local hostname to listen for callbacks. The value {@code 0} will choose a random port - * @param listenHostname a valid local hostname - */ - public void setListenHostname(String listenHostname) { - this.listenHostname = listenHostname; - } - - public void login() throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException { - if (isDesktopSupported()) { - loginDesktop(); - } else { - loginManual(); - } - } - - public void login(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException { - if (isDesktopSupported()) { - loginDesktop(); - } else { - loginManual(printer, reader); - } - } - - public void logout() throws IOException, InterruptedException, URISyntaxException { - if (status == Status.LOGGED_DESKTOP) { - logoutDesktop(); - } - - tokenString = null; - token = null; - - idTokenString = null; - idToken = null; - - refreshToken = null; - - status = null; - } - - public void loginDesktop() throws IOException, VerificationException, OAuthErrorException, URISyntaxException, ServerRequest.HttpFailure, InterruptedException { - callback = new CallbackListener(); - callback.start(); - - String redirectUri = getRedirectUri(callback); - String state = UUID.randomUUID().toString(); - Pkce pkce = deployment.isPkce() ? generatePkce() : null; - - String authUrl = createAuthUrl(redirectUri, state, pkce); - - desktopProvider.browse(new URI(authUrl)); - - try { - callback.await(); - } catch (InterruptedException e) { - callback.stop(); - throw e; - } - - if (callback.error != null) { - throw new OAuthErrorException(callback.error, callback.errorDescription); - } - - if (!state.equals(callback.state)) { - throw new VerificationException("Invalid state"); - } - - processCode(callback.code, redirectUri, pkce); - - status = Status.LOGGED_DESKTOP; - } - - public void close() { - if (callback != null) { - callback.stop(); - } - } - - protected String createAuthUrl(String redirectUri, String state, Pkce pkce) { - - KeycloakUriBuilder builder = deployment.getAuthUrl().clone() - .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) - .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID); - - if (state != null) { - builder.queryParam(OAuth2Constants.STATE, state); - } - - if (locale != null) { - builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage()); - } - - if (pkce != null) { - builder.queryParam(OAuth2Constants.CODE_CHALLENGE, pkce.getCodeChallenge()); - builder.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, "S256"); - } - - return builder.build().toString(); - } - - protected Pkce generatePkce(){ - return Pkce.generatePkce(); - } - - private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException { - CallbackListener callback = new CallbackListener(); - callback.start(); - - String redirectUri = getRedirectUri(callback); - - // pass the id_token_hint so that sessions is invalidated for this particular session - String logoutUrl = deployment.getLogoutUrl().clone() - .queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri) - .queryParam("id_token_hint", idTokenString) - .build().toString(); - - desktopProvider.browse(new URI(logoutUrl)); - - try { - callback.await(); - } catch (InterruptedException e) { - callback.stop(); - throw e; - } - } - - private String getRedirectUri(CallbackListener callback) { - return String.format("http://%s:%s", getListenHostname(), callback.getLocalPort()); - } - - public void loginManual() throws IOException, ServerRequest.HttpFailure, VerificationException { - loginManual(System.out, new InputStreamReader(System.in)); - } - - public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException { - - String redirectUri = "urn:ietf:wg:oauth:2.0:oob"; - - Pkce pkce = generatePkce(); - - String authUrl = createAuthUrl(redirectUri, null, pkce); - - printer.println("Open the following URL in a browser. After login copy/paste the code back and press "); - printer.println(authUrl); - printer.println(); - printer.print("Code: "); - - String code = readCode(reader); - processCode(code, redirectUri, pkce); - - status = Status.LOGGED_MANUAL; - } - - public String getTokenString() { - return tokenString; - } - - public String getTokenString(long minValidity, TimeUnit unit) throws VerificationException, IOException, ServerRequest.HttpFailure { - long expires = ((long) token.getExpiration()) * 1000 - unit.toMillis(minValidity); - if (expires < System.currentTimeMillis()) { - refreshToken(); - } - - return tokenString; - } - - public void refreshToken() throws IOException, ServerRequest.HttpFailure, VerificationException { - AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken); - parseAccessToken(tokenResponse); - } - - public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException { - AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken); - parseAccessToken(tokenResponse); - - } - - private void parseAccessToken(AccessTokenResponse tokenResponse) throws VerificationException { - this.tokenResponse = tokenResponse; - tokenString = tokenResponse.getToken(); - refreshToken = tokenResponse.getRefreshToken(); - idTokenString = tokenResponse.getIdToken(); - - AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment); - token = tokens.getAccessToken(); - idToken = tokens.getIdToken(); - } - - public AccessToken getToken() { - return token; - } - - public IDToken getIdToken() { - return idToken; - } - - public String getIdTokenString() { - return idTokenString; - } - - public String getRefreshToken() { - return refreshToken; - } - - public AccessTokenResponse getTokenResponse() { - return tokenResponse; - } - - public void setDesktopProvider(DesktopProvider desktopProvider) { - this.desktopProvider = desktopProvider; - } - - public boolean isDesktopSupported() { - return desktopProvider.isDesktopSupported(); - } - - public KeycloakDeployment getDeployment() { - return deployment; - } - - private void processCode(String code, String redirectUri, Pkce pkce) throws IOException, ServerRequest.HttpFailure, VerificationException { - - AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null, pkce == null ? null : pkce.getCodeVerifier()); - parseAccessToken(tokenResponse); - } - - private String readCode(Reader reader) throws IOException { - StringBuilder sb = new StringBuilder(); - - char cb[] = new char[1]; - while (reader.read(cb) != -1) { - char c = cb[0]; - if ((c == ' ') || (c == '\n') || (c == '\r')) { - break; - } else { - sb.append(c); - } - } - - return sb.toString(); - } - - class CallbackListener implements HttpHandler { - private final CountDownLatch shutdownSignal = new CountDownLatch(1); - - private String code; - private String error; - private String errorDescription; - private String state; - private Undertow server; - - private GracefulShutdownHandler gracefulShutdownHandler; - - public void start() { - PathHandler pathHandler = Handlers.path().addExactPath("/", this); - AllowedMethodsHandler allowedMethodsHandler = new AllowedMethodsHandler(pathHandler, Methods.GET); - gracefulShutdownHandler = Handlers.gracefulShutdown(allowedMethodsHandler); - - server = Undertow.builder() - .setIoThreads(1) - .setWorkerThreads(1) - .addHttpListener(getListenPort(), getListenHostname()) - .setHandler(gracefulShutdownHandler) - .build(); - - server.start(); - } - - public void stop() { - try { - server.stop(); - } catch (Exception ignore) { - // it is OK to happen if thread is modified while stopping the server, specially when a security manager is enabled - } - shutdownSignal.countDown(); - } - - public int getLocalPort() { - return ((InetSocketAddress) server.getListenerInfo().get(0).getAddress()).getPort(); - } - - public void await() throws InterruptedException { - shutdownSignal.await(); - } - - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - gracefulShutdownHandler.shutdown(); - - if (!exchange.getQueryParameters().isEmpty()) { - readQueryParameters(exchange); - } - - exchange.setStatusCode(StatusCodes.FOUND); - exchange.getResponseHeaders().add(Headers.LOCATION, getRedirectUrl()); - exchange.endExchange(); - - shutdownSignal.countDown(); - - ForkJoinPool.commonPool().execute(this::stop); - } - - private void readQueryParameters(HttpServerExchange exchange) { - code = getQueryParameterIfPresent(exchange, OAuth2Constants.CODE); - error = getQueryParameterIfPresent(exchange, OAuth2Constants.ERROR); - errorDescription = getQueryParameterIfPresent(exchange, OAuth2Constants.ERROR_DESCRIPTION); - state = getQueryParameterIfPresent(exchange, OAuth2Constants.STATE); - } - - private String getQueryParameterIfPresent(HttpServerExchange exchange, String name) { - Map> queryParameters = exchange.getQueryParameters(); - return queryParameters.containsKey(name) ? queryParameters.get(name).getFirst() : null; - } - - private String getRedirectUrl() { - String redirectUrl = deployment.getTokenUrl().replace("/token", "/delegated"); - - if (error != null) { - redirectUrl += "?error=true"; - } - - return redirectUrl; - } - } - - public static class Pkce { - // https://tools.ietf.org/html/rfc7636#section-4.1 - public static final int PKCE_CODE_VERIFIER_MAX_LENGTH = 128; - - private final String codeChallenge; - private final String codeVerifier; - - public Pkce(String codeVerifier, String codeChallenge) { - this.codeChallenge = codeChallenge; - this.codeVerifier = codeVerifier; - } - - public String getCodeChallenge() { - return codeChallenge; - } - - public String getCodeVerifier() { - return codeVerifier; - } - - public static Pkce generatePkce() { - try { - String codeVerifier = SecretGenerator.getInstance().randomString(PKCE_CODE_VERIFIER_MAX_LENGTH); - String codeChallenge = generateS256CodeChallenge(codeVerifier); - return new Pkce(codeVerifier, codeChallenge); - } catch (Exception ex){ - throw new RuntimeException("Could not generate PKCE", ex); - } - } - - // https://tools.ietf.org/html/rfc7636#section-4.6 - private static String generateS256CodeChallenge(String codeVerifier) throws Exception { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(codeVerifier.getBytes(StandardCharsets.ISO_8859_1)); - return Base64Url.encode(md.digest()); - } - } - - public static class DesktopProvider { - public boolean isDesktopSupported() { - return Desktop.isDesktopSupported(); - } - - public void browse(URI uri) throws IOException { - Desktop.getDesktop().browse(uri); - } - } -} diff --git a/adapters/oidc/jakarta-servlet-filter/pom.xml b/adapters/oidc/jakarta-servlet-filter/pom.xml deleted file mode 100755 index c2105f8e1e94..000000000000 --- a/adapters/oidc/jakarta-servlet-filter/pom.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-jakarta-servlet-filter-adapter - Keycloak Servlet Filter Adapter Integration - - - - - - org.keycloak.adapters.servlet.* - - - jakarta.servlet.*;version="[3.1,5)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - ${project.basedir}/../servlet-filter/src - ${project.basedir}/src - - - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-jakarta-servlet-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.keycloak - keycloak-policy-enforcer - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - jakarta.servlet - jakarta.servlet-api - provided - - - junit - junit - test - - - - - - - maven-antrun-plugin - 3.0.0 - - - transform - initialize - - run - - - - - - - - - - - - - - - - - - - org.eclipse.transformer - org.eclipse.transformer.cli - 0.2.0 - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/oidc/jaxrs-oauth-client/pom.xml b/adapters/oidc/jaxrs-oauth-client/pom.xml deleted file mode 100755 index 1d5b773a2d1f..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/pom.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-jaxrs-oauth-client - Keycloak JAX-RS OAuth Client - - - - - org.jboss.spec.javax.ws.rs - jboss-jaxrs-api_2.1_spec - provided - - - org.jboss.resteasy - resteasy-client - provided - - - org.keycloak - keycloak-core - provided - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-adapter-core - provided - - - com.fasterxml.jackson.core - jackson-core - provided - - - com.fasterxml.jackson.core - jackson-databind - provided - - - org.jboss.resteasy - resteasy-jackson2-provider - provided - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - javax.annotation - javax.annotation-api - provided - - - junit - junit - test - - - - org.osgi - org.osgi.core - provided - - - - diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java deleted file mode 100644 index 9e66bc0ee00c..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016 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.jaxrs; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.container.ContainerRequestFilter; -import javax.ws.rs.container.PreMatching; - -/** - * @author Marek Posolda - * @deprecated Class is deprecated and may be removed in the future. If you want to maintain this class for Keycloak community, please - * contact Keycloak team on keycloak-dev mailing list. You can fork it into your github repository and - * Keycloak team will reference it from "Keycloak Extensions" page. - */ -@PreMatching -@Priority(Priorities.AUTHENTICATION) -@Deprecated -public interface JaxrsBearerTokenFilter extends ContainerRequestFilter { -} diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java deleted file mode 100755 index a44a4e566056..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright 2016 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.jaxrs; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.BasicAuthRequestAuthenticator; -import org.keycloak.adapters.BearerTokenRequestAuthenticator; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.keycloak.common.constants.GenericConstants; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.PreMatching; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.security.Principal; -import java.util.List; -import java.util.Set; -import java.util.logging.Logger; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - * @deprecated Class is deprecated and may be removed in the future. If you want to maintain this class for Keycloak community, please - * contact Keycloak team on keycloak-dev mailing list. You can fork it into your github repository and - * Keycloak team will reference it from "Keycloak Extensions" page. - */ -@PreMatching -@Priority(Priorities.AUTHENTICATION) -@Deprecated -public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { - - private final static Logger log = Logger.getLogger("" + JaxrsBearerTokenFilterImpl.class); - - private String keycloakConfigFile; - private String keycloakConfigResolverClass; - protected volatile boolean started; - - protected AdapterDeploymentContext deploymentContext; - - // TODO: Should also somehow handle stop lifecycle for de-registration - protected NodesRegistrationManagement nodesRegistrationManagement; - protected UserSessionManagement userSessionManagement = new EmptyUserSessionManagement(); - - public void setKeycloakConfigFile(String configFile) { - this.keycloakConfigFile = configFile; - attemptStart(); - } - - public String getKeycloakConfigFile() { - return this.keycloakConfigFile; - } - - public String getKeycloakConfigResolverClass() { - return keycloakConfigResolverClass; - } - - public void setKeycloakConfigResolverClass(String keycloakConfigResolverClass) { - this.keycloakConfigResolverClass = keycloakConfigResolverClass; - attemptStart(); - } - - // INITIALIZATION AND STARTUP - - protected void attemptStart() { - if (started) { - throw new IllegalStateException("Filter already started. Make sure to specify just keycloakConfigResolver or keycloakConfigFile but not both"); - } - - if (isInitialized()) { - start(); - } else { - log.fine("Not yet initialized"); - } - } - - protected boolean isInitialized() { - return this.keycloakConfigFile != null || this.keycloakConfigResolverClass != null; - } - - protected void start() { - if (started) { - throw new IllegalStateException("Filter already started. Make sure to specify just keycloakConfigResolver or keycloakConfigFile but not both"); - } - - if (keycloakConfigResolverClass != null) { - Class resolverClass = loadResolverClass(); - - try { - KeycloakConfigResolver resolver = resolverClass.newInstance(); - log.info("Using " + resolver + " to resolve Keycloak configuration on a per-request basis."); - this.deploymentContext = new AdapterDeploymentContext(resolver); - } catch (Exception e) { - throw new RuntimeException("Unable to instantiate resolver " + resolverClass); - } - } else { - if (keycloakConfigFile == null) { - throw new IllegalArgumentException("You need to specify either keycloakConfigResolverClass or keycloakConfigFile in configuration"); - } - InputStream is = loadKeycloakConfigFile(); - KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is); - deploymentContext = new AdapterDeploymentContext(kd); - log.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile); - } - - nodesRegistrationManagement = new NodesRegistrationManagement(); - started = true; - } - - // TODO: Use 'Reflections.classForName' - protected Class loadResolverClass() { - try { - return (Class)getClass().getClassLoader().loadClass(keycloakConfigResolverClass); - } catch (ClassNotFoundException cnfe) { - // Fallback to tccl - try { - return (Class)Thread.currentThread().getContextClassLoader().loadClass(keycloakConfigResolverClass); - } catch (ClassNotFoundException cnfe2) { - throw new RuntimeException("Unable to find resolver class: " + keycloakConfigResolverClass); - } - } - } - - protected InputStream loadKeycloakConfigFile() { - if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { - String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, ""); - log.fine("Loading config from classpath on location: " + classPathLocation); - // Try current class classloader first - InputStream is = getClass().getClassLoader().getResourceAsStream(classPathLocation); - if (is == null) { - is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation); - } - - if (is != null) { - return is; - } else { - throw new RuntimeException("Unable to find config from classpath: " + keycloakConfigFile); - } - } else { - // Fallback to file - try { - log.fine("Loading config from file: " + keycloakConfigFile); - return new FileInputStream(keycloakConfigFile); - } catch (FileNotFoundException fnfe) { - log.severe("Config not found on " + keycloakConfigFile); - throw new RuntimeException(fnfe); - } - } - } - - // REQUEST HANDLING - - @Override - public void filter(ContainerRequestContext request) throws IOException { - SecurityContext securityContext = getRequestSecurityContext(request); - JaxrsHttpFacade facade = new JaxrsHttpFacade(request, securityContext); - if (handlePreauth(facade)) { - return; - } - - KeycloakDeployment resolvedDeployment = deploymentContext.resolveDeployment(facade); - - nodesRegistrationManagement.tryRegister(resolvedDeployment); - - bearerAuthentication(facade, request, resolvedDeployment); - } - - protected boolean handlePreauth(JaxrsHttpFacade facade) { - PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade); - if (handler.handleRequest()) { - // Send response now (if not already sent) - if (!facade.isResponseFinished()) { - facade.getResponse().end(); - } - return true; - } - - return false; - } - - protected void bearerAuthentication(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment) { - BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(resolvedDeployment); - AuthOutcome outcome = authenticator.authenticate(facade); - - if (outcome == AuthOutcome.NOT_ATTEMPTED && resolvedDeployment.isEnableBasicAuth()) { - authenticator = new BasicAuthRequestAuthenticator(resolvedDeployment); - outcome = authenticator.authenticate(facade); - } - - if (outcome == AuthOutcome.FAILED || outcome == AuthOutcome.NOT_ATTEMPTED) { - AuthChallenge challenge = authenticator.getChallenge(); - log.fine("Authentication outcome: " + outcome); - boolean challengeSent = challenge.challenge(facade); - if (!challengeSent) { - // Use some default status code - facade.getResponse().setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); - } - - // Send response now (if not already sent) - if (!facade.isResponseFinished()) { - facade.getResponse().end(); - } - return; - } else { - if (verifySslFailed(facade, resolvedDeployment)) { - return; - } - } - - propagateSecurityContext(facade, request, resolvedDeployment, authenticator); - handleAuthActions(facade, resolvedDeployment); - } - - protected void propagateSecurityContext(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment, BearerTokenRequestAuthenticator bearer) { - RefreshableKeycloakSecurityContext skSession = new RefreshableKeycloakSecurityContext(resolvedDeployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); - - // Not needed to do resteasy specifics as KeycloakSecurityContext can be always retrieved from SecurityContext by typecast SecurityContext.getUserPrincipal to KeycloakPrincipal - // ResteasyProviderFactory.pushContext(KeycloakSecurityContext.class, skSession); - - facade.setSecurityContext(skSession); - String principalName = AdapterUtils.getPrincipalName(resolvedDeployment, bearer.getToken()); - final KeycloakPrincipal principal = new KeycloakPrincipal(principalName, skSession); - SecurityContext anonymousSecurityContext = getRequestSecurityContext(request); - final boolean isSecure = anonymousSecurityContext.isSecure(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(skSession); - - SecurityContext ctx = new SecurityContext() { - @Override - public Principal getUserPrincipal() { - return principal; - } - - @Override - public boolean isUserInRole(String role) { - return roles.contains(role); - } - - @Override - public boolean isSecure() { - return isSecure; - } - - @Override - public String getAuthenticationScheme() { - return "OAUTH_BEARER"; - } - }; - request.setSecurityContext(ctx); - } - - protected boolean verifySslFailed(JaxrsHttpFacade facade, KeycloakDeployment deployment) { - if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - log.warning("SSL is required to authenticate, but request is not secured"); - facade.getResponse().sendError(403, "SSL required!"); - return true; - } - return false; - } - - protected SecurityContext getRequestSecurityContext(ContainerRequestContext request) { - return request.getSecurityContext(); - } - - protected void handleAuthActions(JaxrsHttpFacade facade, KeycloakDeployment deployment) { - AuthenticatedActionsHandler authActionsHandler = new AuthenticatedActionsHandler(deployment, facade); - if (authActionsHandler.handledRequest()) { - // Send response now (if not already sent) - if (!facade.isResponseFinished()) { - facade.getResponse().end(); - } - } - } - - // We don't have any sessions to manage with pure jaxrs filter - private static class EmptyUserSessionManagement implements UserSessionManagement { - - @Override - public void logoutAll() { - } - - @Override - public void logoutHttpSessions(List ids) { - } - } - -} diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java deleted file mode 100755 index 66599c45d908..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2016 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.jaxrs; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.HostUtils; - -import javax.security.cert.X509Certificate; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.SecurityContext; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; -import java.util.Map; - -/** - * @author Marek Posolda - * @deprecated Class is deprecated and may be removed in the future. If you want to maintain this class for Keycloak community, please - * contact Keycloak team on keycloak-dev mailing list. You can fork it into your github repository and - * Keycloak team will reference it from "Keycloak Extensions" page. - */ -@Deprecated -public class JaxrsHttpFacade implements OIDCHttpFacade { - - protected final ContainerRequestContext requestContext; - protected final SecurityContext securityContext; - protected final RequestFacade requestFacade = new RequestFacade(); - protected final ResponseFacade responseFacade = new ResponseFacade(); - protected KeycloakSecurityContext keycloakSecurityContext; - protected boolean responseFinished; - - public JaxrsHttpFacade(ContainerRequestContext containerRequestContext, SecurityContext securityContext) { - this.requestContext = containerRequestContext; - this.securityContext = securityContext; - } - - protected class RequestFacade implements OIDCHttpFacade.Request { - - private InputStream inputStream; - - @Override - public String getFirstParam(String param) { - throw new RuntimeException("NOT IMPLEMENTED"); - } - - @Override - public String getMethod() { - return requestContext.getMethod(); - } - - @Override - public String getURI() { - return requestContext.getUriInfo().getRequestUri().toString(); - } - - @Override - public String getRelativePath() { - return requestContext.getUriInfo().getPath(); - } - - @Override - public boolean isSecure() { - return securityContext.isSecure(); - } - - @Override - public String getQueryParamValue(String param) { - MultivaluedMap queryParams = requestContext.getUriInfo().getQueryParameters(); - if (queryParams == null) - return null; - return queryParams.getFirst(param); - } - - @Override - public Cookie getCookie(String cookieName) { - Map cookies = requestContext.getCookies(); - if (cookies == null) - return null; - javax.ws.rs.core.Cookie cookie = cookies.get(cookieName); - if (cookie == null) - return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public String getHeader(String name) { - return requestContext.getHeaderString(name); - } - - @Override - public List getHeaders(String name) { - MultivaluedMap headers = requestContext.getHeaders(); - return (headers == null) ? null : headers.get(name); - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - return inputStream = new BufferedInputStream(requestContext.getEntityStream()); - } - - return requestContext.getEntityStream(); - } - - @Override - public String getRemoteAddr() { - // TODO: implement properly - return HostUtils.getIpAddress(); - } - - @Override - public void setError(AuthenticationError error) { - requestContext.setProperty(AuthenticationError.class.getName(), error); - } - - @Override - public void setError(LogoutError error) { - requestContext.setProperty(LogoutError.class.getName(), error); - - } - } - - protected class ResponseFacade implements OIDCHttpFacade.Response { - - private javax.ws.rs.core.Response.ResponseBuilder responseBuilder = javax.ws.rs.core.Response.status(204); - - @Override - public void setStatus(int status) { - responseBuilder.status(status); - } - - @Override - public void addHeader(String name, String value) { - responseBuilder.header(name, value); - } - - @Override - public void setHeader(String name, String value) { - responseBuilder.header(name, value); - } - - @Override - public void resetCookie(String name, String path) { - // For now doesn't need to be supported - throw new IllegalStateException("Not supported yet"); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - // For now doesn't need to be supported - throw new IllegalStateException("Not supported yet"); - } - - @Override - public OutputStream getOutputStream() { - // For now doesn't need to be supported - throw new IllegalStateException("Not supported yet"); - } - - @Override - public void sendError(int code) { - javax.ws.rs.core.Response response = responseBuilder.status(code).build(); - requestContext.abortWith(response); - responseFinished = true; - } - - @Override - public void sendError(int code, String message) { - javax.ws.rs.core.Response response = responseBuilder.status(code).entity(message).build(); - requestContext.abortWith(response); - responseFinished = true; - } - - @Override - public void end() { - javax.ws.rs.core.Response response = responseBuilder.build(); - requestContext.abortWith(response); - responseFinished = true; - } - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return keycloakSecurityContext; - } - - public void setSecurityContext(KeycloakSecurityContext securityContext) { - this.keycloakSecurityContext = securityContext; - } - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - throw new IllegalStateException("Not supported yet"); - } - - public boolean isResponseFinished() { - return responseFinished; - } -} diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java deleted file mode 100755 index 2ec416d1794d..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2016 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.jaxrs; - -import org.keycloak.AbstractOAuthClient; -import org.keycloak.OAuth2Constants; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.util.TokenUtil; - -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Cookie; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.Map; -import java.util.logging.Logger; - -/** - * Helper code to obtain oauth access tokens via browser redirects - * - * @author Bill Burke - * @version $Revision: 1 $ - * @deprecated Class is deprecated and may be removed in the future. If you want to maintain this class for Keycloak community, please - * contact Keycloak team on keycloak-dev mailing list. You can fork it into your github repository and - * Keycloak team will reference it from "Keycloak Extensions" page. - */ -@Deprecated -public class JaxrsOAuthClient extends AbstractOAuthClient { - private final static Logger logger = Logger.getLogger("" + JaxrsOAuthClient.class); - protected Client client; - - /** - * closes client - */ - public void stop() { - if (client != null) client.close(); - } - public Client getClient() { - return client; - } - - public void setClient(Client client) { - this.client = client; - } - - public String resolveBearerToken(String redirectUri, String code) { - redirectUri = stripOauthParametersFromRedirect(redirectUri); - Form codeForm = new Form() - .param(OAuth2Constants.GRANT_TYPE, "authorization_code") - .param(OAuth2Constants.CODE, code) - .param(OAuth2Constants.CLIENT_ID, clientId) - .param(OAuth2Constants.REDIRECT_URI, redirectUri); - for (Map.Entry entry : credentials.entrySet()) { - codeForm.param(entry.getKey(), (String) entry.getValue()); - } - Response res = client.target(tokenUrl).request().post(Entity.form(codeForm)); - try { - if (res.getStatus() == 400) { - throw new BadRequestException(); - } else if (res.getStatus() != 200) { - throw new InternalServerErrorException(new Exception("Unknown error when getting acess token")); - } - AccessTokenResponse tokenResponse = res.readEntity(AccessTokenResponse.class); - return tokenResponse.getToken(); - } finally { - res.close(); - } - } - public Response redirect(UriInfo uriInfo, String redirectUri) { - String state = getStateCode(); - String scopeParam = TokenUtil.attachOIDCScope(scope); - - UriBuilder uriBuilder = UriBuilder.fromUri(authUrl) - .queryParam(OAuth2Constants.CLIENT_ID, clientId) - .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) - .queryParam(OAuth2Constants.STATE, state) - .queryParam(OAuth2Constants.SCOPE, scopeParam); - - URI url = uriBuilder.build(); - - NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true); - logger.fine("NewCookie: " + cookie.toString()); - logger.fine("Oauth Redirect to: " + url); - return Response.status(302) - .location(url) - .cookie(cookie).build(); - } - - public String getStateCookiePath(UriInfo uriInfo) { - if (stateCookiePath != null) return stateCookiePath; - return uriInfo.getBaseUri().getRawPath(); - } - - public String getBearerToken(UriInfo uriInfo, HttpHeaders headers) throws BadRequestException, InternalServerErrorException { - String error = getError(uriInfo); - if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error)); - checkStateCookie(uriInfo, headers); - String code = getAccessCode(uriInfo); - if (code == null) throw new BadRequestException(new Exception("code parameter was null")); - return resolveBearerToken(uriInfo.getRequestUri().toString(), code); - } - - public String getError(UriInfo uriInfo) { - return uriInfo.getQueryParameters().getFirst(OAuth2Constants.ERROR); - } - - public String getAccessCode(UriInfo uriInfo) { - return uriInfo.getQueryParameters().getFirst(OAuth2Constants.CODE); - } - - public void checkStateCookie(UriInfo uriInfo, HttpHeaders headers) { - Cookie stateCookie = headers.getCookies().get(stateCookieName); - if (stateCookie == null) throw new BadRequestException("state cookie not set"); - String state = uriInfo.getQueryParameters().getFirst(OAuth2Constants.STATE); - if (state == null) throw new BadRequestException("state parameter was null"); - if (!state.equals(stateCookie.getValue())) { - throw new BadRequestException("state parameter invalid"); - } - } -} diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java deleted file mode 100644 index fca53d87f768..000000000000 --- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016 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.jaxrs; - -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.common.constants.GenericConstants; -import org.osgi.framework.BundleContext; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.container.PreMatching; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.logging.Logger; - -/** - * Variant of JaxrsBearerTokenFilter, which can be used to properly use resources from current osgi bundle - * - * @author Marek Posolda - * @deprecated Class is deprecated and may be removed in the future. If you want to maintain this class for Keycloak community, please - * contact Keycloak team on keycloak-dev mailing list. You can fork it into your github repository and - * Keycloak team will reference it from "Keycloak Extensions" page. - */ -@PreMatching -@Priority(Priorities.AUTHENTICATION) -@Deprecated -public class OsgiJaxrsBearerTokenFilterImpl extends JaxrsBearerTokenFilterImpl { - - private final static Logger log = Logger.getLogger("" + JaxrsBearerTokenFilterImpl.class); - - private BundleContext bundleContext; - - public BundleContext getBundleContext() { - return bundleContext; - } - - public void setBundleContext(BundleContext bundleContext) { - this.bundleContext = bundleContext; - attemptStart(); - } - - @Override - protected boolean isInitialized() { - return super.isInitialized() && bundleContext != null; - } - - @Override - protected Class loadResolverClass() { - String resolverClass = getKeycloakConfigResolverClass(); - try { - return (Class) bundleContext.getBundle().loadClass(resolverClass); - } catch (ClassNotFoundException cnfe) { - log.warning("Not able to find class from bundleContext. Fallback to current classloader"); - return super.loadResolverClass(); - } - } - - @Override - protected InputStream loadKeycloakConfigFile() { - String keycloakConfigFile = getKeycloakConfigFile(); - if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { - - // Load from classpath of current bundle - String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, ""); - log.fine("Loading config from classpath on location: " + classPathLocation); - - URL cfgUrl = bundleContext.getBundle().getResource(classPathLocation); - if (cfgUrl == null) { - log.warning("Not able to find configFile from bundleContext. Fallback to current classloader"); - return super.loadKeycloakConfigFile(); - } - - try { - return cfgUrl.openStream(); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } else { - return super.loadKeycloakConfigFile(); - } - } -} diff --git a/adapters/oidc/jetty/jetty-core/pom.xml b/adapters/oidc/jetty/jetty-core/pom.xml deleted file mode 100755 index f36d9780af3f..000000000000 --- a/adapters/oidc/jetty/jetty-core/pom.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - 4.0.0 - - keycloak-jetty-core - Keycloak Jetty Core Integration - - - org.keycloak.adapters.jetty.core.* - - - org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional, - javax.servlet.*;version="[2.5,4)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-jetty-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.eclipse.jetty - jetty-server - ${jetty94.version} - provided - - - - org.eclipse.jetty - jetty-util - ${jetty94.version} - provided - - - - org.eclipse.jetty - jetty-security - ${jetty94.version} - provided - - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java deleted file mode 100755 index a66252d1ab80..000000000000 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.core; - -import org.eclipse.jetty.security.DefaultUserIdentity; -import org.eclipse.jetty.security.IdentityService; -import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.ServerAuthException; -import org.eclipse.jetty.security.UserAuthentication; -import org.eclipse.jetty.security.authentication.DeferredAuthentication; -import org.eclipse.jetty.security.authentication.FormAuthenticator; -import org.eclipse.jetty.security.authentication.LoginAuthenticator; -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.enums.TokenStore; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import javax.security.auth.Subject; -import javax.servlet.ServletContext; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.HashSet; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthenticator { - public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; - protected static final org.jboss.logging.Logger log = Logger.getLogger(AbstractKeycloakJettyAuthenticator.class); - protected AdapterDeploymentContext deploymentContext; - protected NodesRegistrationManagement nodesRegistrationManagement; - protected AdapterConfig adapterConfig; - protected KeycloakConfigResolver configResolver; - protected String errorPage; - - public AbstractKeycloakJettyAuthenticator() { - super(); - } - - private static InputStream getJSONFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (json == null) { - return null; - } - return new ByteArrayInputStream(json.getBytes()); - } - - public AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { - AdapterTokenStore store = (AdapterTokenStore) request.getAttribute(TOKEN_STORE_NOTE); - if (store != null) { - return store; - } - - if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) { - store = createSessionTokenStore(request, resolvedDeployment); - } else { - store = new JettyCookieTokenStore(request, facade, resolvedDeployment); - } - - request.setAttribute(TOKEN_STORE_NOTE, store); - return store; - } - - public abstract AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment); - - public abstract JettyUserSessionManagement createSessionManagement(Request request); - - public void logoutCurrent(Request request) { - AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getAttribute(AdapterDeploymentContext.class.getName()); - KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); - if (ksc != null) { - JettyHttpFacade facade = new OIDCJettyHttpFacade(request, null); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (ksc instanceof RefreshableKeycloakSecurityContext) { - ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); - } - - AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); - tokenStore.logout(); - request.removeAttribute(KeycloakSecurityContext.class.getName()); - } - } - - public static UserIdentity createIdentity(KeycloakPrincipal principal) { - Set roles = AdapterUtils.getRolesFromSecurityContext(principal.getKeycloakSecurityContext()); - if (roles == null) { - roles = new HashSet(); - } - Subject theSubject = new Subject(); - String[] theRoles = new String[roles.size()]; - roles.toArray(theRoles); - - return new DefaultUserIdentity(theSubject, principal, theRoles); - } - - private static class DummyLoginService implements LoginService { - @Override - public String getName() { - return null; - } - - @Override - public UserIdentity login(String username, Object credentials, ServletRequest var3) { - return null; - } - - @Override - public boolean validate(UserIdentity user) { - return false; - } - - @Override - public IdentityService getIdentityService() { - return null; - } - - @Override - public void setIdentityService(IdentityService service) { - - } - - @Override - public void logout(UserIdentity user) { - - } - } - - @Override - public void setConfiguration(AuthConfiguration configuration) { - //super.setConfiguration(configuration); - initializeKeycloak(); - // need this so that getUserPrincipal does not throw NPE - _loginService = new DummyLoginService(); - String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); - setErrorPage(error); - } - - private void setErrorPage(String path) { - if (path == null || path.trim().length() == 0) { - } else { - if (!path.startsWith("/")) { - path = "/" + path; - } - errorPage = path; - - if (errorPage.indexOf('?') > 0) - errorPage = errorPage.substring(0, errorPage.indexOf('?')); - } - } - - @Override - public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException { - return true; - } - - public AdapterConfig getAdapterConfig() { - return adapterConfig; - } - - public void setAdapterConfig(AdapterConfig adapterConfig) { - this.adapterConfig = adapterConfig; - } - - public KeycloakConfigResolver getConfigResolver() { - return configResolver; - } - - public void setConfigResolver(KeycloakConfigResolver configResolver) { - this.configResolver = configResolver; - } - - @SuppressWarnings("UseSpecificCatch") - public void initializeKeycloak() { - nodesRegistrationManagement = new NodesRegistrationManagement(); - - ServletContext theServletContext = null; - ContextHandler.Context currentContext = ContextHandler.getCurrentContext(); - if (currentContext != null) { - String contextPath = currentContext.getContextPath(); - - if ("".equals(contextPath)) { - // This could be the case in osgi environment when deploying apps through pax whiteboard extension. - theServletContext = currentContext; - } else { - theServletContext = currentContext.getContext(contextPath); - } - } - - // Jetty 9.1.x servlet context will be null :( - if (configResolver == null && theServletContext != null) { - String configResolverClass = theServletContext.getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - configResolver = (KeycloakConfigResolver) ContextHandler.getCurrentContext().getClassLoader().loadClass(configResolverClass).newInstance(); - log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.infov("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); - } - } - } - - if (configResolver != null) { - deploymentContext = new AdapterDeploymentContext(configResolver); - } else if (adapterConfig != null) { - KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig); - deploymentContext = new AdapterDeploymentContext(kd); - } else if (theServletContext != null) { - InputStream configInputStream = getConfigInputStream(theServletContext); - if (configInputStream != null) { - deploymentContext = new AdapterDeploymentContext(KeycloakDeploymentBuilder.build(configInputStream)); - } - } - if (deploymentContext == null) { - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } - if (theServletContext != null) - theServletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); - } - - private InputStream getConfigInputStream(ServletContext servletContext) { - InputStream is = getJSONFromServletContext(servletContext); - if (is == null) { - String path = servletContext.getInitParameter("keycloak.config.file"); - if (path == null) { - is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } - return is; - } - - @Override - public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException { - if (log.isTraceEnabled()) { - log.trace("*** authenticate"); - } - Request request = resolveRequest(req); - OIDCJettyHttpFacade facade = new OIDCJettyHttpFacade(request, (HttpServletResponse) res); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - log.debug("*** deployment isn't configured return false"); - return Authentication.UNAUTHENTICATED; - } - PreAuthActionsHandler handler = new PreAuthActionsHandler(createSessionManagement(request), deploymentContext, facade); - if (handler.handleRequest()) { - return Authentication.SEND_SUCCESS; - } - if (!mandatory) - return new DeferredAuthentication(this); - AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); - nodesRegistrationManagement.tryRegister(deployment); - - tokenStore.checkCurrentToken(); - JettyRequestAuthenticator authenticator = createRequestAuthenticator(request, facade, deployment, tokenStore); - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - if (facade.isEnded()) { - return Authentication.SEND_SUCCESS; - } - - Authentication authentication = register(request, authenticator.principal); - AuthenticatedActionsHandler authenticatedActionsHandler = new AuthenticatedActionsHandler(deployment, facade); - if (authenticatedActionsHandler.handledRequest()) { - return Authentication.SEND_SUCCESS; - } - return authentication; - - } - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - return Authentication.SEND_CONTINUE; - } - - - protected abstract Request resolveRequest(ServletRequest req); - - protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade, - KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - return new JettyRequestAuthenticator(facade, deployment, tokenStore, -1, request); - } - - @Override - public String getAuthMethod() { - return "KEYCLOAK"; - } - - protected Authentication register(Request request, KeycloakPrincipal principal) { - request.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); - Authentication authentication = request.getAuthentication(); - if (!(authentication instanceof KeycloakAuthentication)) { - UserIdentity userIdentity = createIdentity(principal); - authentication = createAuthentication(userIdentity, request); - request.setAuthentication(authentication); - } - return authentication; - } - - protected abstract Authentication createAuthentication(UserIdentity userIdentity, Request request); - - public static abstract class KeycloakAuthentication extends UserAuthentication { - public KeycloakAuthentication(String method, UserIdentity userIdentity) { - super(method, userIdentity); - } - - } -} diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java deleted file mode 100755 index 3abcb74499ed..000000000000 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyCookieTokenStore.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.core; - -import org.eclipse.jetty.server.Request; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.CookieTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * Handle storage of token info in cookie. Per-request object. - * - * @author Marek Posolda - */ -public class JettyCookieTokenStore implements AdapterTokenStore { - - private static final Logger log = Logger.getLogger(JettyCookieTokenStore.class); - - private Request request; - private HttpFacade facade; - private KeycloakDeployment deployment; - - private KeycloakPrincipal authenticatedPrincipal; - - public JettyCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) { - this.request = request; - this.facade = facade; - this.deployment = deployment; - } - - - @Override - public void checkCurrentToken() { - this.authenticatedPrincipal = checkPrincipalFromCookie(); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - // Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request - if (authenticatedPrincipal != null) { - log.debug("remote logged in already. Establish state from cookie"); - RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext(); - - if (!securityContext.getRealm().equals(deployment.getRealm())) { - log.debug("Account from cookie is from a different realm than for the request."); - return false; - } - - securityContext.setCurrentRequestInfo(deployment, this); - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - - JettyRequestAuthenticator jettyAuthenticator = (JettyRequestAuthenticator) authenticator; - KeycloakPrincipal principal = AdapterUtils.createPrincipal(deployment, securityContext); - jettyAuthenticator.principal = principal; - return true; - } else { - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); - CookieTokenStore.setTokenCookie(deployment, facade, securityContext); - } - - @Override - public void logout() { - CookieTokenStore.removeCookie(deployment, facade); - - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext secContext) { - CookieTokenStore.setTokenCookie(deployment, facade, secContext); - } - - /** - * Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active - * - * @return valid principal - */ - protected KeycloakPrincipal checkPrincipalFromCookie() { - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); - if (principal == null) { - log.debug("Account was not in cookie or was invalid"); - return null; - } - - RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext(); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal; - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return principal; - - log.debugf("Cleanup and expire cookie for user %s after failed refresh", principal.getName()); - CookieTokenStore.removeCookie(deployment, facade); - return null; - } - - @Override - public void saveRequest() { - - } - - @Override - public boolean restoreRequest() { - return false; - } -} diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyRequestAuthenticator.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyRequestAuthenticator.java deleted file mode 100755 index 38a2480b540f..000000000000 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettyRequestAuthenticator.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.core; - -import org.eclipse.jetty.server.Request; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -import javax.servlet.http.HttpSession; -import java.security.Principal; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettyRequestAuthenticator extends RequestAuthenticator { - protected static final Logger log = Logger.getLogger(JettyRequestAuthenticator.class); - protected Request request; - protected KeycloakPrincipal principal; - - public JettyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort, Request request) { - super(facade, deployment, tokenStore, sslRedirectPort); - this.request = request; - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { - principal = skp; - final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - OidcKeycloakAccount account = new OidcKeycloakAccount() { - - @Override - public Principal getPrincipal() { - return skp; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public KeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - }; - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - this.tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { - this.principal = principal; - RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - if (log.isDebugEnabled()) { - log.debug("Completing bearer authentication. Bearer roles: " + roles); - } - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - - - @Override - protected String changeHttpSessionId(boolean create) { - HttpSession session = request.getSession(create); - return session != null ? session.getId() : null; - } - - -} diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettySessionTokenStore.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettySessionTokenStore.java deleted file mode 100755 index aada9c6fded0..000000000000 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/JettySessionTokenStore.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.core; - -import org.eclipse.jetty.server.Request; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.AdapterSessionStore; - -import javax.servlet.http.HttpSession; - -/** - * Handle storage of token info in HTTP Session. Per-request object - * - * @author Marek Posolda - */ -public class JettySessionTokenStore implements AdapterTokenStore { - - private static final Logger log = Logger.getLogger(JettySessionTokenStore.class); - - private Request request; - protected KeycloakDeployment deployment; - protected AdapterSessionStore sessionStore; - - public JettySessionTokenStore(Request request, KeycloakDeployment deployment, AdapterSessionStore sessionStore) { - this.request = request; - this.deployment = deployment; - this.sessionStore = sessionStore; - } - - @Override - public void checkCurrentToken() { - if (request.getSession(false) == null) return; - RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); - if (session == null) return; - - // just in case session got serialized - if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return; - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - request.getSession().removeAttribute(KeycloakSecurityContext.class.getName()); - request.getSession().invalidate(); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - if (request.getSession(false) == null || request.getSession().getAttribute(KeycloakSecurityContext.class.getName()) == null) - return false; - log.debug("remote logged in already. Establish state from session"); - - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); - if (!deployment.getRealm().equals(securityContext.getRealm())) { - log.debug("Account from cookie is from a different realm than for the request."); - return false; - } - - securityContext.setCurrentRequestInfo(deployment, this); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - - JettyRequestAuthenticator jettyAuthenticator = (JettyRequestAuthenticator) authenticator; - KeycloakPrincipal principal = AdapterUtils.createPrincipal(deployment, securityContext); - jettyAuthenticator.principal = principal; - restoreRequest(); - return true; - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); - request.getSession().setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - - @Override - public void logout() { - HttpSession session = request.getSession(false); - if (session != null) { - session.removeAttribute(KeycloakSecurityContext.class.getName()); - } - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } - - @Override - public void saveRequest() { - sessionStore.saveRequest(); - - } - - @Override - public boolean restoreRequest() { - return sessionStore.restoreRequest(); - } -} diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/OIDCJettyHttpFacade.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/OIDCJettyHttpFacade.java deleted file mode 100755 index 78fa6f1d4449..000000000000 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/OIDCJettyHttpFacade.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.core; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; - -import javax.servlet.http.HttpServletResponse; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCJettyHttpFacade extends JettyHttpFacade implements OIDCHttpFacade { - - public OIDCJettyHttpFacade(org.eclipse.jetty.server.Request request, HttpServletResponse response) { - super(request, response); - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); - } - -} diff --git a/adapters/oidc/jetty/jetty9.4/pom.xml b/adapters/oidc/jetty/jetty9.4/pom.xml deleted file mode 100644 index 3ddd0b2fa930..000000000000 --- a/adapters/oidc/jetty/jetty9.4/pom.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - 4.0.0 - - keycloak-jetty94-adapter - Keycloak Jetty 9.4.x Integration - - - org.keycloak.adapters.jetty.* - - - org.eclipse.jetty.*;resolution:=optional, - javax.servlet.*;version="[3.0,4)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-core - - - org.keycloak - keycloak-jetty-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.eclipse.jetty - jetty-server - ${jetty94.version} - provided - - - - org.eclipse.jetty - jetty-util - ${jetty94.version} - provided - - - - org.eclipse.jetty - jetty-security - ${jetty94.version} - provided - - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94RequestAuthenticator.java b/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94RequestAuthenticator.java deleted file mode 100644 index fa6d75f7e9c4..000000000000 --- a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94RequestAuthenticator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty; - -import org.eclipse.jetty.server.Request; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class Jetty94RequestAuthenticator extends JettyRequestAuthenticator { - public Jetty94RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort, Request request) { - super(facade, deployment, tokenStore, sslRedirectPort, request); - } - - @Override - protected String changeHttpSessionId(boolean create) { - Request request = this.request; - HttpSession session = request.getSession(false); - if (session == null) { - return request.getSession(true).getId(); - } - if (!deployment.isTurnOffChangeSessionIdOnLogin()) return request.changeSessionId(); - else return session.getId(); - } -} diff --git a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94SessionManager.java b/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94SessionManager.java deleted file mode 100755 index 044e30cd8f1e..000000000000 --- a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/Jetty94SessionManager.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty; - -import org.eclipse.jetty.server.session.Session; -import org.eclipse.jetty.server.session.SessionHandler; -import org.keycloak.adapters.jetty.spi.JettySessionManager; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class Jetty94SessionManager implements JettySessionManager { - protected SessionHandler sessionHandler; - - public Jetty94SessionManager(SessionHandler sessionHandler) { - this.sessionHandler = sessionHandler; - } - - @Override - public HttpSession getHttpSession(String extendedId) { - // inlined code from sessionHandler.getHttpSession(extendedId) since the method visibility changed to protected - - String id = sessionHandler.getSessionIdManager().getId(extendedId); - Session session = sessionHandler.getSession(id); - - if (session != null && !session.getExtendedId().equals(extendedId)) { - session.setIdChanged(true); - } - return session; - } -} diff --git a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/JettyAdapterSessionStore.java b/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/JettyAdapterSessionStore.java deleted file mode 100644 index 642bdf566394..000000000000 --- a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/JettyAdapterSessionStore.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty; - -import org.eclipse.jetty.security.authentication.FormAuthenticator; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.util.MultiMap; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.common.util.MultivaluedHashMap; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettyAdapterSessionStore implements AdapterSessionStore { - public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS"; - protected Request myRequest; - - public JettyAdapterSessionStore(Request request) { - this.myRequest = request; // for IDE/compilation purposes - } - - protected MultiMap extractFormParameters(Request base_request) { - MultiMap formParameters = new MultiMap(); - base_request.extractFormParameters(formParameters); - return formParameters; - } - protected void restoreFormParameters(MultiMap j_post, Request base_request) { - base_request.setContentParameters(j_post); - } - - public boolean restoreRequest() { - HttpSession session = myRequest.getSession(false); - if (session == null) return false; - synchronized (session) { - String j_uri = (String) session.getAttribute(FormAuthenticator.__J_URI); - if (j_uri != null) { - // check if the request is for the same url as the original and restore - // params if it was a post - StringBuffer buf = myRequest.getRequestURL(); - if (myRequest.getQueryString() != null) - buf.append("?").append(myRequest.getQueryString()); - if (j_uri.equals(buf.toString())) { - String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD); - myRequest.setMethod(method); - MultivaluedHashMap j_post = (MultivaluedHashMap) session.getAttribute(CACHED_FORM_PARAMETERS); - if (j_post != null) { - myRequest.setContentType("application/x-www-form-urlencoded"); - MultiMap map = new MultiMap(); - for (String key : j_post.keySet()) { - for (String val : j_post.getList(key)) { - map.add(key, val); - } - } - restoreFormParameters(map, myRequest); - } - session.removeAttribute(FormAuthenticator.__J_URI); - session.removeAttribute(JettyHttpFacade.__J_METHOD); - session.removeAttribute(FormAuthenticator.__J_POST); - } - return true; - } - } - return false; - } - - public void saveRequest() { - // remember the current URI - HttpSession session = myRequest.getSession(); - synchronized (session) { - // But only if it is not set already, or we save every uri that leads to a login form redirect - if (session.getAttribute(FormAuthenticator.__J_URI) == null) { - StringBuffer buf = myRequest.getRequestURL(); - if (myRequest.getQueryString() != null) - buf.append("?").append(myRequest.getQueryString()); - session.setAttribute(FormAuthenticator.__J_URI, buf.toString()); - session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod()); - - if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) { - MultiMap formParameters = extractFormParameters(myRequest); - MultivaluedHashMap map = new MultivaluedHashMap(); - for (String key : formParameters.keySet()) { - for (Object value : formParameters.getValues(key)) { - map.add(key, (String) value); - } - } - session.setAttribute(CACHED_FORM_PARAMETERS, map); - } - } - } - } - -} diff --git a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/KeycloakJettyAuthenticator.java b/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/KeycloakJettyAuthenticator.java deleted file mode 100644 index 3ae9327cfec5..000000000000 --- a/adapters/oidc/jetty/jetty9.4/src/main/java/org/keycloak/adapters/jetty/KeycloakJettyAuthenticator.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty; - -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.UserIdentity; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator; -import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator; -import org.keycloak.adapters.jetty.core.JettySessionTokenStore; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; - -import javax.servlet.ServletRequest; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticator { - - public KeycloakJettyAuthenticator() { - super(); - } - - - @Override - protected Request resolveRequest(ServletRequest req) { - return Request.getBaseRequest(req); - } - - @Override - protected Authentication createAuthentication(UserIdentity userIdentity, final Request request) { - return new KeycloakAuthentication(getAuthMethod(), userIdentity) { - @Override - public Authentication logout(ServletRequest servletRequest) { - logoutCurrent((Request) servletRequest); - return super.logout(servletRequest); - } - }; - } - - @Override - public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) { - return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request)); - } - - @Override - public JettyUserSessionManagement createSessionManagement(Request request) { - return new JettyUserSessionManagement(new Jetty94SessionManager(request.getSessionHandler())); - } - - @Override - protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade, - KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - return new Jetty94RequestAuthenticator(facade, deployment, tokenStore, -1, request); - } - -} diff --git a/adapters/oidc/jetty/pom.xml b/adapters/oidc/jetty/pom.xml deleted file mode 100755 index 30b8b7706bea..000000000000 --- a/adapters/oidc/jetty/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - Keycloak Jetty Integration - - 4.0.0 - - keycloak-jetty-integration-pom - pom - - - jetty-core - jetty9.4 - - diff --git a/adapters/oidc/js/pom.xml b/adapters/oidc/js/pom.xml deleted file mode 100644 index f2f1488e0333..000000000000 --- a/adapters/oidc/js/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - 4.0.0 - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-js-adapter-jar - - Keycloak JavaScript Adapter (JAR) - A build of the Keycloak JavaScript adapter that puts the compiled result in a JAR (for inclusion in the Keycloak server). - - - ../../../js - ${js.projectDir}/libs/keycloak-js - ${js.adapter.projectDir}/dist - - ${maven.multiModuleProjectDirectory}/js/libs/keycloak-js/node_modules - - ${maven.multiModuleProjectDirectory}/js/libs/keycloak-js/dist - - - - - - ${js.adapter.distDir} - - *.js - - - - - - - maven-clean-plugin - - - - ${js.adapter.distDir} - - *.d.ts - - - - - - - com.github.eirslett - frontend-maven-plugin - - - - install-node-and-pnpm - - - - pnpm-install - - pnpm - - - ${pnpm.args.install} - - - - run-build - - pnpm - - - run build - - - - - ${js.projectDir}/libs/keycloak-js - - - - - \ No newline at end of file diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml deleted file mode 100755 index f4e37273be18..000000000000 --- a/adapters/oidc/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../pom.xml - - Keycloak OIDC Client Adapter Modules - - 4.0.0 - - keycloak-oidc-client-adapter-pom - pom - - - adapter-core - installed - jaxrs-oauth-client - jetty - js - servlet-filter - jakarta-servlet-filter - spring-boot2 - spring-boot-adapter-core - spring-boot-container-bundle - spring-security - tomcat - undertow - wildfly - wildfly-elytron - - diff --git a/adapters/oidc/servlet-filter/pom.xml b/adapters/oidc/servlet-filter/pom.xml deleted file mode 100755 index 1e26928aa9b0..000000000000 --- a/adapters/oidc/servlet-filter/pom.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-servlet-filter-adapter - Keycloak Servlet Filter Adapter Integration - - - - - org.keycloak.adapters.servlet.* - - - javax.servlet.*;version="[3.1,5)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-servlet-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.keycloak - keycloak-policy-enforcer - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java deleted file mode 100755 index 11bd5aed0ea2..000000000000 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.KeycloakAccount; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import java.security.Principal; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author Davide Ungari - * @version $Revision: 1 $ - */ -public class FilterRequestAuthenticator extends RequestAuthenticator { - private static final Logger log = Logger.getLogger(""+FilterRequestAuthenticator.class); - protected HttpServletRequest request; - - public FilterRequestAuthenticator(KeycloakDeployment deployment, - AdapterTokenStore tokenStore, - OIDCHttpFacade facade, - HttpServletRequest request, - int sslRedirectPort) { - super(facade, deployment, tokenStore, sslRedirectPort); - this.request = request; - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { - final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - OidcKeycloakAccount account = new OidcKeycloakAccount() { - - @Override - public Principal getPrincipal() { - return skp; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public KeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - }; - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - this.tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(final KeycloakPrincipal principal, String method) { - final RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - if (log.isLoggable(Level.FINE)) { - log.fine("Completing bearer authentication. Bearer roles: " + roles); - } - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - OidcKeycloakAccount account = new OidcKeycloakAccount() { - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public KeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - }; - // need this here to obtain UserPrincipal - request.setAttribute(KeycloakAccount.class.getName(), account); - } - - @Override - protected String changeHttpSessionId(boolean create) { - HttpSession session = request.getSession(create); - return session != null ? session.getId() : null; - } - -} diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java deleted file mode 100755 index e8e7f8dd19a0..000000000000 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.spi.UserSessionManagement; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class KeycloakOIDCFilter implements Filter { - - private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class); - - public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern"; - - public static final String ID_MAPPER_PARAM = "keycloak.config.idMapper"; - - public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver"; - - public static final String CONFIG_FILE_PARAM = "keycloak.config.file"; - - public static final String CONFIG_PATH_PARAM = "keycloak.config.path"; - - protected AdapterDeploymentContext deploymentContext; - - protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); - - protected NodesRegistrationManagement nodesRegistrationManagement; - - protected Pattern skipPattern; - - private final KeycloakConfigResolver definedconfigResolver; - - /** - * Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to - * provide the {@code KeycloakDeployment}. - * @param definedconfigResolver the resolver - */ - public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) { - this.definedconfigResolver = definedconfigResolver; - } - - public KeycloakOIDCFilter() { - this(null); - } - - @Override - public void init(final FilterConfig filterConfig) throws ServletException { - String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM); - if (skipPatternDefinition != null) { - skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL); - } - - String idMapperClassName = filterConfig.getInitParameter(ID_MAPPER_PARAM); - if (idMapperClassName != null) { - try { - final Class idMapperClass = getClass().getClassLoader().loadClass(idMapperClassName); - final Constructor idMapperConstructor = idMapperClass.getDeclaredConstructor(); - Object idMapperInstance = null; - // for KEYCLOAK-13745 test - if (idMapperConstructor.getModifiers() == Modifier.PRIVATE) { - idMapperInstance = idMapperClass.getMethod("getInstance").invoke(null); - } else { - idMapperInstance = idMapperConstructor.newInstance(); - } - if(idMapperInstance instanceof SessionIdMapper) { - this.idMapper = (SessionIdMapper) idMapperInstance; - } else { - log.log(Level.WARNING, "SessionIdMapper class {0} is not instance of org.keycloak.adapters.spi.SessionIdMapper", idMapperClassName); - } - } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { - log.log(Level.WARNING, "SessionIdMapper class could not be instanced", e); - } - } - - if (definedconfigResolver != null) { - deploymentContext = new AdapterDeploymentContext(definedconfigResolver); - log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass()); - } else { - String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM); - if (configResolverClass != null) { - try { - KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new AdapterDeploymentContext(configResolver); - log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } - } else { - String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM); - InputStream is = null; - if (fp != null) { - try { - is = new FileInputStream(fp); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } else { - String path = "/WEB-INF/keycloak.json"; - String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM); - if (pathParam != null) path = pathParam; - is = filterConfig.getServletContext().getResourceAsStream(path); - } - KeycloakDeployment kd = createKeycloakDeploymentFrom(is); - deploymentContext = new AdapterDeploymentContext(kd); - log.fine("Keycloak is using a per-deployment configuration."); - } - } - filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); - nodesRegistrationManagement = new NodesRegistrationManagement(); - } - - private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) { - if (is == null) { - log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - return new KeycloakDeployment(); - } - return KeycloakDeploymentBuilder.build(is); - } - - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - log.fine("Keycloak OIDC Filter"); - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - if (shouldSkip(request)) { - chain.doFilter(req, res); - return; - } - - OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - response.sendError(403); - log.fine("deployment not configured"); - return; - } - - PreAuthActionsHandler preActions = new PreAuthActionsHandler(new IdMapperUserSessionManagement(), deploymentContext, facade); - - if (preActions.handleRequest()) { - //System.err.println("**************** preActions.handleRequest happened!"); - return; - } - - - nodesRegistrationManagement.tryRegister(deployment); - OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, deployment, idMapper); - tokenStore.checkCurrentToken(); - - - FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(deployment, tokenStore, facade, request, 8443); - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - log.fine("AUTHENTICATED"); - if (facade.isEnded()) { - return; - } - AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade); - if (actions.handledRequest()) { - return; - } else { - HttpServletRequestWrapper wrapper = tokenStore.buildWrapper(); - chain.doFilter(wrapper, res); - return; - } - } - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - log.fine("challenge"); - challenge.challenge(facade); - return; - } - response.sendError(403); - - } - - /** - * Decides whether this {@link Filter} should skip the given {@link HttpServletRequest} based on the configured {@link KeycloakOIDCFilter#skipPattern}. - * Patterns are matched against the {@link HttpServletRequest#getRequestURI() requestURI} of a request without the context-path. - * A request for {@code /myapp/index.html} would be tested with {@code /index.html} against the skip pattern. - * Skipped requests will not be processed further by {@link KeycloakOIDCFilter} and immediately delegated to the {@link FilterChain}. - * - * @param request the request to check - * @return {@code true} if the request should not be handled, - * {@code false} otherwise. - */ - private boolean shouldSkip(HttpServletRequest request) { - - if (skipPattern == null) { - return false; - } - - String requestPath = request.getRequestURI().substring(request.getContextPath().length()); - return skipPattern.matcher(requestPath).matches(); - } - - @Override - public void destroy() { - - } - - private class IdMapperUserSessionManagement implements UserSessionManagement { - @Override - public void logoutAll() { - if (idMapper != null) { - idMapper.clear(); - } - } - - @Override - public void logoutHttpSessions(List ids) { - log.fine("**************** logoutHttpSessions"); - //System.err.println("**************** logoutHttpSessions"); - for (String id : ids) { - log.finest("removed idMapper: " + id); - idMapper.removeSession(id); - } - - } - } -} diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java deleted file mode 100755 index 991c85fdf499..000000000000 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.common.util.DelegatingSerializationFilter; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpSession; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; -import java.security.Principal; -import java.util.Set; -import java.util.logging.Logger; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCFilterSessionStore extends FilterSessionStore implements AdapterTokenStore { - protected final KeycloakDeployment deployment; - private static final Logger log = Logger.getLogger("" + OIDCFilterSessionStore.class); - protected final SessionIdMapper idMapper; - - public OIDCFilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, KeycloakDeployment deployment, SessionIdMapper idMapper) { - super(request, facade, maxBuffer); - this.deployment = deployment; - this.idMapper = idMapper; - } - - public HttpServletRequestWrapper buildWrapper() { - HttpSession session = request.getSession(false); - KeycloakAccount account = null; - if (session != null) { - account = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName()); - if (account == null) { - account = (KeycloakAccount) request.getAttribute(KeycloakAccount.class.getName()); - } - } - if (account == null) { - account = (KeycloakAccount) request.getAttribute(KeycloakAccount.class.getName()); - } - return buildWrapper(session, account); - } - - @Override - public void checkCurrentToken() { - HttpSession httpSession = request.getSession(false); - if (httpSession == null) return; - SerializableKeycloakAccount account = (SerializableKeycloakAccount)httpSession.getAttribute(KeycloakAccount.class.getName()); - if (account == null) { - return; - } - - RefreshableKeycloakSecurityContext session = account.getKeycloakSecurityContext(); - if (session == null) return; - - // just in case session got serialized - if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return; - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - //log.fine("Cleanup and expire session " + httpSession.getId() + " after failed refresh"); - cleanSession(httpSession); - httpSession.invalidate(); - } - - protected void cleanSession(HttpSession session) { - session.removeAttribute(KeycloakAccount.class.getName()); - session.removeAttribute(KeycloakSecurityContext.class.getName()); - clearSavedRequest(session); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - HttpSession httpSession = request.getSession(false); - if (httpSession == null) return false; - SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName()); - if (account == null) { - return false; - } - - log.fine("remote logged in already. Establish state from session"); - - RefreshableKeycloakSecurityContext securityContext = account.getKeycloakSecurityContext(); - - if (!deployment.getRealm().equals(securityContext.getRealm())) { - log.fine("Account from cookie is from a different realm than for the request."); - cleanSession(httpSession); - return false; - } - - if (idMapper != null && !idMapper.hasSession(httpSession.getId())) { - log.fine("idMapper does not have session: " + httpSession.getId()); - //System.err.println("idMapper does not have session: " + httpSession.getId()); - cleanSession(httpSession); - return false; - } - - - securityContext.setCurrentRequestInfo(deployment, this); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - needRequestRestore = restoreRequest(); - return true; - } - - public static class SerializableKeycloakAccount implements OidcKeycloakAccount, Serializable { - protected Set roles; - protected Principal principal; - protected RefreshableKeycloakSecurityContext securityContext; - - public SerializableKeycloakAccount(Set roles, Principal principal, RefreshableKeycloakSecurityContext securityContext) { - this.roles = roles; - this.principal = principal; - this.securityContext = securityContext; - } - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - DelegatingSerializationFilter.builder() - .addAllowedClass(OIDCFilterSessionStore.SerializableKeycloakAccount.class) - .addAllowedClass(RefreshableKeycloakSecurityContext.class) - .addAllowedClass(KeycloakSecurityContext.class) - .addAllowedClass(KeycloakPrincipal.class) - .setFilter(in); - - in.defaultReadObject(); - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); - Set roles = account.getRoles(); - - SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext); - HttpSession httpSession = request.getSession(); - httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount); - httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext()); - if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getSessionState(), account.getPrincipal().getName(), httpSession.getId()); - //String username = securityContext.getToken().getSubject(); - //log.fine("userSessionManagement.login: " + username); - } - - @Override - public void logout() { - HttpSession httpSession = request.getSession(false); - if (httpSession != null) { - SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName()); - if (account != null) { - account.getKeycloakSecurityContext().logout(deployment); - } - cleanSession(httpSession); - } - } - - @Override - public void servletRequestLogout() { - logout(); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } -} diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java deleted file mode 100755 index 1d632aa760dd..000000000000 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCServletHttpFacade extends ServletHttpFacade implements OIDCHttpFacade { - - public OIDCServletHttpFacade(HttpServletRequest request, HttpServletResponse response) { - super(request, response); - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); - } -} diff --git a/adapters/oidc/spring-boot-adapter-core/pom.xml b/adapters/oidc/spring-boot-adapter-core/pom.xml deleted file mode 100755 index ea1f9c6a6736..000000000000 --- a/adapters/oidc/spring-boot-adapter-core/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-spring-boot-adapter-core - Keycloak Spring Boot Adapter Core - - - - 2.0.5.RELEASE - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - spring-boot-container-bundle - ${project.version} - true - compile - - - org.keycloak - keycloak-spring-security-adapter - ${project.version} - compile - true - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - compile - true - - - io.undertow - undertow-servlet - compile - true - - - org.eclipse.jetty - jetty-server - ${jetty9.version} - compile - true - - - - org.eclipse.jetty - jetty-security - ${jetty9.version} - compile - true - - - - org.eclipse.jetty - jetty-webapp - ${jetty9.version} - compile - true - - - junit - junit - test - - - org.springframework.boot - spring-boot-configuration-processor - true - ${spring-boot.version} - - - diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java deleted file mode 100755 index 92234c106b8c..000000000000 --- a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2016 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.adapters.springboot; - -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.SecurityInfo.EmptyRoleSemantic; -import io.undertow.servlet.api.WebResourceCollection; -import org.apache.catalina.Context; -import org.apache.tomcat.util.descriptor.web.LoginConfig; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.HandlerList; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.security.Constraint; -import org.eclipse.jetty.webapp.WebAppContext; -import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator; -import org.keycloak.adapters.undertow.KeycloakServletExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -/** - * Keycloak authentication base integration for Spring Boot - base to be extended for particular boot versions. - */ -public class KeycloakBaseSpringBootConfiguration { - - protected KeycloakSpringBootProperties keycloakProperties; - - @Autowired - public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; - KeycloakSpringBootConfigResolverWrapper.setAdapterConfig(keycloakProperties); - } - - @Autowired - public void setApplicationContext(ApplicationContext context) { - KeycloakSpringBootConfigResolverWrapper.setApplicationContext(context); - } - - static class KeycloakBaseUndertowDeploymentInfoCustomizer { - - protected final KeycloakSpringBootProperties keycloakProperties; - - public KeycloakBaseUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; - } - - public void customize(DeploymentInfo deploymentInfo) { - - io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm()); - loginConfig.addFirstAuthMethod("KEYCLOAK"); - - deploymentInfo.setLoginConfig(loginConfig); - - deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolverWrapper.class.getName()); - - - /* Support for '*' as all roles allowed - * We clear out the role in the SecurityConstraints - * and set the EmptyRoleSemantic to Authenticate - * But we will set EmptyRoleSemantic to DENY (default) - * if roles are non existing or left empty - */ - Iterator it = this.getSecurityConstraints().iterator(); - while (it.hasNext()) { - io.undertow.servlet.api.SecurityConstraint securityConstraint = it.next(); - Set rolesAllowed = securityConstraint.getRolesAllowed(); - - if (rolesAllowed.contains("*") || rolesAllowed.contains("**") ) { - io.undertow.servlet.api.SecurityConstraint allRolesAllowed = new io.undertow.servlet.api.SecurityConstraint(); - allRolesAllowed.setEmptyRoleSemantic(EmptyRoleSemantic.AUTHENTICATE); - allRolesAllowed.setTransportGuaranteeType(securityConstraint.getTransportGuaranteeType()); - for (WebResourceCollection wr : securityConstraint.getWebResourceCollections()) { - allRolesAllowed.addWebResourceCollection(wr); - } - deploymentInfo.addSecurityConstraint(allRolesAllowed); - } else // left empty will fall back on default EmptyRoleSemantic.DENY - deploymentInfo.addSecurityConstraint(securityConstraint); - - } - deploymentInfo.addServletExtension(new KeycloakServletExtension()); - } - - private List getSecurityConstraints() { - - List undertowSecurityConstraints = new ArrayList(); - for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { - - io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint(); - undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles()); - - for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) { - - WebResourceCollection webResourceCollection = new WebResourceCollection(); - webResourceCollection.addHttpMethods(collectionDefinition.getMethods()); - webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods()); - webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns()); - - undertowSecurityConstraint.addWebResourceCollections(webResourceCollection); - - } - - undertowSecurityConstraints.add(undertowSecurityConstraint); - } - return undertowSecurityConstraints; - } - } - - static class KeycloakBaseJettyServerCustomizer { - - protected final KeycloakSpringBootProperties keycloakProperties; - - public KeycloakBaseJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; - } - - public void customize(Server server) { - - KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator(); - keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolverWrapper()); - - /* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example - on how to map servlet spec to Constraints */ - - List jettyConstraintMappings = new ArrayList(); - for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { - - for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition - .getSecurityCollections()) { - // securityCollection matches servlet spec's web-resource-collection - Constraint jettyConstraint = new Constraint(); - - if (constraintDefinition.getAuthRoles().size() > 0) { - jettyConstraint.setAuthenticate(true); - jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0])); - } - - jettyConstraint.setName(securityCollectionDefinition.getName()); - - // according to the servlet spec each security-constraint has at least one URL pattern - for(String pattern : securityCollectionDefinition.getPatterns()) { - - /* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method, - but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed - mapping but only one mapping in the cases of omitted methods or no methods. - */ - - if (securityCollectionDefinition.getMethods().size() > 0) { - // according to the servlet spec we have either methods ... - for(String method : securityCollectionDefinition.getMethods()) { - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - jettyConstraintMapping.setMethod(method); - } - } else if (securityCollectionDefinition.getOmittedMethods().size() > 0){ - // ... omitted methods ... - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - jettyConstraintMapping.setMethodOmissions( - securityCollectionDefinition.getOmittedMethods().toArray(new String[0])); - } else { - // ... or no methods at all - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - } - - } - - } - } - - WebAppContext webAppContext = server.getBean(WebAppContext.class); - //if not found as registered bean let's try the handler - if(webAppContext==null){ - webAppContext = getWebAppContext(server.getHandlers()); - } - - ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); - securityHandler.setConstraintMappings(jettyConstraintMappings); - securityHandler.setAuthenticator(keycloakJettyAuthenticator); - - webAppContext.setSecurityHandler(securityHandler); - } - - private WebAppContext getWebAppContext(Handler... handlers) { - for (Handler handler : handlers) { - if (handler instanceof WebAppContext) { - return (WebAppContext) handler; - } else if (handler instanceof HandlerList) { - return getWebAppContext(((HandlerList) handler).getHandlers()); - } else if (handler instanceof HandlerCollection) { - return getWebAppContext(((HandlerCollection) handler).getHandlers()); - } else if (handler instanceof HandlerWrapper) { - return getWebAppContext(((HandlerWrapper) handler).getHandlers()); - } - } - throw new RuntimeException("No WebAppContext found in Jetty server handlers"); - } - } - - static class KeycloakBaseTomcatContextCustomizer { - - protected final KeycloakSpringBootProperties keycloakProperties; - - public KeycloakBaseTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; - } - - public void customize(Context context) { - LoginConfig loginConfig = new LoginConfig(); - loginConfig.setAuthMethod("KEYCLOAK"); - context.setLoginConfig(loginConfig); - - Set authRoles = new HashSet(); - for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { - for (String authRole : constraint.getAuthRoles()) { - if (!authRoles.contains(authRole)) { - context.addSecurityRole(authRole); - authRoles.add(authRole); - } - } - } - - for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { - SecurityConstraint tomcatConstraint = new SecurityConstraint(); - for (String authRole : constraint.getAuthRoles()) { - tomcatConstraint.addAuthRole(authRole); - if(authRole.equals("*") || authRole.equals("**")) { - // For some reasons embed tomcat don't set the auth constraint on true when wildcard is used - tomcatConstraint.setAuthConstraint(true); - } - } - - for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) { - SecurityCollection tomcatSecCollection = new SecurityCollection(); - - if (collection.getName() != null) { - tomcatSecCollection.setName(collection.getName()); - } - if (collection.getDescription() != null) { - tomcatSecCollection.setDescription(collection.getDescription()); - } - - for (String pattern : collection.getPatterns()) { - tomcatSecCollection.addPattern(pattern); - } - - for (String method : collection.getMethods()) { - tomcatSecCollection.addMethod(method); - } - - for (String method : collection.getOmittedMethods()) { - tomcatSecCollection.addOmittedMethod(method); - } - - tomcatConstraint.addCollection(tomcatSecCollection); - } - - context.addConstraint(tomcatConstraint); - } - - context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolverWrapper.class.getName()); - } - } -} diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java deleted file mode 100755 index bd425e2650d5..000000000000 --- a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 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.adapters.springboot; - -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.representations.adapters.config.AdapterConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class KeycloakSpringBootConfigResolver implements org.keycloak.adapters.KeycloakConfigResolver { - - private KeycloakDeployment keycloakDeployment; - - @Autowired(required=false) - private AdapterConfig adapterConfig; - - @Override - public KeycloakDeployment resolve(OIDCHttpFacade.Request request) { - if (keycloakDeployment != null) { - return keycloakDeployment; - } - - keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfig); - - return keycloakDeployment; - } - - void setAdapterConfig(AdapterConfig adapterConfig) { - this.adapterConfig = adapterConfig; - } -} diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolverWrapper.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolverWrapper.java deleted file mode 100644 index c14dbfe6cd0d..000000000000 --- a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolverWrapper.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 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.adapters.springboot; - -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.springsecurity.config.KeycloakSpringConfigResolverWrapper; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; - -/** - *

A specific implementation of {@link KeycloakSpringConfigResolverWrapper} that first tries to register any {@link KeycloakConfigResolver} - * instance provided by the application. if none is provided, {@link KeycloakSpringBootConfigResolver} is set. - * - *

This implementation is specially useful when using Spring Boot and Spring Security in the same application where the same {@link KeycloakConfigResolver} - * instance must be used across the different stacks. - * - * @author Pedro Igor - */ -public class KeycloakSpringBootConfigResolverWrapper extends KeycloakSpringConfigResolverWrapper { - - private static ApplicationContext context; - private static KeycloakSpringBootProperties adapterConfig; - - public KeycloakSpringBootConfigResolverWrapper() { - super(new KeycloakSpringBootConfigResolver()); - try { - setDelegate(context.getBean(KeycloakConfigResolver.class)); - } catch (NoSuchBeanDefinitionException ignore) { - } - if (getDelegate() instanceof KeycloakSpringBootConfigResolver) { - KeycloakSpringBootConfigResolver.class.cast(getDelegate()).setAdapterConfig(adapterConfig); - } - } - - public static void setApplicationContext(ApplicationContext context) { - KeycloakSpringBootConfigResolverWrapper.context = context; - } - - public static void setAdapterConfig(KeycloakSpringBootProperties adapterConfig) { - KeycloakSpringBootConfigResolverWrapper.adapterConfig = adapterConfig; - } -} diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java deleted file mode 100644 index 9c28a9a73113..000000000000 --- a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2016 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.adapters.springboot; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.keycloak.representations.adapters.config.AdapterConfig; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@ConfigurationProperties(prefix = "keycloak", ignoreUnknownFields = false) -public class KeycloakSpringBootProperties extends AdapterConfig { - - /* this is a dummy property to avoid re-rebinding problem with property keycloak.config.resolver - when using spring cloud - see KEYCLOAK-2977 */ - @JsonIgnore - private Map config = new HashMap(); - - /** - * Allow enabling of Keycloak Spring Boot adapter by configuration. - */ - private boolean enabled = true; - - public Map getConfig() { - return config; - } - - /** - * To provide Java EE security constraints - */ - private List securityConstraints = new ArrayList(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * This matches security-constraint of the servlet spec - */ - @ConfigurationProperties() - public static class SecurityConstraint { - /** - * A list of security collections - */ - private List securityCollections = new ArrayList(); - private List authRoles = new ArrayList(); - - public List getAuthRoles() { - return authRoles; - } - - public List getSecurityCollections() { - return securityCollections; - } - - public void setSecurityCollections(List securityCollections) { - this.securityCollections = securityCollections; - } - - public void setAuthRoles(List authRoles) { - this.authRoles = authRoles; - } - - } - - /** - * This matches web-resource-collection of the servlet spec - */ - @ConfigurationProperties() - public static class SecurityCollection { - /** - * The name of your security constraint - */ - private String name; - /** - * The description of your security collection - */ - private String description; - /** - * A list of URL patterns that should match to apply the security collection - */ - private List patterns = new ArrayList(); - /** - * A list of HTTP methods that applies for this security collection - */ - private List methods = new ArrayList(); - /** - * A list of HTTP methods that will be omitted for this security collection - */ - private List omittedMethods = new ArrayList(); - - public List getPatterns() { - return patterns; - } - - public List getMethods() { - return methods; - } - - public String getDescription() { - return description; - } - - public String getName() { - return name; - } - - public List getOmittedMethods() { - return omittedMethods; - } - - public void setName(String name) { - this.name = name; - } - - public void setDescription(String description) { - this.description = description; - } - - public void setPatterns(List patterns) { - this.patterns = patterns; - } - - public void setMethods(List methods) { - this.methods = methods; - } - - public void setOmittedMethods(List omittedMethods) { - this.omittedMethods = omittedMethods; - } - } - - public List getSecurityConstraints() { - return securityConstraints; - } - - public void setSecurityConstraints(List securityConstraints) { - this.securityConstraints = securityConstraints; - } -} diff --git a/adapters/oidc/spring-boot-container-bundle/pom.xml b/adapters/oidc/spring-boot-container-bundle/pom.xml deleted file mode 100644 index fd0be595f8d6..000000000000 --- a/adapters/oidc/spring-boot-container-bundle/pom.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - spring-boot-container-bundle - jar - - - org.keycloak - keycloak-adapter-core - compile - - - org.keycloak - keycloak-tomcat-adapter - compile - - - org.keycloak - keycloak-undertow-adapter - compile - - - org.keycloak - keycloak-jetty94-adapter - compile - - - - - - org.apache.maven.plugins - maven-shade-plugin - - - package - - shade - - - - - org.keycloak:keycloak-tomcat-adapter - org.keycloak:keycloak-undertow-adapter - org.keycloak:keycloak-jetty94-adapter - org.keycloak:keycloak-tomcat-core-adapter - org.keycloak:keycloak-tomcat-adapter-spi - org.keycloak:keycloak-undertow-adapter - org.keycloak:keycloak-undertow-adapter-spi - org.keycloak:keycloak-jetty-core - org.keycloak:keycloak-jetty-adapter-spi - - - true - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - org.keycloak:keycloak-adapter-core - - - - - diff --git a/adapters/oidc/spring-boot2/pom.xml b/adapters/oidc/spring-boot2/pom.xml deleted file mode 100755 index 86115f6004fa..000000000000 --- a/adapters/oidc/spring-boot2/pom.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-spring-boot-2-adapter - Keycloak Spring Boot 2 Integration - - - - 2.0.5.RELEASE - 5.0.2.RELEASE - 1.9.5 - - - - - - org.keycloak - keycloak-spring-boot-adapter-core - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - spring-boot-container-bundle - ${project.version} - true - compile - - - org.keycloak - keycloak-spring-security-adapter - ${project.version} - compile - - - com.fasterxml.jackson.core - jackson-databind - provided - - - com.fasterxml.jackson.core - jackson-annotations - provided - - - org.springframework - spring-core - ${spring.version} - provided - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - - - io.undertow - undertow-servlet - compile - true - - - org.eclipse.jetty - jetty-server - ${jetty9.version} - compile - true - - - - org.eclipse.jetty - jetty-security - ${jetty9.version} - compile - true - - - - org.eclipse.jetty - jetty-webapp - ${jetty9.version} - compile - true - - - junit - junit - test - - - org.springframework - spring-test - ${spring.version} - test - - - org.mockito - mockito-all - ${mockito.version} - test - - - org.springframework.boot - spring-boot-configuration-processor - true - ${spring-boot.version} - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - ${spring-boot.version} - - - diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java deleted file mode 100755 index 6b16541a28b0..000000000000 --- a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016 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.adapters.springboot; - -import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; -import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; -import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; -import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; -import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer; -import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; - - - - -/** - * Keycloak authentication integration for Spring Boot 2 - * - */ -@Configuration -@ConditionalOnWebApplication -@EnableConfigurationProperties(KeycloakSpringBootProperties.class) -@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true) -public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration { - - - @Bean - public WebServerFactoryCustomizer getKeycloakContainerCustomizer() { - return new WebServerFactoryCustomizer() { - @Override - public void customize(ConfigurableServletWebServerFactory configurableServletWebServerFactory) { - if(configurableServletWebServerFactory instanceof TomcatServletWebServerFactory){ - - TomcatServletWebServerFactory container = (TomcatServletWebServerFactory)configurableServletWebServerFactory; - container.addContextValves(new KeycloakAuthenticatorValve()); - container.addContextCustomizers(tomcatKeycloakContextCustomizer()); - - } else if (configurableServletWebServerFactory instanceof UndertowServletWebServerFactory){ - - UndertowServletWebServerFactory container = (UndertowServletWebServerFactory)configurableServletWebServerFactory; - container.addDeploymentInfoCustomizers(undertowKeycloakContextCustomizer()); - - } else if (configurableServletWebServerFactory instanceof JettyServletWebServerFactory){ - - JettyServletWebServerFactory container = (JettyServletWebServerFactory)configurableServletWebServerFactory; - container.addServerCustomizers(jettyKeycloakServerCustomizer()); - } - } - - }; - } - - @Bean - @ConditionalOnClass(name = {"org.eclipse.jetty.webapp.WebAppContext"}) - public JettyServerCustomizer jettyKeycloakServerCustomizer() { - return new KeycloakJettyServerCustomizer(keycloakProperties); - } - - @Bean - @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"}) - public TomcatContextCustomizer tomcatKeycloakContextCustomizer() { - return new KeycloakTomcatContextCustomizer(keycloakProperties); - } - - @Bean - @ConditionalOnClass(name = {"io.undertow.Undertow"}) - public UndertowDeploymentInfoCustomizer undertowKeycloakContextCustomizer() { - return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties); - } - - static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer { - - - public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) { - super(keycloakProperties); - } - - } - - static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer { - - public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) { - super(keycloakProperties); - } - } - - static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer { - - public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties){ - super(keycloakProperties); - } - } -} diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java deleted file mode 100644 index ae4836c7139f..000000000000 --- a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.keycloak.adapters.springboot.client; - -import org.springframework.boot.web.client.RestTemplateCustomizer; -import org.springframework.web.client.RestTemplate; - -public class KeycloakRestTemplateCustomizer implements RestTemplateCustomizer { - - private final KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor; - - public KeycloakRestTemplateCustomizer() { - this(new KeycloakSecurityContextClientRequestInterceptor()); - } - - protected KeycloakRestTemplateCustomizer( - KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor - ) { - this.keycloakInterceptor = keycloakInterceptor; - } - - @Override - public void customize(RestTemplate restTemplate) { - restTemplate.getInterceptors().add(keycloakInterceptor); - } -} diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java deleted file mode 100644 index 200a9035f1e5..000000000000 --- a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.keycloak.adapters.springboot.client; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.io.IOException; -import java.security.Principal; - -/** - * Interceptor for {@link ClientHttpRequestExecution} objects created for server to server secured - * communication using OAuth2 bearer tokens issued by Keycloak. - * - * @author James McShane - * @version $Revision: 1 $ - */ -public class KeycloakSecurityContextClientRequestInterceptor implements ClientHttpRequestInterceptor { - - private static final String AUTHORIZATION_HEADER = "Authorization"; - - /** - * Returns the {@link KeycloakSecurityContext} from the Spring {@link ServletRequestAttributes}'s {@link Principal}. - * - * The principal must support retrieval of the KeycloakSecurityContext, so at this point, only {@link KeycloakPrincipal} - * values are supported - * - * @return the current KeycloakSecurityContext - */ - protected KeycloakSecurityContext getKeycloakSecurityContext() { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - Principal principal = attributes.getRequest().getUserPrincipal(); - if (principal == null) { - throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal"); - } - if (!(principal instanceof KeycloakPrincipal)) { - throw new IllegalStateException( - String.format( - "Cannot set authorization header because the principal type %s does not provide the KeycloakSecurityContext", - principal.getClass())); - } - return ((KeycloakPrincipal) principal).getKeycloakSecurityContext(); - } - - @Override - public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { - KeycloakSecurityContext context = this.getKeycloakSecurityContext(); - httpRequest.getHeaders().set(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString()); - return clientHttpRequestExecution.execute(httpRequest, bytes); - } -} diff --git a/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories b/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 0c80e3bd8b2f..000000000000 --- a/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.keycloak.adapters.springboot.KeycloakAutoConfiguration \ No newline at end of file diff --git a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java deleted file mode 100644 index e8e599e40d3d..000000000000 --- a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.keycloak.adapters.springboot.client; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.web.client.RestTemplate; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -public class KeycloakRestTemplateCustomizerTest { - - private KeycloakRestTemplateCustomizer customizer; - private KeycloakSecurityContextClientRequestInterceptor interceptor = - mock(KeycloakSecurityContextClientRequestInterceptor.class); - - @Before - public void setup() { - customizer = new KeycloakRestTemplateCustomizer(interceptor); - } - - @Test - public void interceptorIsAddedToRequest() { - RestTemplate restTemplate = new RestTemplate(); - customizer.customize(restTemplate); - assertTrue(restTemplate.getInterceptors().contains(interceptor)); - } - -} diff --git a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java deleted file mode 100644 index 689cc65274dd..000000000000 --- a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 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.adapters.springboot.client; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.security.Principal; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.when; - -/** - * Keycloak spring boot client request factory tests. - */ -public class KeycloakSecurityContextClientRequestInterceptorTest { - - @Spy - private KeycloakSecurityContextClientRequestInterceptor factory; - - private MockHttpServletRequest servletRequest; - - @Mock - private KeycloakSecurityContext keycloakSecurityContext; - - @Mock - private KeycloakPrincipal keycloakPrincipal; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - servletRequest = new MockHttpServletRequest(); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest)); - servletRequest.setUserPrincipal(keycloakPrincipal); - when(keycloakPrincipal.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext); - } - - @Test - public void testGetKeycloakSecurityContext() throws Exception { - KeycloakSecurityContext context = factory.getKeycloakSecurityContext(); - assertNotNull(context); - assertEquals(keycloakSecurityContext, context); - } - - @Test(expected = IllegalStateException.class) - public void testGetKeycloakSecurityContextInvalidPrincipal() throws Exception { - servletRequest.setUserPrincipal(new MarkerPrincipal()); - factory.getKeycloakSecurityContext(); - } - - @Test(expected = IllegalStateException.class) - public void testGetKeycloakSecurityContextNullAuthentication() throws Exception { - servletRequest.setUserPrincipal(null); - factory.getKeycloakSecurityContext(); - } - - private static class MarkerPrincipal implements Principal { - @Override - public String getName() { - return null; - } - } -} diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml deleted file mode 100644 index c85b8180a240..000000000000 --- a/adapters/oidc/spring-security/pom.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-spring-security-adapter - Keycloak Spring Security Integration - - - - 5.2.9.RELEASE - 5.2.9.RELEASE - 1.9.5 - 4.3.6 - - - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.keycloak - keycloak-policy-enforcer - - - org.jboss.spec.javax.servlet - jboss-servlet-api_4.0_spec - provided - - - org.springframework.security - spring-security-config - ${spring-security.version} - true - compile - - - org.springframework.security - spring-security-web - ${spring-security.version} - true - compile - - - org.slf4j - slf4j-api - compile - - - org.jboss.logging - jboss-logging - compile - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - runtime - - - com.fasterxml.jackson.core - jackson-core - compile - - - com.fasterxml.jackson.core - jackson-databind - runtime - - - com.fasterxml.jackson.core - jackson-annotations - runtime - - - - org.springframework - spring-test - ${spring.version} - test - - - junit - junit - test - - - org.mockito - mockito-all - ${mockito.version} - test - - - org.slf4j - slf4j-simple - ${slf4j.version} - test - - - diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java deleted file mode 100644 index 788564a2e159..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.io.Resource; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Objects; - -/** - * {@link FactoryBean} that creates an {@link AdapterDeploymentContext} given a {@link Resource} defining the Keycloak - * client configuration or a {@link KeycloakConfigResolver} for multi-tenant environments. - * - * @author Thomas Raehalme - */ -public class AdapterDeploymentContextFactoryBean - implements FactoryBean, InitializingBean { - private static final Logger log = - LoggerFactory.getLogger(AdapterDeploymentContextFactoryBean.class); - private final Resource keycloakConfigFileResource; - private final KeycloakConfigResolver keycloakConfigResolver; - private AdapterDeploymentContext adapterDeploymentContext; - - public AdapterDeploymentContextFactoryBean(Resource keycloakConfigFileResource) { - this.keycloakConfigFileResource = Objects.requireNonNull(keycloakConfigFileResource); - this.keycloakConfigResolver = null; - } - - public AdapterDeploymentContextFactoryBean(KeycloakConfigResolver keycloakConfigResolver) { - this.keycloakConfigResolver = Objects.requireNonNull(keycloakConfigResolver); - this.keycloakConfigFileResource = null; - } - - @Override - public Class getObjectType() { - return AdapterDeploymentContext.class; - } - - @Override - public boolean isSingleton() { - return true; - } - - @Override - public void afterPropertiesSet() throws Exception { - if (keycloakConfigResolver != null) { - adapterDeploymentContext = new AdapterDeploymentContext(keycloakConfigResolver); - } - else { - log.info("Loading Keycloak deployment from configuration file: {}", keycloakConfigFileResource); - - KeycloakDeployment deployment = loadKeycloakDeployment(); - adapterDeploymentContext = new AdapterDeploymentContext(deployment); - } - } - - private KeycloakDeployment loadKeycloakDeployment() throws IOException { - if (!keycloakConfigFileResource.isReadable()) { - throw new FileNotFoundException(String.format("Unable to locate Keycloak configuration file: %s", - keycloakConfigFileResource.getFilename())); - } - - return KeycloakDeploymentBuilder.build(keycloakConfigFileResource.getInputStream()); - } - - @Override - public AdapterDeploymentContext getObject() throws Exception { - return adapterDeploymentContext; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakAuthenticationException.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakAuthenticationException.java deleted file mode 100644 index 09aef71f8b8b..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakAuthenticationException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity; - -import org.springframework.security.core.AuthenticationException; - -public class KeycloakAuthenticationException extends AuthenticationException { - public KeycloakAuthenticationException(String msg, Throwable t) { - super(msg, t); - } - - public KeycloakAuthenticationException(String msg) { - super(msg); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java deleted file mode 100644 index f434b97baa59..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.keycloak.adapters.springsecurity; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Add this annotation to a class that extends {@code KeycloakWebSecurityConfigurerAdapter} to provide - * a keycloak based Spring security configuration. - * - * @author Hendrik Ebbers - */ -@Retention(value = RUNTIME) -@Target(value = { TYPE }) -@Configuration -@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) -@EnableWebSecurity -public @interface KeycloakConfiguration { -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java deleted file mode 100644 index ec26ac3896b6..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity; - -/** - * Locator interface for Spring context component scanning. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public interface KeycloakSecurityComponents { -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java deleted file mode 100644 index f83549deb9c7..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.account; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.Assert; - -/** - * Represents an authority granted to an {@link Authentication} by the Keycloak server. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakRole implements GrantedAuthority { - - private String role; - - /** - * Creates a new granted authority from the given Keycloak role. - * - * @param role the name of this granted authority - */ - public KeycloakRole(String role) { - Assert.notNull(role, "role cannot be null"); - this.role = role; - } - - @Override - public String getAuthority() { - return role; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof GrantedAuthority)) { - return false; - } - - GrantedAuthority that = (GrantedAuthority) o; - - if (!role.equals(that.getAuthority())) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - return 3 * role.hashCode(); - } - - @Override - public String toString() { - return "KeycloakRole{" + - "role='" + role + '\'' + - '}'; - } - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java deleted file mode 100755 index 95a39c6b51d9..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.account; - -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; - -import java.io.Serializable; -import java.security.Principal; -import java.util.Set; - -/** - * Concrete, serializable {@link org.keycloak.adapters.OidcKeycloakAccount} implementation. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class SimpleKeycloakAccount implements OidcKeycloakAccount, Serializable { - - private Set roles; - private Principal principal; - private RefreshableKeycloakSecurityContext securityContext; - - public SimpleKeycloakAccount(Principal principal, Set roles, RefreshableKeycloakSecurityContext securityContext) { - this.principal = principal; - this.roles = roles; - this.securityContext = securityContext; - } - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java deleted file mode 100644 index 3f18a8bf8e8a..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.apache.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.security.web.util.matcher.RequestMatcher; - -import javax.servlet.http.HttpServletRequest; - -/** - * {@link RequestMatcher} that determines if a given request is an API request or an - * interactive login request. - * - * @author Scott Rossillo - * @see RequestMatcher - */ -public class HttpHeaderInspectingApiRequestMatcher implements RequestMatcher { - - protected static final String X_REQUESTED_WITH_HEADER = "X-Requested-With"; - protected static final String X_REQUESTED_WITH_HEADER_AJAX_VALUE = "XMLHttpRequest"; - - /** - * Returns true if the given request is an API request or false if it's an interactive - * login request. - * - * @param request the HttpServletRequest - * @return true if the given request is an API request; - * false otherwise - */ - @Override - public boolean matches(HttpServletRequest request) { - return X_REQUESTED_WITH_HEADER_AJAX_VALUE.equals(request.getHeader(X_REQUESTED_WITH_HEADER)); - } - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java deleted file mode 100644 index 95717fc2fbbe..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.http.HttpHeaders; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}. Uses a - * {@link RequestMatcher} to determine if the request is an interactive login request or a - * API request, which should not be redirected to an interactive login page. By default, - * this entry point uses a {@link HttpHeaderInspectingApiRequestMatcher} but can be overridden using in the - * constructor. - * - * @author Scott Rossillo - * - * @see HttpHeaderInspectingApiRequestMatcher - */ -public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoint { - - /** - * Default Keycloak authentication login URI - */ - public static final String DEFAULT_LOGIN_URI = "/sso/login"; - private static final String DEFAULT_REALM = "Unknown"; - private static final RequestMatcher DEFAULT_API_REQUEST_MATCHER = new HttpHeaderInspectingApiRequestMatcher(); - - private final static Logger log = LoggerFactory.getLogger(KeycloakAuthenticationEntryPoint.class); - - private final RequestMatcher apiRequestMatcher; - private String loginUri = DEFAULT_LOGIN_URI; - private String realm = DEFAULT_REALM; - - private AdapterDeploymentContext adapterDeploymentContext; - - /** - * Creates a new Keycloak authentication entry point. - */ - public KeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) { - this(adapterDeploymentContext, DEFAULT_API_REQUEST_MATCHER); - } - - /** - * Creates a new Keycloak authentication entry point using the given request - * matcher to determine if the current request is an API request or a browser request. - * - * @param apiRequestMatcher the RequestMatcher to use to determine - * if the current request is an API request or a browser request (required) - */ - public KeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) { - Assert.notNull(apiRequestMatcher, "apiRequestMatcher required"); - Assert.notNull(adapterDeploymentContext, "adapterDeploymentContext required"); - this.adapterDeploymentContext = adapterDeploymentContext; - this.apiRequestMatcher = apiRequestMatcher; - } - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - HttpFacade facade = new SimpleHttpFacade(request, response); - if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) { - commenceUnauthorizedResponse(request, response); - } else { - commenceLoginRedirect(request, response); - } - } - - /** - * Redirects to the login page. If HTTP sessions are disabled, the redirect URL is saved in a - * cookie now, to be retrieved by the {@link KeycloakAuthenticationSuccessHandler} or the - * {@link KeycloakAuthenticationFailureHandler} when the login sequence completes. - */ - protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { - if (request.getSession(false) == null && KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) == null) { - // If no session exists yet at this point, then apparently the redirect URL is not - // stored in a session. We'll store it in a cookie instead. - response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(request.getRequestURI())); - } - - String queryParameters = ""; - if (!StringUtils.isEmpty(request.getQueryString())) { - queryParameters = "?" + request.getQueryString(); - } - - String contextAwareLoginUri = request.getContextPath() + loginUri + queryParameters; - log.debug("Redirecting to login URI {}", contextAwareLoginUri); - response.sendRedirect(contextAwareLoginUri); - } - - protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { - response.addHeader(HttpHeaders.WWW_AUTHENTICATE, String.format("Bearer realm=\"%s\"", realm)); - response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); - } - - public void setLoginUri(String loginUri) { - Assert.notNull(loginUri, "loginUri cannot be null"); - this.loginUri = loginUri; - } - - public void setRealm(String realm) { - Assert.notNull(realm, "realm cannot be null"); - this.realm = realm; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java deleted file mode 100644 index fdb6cb84c18d..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * To return the forbidden code with the corresponding message. - * - * @author emilienbondu - * - */ -public class KeycloakAuthenticationFailureHandler implements AuthenticationFailureHandler { - - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - // Check that the response was not committed yet (this may happen when another - // part of the Keycloak adapter sends a challenge or a redirect). - if (!response.isCommitted()) { - if (KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) != null) { - response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null)); - } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate using the Authorization header"); - } else { - if (200 <= response.getStatus() && response.getStatus() < 300) { - throw new RuntimeException("Success response was committed while authentication failed!", exception); - } - } - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java deleted file mode 100644 index 1ac6515ea76c..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.keycloak.adapters.springsecurity.account.KeycloakRole; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Performs authentication on a {@link KeycloakAuthenticationToken}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakAuthenticationProvider implements AuthenticationProvider { - private GrantedAuthoritiesMapper grantedAuthoritiesMapper; - - public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) { - this.grantedAuthoritiesMapper = grantedAuthoritiesMapper; - } - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication; - List grantedAuthorities = new ArrayList(); - - for (String role : token.getAccount().getRoles()) { - grantedAuthorities.add(new KeycloakRole(role)); - } - return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities)); - } - - private Collection mapAuthorities( - Collection authorities) { - return grantedAuthoritiesMapper != null - ? grantedAuthoritiesMapper.mapAuthorities(authorities) - : authorities; - } - - @Override - public boolean supports(Class aClass) { - return KeycloakAuthenticationToken.class.isAssignableFrom(aClass); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java deleted file mode 100644 index 7a0eab9b6a86..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; - -/** - * Wrapper for an authentication success handler that sends a redirect if a redirect URL was set in - * a cookie. - * - * @author Sjoerd Cranen - * - * @see KeycloakCookieBasedRedirect - * @see KeycloakAuthenticationEntryPoint#commenceLoginRedirect - */ -public class KeycloakAuthenticationSuccessHandler implements AuthenticationSuccessHandler { - - private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationSuccessHandler.class); - - private final AuthenticationSuccessHandler fallback; - - public KeycloakAuthenticationSuccessHandler(AuthenticationSuccessHandler fallback) { - this.fallback = fallback; - } - - @Override - public void onAuthenticationSuccess( - HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException, ServletException { - String location = KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request); - if (location == null) { - if (fallback != null) { - fallback.onAuthenticationSuccess(request, response, authentication); - } - } else { - try { - response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null)); - response.sendRedirect(location); - } catch (IOException e) { - LOG.warn("Unable to redirect user after login", e); - } - } - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java deleted file mode 100644 index b3b8e7e5f781..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; - -/** - * Utility class that provides methods to create and retrieve cookies used for login redirects. - * - * @author Sjoerd Cranen - */ -public final class KeycloakCookieBasedRedirect { - - private static final String REDIRECT_COOKIE = "KC_REDIRECT"; - - private KeycloakCookieBasedRedirect() {} - - /** - * Checks if a cookie with name {@value REDIRECT_COOKIE} exists, and if so, returns its value. - * If multiple cookies of the same name exist, the value of the first cookie is returned. - * - * @param request the request to retrieve the cookie from. - * @return the value of the cookie, if it exists, or else {@code null}. - */ - public static String getRedirectUrlFromCookie(HttpServletRequest request) { - if (request.getCookies() == null) { - return null; - } - for (Cookie cookie : request.getCookies()) { - if (REDIRECT_COOKIE.equals(cookie.getName())) { - return cookie.getValue(); - } - } - return null; - } - - /** - * Creates a cookie with name {@value REDIRECT_COOKIE} and the given URL as value. - * - * @param url the value that the cookie should have. If {@code null}, a cookie is created that - * expires immediately and has an empty string as value. - * @return a cookie that can be added to a response. - */ - public static Cookie createCookieFromRedirectUrl(String url) { - Cookie cookie = new Cookie(REDIRECT_COOKIE, url == null ? "" : url); - cookie.setHttpOnly(true); - cookie.setPath("/"); - if (url == null) { - cookie.setMaxAge(0); - } - return cookie; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java deleted file mode 100644 index 213788655f0c..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.util.Assert; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Logs the current user out of Keycloak. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakLogoutHandler implements LogoutHandler { - - private static final Logger log = LoggerFactory.getLogger(KeycloakLogoutHandler.class); - - private AdapterDeploymentContext adapterDeploymentContext; - private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); - - public KeycloakLogoutHandler(AdapterDeploymentContext adapterDeploymentContext) { - Assert.notNull(adapterDeploymentContext); - this.adapterDeploymentContext = adapterDeploymentContext; - } - - public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory adapterTokenStoreFactory) { - this.adapterTokenStoreFactory = adapterTokenStoreFactory; - } - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - if (authentication == null) { - log.warn("Cannot log out without authentication"); - return; - } - else if (!KeycloakAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { - log.warn("Cannot log out a non-Keycloak authentication: {}", authentication); - return; - } - - handleSingleSignOut(request, response, (KeycloakAuthenticationToken) authentication); - } - - protected void handleSingleSignOut(HttpServletRequest request, HttpServletResponse response, KeycloakAuthenticationToken authenticationToken) { - HttpFacade facade = new SimpleHttpFacade(request, response); - KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade); - adapterTokenStoreFactory.createAdapterTokenStore(deployment, request, response).logout(); - RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) authenticationToken.getAccount().getKeycloakSecurityContext(); - session.logout(deployment); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/RequestAuthenticatorFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/RequestAuthenticatorFactory.java deleted file mode 100644 index e640f1b8eb0c..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/RequestAuthenticatorFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.keycloak.adapters.springsecurity.authentication; - -import javax.servlet.http.HttpServletRequest; - -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * Creates {@link RequestAuthenticator}s. - */ -public interface RequestAuthenticatorFactory { - /** - * Creates new {@link RequestAuthenticator} instances on a per-request basis. - */ - RequestAuthenticator createRequestAuthenticator(HttpFacade facade, HttpServletRequest request, - KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort); -} \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java deleted file mode 100755 index e9b850d992c0..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import java.util.Set; - -/** - * Request authenticator adapter for Spring Security. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class SpringSecurityRequestAuthenticator extends RequestAuthenticator { - - private static final Logger logger = LoggerFactory.getLogger(SpringSecurityRequestAuthenticator.class); - private final HttpServletRequest request; - - /** - * Creates a new Spring Security request authenticator. - * - * @param facade the current HttpFacade (required) - * @param request the current HttpServletRequest (required) - * @param deployment the KeycloakDeployment (required) - * @param tokenStore the AdapterTokenStore (required) - * @param sslRedirectPort the SSL redirect port (required) - */ - public SpringSecurityRequestAuthenticator( - HttpFacade facade, - HttpServletRequest request, - KeycloakDeployment deployment, - AdapterTokenStore tokenStore, - int sslRedirectPort) { - - super(facade, deployment, tokenStore, sslRedirectPort); - this.request = request; - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(final KeycloakPrincipal principal) { - - final RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - final OidcKeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext); - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - this.tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { - - RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext); - - logger.debug("Completing bearer authentication. Bearer roles: {} ",roles); - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(new KeycloakAuthenticationToken(account, false)); - SecurityContextHolder.setContext(context); - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - - @Override - protected String changeHttpSessionId(boolean create) { - HttpSession session = request.getSession(create); - return session != null ? session.getId() : null; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorFactory.java deleted file mode 100644 index 15aed06d4d22..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.adapters.springsecurity.authentication; - -import javax.servlet.http.HttpServletRequest; - -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -public class SpringSecurityRequestAuthenticatorFactory implements RequestAuthenticatorFactory { - - @Override - public RequestAuthenticator createRequestAuthenticator(HttpFacade facade, - HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) { - return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort); - } -} \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java deleted file mode 100644 index a5954694e0c8..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.client; - -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.impl.client.HttpClients; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Scope; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -/** - * Factory for {@link ClientHttpRequest} objects created for server to server secured - * communication using OAuth2 bearer tokens issued by Keycloak. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -@Component -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) -public class KeycloakClientRequestFactory extends HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory { - - public static final String AUTHORIZATION_HEADER = "Authorization"; - - public KeycloakClientRequestFactory() { - super(HttpClients.custom() - .disableCookieManagement() - .build() - ); - } - - @Override - protected void postProcessHttpRequest(HttpUriRequest request) { - KeycloakSecurityContext context = this.getKeycloakSecurityContext(); - request.setHeader(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString()); - } - - /** - * Returns the {@link KeycloakSecurityContext} from the Spring {@link SecurityContextHolder}'s {@link Authentication}. - * - * @return the current KeycloakSecurityContext - */ - protected KeycloakSecurityContext getKeycloakSecurityContext() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - KeycloakAuthenticationToken token; - KeycloakSecurityContext context; - - if (authentication == null) { - throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal"); - } - - if (!KeycloakAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { - throw new IllegalStateException( - String.format( - "Cannot set authorization header because Authentication is of type %s but %s is required", - authentication.getClass(), KeycloakAuthenticationToken.class) - ); - } - - token = (KeycloakAuthenticationToken) authentication; - context = token.getAccount().getKeycloakSecurityContext(); - - return context; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java deleted file mode 100644 index 7780d59a94aa..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.client; - -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; - -/** - * Extends Spring's central class for client-side HTTP access, {@link RestTemplate}, adding - * automatic authentication for service to service calls using the currently authenticated Keycloak principal. - * This class is designed to work with other services secured by Keycloak. - * - *

- * The main advantage to using this class over Spring's RestTemplate is that authentication - * is handled automatically when both the service making the API call and the service being called are - * protected by Keycloak authentication. - *

- * - * @see RestOperations - * @see RestTemplate - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakRestTemplate extends RestTemplate implements RestOperations { - - /** - * Create a new instance based on the given {@link KeycloakClientRequestFactory}. - * - * @param factory the KeycloakClientRequestFactory to use when creating new requests - */ - public KeycloakRestTemplate(KeycloakClientRequestFactory factory) { - super(factory); - } - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakSpringConfigResolverWrapper.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakSpringConfigResolverWrapper.java deleted file mode 100644 index 4fa0bba7f628..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakSpringConfigResolverWrapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2018 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.adapters.springsecurity.config; - -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * Spring applications may use different security stacks in order to enforce access based on the configuration provided - * by a {@code KeycloakDeployment}. This implementation of {@code KeycloakConfigResolver} wraps and avoid calling multiple - * {@code KeycloakConfigResolver} instances but only those defined by applications or set as default by the configuration. - * - * @author Pedro Igor - */ -public class KeycloakSpringConfigResolverWrapper implements KeycloakConfigResolver { - - private KeycloakConfigResolver delegate; - - public KeycloakSpringConfigResolverWrapper(KeycloakConfigResolver delegate) { - this.delegate = delegate; - } - - @Override - public KeycloakDeployment resolve(HttpFacade.Request facade) { - return delegate.resolve(facade); - } - - protected void setDelegate(KeycloakConfigResolver delegate) { - this.delegate = delegate; - } - - protected KeycloakConfigResolver getDelegate() { - return delegate; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java deleted file mode 100644 index 034189db1fc1..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.config; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextFactoryBean; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler; -import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakCsrfRequestMatcher; -import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter; -import org.keycloak.adapters.springsecurity.management.HttpSessionManager; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.core.io.Resource; -import org.springframework.security.config.annotation.web.WebSecurityConfigurer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; - -/** - * Provides a convenient base class for creating a {@link WebSecurityConfigurer} - * instance secured by Keycloak. This implementation allows customization by overriding methods. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - * @see EnableWebSecurity - * @see EnableWebMvcSecurity - */ -public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter implements WebSecurityConfigurer { - - @Value("${keycloak.configurationFile:WEB-INF/keycloak.json}") - private Resource keycloakConfigFileResource; - @Autowired(required = false) - private KeycloakConfigResolver keycloakConfigResolver; - - @Bean - protected AdapterDeploymentContext adapterDeploymentContext() throws Exception { - AdapterDeploymentContextFactoryBean factoryBean; - if (keycloakConfigResolver != null) { - factoryBean = new AdapterDeploymentContextFactoryBean(new KeycloakSpringConfigResolverWrapper(keycloakConfigResolver)); - } - else { - factoryBean = new AdapterDeploymentContextFactoryBean(keycloakConfigFileResource); - } - factoryBean.afterPropertiesSet(); - return factoryBean.getObject(); - } - - protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception { - return new KeycloakAuthenticationEntryPoint(adapterDeploymentContext()); - } - - protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { - return new KeycloakAuthenticationProvider(); - } - - @Bean - protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception { - KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean()); - filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy()); - return filter; - } - - @Bean - protected KeycloakPreAuthActionsFilter keycloakPreAuthActionsFilter() { - return new KeycloakPreAuthActionsFilter(httpSessionManager()); - } - - protected KeycloakCsrfRequestMatcher keycloakCsrfRequestMatcher() { - return new KeycloakCsrfRequestMatcher(); - } - - @Bean - protected HttpSessionManager httpSessionManager() { - return new HttpSessionManager(); - } - - protected KeycloakLogoutHandler keycloakLogoutHandler() throws Exception { - return new KeycloakLogoutHandler(adapterDeploymentContext()); - } - - protected abstract SessionAuthenticationStrategy sessionAuthenticationStrategy(); - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http - .csrf().requireCsrfProtectionMatcher(keycloakCsrfRequestMatcher()) - .and() - .sessionManagement() - .sessionAuthenticationStrategy(sessionAuthenticationStrategy()) - .and() - .addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class) - .addFilterBefore(keycloakAuthenticationProcessingFilter(), LogoutFilter.class) - .addFilterAfter(keycloakSecurityContextRequestFilter(), SecurityContextHolderAwareRequestFilter.class) - .addFilterAfter(keycloakAuthenticatedActionsRequestFilter(), KeycloakSecurityContextRequestFilter.class) - .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) - .and() - .logout() - .addLogoutHandler(keycloakLogoutHandler()) - .logoutUrl("/sso/logout").permitAll() - .logoutSuccessUrl("/"); - } - - @Bean - protected KeycloakSecurityContextRequestFilter keycloakSecurityContextRequestFilter() { - return new KeycloakSecurityContextRequestFilter(); - } - - @Bean - protected KeycloakAuthenticatedActionsFilter keycloakAuthenticatedActionsRequestFilter() { - return new KeycloakAuthenticatedActionsFilter(); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java deleted file mode 100755 index 2c9876eace32..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.facade; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.Assert; - -import javax.security.cert.X509Certificate; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Simple {@link org.keycloak.adapters.OIDCHttpFacade} wrapping an {@link HttpServletRequest} and {@link HttpServletResponse}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class SimpleHttpFacade implements OIDCHttpFacade { - - private final HttpServletRequest request; - private final HttpServletResponse response; - - /** - * Creates a new simple HTTP facade for the given request and response. - * - * @param request the current HttpServletRequest (required) - * @param response the current HttpServletResponse (required) - */ - public SimpleHttpFacade(HttpServletRequest request, HttpServletResponse response) { - Assert.notNull(request, "HttpServletRequest required"); - Assert.notNull(response, "HttpServletResponse required"); - this.request = request; - this.response = response; - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - - SecurityContext context = SecurityContextHolder.getContext(); - - if (context != null && context.getAuthentication() != null) { - KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) context.getAuthentication(); - return authentication.getAccount().getKeycloakSecurityContext(); - } - - return null; - } - - @Override - public Request getRequest() { - return new WrappedHttpServletRequest(request); - } - - @Override - public Response getResponse() { - return new WrappedHttpServletResponse(response); - } - - @Override - public X509Certificate[] getCertificateChain() { - // TODO: implement me - return new X509Certificate[0]; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java deleted file mode 100755 index f4fa0746911f..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.facade; - -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade.Cookie; -import org.keycloak.adapters.spi.HttpFacade.Request; -import org.keycloak.adapters.spi.LogoutError; -import org.springframework.util.Assert; - -import javax.servlet.http.HttpServletRequest; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; - -/** - * Concrete Keycloak {@link Request request} implementation wrapping an {@link HttpServletRequest}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -class WrappedHttpServletRequest implements Request { - - private final HttpServletRequest request; - private InputStream inputStream; - - /** - * Creates a new request for the given HttpServletRequest - * - * @param request the current HttpServletRequest (required) - */ - public WrappedHttpServletRequest(HttpServletRequest request) { - Assert.notNull(request, "HttpServletRequest required"); - this.request = request; - } - - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getURI() { - StringBuffer buf = request.getRequestURL(); - if (request.getQueryString() != null) { - buf.append('?').append(request.getQueryString()); - } - return buf.toString(); - } - - @Override - public String getRelativePath() { - return request.getServletPath(); - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getQueryParamValue(String param) { - return request.getParameter(param); - } - - @Override - public Cookie getCookie(String cookieName) { - - javax.servlet.http.Cookie[] cookies = request.getCookies(); - - if (cookies == null) { - return null; - } - - for (javax.servlet.http.Cookie cookie : request.getCookies()) { - if (cookie.getName().equals(cookieName)) { - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - } - - return null; - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public List getHeaders(String name) { - Enumeration values = request.getHeaders(name); - List array = new ArrayList(); - - while (values.hasMoreElements()) { - array.add(values.nextElement()); - } - - return Collections.unmodifiableList(array); - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java deleted file mode 100644 index c246e1328a07..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.facade; - -import org.keycloak.adapters.spi.HttpFacade.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.OutputStream; -import java.lang.reflect.Method; - -/** - * Concrete Keycloak {@link Response response} implementation wrapping an {@link HttpServletResponse}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -class WrappedHttpServletResponse implements Response { - - private static final Logger log = LoggerFactory.getLogger(WrappedHttpServletResponse.class); - private final HttpServletResponse response; - - /** - * Creates a new response for the given HttpServletResponse. - * - * @param response the current HttpServletResponse (required) - */ - public WrappedHttpServletResponse(HttpServletResponse response) { - this.response = response; - } - - @Override - public void resetCookie(String name, String path) { - Cookie cookie = new Cookie(name, ""); - cookie.setMaxAge(0); - if (path != null) { - cookie.setPath(path); - } - response.addCookie(cookie); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - Cookie cookie = new Cookie(name, value); - - if (path != null) { - cookie.setPath(path); - } - - if (domain != null) { - cookie.setDomain(domain); - } - - cookie.setMaxAge(maxAge); - cookie.setSecure(secure); - this.setHttpOnly(cookie, httpOnly); - - response.addCookie(cookie); - } - - private void setHttpOnly(Cookie cookie, boolean httpOnly) { - Method method; - try { - method = Cookie.class.getMethod("setHttpOnly", boolean.class); - method.invoke(cookie, httpOnly); - } catch (NoSuchMethodException e) { - log.warn("Unable to set httpOnly on cookie [{}]; no such method on javax.servlet.http.Cookie", cookie.getName()); - } catch (ReflectiveOperationException e) { - log.error("Unable to set httpOnly on cookie [{}]", cookie.getName(), e); - } - } - - @Override - public void setStatus(int status) { - response.setStatus(status); - } - - @Override - public void addHeader(String name, String value) { - response.addHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public OutputStream getOutputStream() { - try { - return response.getOutputStream(); - } catch (IOException e) { - throw new RuntimeException("Unable to return response output stream", e); - } - } - - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException("Unable to set HTTP status", e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException("Unable to set HTTP status", e); - } - } - - @Override - public void end() { - // TODO: do we need this? - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java deleted file mode 100755 index 81b391a9e842..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2016 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. - */ - -/** - * Provides an {@link org.keycloak.adapters.OIDCHttpFacade} implementation. - */ -package org.keycloak.adapters.springsecurity.facade; \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java deleted file mode 100644 index bee9d15000ca..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import org.keycloak.constants.AdapterConstants; -import org.springframework.security.web.util.matcher.RequestMatcher; - -/** - * Matches a request if it contains a {@value AdapterConstants#KEYCLOAK_ADAPTER_STATE_COOKIE} - * cookie. - * - * @author Sjoerd Cranen - */ -public class AdapterStateCookieRequestMatcher implements RequestMatcher { - - @Override - public boolean matches(HttpServletRequest request) { - if (request.getCookies() == null) { - return false; - } - for (Cookie cookie: request.getCookies()) { - if (AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE.equals(cookie.getName())) { - return true; - } - } - return false; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticatedActionsFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticatedActionsFilter.java deleted file mode 100644 index 3ef324fa4e52..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticatedActionsFilter.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2018 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.adapters.springsecurity.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.GenericFilterBean; - -/** - * @author Pedro Igor - */ -public class KeycloakAuthenticatedActionsFilter extends GenericFilterBean implements ApplicationContextAware { - - private static final String FILTER_APPLIED = KeycloakAuthenticatedActionsFilter.class.getPackage().getName() + ".authenticated-actions"; - - private ApplicationContext applicationContext; - private AdapterDeploymentContext deploymentContext; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { - if (request.getAttribute(FILTER_APPLIED) != null) { - filterChain.doFilter(request, response); - return; - } - - request.setAttribute(FILTER_APPLIED, Boolean.TRUE); - - KeycloakSecurityContext keycloakSecurityContext = getKeycloakPrincipal(); - - if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) { - HttpFacade facade = new SimpleHttpFacade((HttpServletRequest) request, (HttpServletResponse) response); - KeycloakDeployment deployment = resolveDeployment(request, response); - AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, OIDCHttpFacade.class.cast(facade)); - if (actions.handledRequest()) { - return; - } - } - - filterChain.doFilter(request, response); - } - - @Override - protected void initFilterBean() { - deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - private KeycloakSecurityContext getKeycloakPrincipal() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null) { - Object principal = authentication.getPrincipal(); - - if (principal instanceof KeycloakPrincipal) { - return KeycloakPrincipal.class.cast(principal).getKeycloakSecurityContext(); - } - } - - return null; - } - - private KeycloakDeployment resolveDeployment(ServletRequest servletRequest, ServletResponse servletResponse) { - return deploymentContext.resolveDeployment(new SimpleHttpFacade(HttpServletRequest.class.cast(servletRequest), HttpServletResponse.class.cast(servletResponse))); - } - - private void clearAuthenticationContext() { - SecurityContextHolder.clearContext(); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java deleted file mode 100644 index 930867efd64a..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationSuccessHandler; -import org.keycloak.adapters.springsecurity.authentication.RequestAuthenticatorFactory; -import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticatorFactory; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -/** - * Provides a Keycloak authentication processing filter. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware { - public static final String AUTHORIZATION_HEADER = "Authorization"; - - /** - * Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI} - * and any request with a Authorization header or an {@link AdapterStateCookieRequestMatcher adapter state cookie}. - */ - public static final RequestMatcher DEFAULT_REQUEST_MATCHER = - new OrRequestMatcher( - new AntPathRequestMatcher(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI), - new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER), - new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN), - new AdapterStateCookieRequestMatcher() - ); - - private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class); - - private ApplicationContext applicationContext; - private AdapterDeploymentContext adapterDeploymentContext; - private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); - private AuthenticationManager authenticationManager; - private RequestAuthenticatorFactory requestAuthenticatorFactory = new SpringSecurityRequestAuthenticatorFactory(); - - /** - * Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and the - * {@link KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER default request matcher}. - * - * @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null) - * @see KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER - */ - public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) { - this(authenticationManager, DEFAULT_REQUEST_MATCHER); - setAuthenticationFailureHandler(new KeycloakAuthenticationFailureHandler()); - setAuthenticationSuccessHandler(new KeycloakAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler())); - } - - /** - * Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and - * {@link RequestMatcher}. - *

- * Note: the given request matcher must support matching the Authorization header if - * bearer token authentication is to be accepted. - *

- * - * @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null) - * @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to determine if authentication - * is required (cannot be null) - * - * @see RequestHeaderRequestMatcher - * @see OrRequestMatcher - * - */ - public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher - requiresAuthenticationRequestMatcher) { - super(requiresAuthenticationRequestMatcher); - Assert.notNull(authenticationManager, "authenticationManager cannot be null"); - this.authenticationManager = authenticationManager; - super.setAuthenticationManager(authenticationManager); - super.setAllowSessionCreation(false); - super.setContinueChainBeforeSuccessfulAuthentication(false); - } - - @Override - public void afterPropertiesSet() { - adapterDeploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); - super.afterPropertiesSet(); - } - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException, ServletException { - - log.debug("Attempting Keycloak authentication"); - - HttpFacade facade = new SimpleHttpFacade(request, response); - KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade); - - // using Spring authenticationFailureHandler - deployment.setDelegateBearerErrorResponseSending(true); - - AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request, response); - RequestAuthenticator authenticator - = requestAuthenticatorFactory.createRequestAuthenticator(facade, request, deployment, tokenStore, -1); - - AuthOutcome result = authenticator.authenticate(); - log.debug("Auth outcome: {}", result); - - if (AuthOutcome.FAILED.equals(result)) { - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details"); - } - - if (AuthOutcome.NOT_ATTEMPTED.equals(result)) { - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - if (deployment.isBearerOnly()) { - // no redirection in this mode, throwing exception for the spring handler - throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header"); - } else { - // let continue if challenged, it may redirect - return null; - } - } - - else if (AuthOutcome.AUTHENTICATED.equals(result)) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Assert.notNull(authentication, "Authentication SecurityContextHolder was null"); - return authenticationManager.authenticate(authentication); - } - else { - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - return null; - } - } - - @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, - Authentication authResult) throws IOException, ServletException { - if (authResult instanceof KeycloakAuthenticationToken && ((KeycloakAuthenticationToken) authResult).isInteractive()) { - super.successfulAuthentication(request, response, chain, authResult); - return; - } - - if (log.isDebugEnabled()) { - log.debug("Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: {}", authResult); - } - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authResult); - SecurityContextHolder.setContext(context); - - try { - // Fire event - if (this.eventPublisher != null) { - eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); - } - chain.doFilter(request, response); - } finally { - SecurityContextHolder.clearContext(); - } - } - - @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException, ServletException { - super.unsuccessfulAuthentication(request, response, failed); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * Sets the adapter token store factory to use when creating per-request adapter token stores. - * - * @param adapterTokenStoreFactory the AdapterTokenStoreFactory to use - */ - public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory adapterTokenStoreFactory) { - Assert.notNull(adapterTokenStoreFactory, "AdapterTokenStoreFactory cannot be null"); - this.adapterTokenStoreFactory = adapterTokenStoreFactory; - } - - /** - * This filter does not support explicitly enabling session creation. - * - * @throws UnsupportedOperationException this filter does not support explicitly enabling session creation. - */ - @Override - public final void setAllowSessionCreation(boolean allowSessionCreation) { - throw new UnsupportedOperationException("This filter does not support explicitly setting a session creation policy"); - } - - /** - * This filter does not support explicitly setting a continue chain before success policy - * - * @throws UnsupportedOperationException this filter does not support explicitly setting a continue chain before success policy - */ - @Override - public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) { - throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy"); - } - - /** - * Sets the request authenticator factory to use when creating per-request authenticators. - * - * @param requestAuthenticatorFactory the RequestAuthenticatorFactory to use - */ - public void setRequestAuthenticatorFactory(RequestAuthenticatorFactory requestAuthenticatorFactory) { - Assert.notNull(requestAuthenticatorFactory, "RequestAuthenticatorFactory cannot be null"); - this.requestAuthenticatorFactory = requestAuthenticatorFactory; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java deleted file mode 100644 index 93a48f57378c..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import org.keycloak.constants.AdapterConstants; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.StringUtils; - -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -/** - * CSRF protection matcher that allows administrative POST requests from the Keycloak server. - * - * @author Scott Rossillo - */ -public class KeycloakCsrfRequestMatcher implements RequestMatcher { - - private static final List ALLOWED_ENDPOINTS = Arrays.asList( - AdapterConstants.K_LOGOUT, - AdapterConstants.K_PUSH_NOT_BEFORE, - AdapterConstants.K_QUERY_BEARER_TOKEN, - AdapterConstants.K_TEST_AVAILABLE - ); - - private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$"); - private Pattern allowedEndpoints = Pattern.compile(String.format("^\\/(%s)$", StringUtils.arrayToDelimitedString(ALLOWED_ENDPOINTS.toArray(), "|"))); - - /* (non-Javadoc) - * @see org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.servlet.http.HttpServletRequest) - */ - public boolean matches(HttpServletRequest request) { - String uri = request.getRequestURI().replaceFirst(request.getContextPath(), ""); - return !allowedEndpoints.matcher(uri).matches() && !allowedMethods.matcher(request.getMethod()).matches(); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java deleted file mode 100755 index 75bc815f27a5..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.web.filter.GenericFilterBean; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Exposes a Keycloak adapter {@link PreAuthActionsHandler} as a Spring Security filter. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakPreAuthActionsFilter extends GenericFilterBean implements ApplicationContextAware { - - private static final Logger log = LoggerFactory.getLogger(KeycloakPreAuthActionsFilter.class); - - private NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement(); - private ApplicationContext applicationContext; - private AdapterDeploymentContext deploymentContext; - private UserSessionManagement userSessionManagement; - private PreAuthActionsHandlerFactory preAuthActionsHandlerFactory = new PreAuthActionsHandlerFactory(); - - public KeycloakPreAuthActionsFilter() { - super(); - } - - public KeycloakPreAuthActionsFilter(UserSessionManagement userSessionManagement) { - this.userSessionManagement = userSessionManagement; - } - - @Override - protected void initFilterBean() throws ServletException { - deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); - } - - @Override - public void destroy() { - log.debug("Unregistering deployment"); - nodesRegistrationManagement.stop(); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - HttpFacade facade = new SimpleHttpFacade((HttpServletRequest)request, (HttpServletResponse)response); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - - if (deployment == null) { - return; - } - - if (deployment.isConfigured()) { - nodesRegistrationManagement.tryRegister(deploymentContext.resolveDeployment(facade)); - } - - PreAuthActionsHandler handler = preAuthActionsHandlerFactory.createPreAuthActionsHandler(facade); - if (handler.handleRequest()) { - log.debug("Pre-auth filter handled request: {}", ((HttpServletRequest) request).getRequestURI()); - } else { - chain.doFilter(request, response); - } - } - - public void setUserSessionManagement(UserSessionManagement userSessionManagement) { - this.userSessionManagement = userSessionManagement; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - void setNodesRegistrationManagement(NodesRegistrationManagement nodesRegistrationManagement) { - this.nodesRegistrationManagement = nodesRegistrationManagement; - } - - void setPreAuthActionsHandlerFactory(PreAuthActionsHandlerFactory preAuthActionsHandlerFactory) { - this.preAuthActionsHandlerFactory = preAuthActionsHandlerFactory; - } - - /** - * Creates {@link PreAuthActionsHandler}s. - * - * Package-private class to enable mocking. - */ - class PreAuthActionsHandlerFactory { - PreAuthActionsHandler createPreAuthActionsHandler(HttpFacade facade) { - return new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade); - } - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java deleted file mode 100644 index 4e17183ad30b..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2017 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.adapters.springsecurity.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory; -import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.GenericFilterBean; - -/** - * @author Pedro Igor - */ -public class KeycloakSecurityContextRequestFilter extends GenericFilterBean implements ApplicationContextAware { - private static final Logger log = LoggerFactory.getLogger(KeycloakSecurityContextRequestFilter.class); - - private static final String FILTER_APPLIED = KeycloakSecurityContext.class.getPackage().getName() + ".token-refreshed"; - private final AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); - - private ApplicationContext applicationContext; - private AdapterDeploymentContext deploymentContext; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { - if (request.getAttribute(FILTER_APPLIED) != null) { - filterChain.doFilter(request, response); - return; - } - - request.setAttribute(FILTER_APPLIED, Boolean.TRUE); - - KeycloakSecurityContext keycloakSecurityContext = getKeycloakSecurityContext(); - - if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) { - RefreshableKeycloakSecurityContext refreshableSecurityContext = (RefreshableKeycloakSecurityContext) keycloakSecurityContext; - KeycloakDeployment deployment = resolveDeployment(request, response); - - // just in case session got serialized - if (refreshableSecurityContext.getDeployment()==null) { - log.trace("Recreating missing deployment and related fields in deserialized context"); - AdapterTokenStore adapterTokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, (HttpServletRequest) request, - (HttpServletResponse) response); - refreshableSecurityContext.setCurrentRequestInfo(deployment, adapterTokenStore); - } - - if (!refreshableSecurityContext.isActive() || deployment.isAlwaysRefreshToken()) { - if (refreshableSecurityContext.refreshExpiredToken(false)) { - request.setAttribute(KeycloakSecurityContext.class.getName(), refreshableSecurityContext); - } else { - clearAuthenticationContext(); - } - } - - request.setAttribute(KeycloakSecurityContext.class.getName(), keycloakSecurityContext); - } - - filterChain.doFilter(request, response); - } - - @Override - protected void initFilterBean() { - deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - private KeycloakSecurityContext getKeycloakSecurityContext() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null) { - Object principal = authentication.getPrincipal(); - - if (principal instanceof KeycloakPrincipal) { - return KeycloakPrincipal.class.cast(principal).getKeycloakSecurityContext(); - } - } - - return null; - } - - private KeycloakDeployment resolveDeployment(ServletRequest servletRequest, ServletResponse servletResponse) { - return deploymentContext.resolveDeployment(new SimpleHttpFacade(HttpServletRequest.class.cast(servletRequest), HttpServletResponse.class.cast(servletResponse))); - } - - private void clearAuthenticationContext() { - SecurityContextHolder.clearContext(); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcher.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcher.java deleted file mode 100644 index 76164638e913..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcher.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017 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.adapters.springsecurity.filter; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.security.web.util.matcher.RequestMatcher; - -/** - * Spring RequestMatcher that checks for the presence of a query parameter. - * - * @author Gabriel Lavoie - */ -public class QueryParamPresenceRequestMatcher implements RequestMatcher { - private String param; - - public QueryParamPresenceRequestMatcher(String param) { - this.param = param; - } - - @Override - public boolean matches(HttpServletRequest httpServletRequest) { - return param != null && httpServletRequest.getParameter(param) != null; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java deleted file mode 100644 index c0b3c9aea9ef..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2016 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. - */ - -/** - * Provides Spring Security filters for Keycloak. - */ -package org.keycloak.adapters.springsecurity.filter; \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java deleted file mode 100644 index f711b3919d39..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.management; - -import java.util.List; - -import javax.servlet.http.HttpSession; - -import org.keycloak.adapters.spi.UserSessionManagement; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.security.web.session.HttpSessionCreatedEvent; -import org.springframework.security.web.session.HttpSessionDestroyedEvent; - -/** - * User session manager for handling logout of Spring Secured sessions. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class HttpSessionManager implements ApplicationListener, UserSessionManagement { - - private static final Logger log = LoggerFactory.getLogger(HttpSessionManager.class); - private SessionManagementStrategy sessions = new LocalSessionManagementStrategy(); - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof HttpSessionCreatedEvent) { - HttpSessionCreatedEvent e = (HttpSessionCreatedEvent) event; - HttpSession session = e.getSession(); - log.debug("Session created: {}", session.getId()); - sessions.store(session); - } else if (event instanceof HttpSessionDestroyedEvent) { - HttpSessionDestroyedEvent e = (HttpSessionDestroyedEvent) event; - HttpSession session = e.getSession(); - sessions.remove(session.getId()); - log.debug("Session destroyed: {}", session.getId()); - } - } - - @Override - public void logoutAll() { - log.info("Received request to log out all users."); - for (HttpSession session : sessions.getAll()) { - session.invalidate(); - } - sessions.clear(); - } - - @Override - public void logoutHttpSessions(List ids) { - log.info("Received request to log out {} session(s): {}", ids.size(), ids); - for (String id : ids) { - HttpSession session = sessions.remove(id); - if (session != null) { - session.invalidate(); - } - } - } - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java deleted file mode 100644 index 7487ac5f5130..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.management; - -import javax.servlet.http.HttpSession; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Created by scott on 4/24/15. - */ -public class LocalSessionManagementStrategy implements SessionManagementStrategy { - - private final Map sessions = new ConcurrentHashMap(); - - @Override - public void clear() { - sessions.clear(); - } - - @Override - public Collection getAll() { - return sessions.values(); - } - - @Override - public void store(HttpSession session) { - sessions.put(session.getId(), session); - } - - @Override - public HttpSession remove(String id) { - return sessions.remove(id); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java deleted file mode 100644 index b39ca1b562c4..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.management; - -import javax.servlet.http.HttpSession; -import java.util.Collection; - -/** - * Defines a session management strategy. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public interface SessionManagementStrategy { - - /** - * Removes all sessions. - */ - void clear(); - - /** - * Returns a collection containing all sessions. - * - * @return a Collection of all known HttpSessions, if any; - * an empty Collection otherwise - */ - Collection getAll(); - - /** - * Stores the given session. - * - * @param session the HttpSession to store (required) - */ - void store(HttpSession session); - - /** - * The unique identifier for the session to remove. - * - * @param id the unique identifier for the session to remove (required) - * @return the HttpSession if it exists; null otherwise - */ - HttpSession remove(String id); -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java deleted file mode 100644 index b3b990e05fad..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2016 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. - */ - -/** - * Provides a Keycloak adapter for Spring Security. - */ -package org.keycloak.adapters.springsecurity; \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java deleted file mode 100644 index 6a7680cc842d..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.registration; - -import org.keycloak.adapters.KeycloakDeployment; - -/** - * Manages registration of application nodes. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public interface NodeManager { - - /** - * Registers the given deployment with the Keycloak server. - * - * @param deployment the deployment to register (required) - */ - void register(KeycloakDeployment deployment); - - /** - * Unregisters the give deployment from the Keycloak server - * . - * @param deployment the deployment to unregister (required) - */ - void unregister(KeycloakDeployment deployment); - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java deleted file mode 100644 index b8a60ce7d204..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import javax.servlet.http.HttpServletResponse; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; - -import javax.servlet.http.HttpServletRequest; - -/** - * Creates a per-request adapter token store. - * - * @author Scott Rossillo - */ -public interface AdapterTokenStoreFactory { - - /** - * Returns a new {@link AdapterTokenStore} for the given {@link KeycloakDeployment} and {@link HttpServletRequest request}. - * - * @param deployment the KeycloakDeployment (required) - * @param request the current HttpServletRequest (required) - * @param response the current HttpServletResponse (required when using cookies) - * - * @return a new AdapterTokenStore for the given deployment, request and response - * @throws IllegalArgumentException if any required parameter is null - */ - AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request, HttpServletResponse response); - -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java deleted file mode 100755 index fad2782c3030..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.Assert; - -import java.security.Principal; -import java.util.Collection; - -/** - * Represents the token for a Keycloak authentication request or for an authenticated principal once the request has been - * processed by the {@link AuthenticationManager#authenticate(Authentication)}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class KeycloakAuthenticationToken extends AbstractAuthenticationToken implements Authentication { - - private Principal principal; - private boolean interactive; - - /** - * Creates a new, unauthenticated Keycloak security token for the given account. - */ - public KeycloakAuthenticationToken(KeycloakAccount account, boolean interactive) { - super(null); - Assert.notNull(account, "KeycloakAccount cannot be null"); - Assert.notNull(account.getPrincipal(), "KeycloakAccount.getPrincipal() cannot be null"); - this.principal = account.getPrincipal(); - this.setDetails(account); - this.interactive = interactive; - } - - public KeycloakAuthenticationToken(KeycloakAccount account, boolean interactive, Collection authorities) { - super(authorities); - Assert.notNull(account, "KeycloakAccount cannot be null"); - Assert.notNull(account.getPrincipal(), "KeycloakAccount.getPrincipal() cannot be null"); - this.principal = account.getPrincipal(); - this.setDetails(account); - this.interactive = interactive; - setAuthenticated(true); - } - - @Override - public Object getCredentials() { - return this.getAccount().getKeycloakSecurityContext(); - } - - @Override - public Object getPrincipal() { - return principal; - } - - public OidcKeycloakAccount getAccount() { - return (OidcKeycloakAccount) this.getDetails(); - } - - public boolean isInteractive() { - return interactive; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java deleted file mode 100644 index 321744cede52..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.enums.TokenStore; -import org.springframework.util.Assert; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * {@link AdapterTokenStoreFactory} that returns a new {@link SpringSecurityTokenStore} for each request. - * - * @author Scott Rossillo - */ -public class SpringSecurityAdapterTokenStoreFactory implements AdapterTokenStoreFactory { - - @Override - public AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request, HttpServletResponse response) { - Assert.notNull(deployment, "KeycloakDeployment is required"); - if (deployment.getTokenStore() == TokenStore.COOKIE) { - return new SpringSecurityCookieTokenStore(deployment, request, response); - } - return new SpringSecurityTokenStore(deployment, request); - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java deleted file mode 100644 index 92699f82b425..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.CookieTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.Assert; - -/** - * Extension of {@link SpringSecurityTokenStore} that stores the obtains tokens in a cookie. - * - * @author Sjoerd Cranen - */ -public class SpringSecurityCookieTokenStore extends SpringSecurityTokenStore { - - private final Logger logger = LoggerFactory.getLogger(SpringSecurityCookieTokenStore.class); - - private final KeycloakDeployment deployment; - private final HttpFacade facade; - private volatile boolean cookieChecked = false; - - public SpringSecurityCookieTokenStore( - KeycloakDeployment deployment, - HttpServletRequest request, - HttpServletResponse response) { - super(deployment, request); - Assert.notNull(response, "HttpServletResponse is required"); - this.deployment = deployment; - this.facade = new SimpleHttpFacade(request, response); - } - - @Override - public void checkCurrentToken() { - final KeycloakPrincipal principal = - checkPrincipalFromCookie(); - if (principal != null) { - final RefreshableKeycloakSecurityContext securityContext = - principal.getKeycloakSecurityContext(); - KeycloakSecurityContext current = ((OIDCHttpFacade) facade).getSecurityContext(); - if (current != null) { - securityContext.setAuthorizationContext(current.getAuthorizationContext()); - } - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - final OidcKeycloakAccount account = - new SimpleKeycloakAccount(principal, roles, securityContext); - SecurityContextHolder.getContext() - .setAuthentication(new KeycloakAuthenticationToken(account, false)); - } else { - super.checkCurrentToken(); - } - cookieChecked = true; - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - if (!cookieChecked) { - checkCurrentToken(); - } - return super.isCached(authenticator); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - super.refreshCallback(securityContext); - CookieTokenStore.setTokenCookie(deployment, facade, securityContext); - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - super.saveAccountInfo(account); - RefreshableKeycloakSecurityContext securityContext = - (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); - CookieTokenStore.setTokenCookie(deployment, facade, securityContext); - } - - @Override - public void logout() { - CookieTokenStore.removeCookie(deployment, facade); - super.logout(); - } - - /** - * Verify if we already have authenticated and active principal in cookie. Perform refresh if - * it's not active - * - * @return valid principal - */ - private KeycloakPrincipal checkPrincipalFromCookie() { - KeycloakPrincipal principal = - CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); - if (principal == null) { - logger.debug("Account was not in cookie or was invalid"); - return null; - } - - RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext(); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal; - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) { - refreshCallback(session); - return principal; - } - - logger.debug( - "Cleanup and expire cookie for user {} after failed refresh", principal.getName()); - CookieTokenStore.removeCookie(deployment, facade); - return null; - } -} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java deleted file mode 100755 index a932dda6c323..000000000000 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.Assert; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -/** - * Simple Spring {@link SecurityContext security context} aware {@link AdapterTokenStore adapter token store}. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class SpringSecurityTokenStore implements AdapterTokenStore { - - private final Logger logger = LoggerFactory.getLogger(SpringSecurityTokenStore.class); - - private final KeycloakDeployment deployment; - private final HttpServletRequest request; - - public SpringSecurityTokenStore(KeycloakDeployment deployment, HttpServletRequest request) { - Assert.notNull(deployment, "KeycloakDeployment is required"); - Assert.notNull(request, "HttpServletRequest is required"); - this.deployment = deployment; - this.request = request; - } - - @Override - public void checkCurrentToken() { - // no-op - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - - logger.debug("Checking if {} is cached", authenticator); - SecurityContext context = SecurityContextHolder.getContext(); - KeycloakAuthenticationToken token; - KeycloakSecurityContext keycloakSecurityContext; - - if (context == null || context.getAuthentication() == null) { - return false; - } - - if (!KeycloakAuthenticationToken.class.isAssignableFrom(context.getAuthentication().getClass())) { - logger.warn("Expected a KeycloakAuthenticationToken, but found {}", context.getAuthentication()); - return false; - } - - logger.debug("Remote logged in already. Establishing state from security context."); - token = (KeycloakAuthenticationToken) context.getAuthentication(); - keycloakSecurityContext = token.getAccount().getKeycloakSecurityContext(); - - if (!deployment.getRealm().equals(keycloakSecurityContext.getRealm())) { - logger.debug("Account from security context is from a different realm than for the request."); - logout(); - return false; - } - - if (keycloakSecurityContext.getToken().isExpired()) { - logger.warn("Security token expired ... not returning from cache"); - return false; - } - - request.setAttribute(KeycloakSecurityContext.class.getName(), keycloakSecurityContext); - - return true; - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null) { - throw new IllegalStateException(String.format("Went to save Keycloak account %s, but already have %s", account, authentication)); - } - - logger.debug("Saving account info {}", account); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(new KeycloakAuthenticationToken(account, true)); - SecurityContextHolder.setContext(context); - } - - @Override - public void logout() { - - logger.debug("Handling logout request"); - HttpSession session = request.getSession(false); - - if (session != null) { - session.setAttribute(KeycloakSecurityContext.class.getName(), null); - session.invalidate(); - } - - SecurityContextHolder.clearContext(); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } - - @Override - public void saveRequest() { - // no-op, Spring Security will handle this - } - - @Override - public boolean restoreRequest() { - // no-op, Spring Security will handle this - return false; - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java deleted file mode 100644 index 3546b5b05d4d..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.spi.HttpFacade; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; - -import java.io.FileNotFoundException; - -import static org.junit.Assert.assertNotNull; - -public class AdapterDeploymentContextFactoryBeanTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private AdapterDeploymentContextFactoryBean adapterDeploymentContextFactoryBean; - - @Test - public void should_create_adapter_deployment_context_from_configuration_file() throws Exception { - // given: - adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getCorrectResource()); - - // when: - adapterDeploymentContextFactoryBean.afterPropertiesSet(); - - // then - assertNotNull(adapterDeploymentContextFactoryBean.getObject()); - } - - private Resource getCorrectResource() { - return new ClassPathResource("keycloak.json"); - } - - @Test - public void should_throw_exception_when_configuration_file_was_not_found() throws Exception { - // given: - adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getEmptyResource()); - - // then: - expectedException.expect(FileNotFoundException.class); - expectedException.expectMessage("Unable to locate Keycloak configuration file: no-file.json"); - - // when: - adapterDeploymentContextFactoryBean.afterPropertiesSet(); - } - - private Resource getEmptyResource() { - return new ClassPathResource("no-file.json"); - } - - @Test - public void should_create_adapter_deployment_context_from_keycloak_config_resolver() throws Exception { - // given: - adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getKeycloakConfigResolver()); - - // when: - adapterDeploymentContextFactoryBean.afterPropertiesSet(); - - // then: - assertNotNull(adapterDeploymentContextFactoryBean.getObject()); - } - - private KeycloakConfigResolver getKeycloakConfigResolver() { - return new KeycloakConfigResolver() { - @Override - public KeycloakDeployment resolve(HttpFacade.Request facade) { - return null; - } - }; - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java deleted file mode 100644 index d050eb555af1..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.apache.http.HttpHeaders; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.web.util.matcher.RequestMatcher; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * HTTP header inspecting API request matcher tests. - */ -public class HttpHeaderInspectingApiRequestMatcherTest { - - private RequestMatcher apiRequestMatcher = new HttpHeaderInspectingApiRequestMatcher(); - private MockHttpServletRequest request; - - @Before - public void setUp() throws Exception { - request = new MockHttpServletRequest(); - } - - @Test - public void testMatchesBrowserRequest() throws Exception { - request.addHeader(HttpHeaders.ACCEPT, "application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - assertFalse(apiRequestMatcher.matches(request)); - } - - @Test - public void testMatchesRequestedWith() throws Exception { - request.addHeader( - HttpHeaderInspectingApiRequestMatcher.X_REQUESTED_WITH_HEADER, - HttpHeaderInspectingApiRequestMatcher.X_REQUESTED_WITH_HEADER_AJAX_VALUE); - - assertTrue(apiRequestMatcher.matches(request)); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java deleted file mode 100644 index 76c1f67701e6..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.apache.http.HttpHeaders; -import org.junit.Before; -import org.junit.Test; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.spi.HttpFacade; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.web.util.matcher.RequestMatcher; - -/** - * Keycloak authentication entry point tests. - */ -public class KeycloakAuthenticationEntryPointTest { - - private KeycloakAuthenticationEntryPoint authenticationEntryPoint; - private MockHttpServletRequest request; - private MockHttpServletResponse response; - @Mock - private ApplicationContext applicationContext; - - @Mock - private AdapterDeploymentContext adapterDeploymentContext; - - @Mock - private KeycloakDeployment keycloakDeployment; - - @Mock - private RequestMatcher requestMatcher; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - authenticationEntryPoint = new KeycloakAuthenticationEntryPoint(adapterDeploymentContext); - request = new MockHttpServletRequest(); - response = new MockHttpServletResponse(); - when(applicationContext.getBean(eq(AdapterDeploymentContext.class))).thenReturn(adapterDeploymentContext); - when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment); - when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE); - } - - @Test - public void testCommenceWithRedirect() throws Exception { - configureBrowserRequest(); - authenticationEntryPoint.commence(request, response, null); - assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - assertEquals(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location")); - } - - @Test - public void testCommenceWithRedirectAndQueryParameters() throws Exception { - configureBrowserRequest(); - request.setQueryString("prompt=login"); - authenticationEntryPoint.commence(request, response, null); - assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - assertNotEquals(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location")); - assertThat(response.getHeader("Location"), containsString(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI)); - assertThat(response.getHeader("Location"), containsString("prompt=login")); - } - - @Test - public void testCommenceWithRedirectNotRootContext() throws Exception { - configureBrowserRequest(); - String contextPath = "/foo"; - request.setContextPath(contextPath); - authenticationEntryPoint.commence(request, response, null); - assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - assertEquals(contextPath + KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location")); - } - - @Test - public void testCommenceWithUnauthorizedWithAccept() throws Exception { - request.addHeader(HttpHeaders.ACCEPT, "application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - authenticationEntryPoint.commence(request, response, null); - assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - assertNull(response.getHeader(HttpHeaders.WWW_AUTHENTICATE)); - } - - @Test - public void testSetLoginUri() throws Exception { - configureBrowserRequest(); - final String logoutUri = "/foo"; - authenticationEntryPoint.setLoginUri(logoutUri); - authenticationEntryPoint.commence(request, response, null); - assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - assertEquals(logoutUri, response.getHeader("Location")); - } - - @Test - public void testCommenceWithCustomRequestMatcher() throws Exception { - new KeycloakAuthenticationEntryPoint(adapterDeploymentContext, requestMatcher) - .commence(request, response, null); - - verify(requestMatcher).matches(request); - } - - private void configureBrowserRequest() { - request.addHeader(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java deleted file mode 100644 index 9515b4b8badf..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.mockito.internal.util.collections.Sets; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; - -import java.security.Principal; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -/** - * Keycloak authentication provider tests. - */ -public class KeycloakAuthenticationProviderTest { - private KeycloakAuthenticationProvider provider = new KeycloakAuthenticationProvider(); - private KeycloakAuthenticationToken token; - private KeycloakAuthenticationToken interactiveToken; - private Set roles = Sets.newSet("user", "admin"); - - @Before - public void setUp() throws Exception { - Principal principal = mock(Principal.class); - RefreshableKeycloakSecurityContext securityContext = mock(RefreshableKeycloakSecurityContext.class); - KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext); - - token = new KeycloakAuthenticationToken(account, false); - interactiveToken = new KeycloakAuthenticationToken(account, true); - } - - @Test - public void testAuthenticate() throws Exception { - assertAuthenticationResult(provider.authenticate(token)); - } - - @Test - public void testAuthenticateInteractive() throws Exception { - assertAuthenticationResult(provider.authenticate(interactiveToken)); - } - - @Test - public void testSupports() throws Exception { - assertTrue(provider.supports(KeycloakAuthenticationToken.class)); - assertFalse(provider.supports(PreAuthenticatedAuthenticationToken.class)); - } - - @Test - public void testGrantedAuthoritiesMapper() throws Exception { - SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper(); - grantedAuthorityMapper.setPrefix("ROLE_"); - grantedAuthorityMapper.setConvertToUpperCase(true); - provider.setGrantedAuthoritiesMapper(grantedAuthorityMapper); - - Authentication result = provider.authenticate(token); - assertEquals(Sets.newSet("ROLE_USER", "ROLE_ADMIN"), - AuthorityUtils.authorityListToSet(result.getAuthorities())); - } - - private void assertAuthenticationResult(Authentication result) { - assertNotNull(result); - assertEquals(roles, AuthorityUtils.authorityListToSet(result.getAuthorities())); - assertTrue(result.isAuthenticated()); - assertNotNull(result.getPrincipal()); - assertNotNull(result.getCredentials()); - assertNotNull(result.getDetails()); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java deleted file mode 100755 index 12865de1a833..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.account.KeycloakRole; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.RememberMeAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; - -import java.util.Collection; -import java.util.Collections; -import java.util.UUID; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -/** - * Keycloak logout handler tests. - */ -public class KeycloakLogoutHandlerTest { - - private KeycloakAuthenticationToken keycloakAuthenticationToken; - private KeycloakLogoutHandler keycloakLogoutHandler; - - private MockHttpServletRequest request; - private MockHttpServletResponse response; - - @Mock - private AdapterDeploymentContext adapterDeploymentContext; - - @Mock - private OidcKeycloakAccount keycloakAccount; - - @Mock - private KeycloakDeployment keycloakDeployment; - - @Mock - private RefreshableKeycloakSecurityContext session; - - private Collection authorities = Collections.singleton(new KeycloakRole(UUID.randomUUID().toString())); - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - keycloakAuthenticationToken = mock(KeycloakAuthenticationToken.class); - keycloakLogoutHandler = new KeycloakLogoutHandler(adapterDeploymentContext); - request = new MockHttpServletRequest(); - response = new MockHttpServletResponse(); - - when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment); - when(keycloakAuthenticationToken.getAccount()).thenReturn(keycloakAccount); - when(keycloakAccount.getKeycloakSecurityContext()).thenReturn(session); - } - - @Test - public void testLogout() throws Exception { - keycloakLogoutHandler.logout(request, response, keycloakAuthenticationToken); - verify(session).logout(eq(keycloakDeployment)); - } - - @Test - public void testLogoutAnonymousAuthentication() throws Exception { - Authentication authentication = new AnonymousAuthenticationToken(UUID.randomUUID().toString(), UUID.randomUUID().toString(), authorities); - keycloakLogoutHandler.logout(request, response, authentication); - verifyZeroInteractions(session); - } - - @Test - public void testLogoutUsernamePasswordAuthentication() throws Exception { - Authentication authentication = new UsernamePasswordAuthenticationToken(UUID.randomUUID().toString(), UUID.randomUUID().toString(), authorities); - keycloakLogoutHandler.logout(request, response, authentication); - verifyZeroInteractions(session); - } - - @Test - public void testLogoutRememberMeAuthentication() throws Exception { - Authentication authentication = new RememberMeAuthenticationToken(UUID.randomUUID().toString(), UUID.randomUUID().toString(), authorities); - keycloakLogoutHandler.logout(request, response, authentication); - verifyZeroInteractions(session); - } - - @Test - public void testLogoutNullAuthentication() throws Exception { - keycloakLogoutHandler.logout(request, response, null); - verifyZeroInteractions(session); - } - - @Test - public void testHandleSingleSignOut() throws Exception { - keycloakLogoutHandler.handleSingleSignOut(request, response, keycloakAuthenticationToken); - verify(session).logout(eq(keycloakDeployment)); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java deleted file mode 100755 index 373a8dd41ba8..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.authentication; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.keycloak.representations.AccessToken; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.internal.util.collections.Sets; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Spring Security request authenticator tests. - */ -public class SpringSecurityRequestAuthenticatorTest { - - private SpringSecurityRequestAuthenticator authenticator; - - private MockHttpServletRequest request; - private MockHttpServletResponse response; - - @Mock - private KeycloakDeployment deployment; - - @Mock - private AdapterTokenStore tokenStore; - - @Mock - private KeycloakPrincipal principal; - - @Mock - private AccessToken accessToken; - - @Mock - private AccessToken.Access access; - - @Mock - private RefreshableKeycloakSecurityContext refreshableKeycloakSecurityContext; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - request = spy(new MockHttpServletRequest()); - response = new MockHttpServletResponse(); - HttpFacade facade = new SimpleHttpFacade(request, response); - - authenticator = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, 443); - - // mocks - when(principal.getKeycloakSecurityContext()).thenReturn(refreshableKeycloakSecurityContext); - - when(refreshableKeycloakSecurityContext.getDeployment()).thenReturn(deployment); - when(refreshableKeycloakSecurityContext.getToken()).thenReturn(accessToken); - - when(accessToken.getRealmAccess()).thenReturn(access); - when(access.getRoles()).thenReturn(Sets.newSet("user", "admin")); - - when(deployment.isUseResourceRoleMappings()).thenReturn(false); - } - - @Test - public void testCreateOAuthAuthenticator() throws Exception { - OAuthRequestAuthenticator oathAuthenticator = authenticator.createOAuthAuthenticator(); - assertNotNull(oathAuthenticator); - } - - @Test - public void testCompleteOAuthAuthentication() throws Exception { - authenticator.completeOAuthAuthentication(principal); - verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext)); - verify(tokenStore).saveAccountInfo(any(OidcKeycloakAccount.class)); // FIXME: should verify account - } - - @Test - public void testCompleteBearerAuthentication() throws Exception { - authenticator.completeBearerAuthentication(principal, "foo"); - verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext)); - assertNotNull(SecurityContextHolder.getContext().getAuthentication()); - assertTrue(KeycloakAuthenticationToken.class.isAssignableFrom(SecurityContextHolder.getContext().getAuthentication().getClass())); - } - - @Test - public void testGetHttpSessionIdTrue() throws Exception { - String sessionId = authenticator.changeHttpSessionId(true); - assertNotNull(sessionId); - } - - @Test - public void testGetHttpSessionIdFalse() throws Exception { - String sessionId = authenticator.changeHttpSessionId(false); - assertNull(sessionId); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java deleted file mode 100755 index bfd3bd022c5a..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.client; - -import org.apache.http.client.methods.HttpUriRequest; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.springsecurity.account.KeycloakRole; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; - -import java.util.Collections; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Keycloak client request factory tests. - */ -public class KeycloakClientRequestFactoryTest { - - @Spy - private KeycloakClientRequestFactory factory; - - @Mock - private OidcKeycloakAccount account; - - @Mock - private KeycloakAuthenticationToken keycloakAuthenticationToken; - - @Mock - private KeycloakSecurityContext keycloakSecurityContext; - - @Mock - private HttpUriRequest request; - - private String bearerTokenString; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - bearerTokenString = UUID.randomUUID().toString(); - - SecurityContextHolder.getContext().setAuthentication(keycloakAuthenticationToken); - when(keycloakAuthenticationToken.getAccount()).thenReturn(account); - when(account.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext); - when(keycloakSecurityContext.getTokenString()).thenReturn(bearerTokenString); - } - - @Test - public void testPostProcessHttpRequest() throws Exception { - factory.postProcessHttpRequest(request); - verify(factory).getKeycloakSecurityContext(); - verify(request).setHeader(eq(KeycloakClientRequestFactory.AUTHORIZATION_HEADER), eq("Bearer " + bearerTokenString)); - } - - @Test - public void testGetKeycloakSecurityContext() throws Exception { - KeycloakSecurityContext context = factory.getKeycloakSecurityContext(); - assertNotNull(context); - assertEquals(keycloakSecurityContext, context); - } - - @Test(expected = IllegalStateException.class) - public void testGetKeycloakSecurityContextInvalidAuthentication() throws Exception { - SecurityContextHolder.getContext().setAuthentication( - new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("baz")))); - factory.getKeycloakSecurityContext(); - } - - @Test(expected = IllegalStateException.class) - public void testGetKeycloakSecurityContextNullAuthentication() throws Exception { - SecurityContextHolder.clearContext(); - factory.getKeycloakSecurityContext(); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java deleted file mode 100644 index 451fdc8744d7..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.keycloak.adapters.springsecurity.facade; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.mockito.internal.util.collections.Sets; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.security.Principal; -import java.util.Set; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.mock; - -public class SimpleHttpFacadeTest { - - @Before - public void setup() { - SecurityContext springSecurityContext = SecurityContextHolder.createEmptyContext(); - SecurityContextHolder.setContext(springSecurityContext); - Set roles = Sets.newSet("user"); - Principal principal = mock(Principal.class); - RefreshableKeycloakSecurityContext keycloakSecurityContext = mock(RefreshableKeycloakSecurityContext.class); - KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, keycloakSecurityContext); - KeycloakAuthenticationToken token = new KeycloakAuthenticationToken(account, false); - springSecurityContext.setAuthentication(token); - } - - @Test - public void shouldRetrieveKeycloakSecurityContext() { - SimpleHttpFacade facade = new SimpleHttpFacade(new MockHttpServletRequest(), new MockHttpServletResponse()); - - assertNotNull(facade.getSecurityContext()); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java deleted file mode 100644 index cb259ce5f251..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.facade; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.web.bind.annotation.RequestMethod; - -import javax.servlet.http.Cookie; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Wrapped HTTP servlet request tests. - */ -public class WrappedHttpServletRequestTest { - - private static final String COOKIE_NAME = "oreo"; - private static final String HEADER_MULTI_VALUE = "Multi"; - private static final String HEADER_SINGLE_VALUE = "Single"; - private static final String REQUEST_METHOD = RequestMethod.GET.name(); - private static final String REQUEST_URI = "/foo/bar"; - private static final String QUERY_PARM_1 = "code"; - private static final String QUERY_PARM_2 = "code2"; - - private WrappedHttpServletRequest request; - private MockHttpServletRequest mockHttpServletRequest; - - @Before - public void setUp() throws Exception { - mockHttpServletRequest = new MockHttpServletRequest(); - request = new WrappedHttpServletRequest(mockHttpServletRequest); - - mockHttpServletRequest.setMethod(REQUEST_METHOD); - mockHttpServletRequest.setRequestURI(REQUEST_URI); - - mockHttpServletRequest.setSecure(true); - mockHttpServletRequest.setScheme("https"); - - mockHttpServletRequest.addHeader(HEADER_SINGLE_VALUE, "baz"); - mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "foo"); - mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "bar"); - - mockHttpServletRequest.addParameter(QUERY_PARM_1, "java"); - mockHttpServletRequest.addParameter(QUERY_PARM_2, "groovy"); - mockHttpServletRequest.setQueryString(String.format("%s=%s&%s=%s", QUERY_PARM_1, "java", QUERY_PARM_2, "groovy")); - mockHttpServletRequest.setCookies(new Cookie(COOKIE_NAME, "yum")); - - mockHttpServletRequest.setContent("All work and no play makes Jack a dull boy".getBytes()); - } - - @Test - public void testGetMethod() throws Exception { - assertNotNull(request.getMethod()); - assertEquals(REQUEST_METHOD, request.getMethod()); - } - - @Test - public void testGetURI() throws Exception { - assertEquals("https://localhost:80" + REQUEST_URI + "?code=java&code2=groovy" , request.getURI()); - } - - @Test - public void testIsSecure() throws Exception { - assertTrue(request.isSecure()); - } - - @Test - public void testGetQueryParamValue() throws Exception { - assertNotNull(request.getQueryParamValue(QUERY_PARM_1)); - assertNotNull(request.getQueryParamValue(QUERY_PARM_2)); - } - - @Test - public void testGetCookie() throws Exception { - assertNotNull(request.getCookie(COOKIE_NAME)); - } - - @Test - public void testGetCookieCookiesNull() throws Exception - { - mockHttpServletRequest.setCookies(null); - request.getCookie(COOKIE_NAME); - } - - @Test - public void testGetHeader() throws Exception { - String header = request.getHeader(HEADER_SINGLE_VALUE); - assertNotNull(header); - assertEquals("baz", header); - } - - @Test - public void testGetHeaders() throws Exception { - List headers = request.getHeaders(HEADER_MULTI_VALUE); - assertNotNull(headers); - assertEquals(2, headers.size()); - assertTrue(headers.contains("foo")); - assertTrue(headers.contains("bar")); - } - - @Test - public void testGetInputStream() throws Exception { - assertNotNull(request.getInputStream()); - } - - @Test - public void testGetRemoteAddr() throws Exception { - assertNotNull(request.getRemoteAddr()); - } -} \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java deleted file mode 100644 index 6349fbb840d2..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.facade; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletResponse; - -import javax.servlet.http.Cookie; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -public class WrappedHttpServletResponseTest { - - private static final String COOKIE_DOMAIN = ".keycloak.org"; - private static final String COOKIE_NAME = "foo"; - private static final String COOKIE_PATH = "/bar"; - private static final String COOKIE_VALUE = "onegreatcookie"; - private static final String HEADER = "Test"; - - private WrappedHttpServletResponse response; - private MockHttpServletResponse mockResponse; - - @Before - public void setUp() throws Exception { - mockResponse = spy(new MockHttpServletResponse()); - response = new WrappedHttpServletResponse(mockResponse); - } - - @Test - public void testResetCookie() throws Exception { - response.resetCookie(COOKIE_NAME, COOKIE_PATH); - verify(mockResponse).addCookie(any(Cookie.class)); - assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName()); - assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath()); - assertEquals(0, mockResponse.getCookie(COOKIE_NAME).getMaxAge()); - assertEquals("", mockResponse.getCookie(COOKIE_NAME).getValue()); - } - - @Test - public void testSetCookie() throws Exception { - int maxAge = 300; - response.setCookie(COOKIE_NAME, COOKIE_VALUE, COOKIE_PATH, COOKIE_DOMAIN, maxAge, false, true); - verify(mockResponse).addCookie(any(Cookie.class)); - assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName()); - assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath()); - assertEquals(COOKIE_DOMAIN, mockResponse.getCookie(COOKIE_NAME).getDomain()); - assertEquals(maxAge, mockResponse.getCookie(COOKIE_NAME).getMaxAge()); - assertEquals(COOKIE_VALUE, mockResponse.getCookie(COOKIE_NAME).getValue()); - assertEquals(true, mockResponse.getCookie(COOKIE_NAME).isHttpOnly()); - } - - @Test - public void testSetStatus() throws Exception { - int status = HttpStatus.OK.value(); - response.setStatus(status); - verify(mockResponse).setStatus(eq(status)); - assertEquals(status, mockResponse.getStatus()); - } - - @Test - public void testAddHeader() throws Exception { - String headerValue = "foo"; - response.addHeader(HEADER, headerValue); - verify(mockResponse).addHeader(eq(HEADER), eq(headerValue)); - assertTrue(mockResponse.containsHeader(HEADER)); - } - - @Test - public void testSetHeader() throws Exception { - String headerValue = "foo"; - response.setHeader(HEADER, headerValue); - verify(mockResponse).setHeader(eq(HEADER), eq(headerValue)); - assertTrue(mockResponse.containsHeader(HEADER)); - } - - @Test - public void testGetOutputStream() throws Exception { - assertNotNull(response.getOutputStream()); - verify(mockResponse).getOutputStream(); - } - - @Test - public void testSendError() throws Exception { - int status = HttpStatus.UNAUTHORIZED.value(); - String reason = HttpStatus.UNAUTHORIZED.getReasonPhrase(); - - response.sendError(status, reason); - verify(mockResponse).sendError(eq(status), eq(reason)); - assertEquals(status, mockResponse.getStatus()); - assertEquals(reason, mockResponse.getErrorMessage()); - } - - @Test - @Ignore - public void testEnd() throws Exception { - // TODO: what is an ended response, one that's committed? - } -} \ No newline at end of file diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java deleted file mode 100755 index c0a69c12685c..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; -import org.keycloak.adapters.springsecurity.account.KeycloakRole; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.context.ApplicationContext; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; - -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.startsWith; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Keycloak authentication process filter test cases. - */ -public class KeycloakAuthenticationProcessingFilterTest { - - private KeycloakAuthenticationProcessingFilter filter; - - @Mock - private AuthenticationManager authenticationManager; - - @Mock - private AdapterDeploymentContext adapterDeploymentContext; - - @Mock - private FilterChain chain; - - private MockHttpServletRequest request; - - @Mock - private HttpServletResponse response; - - @Mock - private ApplicationContext applicationContext; - - @Mock - private AuthenticationSuccessHandler successHandler; - - @Mock - private AuthenticationFailureHandler failureHandler; - - private KeycloakAuthenticationFailureHandler keycloakFailureHandler; - - @Mock - private OidcKeycloakAccount keycloakAccount; - - @Mock - private KeycloakDeployment keycloakDeployment; - - @Mock - private KeycloakSecurityContext keycloakSecurityContext; - - private final List authorities = Collections.singletonList(new KeycloakRole("ROLE_USER")); - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - request = spy(new MockHttpServletRequest()); - request.setRequestURI("http://host"); - filter = new KeycloakAuthenticationProcessingFilter(authenticationManager); - keycloakFailureHandler = new KeycloakAuthenticationFailureHandler(); - - filter.setApplicationContext(applicationContext); - filter.setAuthenticationSuccessHandler(successHandler); - filter.setAuthenticationFailureHandler(failureHandler); - - when(applicationContext.getBean(eq(AdapterDeploymentContext.class))).thenReturn(adapterDeploymentContext); - when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment); - when(keycloakAccount.getPrincipal()).thenReturn( - new KeycloakPrincipal(UUID.randomUUID().toString(), keycloakSecurityContext)); - - - filter.afterPropertiesSet(); - } - - @Test - public void testAttemptAuthenticationExpectRedirect() throws Exception { - when(keycloakDeployment.getAuthUrl()).thenReturn(KeycloakUriBuilder.fromUri("http://localhost:8080/auth")); - when(keycloakDeployment.getResourceName()).thenReturn("resource-name"); - when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie"); - when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE); - when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE); - - filter.attemptAuthentication(request, response); - verify(response).setStatus(302); - verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth")); - } - - @Test(expected = KeycloakAuthenticationException.class) - public void testAttemptAuthenticationWithInvalidToken() throws Exception { - request.addHeader("Authorization", "Bearer xxx"); - filter.attemptAuthentication(request, response); - } - - @Test(expected = KeycloakAuthenticationException.class) - public void testAttemptAuthenticationWithInvalidTokenBearerOnly() throws Exception { - when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.TRUE); - request.addHeader("Authorization", "Bearer xxx"); - filter.attemptAuthentication(request, response); - } - - @Test - public void testSuccessfulAuthenticationInteractive() throws Exception { - request.setRequestURI("http://host" + KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI + "?query"); - Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, true, authorities); - filter.successfulAuthentication(request, response, chain, authentication); - - verify(successHandler).onAuthenticationSuccess(eq(request), eq(response), eq(authentication)); - verify(chain, never()).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); - } - - @Test - public void testSuccessfulAuthenticationBearer() throws Exception { - Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, false, authorities); - this.setBearerAuthHeader(request); - filter.successfulAuthentication(request, response, chain, authentication); - - verify(chain).doFilter(eq(request), eq(response)); - verify(successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Authentication.class)); - } - - @Test - public void testSuccessfulAuthenticationBasicAuth() throws Exception { - Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, false, authorities); - this.setBasicAuthHeader(request); - filter.successfulAuthentication(request, response, chain, authentication); - - verify(chain).doFilter(eq(request), eq(response)); - verify(successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Authentication.class)); - } - - @Test - public void testUnsuccessfulAuthenticationInteractive() throws Exception { - AuthenticationException exception = new BadCredentialsException("OOPS"); - filter.unsuccessfulAuthentication(request, response, exception); - verify(failureHandler).onAuthenticationFailure(eq(request), eq(response), eq(exception)); - } - - @Test - public void testUnsuccessfulAuthenticatioBearer() throws Exception { - AuthenticationException exception = new BadCredentialsException("OOPS"); - this.setBearerAuthHeader(request); - filter.unsuccessfulAuthentication(request, response, exception); - verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(AuthenticationException.class)); - } - - @Test - public void testUnsuccessfulAuthenticatioBasicAuth() throws Exception { - AuthenticationException exception = new BadCredentialsException("OOPS"); - this.setBasicAuthHeader(request); - filter.unsuccessfulAuthentication(request, response, exception); - verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(AuthenticationException.class)); - } - - @Test - public void testDefaultFailureHanlder() throws Exception { - AuthenticationException exception = new BadCredentialsException("OOPS"); - filter.setAuthenticationFailureHandler(keycloakFailureHandler); - filter.unsuccessfulAuthentication(request, response, exception); - - verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), any(String.class)); - } - - @Test(expected = UnsupportedOperationException.class) - public void testSetAllowSessionCreation() throws Exception { - filter.setAllowSessionCreation(true); - } - - @Test(expected = UnsupportedOperationException.class) - public void testSetContinueChainBeforeSuccessfulAuthentication() throws Exception { - filter.setContinueChainBeforeSuccessfulAuthentication(true); - } - - private void setBearerAuthHeader(MockHttpServletRequest request) { - setAuthorizationHeader(request, "Bearer"); - } - - private void setBasicAuthHeader(MockHttpServletRequest request) { - setAuthorizationHeader(request, "Basic"); - } - - private void setAuthorizationHeader(MockHttpServletRequest request, String scheme) { - request.addHeader(KeycloakAuthenticationProcessingFilter.AUTHORIZATION_HEADER, scheme + " " + UUID.randomUUID().toString()); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java deleted file mode 100644 index 15a15ad71043..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.filter; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.constants.AdapterConstants; -import org.springframework.http.HttpMethod; -import org.springframework.mock.web.MockHttpServletRequest; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * Keycloak CSRF request matcher tests. - */ -public class KeycloakCsrfRequestMatcherTest { - - private static final String ROOT_CONTEXT_PATH = ""; - private static final String SUB_CONTEXT_PATH = "/foo"; - - private KeycloakCsrfRequestMatcher matcher = new KeycloakCsrfRequestMatcher(); - - private MockHttpServletRequest request; - - @Before - public void setUp() throws Exception { - request = new MockHttpServletRequest(); - } - - @Test - public void testMatchesMethodGet() throws Exception { - request.setMethod(HttpMethod.GET.name()); - assertFalse(matcher.matches(request)); - } - - @Test - public void testMatchesMethodPost() throws Exception { - prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, "some/random/uri"); - assertTrue(matcher.matches(request)); - - prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, "some/random/uri"); - assertTrue(matcher.matches(request)); - } - - @Test - public void testMatchesKeycloakLogout() throws Exception { - - prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_LOGOUT); - assertFalse(matcher.matches(request)); - - prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_LOGOUT); - assertFalse(matcher.matches(request)); - } - - @Test - public void testMatchesKeycloakPushNotBefore() throws Exception { - - prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_PUSH_NOT_BEFORE); - assertFalse(matcher.matches(request)); - - prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_PUSH_NOT_BEFORE); - assertFalse(matcher.matches(request)); - } - - @Test - public void testMatchesKeycloakQueryBearerToken() throws Exception { - - prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_QUERY_BEARER_TOKEN); - assertFalse(matcher.matches(request)); - - prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_QUERY_BEARER_TOKEN); - assertFalse(matcher.matches(request)); - } - - @Test - public void testMatchesKeycloakTestAvailable() throws Exception { - - prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_TEST_AVAILABLE); - assertFalse(matcher.matches(request)); - - prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_TEST_AVAILABLE); - assertFalse(matcher.matches(request)); - } - - private void prepareRequest(HttpMethod method, String contextPath, String uri) { - request.setMethod(method.name()); - request.setContextPath(contextPath); - request.setRequestURI(contextPath + "/" + uri); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilterTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilterTest.java deleted file mode 100644 index d23fd8fea67a..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilterTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.keycloak.adapters.springsecurity.filter; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter.PreAuthActionsHandlerFactory; -import org.mockito.Mock; -import org.springframework.context.ApplicationContext; - -public class KeycloakPreAuthActionsFilterTest { - - private KeycloakPreAuthActionsFilter filter; - - @Mock - private NodesRegistrationManagement nodesRegistrationManagement; - @Mock - private ApplicationContext applicationContext; - @Mock - private AdapterDeploymentContext deploymentContext; - @Mock - private PreAuthActionsHandlerFactory preAuthActionsHandlerFactory; - @Mock - private UserSessionManagement userSessionManagement; - @Mock - private PreAuthActionsHandler preAuthActionsHandler; - @Mock - private KeycloakDeployment deployment; - - @Mock - private HttpServletRequest request; - @Mock - private HttpServletResponse response; - @Mock - private FilterChain chain; - - @Before - public void setUp() throws Exception { - initMocks(this); - filter = new KeycloakPreAuthActionsFilter(userSessionManagement); - filter.setNodesRegistrationManagement(nodesRegistrationManagement); - filter.setApplicationContext(applicationContext); - filter.setPreAuthActionsHandlerFactory(preAuthActionsHandlerFactory); - when(applicationContext.getBean(AdapterDeploymentContext.class)).thenReturn(deploymentContext); - when(deploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(deployment); - when(preAuthActionsHandlerFactory.createPreAuthActionsHandler(any(HttpFacade.class))).thenReturn(preAuthActionsHandler); - when(deployment.isConfigured()).thenReturn(true); - filter.initFilterBean(); - } - - @Test - public void shouldIgnoreChainWhenPreAuthActionHandlerHandled() throws Exception { - when(preAuthActionsHandler.handleRequest()).thenReturn(true); - - filter.doFilter(request, response, chain); - - verifyZeroInteractions(chain); - verify(nodesRegistrationManagement).tryRegister(deployment); - } - - @Test - public void shouldContinueChainWhenPreAuthActionHandlerDidNotHandle() throws Exception { - when(preAuthActionsHandler.handleRequest()).thenReturn(false); - - filter.doFilter(request, response, chain); - - verify(chain).doFilter(request, response);; - verify(nodesRegistrationManagement).tryRegister(deployment); - } - - @After - public void tearDown() { - filter.destroy(); - verify(nodesRegistrationManagement).stop(); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcherTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcherTest.java deleted file mode 100644 index 0e02856df7ff..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/QueryParamPresenceRequestMatcherTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017 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.adapters.springsecurity.filter; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.Collections; -import java.util.Map; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.http.HttpMethod; -import org.springframework.mock.web.MockHttpServletRequest; - -public class QueryParamPresenceRequestMatcherTest { - private static final String ROOT_CONTEXT_PATH = ""; - - private static final String VALID_PARAMETER = "access_token"; - - private QueryParamPresenceRequestMatcher matcher = new QueryParamPresenceRequestMatcher(VALID_PARAMETER); - - private MockHttpServletRequest request; - - @Before - public void setUp() throws Exception { - request = new MockHttpServletRequest(); - } - - @Test - public void testDoesNotMatchWithoutQueryParameter() throws Exception { - prepareRequest(HttpMethod.GET, ROOT_CONTEXT_PATH, "some/random/uri", Collections.EMPTY_MAP); - assertFalse(matcher.matches(request)); - } - - @Test - public void testMatchesWithValidParameter() throws Exception { - prepareRequest(HttpMethod.GET, ROOT_CONTEXT_PATH, "some/random/uri", Collections.singletonMap(VALID_PARAMETER, (Object) "123")); - assertTrue(matcher.matches(request)); - } - - @Test - public void testDoesNotMatchWithInvalidParameter() throws Exception { - prepareRequest(HttpMethod.GET, ROOT_CONTEXT_PATH, "some/random/uri", Collections.singletonMap("some_parameter", (Object) "123")); - assertFalse(matcher.matches(request)); - } - - private void prepareRequest(HttpMethod method, String contextPath, String uri, Map params) { - request.setMethod(method.name()); - request.setContextPath(contextPath); - request.setRequestURI(contextPath + "/" + uri); - request.setParameters(params); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java deleted file mode 100755 index 6984cd5fa53b..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.enums.TokenStore; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -/** - * Spring Security adapter token store factory tests. - */ -public class SpringSecurityAdapterTokenStoreFactoryTest { - - private AdapterTokenStoreFactory factory = new SpringSecurityAdapterTokenStoreFactory(); - - @Mock - private KeycloakDeployment deployment; - - @Mock - private HttpServletRequest request; - - @Mock - private HttpServletResponse response; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testCreateAdapterTokenStore() throws Exception { - when(deployment.getTokenStore()).thenReturn(TokenStore.SESSION); - AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request, response); - assertTrue(store instanceof SpringSecurityTokenStore); - } - - @Test - public void testCreateAdapterTokenStoreUsingCookies() throws Exception { - when(deployment.getTokenStore()).thenReturn(TokenStore.COOKIE); - AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request, response); - assertTrue(store instanceof SpringSecurityCookieTokenStore); - } - - @Test(expected = IllegalArgumentException.class) - public void testCreateAdapterTokenStoreNullDeployment() throws Exception { - factory.createAdapterTokenStore(null, request, response); - } - - @Test(expected = IllegalArgumentException.class) - public void testCreateAdapterTokenStoreNullRequest() throws Exception { - factory.createAdapterTokenStore(deployment, null, response); - } - - @Test - public void testCreateAdapterTokenStoreNullResponse() throws Exception { - when(deployment.getTokenStore()).thenReturn(TokenStore.SESSION); - factory.createAdapterTokenStore(deployment, request, null); - } - - @Test(expected = IllegalArgumentException.class) - public void testCreateAdapterTokenStoreNullResponseUsingCookies() throws Exception { - when(deployment.getTokenStore()).thenReturn(TokenStore.COOKIE); - factory.createAdapterTokenStore(deployment, request, null); - } -} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java deleted file mode 100755 index 8b5ebbc40f2d..000000000000 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2016 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.adapters.springsecurity.token; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.springsecurity.account.KeycloakRole; -import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; - -import java.security.Principal; -import java.util.Collections; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Spring Security token store tests. - */ -public class SpringSecurityTokenStoreTest { - - private SpringSecurityTokenStore store; - - @Mock - private KeycloakDeployment deployment; - - @Mock - private Principal principal; - - @Mock - private RequestAuthenticator requestAuthenticator; - - @Mock - private RefreshableKeycloakSecurityContext keycloakSecurityContext; - - private MockHttpServletRequest request; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - request = new MockHttpServletRequest(); - store = new SpringSecurityTokenStore(deployment, request); - } - - @After - public void tearDown() throws Exception { - SecurityContextHolder.clearContext(); - } - - @Test - public void testIsCached() throws Exception { - Authentication authentication = new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("ROLE_FOO"))); - SecurityContextHolder.getContext().setAuthentication(authentication); - assertFalse(store.isCached(requestAuthenticator)); - } - - @Test - public void testSaveAccountInfo() throws Exception { - OidcKeycloakAccount account = new SimpleKeycloakAccount(principal, Collections.singleton("FOO"), keycloakSecurityContext); - Authentication authentication; - - store.saveAccountInfo(account); - authentication = SecurityContextHolder.getContext().getAuthentication(); - - assertNotNull(authentication); - assertTrue(authentication instanceof KeycloakAuthenticationToken); - } - - @Test(expected = IllegalStateException.class) - public void testSaveAccountInfoInvalidAuthenticationType() throws Exception { - OidcKeycloakAccount account = new SimpleKeycloakAccount(principal, Collections.singleton("FOO"), keycloakSecurityContext); - Authentication authentication = new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("ROLE_FOO"))); - SecurityContextHolder.getContext().setAuthentication(authentication); - store.saveAccountInfo(account); - } - - @Test - public void testLogout() throws Exception { - MockHttpSession session = (MockHttpSession) request.getSession(true); - assertFalse(session.isInvalid()); - store.logout(); - assertTrue(session.isInvalid()); - } -} diff --git a/adapters/oidc/spring-security/src/test/resources/keycloak.json b/adapters/oidc/spring-security/src/test/resources/keycloak.json deleted file mode 100644 index 61e0f9371de9..000000000000 --- a/adapters/oidc/spring-security/src/test/resources/keycloak.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "realm": "spring-security", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCh65Gqi3BSaVe12JHlqChWm8WscICrj46MVqmRoO9FCmqbxEpCQhE1RLjW+GDyc3YdXW3xqUQ3AZxDkTmN1h6BWkhdxPLzA4EnwgWmGurhyJlUF9Id2tKns0jbC+Z7kIb2LcOiKHKL7mRb3q7EtWubNnrvunv8fx+WeXGaQoGEVQIDAQAB", - "auth-server-url": "http://localhost:8080/auth", - "ssl-required": "external", - "resource": "some-resource", - "credentials": { - "secret": "a9c3501e-20dd-4277-8a7b-351063848446" - } -} diff --git a/adapters/oidc/tomcat/pom.xml b/adapters/oidc/tomcat/pom.xml deleted file mode 100755 index e79897be4b29..000000000000 --- a/adapters/oidc/tomcat/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - Keycloak Tomcat Integration - - 4.0.0 - - keycloak-tomcat-integration-pom - pom - - - tomcat-core - tomcat - - - diff --git a/adapters/oidc/tomcat/tomcat-core/pom.xml b/adapters/oidc/tomcat/tomcat-core/pom.xml deleted file mode 100755 index b7859c0d7d20..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/pom.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - keycloak-tomcat-integration-pom - org.keycloak - 999.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-tomcat-core-adapter - Keycloak Tomcat Core Integration - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-tomcat-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - - - org.keycloak - keycloak-authz-client - - - - - org.apache.tomcat - tomcat-catalina - ${tomcat8.version} - compile - - - - junit - junit - test - - - - diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractAuthenticatedActionsValve.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractAuthenticatedActionsValve.java deleted file mode 100644 index 123c2ec0aa48..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractAuthenticatedActionsValve.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Container; -import org.apache.catalina.Valve; -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.apache.catalina.valves.ValveBase; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakDeployment; - -import javax.servlet.ServletException; -import java.io.IOException; - -/** - * Abstract base for pre-installed actions that must be authenticated - *

- * Actions include: - *

- * CORS Origin Check and Response headers - * k_query_bearer_token: Get bearer token from server for Javascripts CORS requests - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractAuthenticatedActionsValve extends ValveBase { - private static final Logger log = Logger.getLogger(AbstractAuthenticatedActionsValve.class); - protected AdapterDeploymentContext deploymentContext; - - public AbstractAuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container) { - this.deploymentContext = deploymentContext; - if (next == null) throw new RuntimeException("Next valve is null!!!"); - setNext(next); - setContainer(container); - } - - @Override - public void invoke(Request request, Response response) throws IOException, ServletException { - log.debugv("AuthenticatedActionsValve.invoke {0}", request.getRequestURI()); - CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment != null && deployment.isConfigured()) { - AuthenticatedActionsHandler handler = new AuthenticatedActionsHandler(deployment, new OIDCCatalinaHttpFacade(request, response)); - if (handler.handledRequest()) { - return; - } - - } - getNext().invoke(request, response); - } -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java deleted file mode 100755 index cfe3f9beef4c..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.*; -import org.apache.catalina.authenticator.FormAuthenticator; -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.constants.AdapterConstants; -import org.keycloak.enums.TokenStore; - -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -/** - * Keycloak authentication valve - * - * @author Davide Ungari - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener { - - public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; - - private final static Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class); - protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); - protected AdapterDeploymentContext deploymentContext; - protected NodesRegistrationManagement nodesRegistrationManagement; - - @Override - public void lifecycleEvent(LifecycleEvent event) { - if (Lifecycle.START_EVENT.equals(event.getType())) { - cache = false; - } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { - keycloakInit(); - } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) { - beforeStop(); - } - } - - protected void logoutInternal(Request request) { - KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); - if (ksc != null) { - CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, null); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (ksc instanceof RefreshableKeycloakSecurityContext) { - ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); - } - - AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); - tokenStore.logout(); - request.removeAttribute(KeycloakSecurityContext.class.getName()); - } - request.setUserPrincipal(null); - } - - protected void beforeStop() { - if (nodesRegistrationManagement != null) { - nodesRegistrationManagement.stop(); - } - } - - - @SuppressWarnings("UseSpecificCatch") - public void keycloakInit() { - // Possible scenarios: - // 1) The deployment has a keycloak.config.resolver specified and it exists: - // Outcome: adapter uses the resolver - // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exist, isn't a resolver, ...) : - // Outcome: adapter is left unconfigured - // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent) - // Outcome: adapter uses it - // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent) - // Outcome: adapter is left unconfigured - - String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new AdapterDeploymentContext(configResolver); - log.debugv("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage()); - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } - } else { - InputStream configInputStream = getConfigInputStream(context); - KeycloakDeployment kd; - if (configInputStream == null) { - log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - kd = new KeycloakDeployment(); - } else { - kd = KeycloakDeploymentBuilder.build(configInputStream); - } - deploymentContext = new AdapterDeploymentContext(kd); - log.debug("Keycloak is using a per-deployment configuration."); - } - - context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); - AbstractAuthenticatedActionsValve actions = createAuthenticatedActionsValve(deploymentContext, getNext(), getContainer()); - setNext(actions); - - nodesRegistrationManagement = new NodesRegistrationManagement(); - } - - - private static InputStream getJSONFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (json == null) { - return null; - } - log.trace("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME); - return new ByteArrayInputStream(json.getBytes()); - } - - private static InputStream getConfigInputStream(Context context) { - InputStream is = getJSONFromServletContext(context.getServletContext()); - if (is == null) { - String path = context.getServletContext().getInitParameter("keycloak.config.file"); - if (path == null) { - log.trace("**** using /WEB-INF/keycloak.json"); - is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - log.errorv("NOT FOUND {0}", path); - throw new RuntimeException(e); - } - } - } - return is; - } - - @Override - public void invoke(Request request, Response response) throws IOException, ServletException { - try { - CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); - Manager sessionManager = request.getContext().getManager(); - CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(userSessionManagement, sessionManager); - PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, deploymentContext, facade); - if (handler.handleRequest()) { - return; - } - checkKeycloakSession(request, facade); - super.invoke(request, response); - } finally { - } - } - - protected abstract PrincipalFactory createPrincipalFactory(); - protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException; - protected abstract AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container); - - protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { - CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - //needed for the EAP6/AS7 adapter relying on the tomcat core adapter - facade.getResponse().sendError(401); - return false; - } - AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); - - nodesRegistrationManagement.tryRegister(deployment); - - CatalinaRequestAuthenticator authenticator = createRequestAuthenticator(request, facade, deployment, tokenStore); - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - if (facade.isEnded()) { - return false; - } - return true; - } - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - return false; - } - - protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, createPrincipalFactory()); - } - - /** - * Checks that access token is still valid. Will attempt refresh of token if it is not. - * - * @param request - */ - protected void checkKeycloakSession(Request request, HttpFacade facade) { - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); - tokenStore.checkCurrentToken(); - } - - public void keycloakSaveRequest(Request request) throws IOException { - saveRequest(request, request.getSessionInternal(true)); - } - - public boolean keycloakRestoreRequest(Request request) { - try { - return restoreRequest(request, request.getSessionInternal()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { - AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE); - if (store != null) { - return store; - } - - if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) { - store = createSessionTokenStore(request, resolvedDeployment); - } else { - store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, createPrincipalFactory()); - } - - request.setNote(TOKEN_STORE_NOTE, store); - return store; - } - - private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) { - AdapterTokenStore store; - store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement, createPrincipalFactory(), this); - return store; - } - -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaAdapterSessionStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaAdapterSessionStore.java deleted file mode 100755 index 53dcde249889..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaAdapterSessionStore.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.connector.Request; -import org.keycloak.adapters.spi.AdapterSessionStore; - -import java.io.IOException; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaAdapterSessionStore implements AdapterSessionStore { - protected Request request; - protected AbstractKeycloakAuthenticatorValve valve; - - public CatalinaAdapterSessionStore(Request request, AbstractKeycloakAuthenticatorValve valve) { - this.request = request; - this.valve = valve; - } - - public void saveRequest() { - try { - valve.keycloakSaveRequest(request); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public boolean restoreRequest() { - return valve.keycloakRestoreRequest(request); - } -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java deleted file mode 100755 index 864711d90fce..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaCookieTokenStore.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.connector.Request; -import org.apache.catalina.realm.GenericPrincipal; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.CookieTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -import java.util.Set; -import java.util.logging.Logger; - -/** - * @author Marek Posolda - */ -public class CatalinaCookieTokenStore implements AdapterTokenStore { - - private static final Logger log = Logger.getLogger(""+CatalinaCookieTokenStore.class); - - private Request request; - private HttpFacade facade; - private KeycloakDeployment deployment; - private PrincipalFactory principalFactory; - - private KeycloakPrincipal authenticatedPrincipal; - - public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment, PrincipalFactory principalFactory) { - this.request = request; - this.facade = facade; - this.deployment = deployment; - this.principalFactory = principalFactory; - } - - - @Override - public void checkCurrentToken() { - this.authenticatedPrincipal = checkPrincipalFromCookie(); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - // Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request - if (authenticatedPrincipal != null) { - log.fine("remote logged in already. Establish state from cookie"); - RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext(); - - if (!securityContext.getRealm().equals(deployment.getRealm())) { - log.fine("Account from cookie is from a different realm than for the request."); - return false; - } - - securityContext.setCurrentRequestInfo(deployment, this); - Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - GenericPrincipal principal = principalFactory.createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles); - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK"); - return true; - } else { - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); - CookieTokenStore.setTokenCookie(deployment, facade, securityContext); - } - - @Override - public void logout() { - CookieTokenStore.removeCookie(deployment, facade); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext secContext) { - CookieTokenStore.setTokenCookie(deployment, facade, secContext); - } - - @Override - public void saveRequest() { - - } - - @Override - public boolean restoreRequest() { - return false; - } - - /** - * Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active - * - * @return valid principal - */ - protected KeycloakPrincipal checkPrincipalFromCookie() { - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); - if (principal == null) { - log.fine("Account was not in cookie or was invalid"); - return null; - } - - RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext(); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal; - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return principal; - - log.fine("Cleanup and expire cookie for user " + principal.getName() + " after failed refresh"); - request.setUserPrincipal(null); - request.setAuthType(null); - CookieTokenStore.removeCookie(deployment, facade); - return null; - } -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java deleted file mode 100755 index 84c39a7655d5..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.connector.Request; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; - -import javax.servlet.http.HttpSession; -import java.security.Principal; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author Davide Ungari - * @version $Revision: 1 $ - */ -public class CatalinaRequestAuthenticator extends RequestAuthenticator { - private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class); - protected Request request; - protected PrincipalFactory principalFactory; - - public CatalinaRequestAuthenticator(KeycloakDeployment deployment, - AdapterTokenStore tokenStore, - CatalinaHttpFacade facade, - Request request, - PrincipalFactory principalFactory) { - super(facade, deployment, tokenStore, request.getConnector().getRedirectPort()); - this.request = request; - this.principalFactory = principalFactory; - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { - final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); - final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - OidcKeycloakAccount account = new OidcKeycloakAccount() { - - @Override - public Principal getPrincipal() { - return skp; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public KeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - }; - - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - this.tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { - RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - if (log.isLoggable(Level.FINE)) { - log.fine("Completing bearer authentication. Bearer roles: " + roles); - } - Principal generalPrincipal = principalFactory.createPrincipal(request.getContext().getRealm(), principal, roles); - request.setUserPrincipal(generalPrincipal); - request.setAuthType(method); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - - @Override - protected String changeHttpSessionId(boolean create) { - HttpSession session = request.getSession(create); - return session != null ? session.getId() : null; - } - -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java deleted file mode 100755 index 5f2ba3edf36e..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Session; -import org.apache.catalina.connector.Request; -import org.apache.catalina.realm.GenericPrincipal; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.common.util.DelegatingSerializationFilter; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; -import java.security.Principal; -import java.util.Set; -import java.util.logging.Logger; - -/** - * @author Marek Posolda - */ -public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore implements AdapterTokenStore { - - private static final Logger log = Logger.getLogger("" + CatalinaSessionTokenStore.class); - - private KeycloakDeployment deployment; - private CatalinaUserSessionManagement sessionManagement; - protected PrincipalFactory principalFactory; - - - public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment, - CatalinaUserSessionManagement sessionManagement, - PrincipalFactory principalFactory, - AbstractKeycloakAuthenticatorValve valve) { - super(request, valve); - this.deployment = deployment; - this.sessionManagement = sessionManagement; - this.principalFactory = principalFactory; - } - - @Override - public void checkCurrentToken() { - Session catalinaSession = request.getSessionInternal(false); - if (catalinaSession == null) return; - SerializableKeycloakAccount account = (SerializableKeycloakAccount) catalinaSession.getSession().getAttribute(SerializableKeycloakAccount.class.getName()); - if (account == null) { - return; - } - - RefreshableKeycloakSecurityContext session = account.getKeycloakSecurityContext(); - if (session == null) return; - - // just in case session got serialized - if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { - request.setAttribute(KeycloakSecurityContext.class.getName(), session); - request.setUserPrincipal(account.getPrincipal()); - request.setAuthType("KEYCLOAK"); - return; - } - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) { - request.setAttribute(KeycloakSecurityContext.class.getName(), session); - request.setUserPrincipal(account.getPrincipal()); - request.setAuthType("KEYCLOAK"); - return; - } - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh"); - request.setUserPrincipal(null); - request.setAuthType(null); - cleanSession(catalinaSession); - catalinaSession.expire(); - } - - protected void cleanSession(Session catalinaSession) { - catalinaSession.getSession().removeAttribute(KeycloakSecurityContext.class.getName()); - catalinaSession.getSession().removeAttribute(SerializableKeycloakAccount.class.getName()); - catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName()); - catalinaSession.setPrincipal(null); - catalinaSession.setAuthType(null); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - Session session = request.getSessionInternal(false); - if (session == null) return false; - SerializableKeycloakAccount account = (SerializableKeycloakAccount) session.getSession().getAttribute(SerializableKeycloakAccount.class.getName()); - if (account == null) { - return false; - } - - log.fine("remote logged in already. Establish state from session"); - - RefreshableKeycloakSecurityContext securityContext = account.getKeycloakSecurityContext(); - - if (!deployment.getRealm().equals(securityContext.getRealm())) { - log.fine("Account from cookie is from a different realm than for the request."); - cleanSession(session); - return false; - } - - securityContext.setCurrentRequestInfo(deployment, this); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); - // in clustered environment in JBossWeb, principal is not serialized or saved - if (principal == null) { - principal = principalFactory.createPrincipal(request.getContext().getRealm(), account.getPrincipal(), account.getRoles()); - session.setPrincipal(principal); - session.setAuthType("KEYCLOAK"); - - } - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK"); - - restoreRequest(); - return true; - } - - public static class SerializableKeycloakAccount implements OidcKeycloakAccount, Serializable { - protected Set roles; - protected Principal principal; - protected RefreshableKeycloakSecurityContext securityContext; - - public SerializableKeycloakAccount(Set roles, Principal principal, RefreshableKeycloakSecurityContext securityContext) { - this.roles = roles; - this.principal = principal; - this.securityContext = securityContext; - } - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { - return securityContext; - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - DelegatingSerializationFilter.builder() - .addAllowedClass(CatalinaSessionTokenStore.SerializableKeycloakAccount.class) - .addAllowedClass(RefreshableKeycloakSecurityContext.class) - .addAllowedClass(KeycloakSecurityContext.class) - .addAllowedClass(KeycloakPrincipal.class) - .setFilter(in); - - in.defaultReadObject(); - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); - Set roles = account.getRoles(); - GenericPrincipal principal = principalFactory.createPrincipal(request.getContext().getRealm(), account.getPrincipal(), roles); - - SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext); - Session session = request.getSessionInternal(true); - session.setPrincipal(principal); - session.setAuthType("KEYCLOAK"); - session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount); - session.getSession().setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - String username = securityContext.getToken().getSubject(); - log.fine("userSessionManagement.login: " + username); - this.sessionManagement.login(session); - } - - @Override - public void logout() { - Session session = request.getSessionInternal(false); - if (session != null) { - cleanSession(session); - } - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } - -} diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/OIDCCatalinaHttpFacade.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/OIDCCatalinaHttpFacade.java deleted file mode 100755 index fb180ac7bfb1..000000000000 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/OIDCCatalinaHttpFacade.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; - -import javax.servlet.http.HttpServletResponse; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCCatalinaHttpFacade extends CatalinaHttpFacade implements OIDCHttpFacade{ - - public OIDCCatalinaHttpFacade(org.apache.catalina.connector.Request request, HttpServletResponse response) { - super(response, request); - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); - } - -} diff --git a/adapters/oidc/tomcat/tomcat/pom.xml b/adapters/oidc/tomcat/tomcat/pom.xml deleted file mode 100755 index 2a5d96149038..000000000000 --- a/adapters/oidc/tomcat/tomcat/pom.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - keycloak-tomcat-integration-pom - org.keycloak - 999.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-tomcat-adapter - Keycloak Tomcat Integration - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-core - - - org.apache.tomcat - tomcat-servlet-api - ${tomcat8.version} - provided - - - org.apache.tomcat - tomcat-catalina - ${tomcat8.version} - provided - - - - org.keycloak - keycloak-tomcat-core-adapter - - - org.apache.tomcat - tomcat-servlet-api - - - org.apache.tomcat - tomcat-catalina - - - org.apache.tomcat - catalina - - - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - junit - junit - test - - - - diff --git a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/AuthenticatedActionsValve.java b/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/AuthenticatedActionsValve.java deleted file mode 100644 index 82796d66abed..000000000000 --- a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/AuthenticatedActionsValve.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.adapters.tomcat; - -import org.apache.catalina.Container; -import org.apache.catalina.Valve; -import org.keycloak.adapters.AdapterDeploymentContext; - -public class AuthenticatedActionsValve extends AbstractAuthenticatedActionsValve { - - public AuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container) { - super(deploymentContext, next, container); - } - - @Override - public boolean isAsyncSupported() { - return true; - } -} diff --git a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java b/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java deleted file mode 100755 index 02868820579b..000000000000 --- a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Container; -import org.apache.catalina.Valve; -import org.apache.catalina.authenticator.FormAuthenticator; -import org.apache.catalina.connector.Request; -import org.apache.catalina.core.StandardContext; -import org.apache.catalina.realm.GenericPrincipal; -import org.apache.tomcat.util.descriptor.web.LoginConfig; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.spi.HttpFacade; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.lang.reflect.Method; -import java.security.Principal; -import java.util.List; - -/** - * Keycloak authentication valve - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve { - - /** - * Method called by Tomcat < 8.5.5 - */ - public boolean authenticate(Request request, HttpServletResponse response) throws IOException { - return authenticateInternal(request, response, request.getContext().getLoginConfig()); - } - - /** - * Method called by Tomcat >= 8.5.5 - */ - protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { - return this.authenticate(request, response); - } - - @Override - protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { - if (loginConfig == null) return false; - LoginConfig config = (LoginConfig)loginConfig; - if (config.getErrorPage() == null) return false; - // had to do this to get around compiler/IDE issues :( - try { - Method method = null; - /* - for (Method m : getClass().getDeclaredMethods()) { - if (m.getName().equals("forwardToErrorPage")) { - method = m; - break; - } - } - */ - method = FormAuthenticator.class.getDeclaredMethod("forwardToErrorPage", Request.class, HttpServletResponse.class, LoginConfig.class); - method.setAccessible(true); - method.invoke(this, request, response, config); - } catch (Exception e) { - throw new RuntimeException(e); - } - return true; - } - - protected void initInternal() { - StandardContext standardContext = (StandardContext) context; - standardContext.addLifecycleListener(this); - } - - public void logout(Request request) { - logoutInternal(request); - } - - @Override - protected GenericPrincipalFactory createPrincipalFactory() { - return new GenericPrincipalFactory() { - @Override - protected GenericPrincipal createPrincipal(Principal userPrincipal, List roles) { - return new GenericPrincipal(userPrincipal.getName(), null, roles, userPrincipal, null); - } - }; - } - - @Override - protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { - return super.getTokenStore(request, facade, resolvedDeployment); - } - - @Override - protected AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container) { - return new AuthenticatedActionsValve(deploymentContext, next, container); - } - - @Override - protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - return new TomcatRequestAuthenticator(deployment, tokenStore, facade, request, createPrincipalFactory()); - } -} diff --git a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/TomcatRequestAuthenticator.java b/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/TomcatRequestAuthenticator.java deleted file mode 100755 index 8103959fcaa0..000000000000 --- a/adapters/oidc/tomcat/tomcat/src/main/java/org/keycloak/adapters/tomcat/TomcatRequestAuthenticator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.connector.Request; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class TomcatRequestAuthenticator extends CatalinaRequestAuthenticator { - public TomcatRequestAuthenticator(KeycloakDeployment deployment, AdapterTokenStore tokenStore, CatalinaHttpFacade facade, Request request, GenericPrincipalFactory principalFactory) { - super(deployment, tokenStore, facade, request, principalFactory); - } - - @Override - protected String changeHttpSessionId(boolean create) { - Request request = this.request; - HttpSession session = request.getSession(false); - if (session == null) { - return request.getSession(true).getId(); - } - if (!deployment.isTurnOffChangeSessionIdOnLogin()) return request.changeSessionId(); - else return session.getId(); - } -} diff --git a/adapters/oidc/undertow/pom.xml b/adapters/oidc/undertow/pom.xml deleted file mode 100755 index 6d179ae84d86..000000000000 --- a/adapters/oidc/undertow/pom.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-undertow-adapter - Keycloak Undertow Integration - - - - - org.keycloak.adapters.undertow.* - - - io.undertow.*;version="[1.4,3)", - javax.servlet.*;version="[3.1,5)";resolution:=optional, - *;resolution:=optional - - - - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-undertow-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - - io.undertow - undertow-servlet - provided - - - io.undertow - undertow-core - provided - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java deleted file mode 100755 index 2398c95c83b2..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.NotificationReceiver; -import io.undertow.security.api.SecurityContext; -import io.undertow.security.api.SecurityNotification; -import io.undertow.server.HttpServerExchange; -import io.undertow.util.AttachmentKey; -import io.undertow.util.Headers; -import io.undertow.util.StatusCodes; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.enums.TokenStore; - -/** - * Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public abstract class AbstractUndertowKeycloakAuthMech implements AuthenticationMechanism { - public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); - protected AdapterDeploymentContext deploymentContext; - protected UndertowUserSessionManagement sessionManagement; - protected String errorPage; - - public AbstractUndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { - this.deploymentContext = deploymentContext; - this.sessionManagement = sessionManagement; - this.errorPage = errorPage; - } - - @Override - public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { - AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY); - if (challenge != null) { - UndertowHttpFacade facade = createFacade(exchange); - if (challenge.challenge(facade)) { - return new ChallengeResult(true, exchange.getResponseCode()); - } - } - return new ChallengeResult(false); - } - - public UndertowHttpFacade createFacade(HttpServerExchange exchange) { - return new OIDCUndertowHttpFacade(exchange); - } - - protected Integer servePage(final HttpServerExchange exchange, final String location) { - sendRedirect(exchange, location); - return StatusCodes.TEMPORARY_REDIRECT; - } - - static void sendRedirect(final HttpServerExchange exchange, final String location) { - // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. - String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; - exchange.getResponseHeaders().put(Headers.LOCATION, loc); - } - - - - protected void registerNotifications(final SecurityContext securityContext) { - - final NotificationReceiver logoutReceiver = new NotificationReceiver() { - @Override - public void handleNotification(SecurityNotification notification) { - if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return; - - HttpServerExchange exchange = notification.getExchange(); - UndertowHttpFacade facade = createFacade(exchange); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - KeycloakSecurityContext ksc = exchange.getAttachment(OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY); - if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) { - ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); - } - AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext); - tokenStore.logout(); - } - }; - - securityContext.registerNotificationReceiver(logoutReceiver); - } - - /** - * Call this inside your authenticate method. - */ - protected AuthenticationMechanismOutcome keycloakAuthenticate(HttpServerExchange exchange, SecurityContext securityContext, RequestAuthenticator authenticator) { - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - registerNotifications(securityContext); - return AuthenticationMechanismOutcome.AUTHENTICATED; - } - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, challenge); - } - - if (outcome == AuthOutcome.FAILED) { - return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; - } - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - - protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) { - if (deployment.getTokenStore() == TokenStore.SESSION) { - return new UndertowSessionTokenStore(exchange, deployment, sessionManagement, securityContext); - } else { - return new UndertowCookieTokenStore(facade, deployment, securityContext); - } - } - -} \ No newline at end of file diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java deleted file mode 100755 index 8e2da9388b1f..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.util.Sessions; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - * @version $Revision: 1 $ - */ -public abstract class AbstractUndertowRequestAuthenticator extends RequestAuthenticator { - protected SecurityContext securityContext; - protected HttpServerExchange exchange; - - - public AbstractUndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, - SecurityContext securityContext, HttpServerExchange exchange, - AdapterTokenStore tokenStore) { - super(facade, deployment, tokenStore, sslRedirectPort); - this.securityContext = securityContext; - this.exchange = exchange; - } - - protected void propagateKeycloakContext(KeycloakUndertowAccount account) { - exchange.putAttachment(OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY, account.getKeycloakSecurityContext()); - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(KeycloakPrincipal principal) { - KeycloakUndertowAccount account = createAccount(principal); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - propagateKeycloakContext(account); - tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { - KeycloakUndertowAccount account = createAccount(principal); - securityContext.authenticationComplete(account, method, false); - propagateKeycloakContext(account); - } - - @Override - protected String changeHttpSessionId(boolean create) { - if (create) { - Session session = Sessions.getOrCreateSession(exchange); - return session.getId(); - } else { - Session session = Sessions.getSession(exchange); - return session != null ? session.getId() : null; - } - } - - /** - * Subclasses need to be able to create their own version of the KeycloakUndertowAccount - * @return The account - */ - protected abstract KeycloakUndertowAccount createAccount(KeycloakPrincipal principal); - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakChallenge.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakChallenge.java deleted file mode 100755 index 2a22d49ef3ee..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakChallenge.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface KeycloakChallenge { - public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext); -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java deleted file mode 100755 index b73832192ab8..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.AuthenticationMechanismFactory; -import io.undertow.security.idm.Account; -import io.undertow.security.idm.Credential; -import io.undertow.security.idm.IdentityManager; -import io.undertow.server.handlers.form.FormParserFactory; -import io.undertow.servlet.ServletExtension; -import io.undertow.servlet.api.AuthMethodConfig; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.InstanceFactory; -import io.undertow.servlet.api.InstanceHandle; -import io.undertow.servlet.api.ListenerInfo; -import io.undertow.servlet.api.LoginConfig; -import io.undertow.servlet.api.ServletSessionConfig; -import io.undertow.servlet.util.ImmediateInstanceHandle; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.constants.AdapterConstants; - -import javax.servlet.ServletContext; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class KeycloakServletExtension implements ServletExtension { - - protected static Logger log = Logger.getLogger(KeycloakServletExtension.class); - private final AdapterDeploymentContext deploymentContext; - - public KeycloakServletExtension() { - this(null); - } - - public KeycloakServletExtension(AdapterDeploymentContext deploymentContext) { - this.deploymentContext = deploymentContext; - } - - // todo when this DeploymentInfo method of the same name is fixed. - public boolean isAuthenticationMechanismPresent(DeploymentInfo deploymentInfo, final String mechanismName) { - LoginConfig loginConfig = deploymentInfo.getLoginConfig(); - if (loginConfig != null) { - for (AuthMethodConfig method : loginConfig.getAuthMethods()) { - if (method.getName().equalsIgnoreCase(mechanismName)) { - return true; - } - } - } - return false; - } - - private static InputStream getJSONFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (json == null) { - return null; - } - return new ByteArrayInputStream(json.getBytes()); - } - - private static InputStream getConfigInputStream(ServletContext context) { - InputStream is = getJSONFromServletContext(context); - if (is == null) { - String path = context.getInitParameter("keycloak.config.file"); - if (path == null) { - log.debug("using /WEB-INF/keycloak.json"); - is = context.getResourceAsStream("/WEB-INF/keycloak.json"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } - return is; - } - - - @Override - @SuppressWarnings("UseSpecificCatch") - public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) { - if (!isAuthenticationMechanismPresent(deploymentInfo, "KEYCLOAK") && deploymentContext == null) { - log.debug("auth-method is not keycloak!"); - return; - } - log.debug("KeycloakServletException initialization"); - - // Possible scenarios: - // 1) The deployment has a keycloak.config.resolver specified and it exists: - // Outcome: adapter uses the resolver - // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exist, isn't a resolver, ...) : - // Outcome: adapter is left unconfigured - // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent) - // Outcome: adapter uses it - // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent) - // Outcome: adapter is left unconfigured - AdapterDeploymentContext deploymentContext = this.deploymentContext; - - if (deploymentContext == null) { - KeycloakConfigResolver configResolver; - String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - configResolver = (KeycloakConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new AdapterDeploymentContext(configResolver); - log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis."); - } catch (Exception ex) { - log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage()); - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } - } else { - InputStream is = getConfigInputStream(servletContext); - final KeycloakDeployment deployment; - if (is == null) { - log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - deployment = new KeycloakDeployment(); - } else { - deployment = KeycloakDeploymentBuilder.build(is); - } - deploymentContext = new AdapterDeploymentContext(deployment); - log.debug("Keycloak is using a per-deployment configuration."); - } - } else { - deploymentContext = this.deploymentContext; - } - - servletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); - UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement(); - final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement(); - final ServletKeycloakAuthMech mech = createAuthenticationMechanism(deploymentInfo, deploymentContext, userSessionManagement, nodesRegistrationManagement); - - UndertowAuthenticatedActionsHandler.Wrapper actions = new UndertowAuthenticatedActionsHandler.Wrapper(deploymentContext); - - // setup handlers - - deploymentInfo.addOuterHandlerChainWrapper(new ServletPreAuthActionsHandler.Wrapper(deploymentContext, userSessionManagement)); - deploymentInfo.addAuthenticationMechanism("KEYCLOAK", new AuthenticationMechanismFactory() { - @Override - public AuthenticationMechanism create(String s, FormParserFactory formParserFactory, Map stringStringMap) { - return mech; - } - }); // authentication - deploymentInfo.addInnerHandlerChainWrapper(actions); // handles authenticated actions and cors. - - deploymentInfo.setIdentityManager(new IdentityManager() { - @Override - public Account verify(Account account) { - return account; - } - - @Override - public Account verify(String id, Credential credential) { - throw new IllegalStateException("Should never be called in Keycloak flow"); - } - - @Override - public Account verify(Credential credential) { - throw new IllegalStateException("Should never be called in Keycloak flow"); - } - }); - - ServletSessionConfig cookieConfig = deploymentInfo.getServletSessionConfig(); - if (cookieConfig == null) { - cookieConfig = new ServletSessionConfig(); - } - if (cookieConfig.getPath() == null) { - log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath()); - cookieConfig.setPath(deploymentInfo.getContextPath()); - deploymentInfo.setServletSessionConfig(cookieConfig); - } - ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo); - deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationManagementWrapper.class, new InstanceFactory() { - - @Override - public InstanceHandle createInstance() throws InstantiationException { - UndertowNodesRegistrationManagementWrapper listener = new UndertowNodesRegistrationManagementWrapper(nodesRegistrationManagement); - return new ImmediateInstanceHandle(listener); - } - - })); - } - - protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, - NodesRegistrationManagement nodesRegistrationManagement) { - log.debug("creating ServletKeycloakAuthMech"); - String errorPage = getErrorPage(deploymentInfo); - return new ServletKeycloakAuthMech(deploymentContext, userSessionManagement, nodesRegistrationManagement, deploymentInfo.getConfidentialPortManager(), errorPage); - } - - protected String getErrorPage(DeploymentInfo deploymentInfo) { - LoginConfig loginConfig = deploymentInfo.getLoginConfig(); - String errorPage = null; - if (loginConfig != null) { - errorPage = loginConfig.getErrorPage(); - } - return errorPage; - } - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java deleted file mode 100755 index 3532312f68d5..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.idm.Account; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; - -import java.io.Serializable; -import java.security.Principal; -import java.util.Set; - -/** -* @author Bill Burke -* @version $Revision: 1 $ -*/ -public class KeycloakUndertowAccount implements Account, Serializable, OidcKeycloakAccount { - protected static Logger log = Logger.getLogger(KeycloakUndertowAccount.class); - protected KeycloakPrincipal principal; - protected Set accountRoles; - - public KeycloakUndertowAccount(KeycloakPrincipal principal) { - this.principal = principal; - setRoles(principal.getKeycloakSecurityContext()); - } - - protected void setRoles(RefreshableKeycloakSecurityContext session) { - Set roles = AdapterUtils.getRolesFromSecurityContext(session); - this.accountRoles = roles; - } - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - return accountRoles; - } - - @Override - public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { - return principal.getKeycloakSecurityContext(); - } - - public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore); - } - - // Check if accessToken is active and try to refresh if it's not - public boolean checkActive() { - // this object may have been serialized, so we need to reset realm config/metadata - RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { - log.debug("session is active"); - return true; - } - - log.debug("session is not active or refresh is enforced. Try refresh"); - boolean success = session.refreshExpiredToken(false); - if (!success || !session.isActive()) { - log.debug("session is not active return with failure"); - - return false; - } - log.debug("refresh succeeded"); - - setRoles(session); - return true; - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - - if (!(other instanceof KeycloakUndertowAccount)) - return false; - - KeycloakUndertowAccount otherAccount = (KeycloakUndertowAccount) other; - - return (this.principal != null ? this.principal.equals(otherAccount.principal) : otherAccount.principal == null) && - (this.accountRoles != null ? this.accountRoles.equals(otherAccount.accountRoles) : otherAccount.accountRoles == null); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (this.principal == null ? 0 : this.principal.hashCode()); - result = prime * result + (this.accountRoles == null ? 0 : this.accountRoles.hashCode()); - return result; - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java deleted file mode 100755 index 27ddae82b6f3..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; - -import static org.keycloak.adapters.undertow.OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCServletUndertowHttpFacade extends ServletHttpFacade implements OIDCHttpFacade { - - public OIDCServletUndertowHttpFacade(HttpServerExchange exchange) { - super(exchange); - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return exchange.getAttachment(KEYCLOAK_SECURITY_CONTEXT_KEY); - } - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCUndertowHttpFacade.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCUndertowHttpFacade.java deleted file mode 100755 index 78bd1c9192f4..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCUndertowHttpFacade.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.util.AttachmentKey; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class OIDCUndertowHttpFacade extends UndertowHttpFacade implements OIDCHttpFacade { - public static final AttachmentKey KEYCLOAK_SECURITY_CONTEXT_KEY = AttachmentKey.create(KeycloakSecurityContext.class); - - public OIDCUndertowHttpFacade(HttpServerExchange exchange) { - super(exchange); - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - return exchange.getAttachment(KEYCLOAK_SECURITY_CONTEXT_KEY); - } - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java deleted file mode 100755 index 6e076568e455..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.api.ConfidentialPortManager; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.util.Headers; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.enums.TokenStore; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import java.io.IOException; - -/** - * @author Bill Burke - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - * @version $Revision: 1 $ - */ -public class ServletKeycloakAuthMech extends AbstractUndertowKeycloakAuthMech { - private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class); - - protected NodesRegistrationManagement nodesRegistrationManagement; - protected ConfidentialPortManager portManager; - - public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, - NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager, - String errorPage) { - super(deploymentContext, userSessionManagement, errorPage); - this.nodesRegistrationManagement = nodesRegistrationManagement; - this.portManager = portManager; - } - - @Override - protected Integer servePage(HttpServerExchange exchange, String location) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ServletRequest req = servletRequestContext.getServletRequest(); - ServletResponse resp = servletRequestContext.getServletResponse(); - RequestDispatcher disp = req.getRequestDispatcher(location); - //make sure the login page is never cached - exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache"); - exchange.getResponseHeaders().add(Headers.EXPIRES, "0"); - - - try { - disp.forward(req, resp); - } catch (ServletException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - @Override - public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { - UndertowHttpFacade facade = createFacade(exchange); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (!deployment.isConfigured()) { - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - - nodesRegistrationManagement.tryRegister(deployment); - - RequestAuthenticator authenticator = createRequestAuthenticator(deployment, exchange, securityContext, facade); - - return keycloakAuthenticate(exchange, securityContext, authenticator); - } - - protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) { - - int confidentialPort = getConfidentilPort(exchange); - AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext); - return new ServletRequestAuthenticator(facade, deployment, - confidentialPort, securityContext, exchange, tokenStore); - } - - protected int getConfidentilPort(HttpServerExchange exchange) { - int confidentialPort = 8443; - if (exchange.getRequestScheme().equalsIgnoreCase("HTTPS")) { - confidentialPort = exchange.getHostPort(); - } else if (portManager != null) { - confidentialPort = portManager.getConfidentialPort(exchange); - } - return confidentialPort; - } - - @Override - protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) { - if (deployment.getTokenStore() == TokenStore.SESSION) { - return new ServletSessionTokenStore(exchange, deployment, sessionManagement, securityContext); - } else { - return new UndertowCookieTokenStore(facade, deployment, securityContext); - } - } - - @Override - public UndertowHttpFacade createFacade(HttpServerExchange exchange) { - return new OIDCServletUndertowHttpFacade(exchange); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java deleted file mode 100755 index 20a8dd883963..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HandlerWrapper; -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.PreAuthActionsHandler; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletPreAuthActionsHandler implements HttpHandler { - - private static final Logger log = Logger.getLogger(ServletPreAuthActionsHandler.class); - protected HttpHandler next; - protected UndertowUserSessionManagement userSessionManagement; - protected AdapterDeploymentContext deploymentContext; - - public static class Wrapper implements HandlerWrapper { - protected AdapterDeploymentContext deploymentContext; - protected UndertowUserSessionManagement userSessionManagement; - - - public Wrapper(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) { - this.deploymentContext = deploymentContext; - this.userSessionManagement = userSessionManagement; - } - - @Override - public HttpHandler wrap(HttpHandler handler) { - return new ServletPreAuthActionsHandler(deploymentContext, userSessionManagement, handler); - } - } - - protected ServletPreAuthActionsHandler(AdapterDeploymentContext deploymentContext, - UndertowUserSessionManagement userSessionManagement, - HttpHandler next) { - this.next = next; - this.deploymentContext = deploymentContext; - this.userSessionManagement = userSessionManagement; - } - - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - UndertowHttpFacade facade = new OIDCServletUndertowHttpFacade(exchange); - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, servletRequestContext.getDeployment().getSessionManager()); - PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deploymentContext, facade); - if (handler.handleRequest()) return; - next.handleRequest(exchange); - } - - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java deleted file mode 100755 index 7f23b3bbd734..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - * @version $Revision: 1 $ - */ -public class ServletRequestAuthenticator extends AbstractUndertowRequestAuthenticator { - - - public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, - SecurityContext securityContext, HttpServerExchange exchange, - AdapterTokenStore tokenStore) { - super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore); - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void propagateKeycloakContext(KeycloakUndertowAccount account) { - super.propagateKeycloakContext(account); - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - req.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - } - - @Override - protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal) { - return new KeycloakUndertowAccount(principal); - } - - @Override - protected String changeHttpSessionId(boolean create) { - if (!deployment.isTurnOffChangeSessionIdOnLogin()) return ChangeSessionId.changeSessionId(exchange, create); - else return getHttpSessionId(create); - } - - protected String getHttpSessionId(boolean create) { - HttpSession session = getSession(create); - return session != null ? session.getId() : null; - } - - protected HttpSession getSession(boolean create) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - return req.getSession(create); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java deleted file mode 100755 index ef6081a21aed..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -/** - * Per-request object. Storage of tokens in servlet HTTP session. - * - * @author Marek Posolda - */ -public class ServletSessionTokenStore implements AdapterTokenStore { - - protected static Logger log = Logger.getLogger(ServletSessionTokenStore.class); - - private final HttpServerExchange exchange; - private final KeycloakDeployment deployment; - private final UndertowUserSessionManagement sessionManagement; - private final SecurityContext securityContext; - - public ServletSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement, - SecurityContext securityContext) { - this.exchange = exchange; - this.deployment = deployment; - this.sessionManagement = sessionManagement; - this.securityContext = securityContext; - } - - @Override - public void checkCurrentToken() { - // no-op on undertow - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - HttpSession session = getSession(false); - if (session == null) { - log.debug("session was null, returning null"); - return false; - } - KeycloakUndertowAccount account = null; - try { - account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - } catch (IllegalStateException e) { - log.debug("session was invalidated. Return false."); - return false; - } - if (account == null) { - log.debug("Account was not in session, returning null"); - return false; - } - - if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { - log.debug("Account in session belongs to a different realm than for this request."); - return false; - } - - account.setCurrentRequestInfo(deployment, this); - if (account.checkActive()) { - log.debug("Cached account found"); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); - restoreRequest(); - return true; - } else { - log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); - try { - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - session.removeAttribute(KeycloakSecurityContext.class.getName()); - session.invalidate(); - } catch (Exception e) { - log.debug("Failed to invalidate session, might already be invalidated"); - } - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSession session = getSession(true); - session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); - } - - @Override - public void logout() { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - req.removeAttribute(KeycloakUndertowAccount.class.getName()); - req.removeAttribute(KeycloakSecurityContext.class.getName()); - HttpSession session = req.getSession(false); - if (session == null) return; - try { - KeycloakUndertowAccount account = (KeycloakUndertowAccount) session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) return; - session.removeAttribute(KeycloakSecurityContext.class.getName()); - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - } catch (IllegalStateException ise) { - // Session may be already logged-out in case that app has adminUrl - log.debugf("Session %s logged-out already", session.getId()); - } - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } - - @Override - public void saveRequest() { - SavedRequest.trySaveRequest(exchange); - - } - - @Override - public boolean restoreRequest() { - HttpSession session = getSession(false); - if (session == null) return false; - SavedRequest.tryRestoreRequest(exchange, session); - return false; - } - - protected HttpSession getSession(boolean create) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - return req.getSession(create); - } - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticatedActionsHandler.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticatedActionsHandler.java deleted file mode 100755 index b7ed15400a2c..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticatedActionsHandler.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HandlerWrapper; -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakDeployment; - -/** - * Bridge for authenticated Keycloak adapter actions - * - * @author Bill Burke - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - * @version $Revision: 1 $ - */ -public class UndertowAuthenticatedActionsHandler implements HttpHandler { - private static final Logger log = Logger.getLogger(UndertowAuthenticatedActionsHandler.class); - protected AdapterDeploymentContext deploymentContext; - protected HttpHandler next; - - public static class Wrapper implements HandlerWrapper { - protected AdapterDeploymentContext deploymentContext; - - public Wrapper(AdapterDeploymentContext deploymentContext) { - this.deploymentContext = deploymentContext; - } - - @Override - public HttpHandler wrap(HttpHandler handler) { - return new UndertowAuthenticatedActionsHandler(deploymentContext, handler); - } - } - - - public UndertowAuthenticatedActionsHandler(AdapterDeploymentContext deploymentContext, HttpHandler next) { - this.deploymentContext = deploymentContext; - this.next = next; - } - - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - OIDCUndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment != null && deployment.isConfigured()) { - AuthenticatedActionsHandler handler = new AuthenticatedActionsHandler(deployment, facade); - if (handler.handledRequest()) return; - } - next.handleRequest(exchange); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java deleted file mode 100755 index 1bc5f370db9a..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.RequestAuthenticator; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowAuthenticationMechanism extends AbstractUndertowKeycloakAuthMech { - protected NodesRegistrationManagement nodesRegistrationManagement; - protected int confidentialPort; - - public UndertowAuthenticationMechanism(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, - NodesRegistrationManagement nodesRegistrationManagement, int confidentialPort, String errorPage) { - super(deploymentContext, sessionManagement, errorPage); - this.nodesRegistrationManagement = nodesRegistrationManagement; - this.confidentialPort = confidentialPort; - } - - @Override - public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { - UndertowHttpFacade facade = createFacade(exchange); - KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); - if (!deployment.isConfigured()) { - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - - nodesRegistrationManagement.tryRegister(deployment); - - AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext); - RequestAuthenticator authenticator = new UndertowRequestAuthenticator(facade, deployment, confidentialPort, securityContext, exchange, tokenStore); - - return keycloakAuthenticate(exchange, securityContext, authenticator); - } - -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java deleted file mode 100755 index a5287d5e0a13..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.CookieTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * Per-request object. Storage of tokens in cookie - * - * @author Marek Posolda - */ -public class UndertowCookieTokenStore implements AdapterTokenStore { - - protected static Logger log = Logger.getLogger(UndertowCookieTokenStore.class); - - private final HttpFacade facade; - private final KeycloakDeployment deployment; - private final SecurityContext securityContext; - - public UndertowCookieTokenStore(HttpFacade facade, KeycloakDeployment deployment, - SecurityContext securityContext) { - this.facade = facade; - this.deployment = deployment; - this.securityContext = securityContext; - } - - @Override - public void checkCurrentToken() { - // no-op on undertow - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); - if (principal == null) { - log.debug("Account was not in cookie or was invalid, returning null"); - return false; - } - KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal); - - if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { - log.debug("Account in session belongs to a different realm than for this request."); - return false; - } - - if (account.checkActive()) { - log.debug("Cached account found"); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); - return true; - } else { - log.debug("Account was not active, removing cookie and returning false"); - CookieTokenStore.removeCookie(deployment, facade); - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); - CookieTokenStore.setTokenCookie(deployment, facade, secContext); - } - - @Override - public void logout() { - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); - if (principal == null) return; - - CookieTokenStore.removeCookie(deployment, facade); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - CookieTokenStore.setTokenCookie(deployment, facade, securityContext); - } - - @Override - public void saveRequest() { - - } - - @Override - public boolean restoreRequest() { - return false; - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationManagementWrapper.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationManagementWrapper.java deleted file mode 100644 index 6978ea54939c..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationManagementWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import org.keycloak.adapters.NodesRegistrationManagement; - -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -/** - * @author Marek Posolda - */ -public class UndertowNodesRegistrationManagementWrapper implements ServletContextListener { - - private final NodesRegistrationManagement delegate; - - public UndertowNodesRegistrationManagementWrapper(NodesRegistrationManagement delegate) { - this.delegate = delegate; - } - - @Override - public void contextInitialized(ServletContextEvent sce) { - } - - @Override - public void contextDestroyed(ServletContextEvent sce) { - delegate.stop(); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java deleted file mode 100755 index 9f44746926e9..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.SessionManager; -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.PreAuthActionsHandler; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowPreAuthActionsHandler implements HttpHandler { - - private static final Logger log = Logger.getLogger(UndertowPreAuthActionsHandler.class); - protected HttpHandler next; - protected SessionManager sessionManager; - protected UndertowUserSessionManagement userSessionManagement; - protected AdapterDeploymentContext deploymentContext; - - public UndertowPreAuthActionsHandler(AdapterDeploymentContext deploymentContext, - UndertowUserSessionManagement userSessionManagement, - SessionManager sessionManager, - HttpHandler next) { - this.next = next; - this.deploymentContext = deploymentContext; - this.sessionManager = sessionManager; - this.userSessionManagement = userSessionManagement; - } - - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - UndertowHttpFacade facade = createFacade(exchange); - SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, sessionManager); - PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deploymentContext, facade); - if (handler.handleRequest()) return; - next.handleRequest(exchange); - } - - public UndertowHttpFacade createFacade(HttpServerExchange exchange) { - return new OIDCUndertowHttpFacade(exchange); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java deleted file mode 100755 index 26f358378383..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowRequestAuthenticator extends AbstractUndertowRequestAuthenticator { - public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, - SecurityContext securityContext, HttpServerExchange exchange, AdapterTokenStore tokenStore) { - super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore); - } - - @Override - protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal) { - return new KeycloakUndertowAccount(principal); - } -} diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java deleted file mode 100755 index 80a71099ffbf..000000000000 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.util.Sessions; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; - -/** - * Per-request object. Storage of tokens in undertow session. - * - * @author Marek Posolda - */ -public class UndertowSessionTokenStore implements AdapterTokenStore { - - protected static Logger log = Logger.getLogger(UndertowSessionTokenStore.class); - - private final HttpServerExchange exchange; - private final KeycloakDeployment deployment; - private final UndertowUserSessionManagement sessionManagement; - private final SecurityContext securityContext; - - public UndertowSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement, - SecurityContext securityContext) { - this.exchange = exchange; - this.deployment = deployment; - this.sessionManagement = sessionManagement; - this.securityContext = securityContext; - } - - @Override - public void checkCurrentToken() { - // no-op on undertow - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - Session session = Sessions.getSession(exchange); - if (session == null) { - log.debug("session was null, returning null"); - return false; - } - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) { - log.debug("Account was not in session, returning null"); - return false; - } - - if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { - log.debug("Account in session belongs to a different realm than for this request."); - return false; - } - - account.setCurrentRequestInfo(deployment, this); - if (account.checkActive()) { - log.debug("Cached account found"); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); - return true; - } else { - log.debug("Account was not active, returning false"); - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - session.removeAttribute(KeycloakSecurityContext.class.getName()); - session.invalidate(exchange); - return false; - } - } - - @Override - public void saveRequest() { - - } - - @Override - public boolean restoreRequest() { - return false; - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - Session session = Sessions.getOrCreateSession(exchange); - session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - sessionManagement.login(session.getSessionManager()); - } - - @Override - public void logout() { - Session session = Sessions.getSession(exchange); - if (session == null) return; - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) return; - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - session.removeAttribute(KeycloakSecurityContext.class.getName()); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - // no-op - } -} diff --git a/adapters/oidc/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/adapters/oidc/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension deleted file mode 100755 index 88f1892fa9dc..000000000000 --- a/adapters/oidc/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016 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. -# - -org.keycloak.adapters.undertow.KeycloakServletExtension diff --git a/adapters/oidc/wildfly-elytron/pom.xml b/adapters/oidc/wildfly-elytron/pom.xml deleted file mode 100755 index 1d8f5c9b5b86..000000000000 --- a/adapters/oidc/wildfly-elytron/pom.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-wildfly-elytron-oidc-adapter - Keycloak Wildfly Elytron OIDC Adapter - - - - - org.wildfly.common - wildfly-common - ${wildfly.common.wildfly.aligned.version} - provided - - - org.wildfly.security - wildfly-elytron - provided - - - org.wildfly.security.elytron-web - undertow-server - provided - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-adapter-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - io.undertow - undertow-servlet - provided - - - diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java deleted file mode 100644 index 4e374f5fc89e..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; - -import java.io.Serializable; -import java.security.Principal; -import java.util.HashSet; -import java.util.Set; - -/** - * @author Pedro Igor - */ -public class ElytronAccount implements Serializable, OidcKeycloakAccount { - - private static final long serialVersionUID = -6775274346765339292L; - protected static Logger log = Logger.getLogger(ElytronAccount.class); - - private final KeycloakPrincipal principal; - - public ElytronAccount(KeycloakPrincipal principal) { - this.principal = principal; - } - - @Override - public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { - return principal.getKeycloakSecurityContext(); - } - - @Override - public Principal getPrincipal() { - return principal; - } - - @Override - public Set getRoles() { - Set roles = new HashSet<>(); - - return roles; - } - - void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore); - } - - public boolean checkActive() { - RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); - - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { - log.debug("session is active"); - return true; - } - - log.debug("session not active"); - - return false; - } - - boolean tryRefresh() { - log.debug("Trying to refresh"); - - RefreshableKeycloakSecurityContext securityContext = getKeycloakSecurityContext(); - - if (securityContext == null) { - log.debug("No security context. Aborting refresh."); - } - - if (securityContext.refreshExpiredToken(false)) { - log.debug("refresh succeeded"); - return true; - } - - return checkActive(); - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java deleted file mode 100644 index 86b6539cdb5d..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.CookieTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.wildfly.security.http.HttpScope; -import org.wildfly.security.http.Scope; - -import javax.security.auth.callback.CallbackHandler; -import java.util.List; - -/** - * @author Pedro Igor - */ -public class ElytronCookieTokenStore implements ElytronTokeStore, UserSessionManagement { - - protected static Logger log = Logger.getLogger(ElytronCookieTokenStore.class); - - private final ElytronHttpFacade httpFacade; - private final CallbackHandler callbackHandler; - - public ElytronCookieTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) { - this.httpFacade = httpFacade; - this.callbackHandler = callbackHandler; - } - - @Override - public void checkCurrentToken() { - KeycloakDeployment deployment = httpFacade.getDeployment(); - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this); - - if (principal == null) { - return; - } - - RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - - if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = securityContext.refreshExpiredToken(false); - if (success && securityContext.isActive()) return; - - saveAccountInfo(new ElytronAccount(principal)); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - KeycloakDeployment deployment = httpFacade.getDeployment(); - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this); - if (principal == null) { - log.debug("Account was not in cookie or was invalid, returning null"); - return false; - } - ElytronAccount account = new ElytronAccount(principal); - - if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { - log.debug("Account in session belongs to a different realm than for this request."); - return false; - } - - boolean active = account.checkActive(); - - if (!active) { - active = account.tryRefresh(); - } - - if (active) { - log.debug("Cached account found"); - restoreRequest(); - httpFacade.authenticationComplete(account, true); - return true; - } else { - log.debug("Account was not active, removing cookie and returning false"); - CookieTokenStore.removeCookie(deployment, httpFacade); - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); - CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), this.httpFacade, secContext); - HttpScope exchange = this.httpFacade.getScope(Scope.EXCHANGE); - - exchange.registerForNotification(httpServerScopes -> logout()); - - exchange.setAttachment(ElytronAccount.class.getName(), account); - exchange.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - - restoreRequest(); - } - - @Override - public void logout() { - logout(false); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), httpFacade, securityContext); - } - - @Override - public void saveRequest() { - - } - - @Override - public boolean restoreRequest() { - return false; - } - - @Override - public void logout(boolean glo) { - KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(this.httpFacade.getDeployment(), this.httpFacade, this); - - if (principal == null) { - return; - } - - CookieTokenStore.removeCookie(this.httpFacade.getDeployment(), this.httpFacade); - - if (glo) { - KeycloakSecurityContext ksc = (KeycloakSecurityContext) principal.getKeycloakSecurityContext(); - - if (ksc == null) { - return; - } - - KeycloakDeployment deployment = httpFacade.getDeployment(); - - if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) { - ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); - } - } - } - - @Override - public void logoutAll() { - //no-op - } - - @Override - public void logoutHttpSessions(List ids) { - //no-op - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java deleted file mode 100644 index 080fa8446a8e..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java +++ /dev/null @@ -1,550 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.CookieImpl; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.UriUtils; -import org.keycloak.enums.TokenStore; -import org.wildfly.security.auth.server.SecurityIdentity; -import org.wildfly.security.http.HttpScope; -import org.wildfly.security.http.HttpServerCookie; -import org.wildfly.security.http.HttpServerRequest; -import org.wildfly.security.http.HttpServerResponse; -import org.wildfly.security.http.Scope; - -import javax.security.auth.callback.CallbackHandler; -import javax.security.cert.X509Certificate; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.regex.Pattern; - -/** - * @author Pedro Igor - */ -class ElytronHttpFacade implements OIDCHttpFacade { - - static final String UNDERTOW_EXCHANGE = ElytronHttpFacade.class.getName() + ".undertow.exchange"; - private static final boolean elyweb163Workaround; - private static final Logger log = Logger.getLogger(ElytronHttpFacade.class); - - private final HttpServerRequest request; - private final CallbackHandler callbackHandler; - private final AdapterTokenStore tokenStore; - private final AdapterDeploymentContext deploymentContext; - private Consumer responseConsumer; - private ElytronAccount account; - private SecurityIdentity securityIdentity; - private boolean restored; - private final Map headers = new HashMap<>(); - protected MultivaluedHashMap queryParameters; - - static { - // Issue #10894: ELYWEB-163 workaround should be applied for previous versions of wildfly/EAP - boolean tmpElyweb163Workaround = false; - String prop = System.getProperty("org.keycloak.adapters.elytronweb.ELYWEB-163.workaround"); - if (prop != null) { - tmpElyweb163Workaround = Boolean.parseBoolean(prop); - log.tracef("Forcing workaround for issue ELYWEB-163 in elytron-web %b", tmpElyweb163Workaround); - } else { - try { - Class clazz = ElytronHttpFacade.class.getClassLoader().loadClass("org.wildfly.elytron.web.undertow.server.ElytronHttpExchange"); - String version = clazz.getPackage().getImplementationVersion(); - Integer[] array = parseVersion(version); - // bug is fixed in 1.9.2 and 1.10.1 - tmpElyweb163Workaround = array != null - && (versionIsLessThan(array, new Integer[]{1, 9, 2}) - || (versionIsLessThan(array, new Integer[]{1, 10, 1}) && versionIsGreaterOrEqualThan(array, new Integer[]{1, 10, 0}))); - log.tracef("Version detected for elytron-web %s workaround for ELYWEB-163 %b", version, tmpElyweb163Workaround); - } catch (Exception e) { - log.tracef(e, "Cannot detect version of elytron-web workaround for ELYWEB-163 %b", tmpElyweb163Workaround); - } - } - elyweb163Workaround = tmpElyweb163Workaround; - } - - private static Integer[] parseVersion(String version) { - if (version != null) { - String[] versionArray = version.split(Pattern.quote(".")); - List versionList = new ArrayList<>(); - for (int i = 0; i < versionArray.length; i++) { - if (versionArray[i].matches("[0-9]+")) { - versionList.add(Integer.parseInt(versionArray[i])); - } - } - if (!versionList.isEmpty()) { - return versionList.toArray(new Integer[0]); - } - } - return null; - } - - private static boolean versionIsLessThan(Integer[] array1, Integer[] array2) { - if (array1 == null || array2 == null || array1.length == 0 || array2.length == 0) { - throw new IllegalArgumentException("Arrays cannot be null or empty"); - } - for (int i = 0; i < array1.length && i < array2.length; i++) { - if (array1[i] < array2[i]) { - return true; - } else if (array1[i] > array2[i]) { - return false; - } - } - // all the numbers are equal til now, 1.1 < 1.1.1 - return array1.length < array2.length; - } - - private static boolean versionIsGreaterOrEqualThan(Integer[] array1, Integer[] array2) { - return !versionIsLessThan(array1, array2); - } - - public ElytronHttpFacade(HttpServerRequest request, AdapterDeploymentContext deploymentContext, CallbackHandler handler) { - this.request = request; - this.deploymentContext = deploymentContext; - this.callbackHandler = handler; - this.tokenStore = createTokenStore(); - this.responseConsumer = response -> {}; - } - - void authenticationComplete(ElytronAccount account, boolean storeToken) { - this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, account.getPrincipal()); - - if (securityIdentity != null) { - this.account = account; - RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext(); - account.setCurrentRequestInfo(keycloakSecurityContext.getDeployment(), this.tokenStore); - if (storeToken) { - this.tokenStore.saveAccountInfo(account); - } - } - } - - void authenticationComplete() { - if (securityIdentity != null) { - HttpScope requestScope = request.getScope(Scope.EXCHANGE); - RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext(); - - requestScope.setAttachment(KeycloakSecurityContext.class.getName(), keycloakSecurityContext); - - this.request.authenticationComplete(response -> { - if (!restored) { - responseConsumer.accept(response); - } - }, () -> ((ElytronTokeStore) tokenStore).logout(true)); - } - } - - void authenticationFailed() { - this.request.authenticationFailed("Authentication Failed", response -> responseConsumer.accept(response)); - } - - void noAuthenticationInProgress() { - this.request.noAuthenticationInProgress(); - } - - void noAuthenticationInProgress(AuthChallenge challenge) { - if (challenge != null) { - challenge.challenge(this); - } - this.request.noAuthenticationInProgress(response -> responseConsumer.accept(response)); - } - - void authenticationInProgress() { - this.request.authenticationInProgress(response -> responseConsumer.accept(response)); - } - - HttpScope getScope(Scope scope) { - return request.getScope(scope); - } - - HttpScope getScope(Scope scope, String id) { - return request.getScope(scope, id); - } - - Collection getScopeIds(Scope scope) { - return request.getScopeIds(scope); - } - - AdapterTokenStore getTokenStore() { - return this.tokenStore; - } - - KeycloakDeployment getDeployment() { - return deploymentContext.resolveDeployment(this); - } - - private AdapterTokenStore createTokenStore() { - KeycloakDeployment deployment = getDeployment(); - - if (TokenStore.SESSION.equals(deployment.getTokenStore())) { - return new ElytronSessionTokenStore(this, this.callbackHandler); - } else { - return new ElytronCookieTokenStore(this, this.callbackHandler); - } - } - - @Override - public Request getRequest() { - return new Request() { - private InputStream inputStream; - - @Override - public String getMethod() { - return request.getRequestMethod(); - } - - @Override - public String getURI() { - if (elyweb163Workaround) { - try { - return URLDecoder.decode(request.getRequestURI().toString(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Failed to decode request URI", e); - } - } else { - return request.getRequestURI().toString(); - } - } - - @Override - public String getRelativePath() { - return request.getRequestPath(); - } - - @Override - public boolean isSecure() { - return request.getRequestURI().getScheme().equals("https"); - } - - @Override - public String getFirstParam(String param) { - return request.getFirstParameterValue(param); - } - - @Override - public String getQueryParamValue(String param) { - if (elyweb163Workaround) { - URI requestURI = request.getRequestURI(); - String query = requestURI.getQuery(); - if (query != null) { - String[] parameters = query.split("&"); - for (String parameter : parameters) { - String[] keyValue = parameter.split("=", 2); - if (keyValue[0].equals(param)) { - try { - return URLDecoder.decode(keyValue[1], "UTF-8"); - } catch (IOException e) { - throw new RuntimeException("Failed to decode request URI", e); - } - } - } - } - return null; - } else { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getRequestURI().getRawQuery()); - } - return queryParameters.getFirst(param); - } - } - - @Override - public Cookie getCookie(final String cookieName) { - List cookies = request.getCookies(); - - if (cookies != null) { - for (HttpServerCookie cookie : cookies) { - if (cookie.getName().equals(cookieName)) { - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - } - } - - return null; - } - - @Override - public String getHeader(String name) { - return request.getFirstRequestHeaderValue(name); - } - - @Override - public List getHeaders(String name) { - return request.getRequestHeaderValues(name); - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - HttpScope exchangeScope = getScope(Scope.EXCHANGE); - HttpServerExchange exchange = ProtectedHttpServerExchange.class.cast(exchangeScope.getAttachment(UNDERTOW_EXCHANGE)).getExchange(); - ServletRequestContext context = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ServletRequest servletRequest = context.getServletRequest(); - - inputStream = new BufferedInputStream(exchange.getInputStream()); - - context.setServletRequest(new HttpServletRequestWrapper((HttpServletRequest) servletRequest) { - @Override - public ServletInputStream getInputStream() { - inputStream.mark(0); - return new ServletInputStream() { - @Override - public int read() throws IOException { - return inputStream.read(); - } - }; - } - }); - return inputStream; - } - - return request.getInputStream(); - } - - @Override - public String getRemoteAddr() { - InetSocketAddress sourceAddress = request.getSourceAddress(); - if (sourceAddress == null) { - return ""; - } - InetAddress address = sourceAddress.getAddress(); - if (address == null) { - // this is unresolved, so we just return the host name not exactly spec, but if the name should be - // resolved then a PeerNameResolvingHandler should be used and this is probably better than just - // returning null - return sourceAddress.getHostString(); - } - return address.getHostAddress(); - } - - @Override - public void setError(AuthenticationError error) { - request.getScope(Scope.EXCHANGE).setAttachment(AuthenticationError.class.getName(), error); - } - - @Override - public void setError(LogoutError error) { - request.getScope(Scope.EXCHANGE).setAttachment(LogoutError.class.getName(), error); - } - }; - } - - @Override - public Response getResponse() { - return new Response() { - - @Override - public void setStatus(final int status) { - if (status < 200 || status > 300) { - responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status)); - } - } - - @Override - public void addHeader(final String name, final String value) { - headers.put(name, value); - responseConsumer = responseConsumer.andThen(new Consumer() { - @Override - public void accept(HttpServerResponse response) { - String latestValue = headers.get(name); - - if (latestValue.equals(value)) { - response.addResponseHeader(name, latestValue); - } - } - }); - } - - @Override - public void setHeader(String name, String value) { - addHeader(name, value); - } - - @Override - public void resetCookie(final String name, final String path) { - responseConsumer = responseConsumer.andThen(response -> setCookie(name, "", path, null, 0, false, false, response)); - HttpScope exchangeScope = getScope(Scope.EXCHANGE); - ProtectedHttpServerExchange undertowExchange = ProtectedHttpServerExchange.class.cast(exchangeScope.getAttachment(UNDERTOW_EXCHANGE)); - - if (undertowExchange != null) { - CookieImpl cookie = new CookieImpl(name, ""); - - cookie.setMaxAge(0); - cookie.setPath(path); - - undertowExchange.getExchange().setResponseCookie(cookie); - } - } - - @Override - public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly) { - responseConsumer = responseConsumer.andThen(response -> setCookie(name, value, path, domain, maxAge, secure, httpOnly, response)); - } - - private void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly, HttpServerResponse response) { - response.setResponseCookie(new HttpServerCookie() { - @Override - public String getName() { - return name; - } - - @Override - public String getValue() { - return value; - } - - @Override - public String getDomain() { - return domain; - } - - @Override - public int getMaxAge() { - return maxAge; - } - - @Override - public String getPath() { - return path; - } - - @Override - public boolean isSecure() { - return secure; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public boolean isHttpOnly() { - return httpOnly; - } - }); - } - - @Override - public OutputStream getOutputStream() { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - responseConsumer = responseConsumer.andThen(new Consumer() { - @Override - public void accept(HttpServerResponse httpServerResponse) { - try { - httpServerResponse.getOutputStream().write(stream.toByteArray()); - } catch (IOException e) { - throw new RuntimeException("Failed to write to response output stream", e); - } - } - }); - return stream; - } - - @Override - public void sendError(int code) { - setStatus(code); - } - - @Override - public void sendError(final int code, final String message) { - responseConsumer = responseConsumer.andThen(response -> { - response.setStatusCode(code); - response.addResponseHeader("Content-Type", "text/html"); - try { - response.getOutputStream().write(message.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - @Override - public void end() { - - } - }; - } - - @Override - public X509Certificate[] getCertificateChain() { - return new X509Certificate[0]; - } - - @Override - public KeycloakSecurityContext getSecurityContext() { - if (account == null) { - return null; - } - return this.account.getKeycloakSecurityContext(); - } - - public boolean restoreRequest() { - restored = this.request.resumeRequest(); - return restored; - } - - public void suspendRequest() { - responseConsumer = responseConsumer.andThen(httpServerResponse -> request.suspendRequest()); - } - - public boolean isAuthorized() { - return this.securityIdentity != null; - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java deleted file mode 100644 index 643a71660d93..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.BearerTokenRequestAuthenticator; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OAuthRequestAuthenticator; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.AuthOutcome; -import org.wildfly.security.http.HttpScope; -import org.wildfly.security.http.Scope; - -import javax.security.auth.callback.CallbackHandler; - -/** - * @author Pedro Igor - */ -public class ElytronRequestAuthenticator extends RequestAuthenticator { - - public ElytronRequestAuthenticator(CallbackHandler callbackHandler, ElytronHttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) { - super(facade, deployment, facade.getTokenStore(), sslRedirectPort); - } - - @Override - public AuthOutcome authenticate() { - AuthOutcome authenticate = super.authenticate(); - - if (AuthOutcome.AUTHENTICATED.equals(authenticate)) { - if (!getElytronHttpFacade().isAuthorized()) { - return AuthOutcome.FAILED; - } - } - - return authenticate; - } - - @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); - } - - @Override - protected void completeOAuthAuthentication(final KeycloakPrincipal principal) { - getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), true); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { - getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), false); - } - - @Override - protected String changeHttpSessionId(boolean create) { - HttpScope session = getElytronHttpFacade().getScope(Scope.SESSION); - - if (create) { - if (!session.exists()) { - session.create(); - } - } - - return session != null ? session.getID() : null; - } - - private ElytronHttpFacade getElytronHttpFacade() { - return (ElytronHttpFacade) facade; - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java deleted file mode 100644 index 9c3033a5fdd5..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import static org.keycloak.adapters.elytron.ElytronHttpFacade.UNDERTOW_EXCHANGE; - -import javax.security.auth.callback.CallbackHandler; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.server.session.SessionConfig; -import io.undertow.server.session.SessionManager; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.jboss.logging.Logger; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.wildfly.security.http.HttpScope; -import org.wildfly.security.http.HttpScopeNotification; -import org.wildfly.security.http.Scope; - -/** - * @author Pedro Igor - */ -public class ElytronSessionTokenStore implements ElytronTokeStore, UserSessionManagement { - - private static Logger log = Logger.getLogger(ElytronSessionTokenStore.class); - - private final ElytronHttpFacade httpFacade; - private final CallbackHandler callbackHandler; - - public ElytronSessionTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) { - this.httpFacade = httpFacade; - this.callbackHandler = callbackHandler; - } - - @Override - public void checkCurrentToken() { - HttpScope session = httpFacade.getScope(Scope.SESSION); - if (session == null || !session.exists()) return; - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName()); - if (securityContext == null) return; - - // just in case session got serialized - if (securityContext.getDeployment() == null) securityContext.setCurrentRequestInfo(httpFacade.getDeployment(), this); - - if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = securityContext.refreshExpiredToken(false); - if (success && securityContext.isActive()) return; - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - session.setAttachment(KeycloakSecurityContext.class.getName(), null); - session.invalidate(); - } - - @Override - public boolean isCached(RequestAuthenticator authenticator) { - HttpScope session = this.httpFacade.getScope(Scope.SESSION); - - if (session == null || !session.supportsAttachments()) { - log.debug("session was null, returning null"); - return false; - } - - ElytronAccount account; - - try { - account = (ElytronAccount) session.getAttachment(ElytronAccount.class.getName()); - } catch (IllegalStateException e) { - log.debug("session was invalidated. Return false."); - return false; - } - if (account == null) { - log.debug("Account was not in session, returning null"); - return false; - } - - KeycloakDeployment deployment = httpFacade.getDeployment(); - - if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { - log.debug("Account in session belongs to a different realm than for this request."); - return false; - } - - boolean active = account.checkActive(); - - if (!active) { - active = account.tryRefresh(); - } - - if (active) { - log.debug("Cached account found"); - restoreRequest(); - httpFacade.authenticationComplete(account, true); - return true; - } else { - log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); - try { - session.setAttachment(KeycloakSecurityContext.class.getName(), null); - session.setAttachment(ElytronAccount.class.getName(), null); - session.invalidate(); - } catch (Exception e) { - log.debug("Failed to invalidate session, might already be invalidated"); - } - return false; - } - } - - @Override - public void saveAccountInfo(OidcKeycloakAccount account) { - HttpScope session = this.httpFacade.getScope(Scope.SESSION); - - if (!session.exists()) { - session.create(); - session.registerForNotification(httpScopeNotification -> { - if (!httpScopeNotification.isOfType(HttpScopeNotification.SessionNotificationType.UNDEPLOY)) { - HttpScope invalidated = httpScopeNotification.getScope(Scope.SESSION); - - if (invalidated != null) { - invalidated.setAttachment(ElytronAccount.class.getName(), null); - invalidated.setAttachment(KeycloakSecurityContext.class.getName(), null); - } - } - }); - } - - session.setAttachment(ElytronAccount.class.getName(), account); - session.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - - HttpScope scope = this.httpFacade.getScope(Scope.EXCHANGE); - - scope.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); - } - - @Override - public void logout() { - logout(false); - } - - @Override - public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { - KeycloakPrincipal principal = new KeycloakPrincipal(AdapterUtils.getPrincipalName(this.httpFacade.getDeployment(), securityContext.getToken()), securityContext); - saveAccountInfo(new ElytronAccount(principal)); - } - - @Override - public void saveRequest() { - this.httpFacade.suspendRequest(); - } - - @Override - public boolean restoreRequest() { - return this.httpFacade.restoreRequest(); - } - - @Override - public void logout(boolean glo) { - HttpScope session = this.httpFacade.getScope(Scope.SESSION); - - if (!session.exists()) { - return; - } - - KeycloakSecurityContext ksc = (KeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName()); - - try { - if (glo && ksc != null) { - KeycloakDeployment deployment = httpFacade.getDeployment(); - - session.invalidate(); - - if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) { - ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); - } - } else { - session.setAttachment(ElytronAccount.class.getName(), null); - session.setAttachment(KeycloakSecurityContext.class.getName(), null); - } - } catch (IllegalStateException ise) { - // Session may be already logged-out in case that app has adminUrl - log.debugf("Session %s logged-out already", session.getID()); - } - } - - @Override - public void logoutAll() { - Collection sessions = httpFacade.getScopeIds(Scope.SESSION); - logoutHttpSessions(new ArrayList<>(sessions)); - } - - @Override - public void logoutHttpSessions(List ids) { - HttpServerExchange exchange = ProtectedHttpServerExchange.class.cast(httpFacade.getScope(Scope.EXCHANGE).getAttachment(UNDERTOW_EXCHANGE)).getExchange(); - ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - SessionManager sessionManager = servletRequestContext.getDeployment().getSessionManager(); - final boolean isDistributableSessionManager = sessionManager.getClass().getName().equals("org.wildfly.clustering.web.undertow.session.DistributableSessionManager"); - - for (String id : ids) { - // TODO: Workaround for WFLY-3345. Remove this once we fix KEYCLOAK-733. Same applies to legacy wildfly adapter. - Session session; - if (isDistributableSessionManager) { - session = sessionManager.getSession(exchange, new SessionConfig() { - - @Override - public void setSessionId(HttpServerExchange exchange, String sessionId) { - } - - @Override - public void clearSession(HttpServerExchange exchange, String sessionId) { - } - - @Override - public String findSessionId(HttpServerExchange exchange) { - return id; - } - - @Override - public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { - return null; - } - - @Override - public String rewriteUrl(String originalUrl, String sessionId) { - return null; - } - - }); - } else { - session = sessionManager.getSession(id); - } - - if (session != null) { - session.invalidate(exchange); - } - } - - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java deleted file mode 100644 index dc1486eb856f..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2016 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.adapters.elytron; - -import org.keycloak.adapters.AdapterTokenStore; - -/** - * @author Pedro Igor - */ -public interface ElytronTokeStore extends AdapterTokenStore { - void logout(boolean glo); -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java deleted file mode 100644 index 29aa2ff676d2..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.constants.AdapterConstants; - -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; - -/** - *

A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration - * as a {@link ServletContext} attribute in order to provide to {@link KeycloakHttpServerAuthenticationMechanism} a way - * to obtain the configuration when processing requests. - * - *

This listener should be automatically registered to a deployment using the subsystem. - * - * @author Pedro Igor - */ -public class KeycloakConfigurationServletListener implements ServletContextListener { - - static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE = AdapterDeploymentContext.class.getName(); - static final String ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON = AdapterDeploymentContext.class.getName() + ".elytron"; - - @Override - public void contextInitialized(ServletContextEvent sce) { - ServletContext servletContext = sce.getServletContext(); - String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver"); - KeycloakConfigResolver configResolver; - AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) servletContext.getAttribute(AdapterDeploymentContext.class.getName()); - - if (deploymentContext == null) { - if (configResolverClass != null) { - try { - configResolver = (KeycloakConfigResolver) servletContext.getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new AdapterDeploymentContext(configResolver); - } catch (Exception ex) { - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } - } else { - InputStream is = getConfigInputStream(servletContext); - - KeycloakDeployment deployment; - - if (is == null) { - deployment = new KeycloakDeployment(); - } else { - deployment = KeycloakDeploymentBuilder.build(is); - } - - deploymentContext = new AdapterDeploymentContext(deployment); - } - } - - servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE, deploymentContext); - servletContext.setAttribute(ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON, deploymentContext); - } - - @Override - public void contextDestroyed(ServletContextEvent sce) { - - } - - private InputStream getConfigInputStream(ServletContext servletContext) { - InputStream is = getJSONFromServletContext(servletContext); - - if (is == null) { - String path = servletContext.getInitParameter("keycloak.config.file"); - - if (path == null) { - is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } - return is; - } - - private InputStream getJSONFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - - if (json == null) { - return null; - } - - return new ByteArrayInputStream(json.getBytes()); - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java deleted file mode 100644 index c7ecb397d9c4..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import java.util.Map; - -import javax.security.auth.callback.CallbackHandler; - -import org.jboss.logging.Logger; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.AuthenticatedActionsHandler; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.keycloak.adapters.PreAuthActionsHandler; -import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.UserSessionManagement; -import org.wildfly.security.http.HttpAuthenticationException; -import org.wildfly.security.http.HttpServerAuthenticationMechanism; -import org.wildfly.security.http.HttpServerRequest; -import org.wildfly.security.http.Scope; - -/** - * @author Pedro Igor - */ -class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticationMechanism { - - static Logger LOGGER = Logger.getLogger(KeycloakHttpServerAuthenticationMechanism.class); - static final String NAME = "KEYCLOAK"; - - private final Map properties; - private final CallbackHandler callbackHandler; - private final AdapterDeploymentContext deploymentContext; - private final NodesRegistrationManagement nodesRegistrationManagement; - - public KeycloakHttpServerAuthenticationMechanism(Map properties, CallbackHandler callbackHandler, AdapterDeploymentContext deploymentContext, NodesRegistrationManagement nodesRegistrationManagement) { - this.properties = properties; - this.callbackHandler = callbackHandler; - this.deploymentContext = deploymentContext; - this.nodesRegistrationManagement = nodesRegistrationManagement; - } - - @Override - public String getMechanismName() { - return NAME; - } - - @Override - public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { - LOGGER.debugf("Evaluating request for path [%s]", request.getRequestURI()); - AdapterDeploymentContext deploymentContext = getDeploymentContext(request); - - if (deploymentContext == null) { - LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName()); - request.noAuthenticationInProgress(); - return; - } - - ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, deploymentContext, callbackHandler); - KeycloakDeployment deployment = httpFacade.getDeployment(); - - if (!deployment.isConfigured()) { - request.noAuthenticationInProgress(); - return; - } - - RequestAuthenticator authenticator = createRequestAuthenticator(request, httpFacade, deployment); - - httpFacade.getTokenStore().checkCurrentToken(); - - if (preActions(httpFacade, deploymentContext)) { - LOGGER.debugf("Pre-actions has aborted the evaluation of [%s]", request.getRequestURI()); - httpFacade.authenticationInProgress(); - return; - } - - AuthOutcome outcome = authenticator.authenticate(); - - if (AuthOutcome.AUTHENTICATED.equals(outcome)) { - if (new AuthenticatedActionsHandler(deployment, httpFacade).handledRequest()) { - httpFacade.authenticationInProgress(); - } else { - httpFacade.authenticationComplete(); - } - return; - } - - AuthChallenge challenge = authenticator.getChallenge(); - - if (challenge != null) { - httpFacade.noAuthenticationInProgress(challenge); - return; - } - - if (AuthOutcome.FAILED.equals(outcome)) { - httpFacade.getResponse().setStatus(403); - httpFacade.authenticationFailed(); - return; - } - - httpFacade.noAuthenticationInProgress(); - } - - private ElytronRequestAuthenticator createRequestAuthenticator(HttpServerRequest request, ElytronHttpFacade httpFacade, KeycloakDeployment deployment) { - return new ElytronRequestAuthenticator(this.callbackHandler, httpFacade, deployment, getConfidentialPort(request)); - } - - private AdapterDeploymentContext getDeploymentContext(HttpServerRequest request) { - if (this.deploymentContext == null) { - return (AdapterDeploymentContext) request.getScope(Scope.APPLICATION).getAttachment(KeycloakConfigurationServletListener.ADAPTER_DEPLOYMENT_CONTEXT_ATTRIBUTE_ELYTRON); - } - - return this.deploymentContext; - } - - private boolean preActions(ElytronHttpFacade httpFacade, AdapterDeploymentContext deploymentContext) { - nodesRegistrationManagement.tryRegister(httpFacade.getDeployment()); - - PreAuthActionsHandler preActions = new PreAuthActionsHandler(UserSessionManagement.class.cast(httpFacade.getTokenStore()), deploymentContext, httpFacade); - - return preActions.handleRequest(); - } - - // TODO: obtain confidential port from Elytron - private int getConfidentialPort(HttpServerRequest request) { - return 8443; - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java deleted file mode 100644 index 421360f51bea..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.NodesRegistrationManagement; -import org.wildfly.security.http.HttpAuthenticationException; -import org.wildfly.security.http.HttpServerAuthenticationMechanism; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; - -import javax.security.auth.callback.CallbackHandler; -import java.util.HashMap; -import java.util.Map; - -/** - * @author Pedro Igor - */ -public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory { - - private final AdapterDeploymentContext deploymentContext; - private final NodesRegistrationManagement nodesRegistrationManagement; - - /** - *

Creates a new instance. - * - *

A default constructor is necessary in order to allow this factory to be loaded via {@link java.util.ServiceLoader}. - */ - public KeycloakHttpServerAuthenticationMechanismFactory() { - this(null); - } - - public KeycloakHttpServerAuthenticationMechanismFactory(AdapterDeploymentContext deploymentContext) { - this.deploymentContext = deploymentContext; - this.nodesRegistrationManagement = new NodesRegistrationManagement(); - } - - @Override - public String[] getMechanismNames(Map properties) { - return new String[] {KeycloakHttpServerAuthenticationMechanism.NAME}; - } - - @Override - public HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map properties, CallbackHandler callbackHandler) throws HttpAuthenticationException { - Map mechanismProperties = new HashMap(); - - mechanismProperties.putAll(properties); - - if (KeycloakHttpServerAuthenticationMechanism.NAME.equals(mechanismName)) { - return new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, nodesRegistrationManagement); - } - - return null; - } - - @Override - public void shutdown() { - this.nodesRegistrationManagement.stop(); - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java deleted file mode 100644 index eef2b269423d..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2016 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.adapters.elytron; - -import java.security.Principal; -import java.security.spec.AlgorithmParameterSpec; -import java.util.Set; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.wildfly.security.auth.SupportLevel; -import org.wildfly.security.auth.server.RealmIdentity; -import org.wildfly.security.auth.server.RealmUnavailableException; -import org.wildfly.security.auth.server.SecurityRealm; -import org.wildfly.security.authz.Attributes; -import org.wildfly.security.authz.AuthorizationIdentity; -import org.wildfly.security.authz.MapAttributes; -import org.wildfly.security.authz.RoleDecoder; -import org.wildfly.security.credential.Credential; -import org.wildfly.security.evidence.Evidence; - -/** - * @author Pedro Igor - */ -public class KeycloakSecurityRealm implements SecurityRealm { - - @Override - public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException { - if (principal instanceof KeycloakPrincipal) { - return createRealmIdentity((KeycloakPrincipal) principal); - } - return RealmIdentity.NON_EXISTENT; - } - - private RealmIdentity createRealmIdentity(KeycloakPrincipal principal) { - return new RealmIdentity() { - @Override - public Principal getRealmIdentityPrincipal() { - return principal; - } - - @Override - public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { - return SupportLevel.UNSUPPORTED; - } - - @Override - public C getCredential(Class credentialType) throws RealmUnavailableException { - return null; - } - - @Override - public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { - return SupportLevel.SUPPORTED; - } - - @Override - public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException { - return principal != null; - } - - @Override - public boolean exists() throws RealmUnavailableException { - return principal != null; - } - - @Override - public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) principal.getKeycloakSecurityContext(); - Attributes attributes = new MapAttributes(); - Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); - - attributes.addAll(RoleDecoder.KEY_ROLES, roles); - - return AuthorizationIdentity.basicIdentity(attributes); - } - }; - } - - @Override - public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { - return SupportLevel.UNSUPPORTED; - } - - @Override - public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { - return SupportLevel.POSSIBLY_SUPPORTED; - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakServletExtension.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakServletExtension.java deleted file mode 100644 index 5adf6e98f27e..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakServletExtension.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 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.adapters.elytron; - -import javax.servlet.ServletContext; -import javax.servlet.ServletRequest; - -import io.undertow.server.HttpHandler; -import io.undertow.servlet.ServletExtension; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.handlers.ServletRequestContext; - -/** - * @author Pedro Igor - */ -public class KeycloakServletExtension implements ServletExtension { - - @Override - public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) { - deploymentInfo.addOuterHandlerChainWrapper(handler -> (HttpHandler) exchange -> { - ServletRequest servletRequest = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY).getServletRequest(); - - servletRequest.setAttribute(ElytronHttpFacade.UNDERTOW_EXCHANGE, new ProtectedHttpServerExchange(exchange)); - - handler.handleRequest(exchange); - }); - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ProtectedHttpServerExchange.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ProtectedHttpServerExchange.java deleted file mode 100644 index 682feb34aa97..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ProtectedHttpServerExchange.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 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.adapters.elytron; - -import io.undertow.server.HttpServerExchange; - -/** - *

A wrapper for {@code {@link HttpServerExchange}} accessible only from classes in the same package. - * - *

This class is used to provide to the elytron mechanism access to the current exchange in order to allow making - * changes to the exchange (e.g. response) during the evaluation of requests. By default, changes to the exchange are only - * propagated after the execution of the mechanism. But in certain situations, such as when making a programmatic logout (HttpServletRequest.logout()) from - * within application code, any change made to the exchange is not propagated. - * - * @author Pedro Igor - */ -class ProtectedHttpServerExchange { - - private final HttpServerExchange exchange; - - public ProtectedHttpServerExchange(HttpServerExchange exchange) { - this.exchange = exchange; - } - - HttpServerExchange getExchange() { - return exchange; - } -} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java deleted file mode 100644 index f95f6eebe294..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.adapters.elytron; - -import java.io.IOException; -import java.security.Principal; - -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.sasl.AuthorizeCallback; - -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; -import org.wildfly.security.auth.callback.EvidenceVerifyCallback; -import org.wildfly.security.auth.callback.IdentityCredentialCallback; -import org.wildfly.security.auth.callback.SecurityIdentityCallback; -import org.wildfly.security.auth.server.SecurityIdentity; -import org.wildfly.security.credential.BearerTokenCredential; -import org.wildfly.security.evidence.Evidence; -import org.wildfly.security.http.HttpAuthenticationException; - -/** - * @author Pedro Igor - */ -final class SecurityIdentityUtil { - - static final SecurityIdentity authorize(CallbackHandler callbackHandler, Principal principal) { - try { - EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(new Evidence() { - @Override - public Principal getPrincipal() { - return principal; - } - }); - - callbackHandler.handle(new Callback[]{evidenceVerifyCallback}); - - if (evidenceVerifyCallback.isVerified()) { - AuthorizeCallback authorizeCallback = new AuthorizeCallback(null, null); - - try { - callbackHandler.handle(new Callback[] {authorizeCallback}); - - authorizeCallback.isAuthorized(); - } catch (Exception e) { - throw new HttpAuthenticationException(e); - } - - SecurityIdentityCallback securityIdentityCallback = new SecurityIdentityCallback(); - IdentityCredentialCallback credentialCallback = new IdentityCredentialCallback(new BearerTokenCredential(KeycloakPrincipal.class.cast(principal).getKeycloakSecurityContext().getTokenString()), true); - - callbackHandler.handle(new Callback[]{credentialCallback, AuthenticationCompleteCallback.SUCCEEDED, securityIdentityCallback}); - - SecurityIdentity securityIdentity = securityIdentityCallback.getSecurityIdentity(); - - return securityIdentity; - } - } catch (UnsupportedCallbackException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return null; - } - -} diff --git a/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension deleted file mode 100644 index 965ca75b8f37..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension +++ /dev/null @@ -1,52 +0,0 @@ -# -# * Copyright 2018 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. -# - -# -# * Copyright 2018 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. -# - -# -# Copyright 2016 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. -# - -org.keycloak.adapters.elytron.KeycloakServletExtension diff --git a/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory b/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory deleted file mode 100644 index 96a0441f3297..000000000000 --- a/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory +++ /dev/null @@ -1,19 +0,0 @@ -# -# JBoss, Home of Professional Open Source. -# Copyright 2016 Red Hat, Inc., and individual 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. -# - -org.keycloak.adapters.elytron.KeycloakHttpServerAuthenticationMechanismFactory \ No newline at end of file diff --git a/adapters/oidc/wildfly/pom.xml b/adapters/oidc/wildfly/pom.xml deleted file mode 100755 index 173bf616d01a..000000000000 --- a/adapters/oidc/wildfly/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - Keycloak WildFly Integration - - 4.0.0 - - keycloak-wildfly-integration-pom - pom - - - wildfly-subsystem - - \ No newline at end of file diff --git a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml deleted file mode 100755 index 15f36da7fef0..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - 4.0.0 - - - org.keycloak - keycloak-parent - 999.0.0-SNAPSHOT - ../../../../pom.xml - - - keycloak-wildfly-subsystem - Keycloak Wildfly Adapter Subsystem - - jar - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - true - - - jboss.home - ${jboss.home} - - - - **/*TestCase.java - - - - - - - - - org.wildfly.core - wildfly-controller - provided - - - org.wildfly.core - wildfly-server - provided - - - ${ee.maven.groupId} - wildfly-web-common - provided - - - org.wildfly.common - wildfly-common - ${wildfly.common.wildfly.aligned.version} - - - org.wildfly.security - wildfly-elytron - - - org.jboss.logging - jboss-logging-annotations - - provided - true - - - - org.jboss.logging - jboss-logging-processor - - provided - true - - - - org.wildfly.core - wildfly-subsystem-test-framework - test - - - junit - junit - test - - - org.keycloak - keycloak-wildfly-elytron-oidc-adapter - - - diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationAddHandler.java deleted file mode 100755 index 0ce8612dcd86..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationAddHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import java.util.List; - -import org.jboss.as.controller.AbstractAddStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.as.controller.capability.RuntimeCapability; -import org.jboss.dmr.ModelNode; - -/** - * Add a deployment to a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -abstract class AbstractAdapterConfigurationAddHandler extends AbstractAddStepHandler { - - private final boolean elytronEnabled; - - AbstractAdapterConfigurationAddHandler(RuntimeCapability runtimeCapability, List attributes) { - super(attributes); - elytronEnabled = runtimeCapability != null; - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.addSecureDeployment(operation, context.resolveExpressions(model), elytronEnabled); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java deleted file mode 100755 index c2d75a6d6ab5..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationDefinition.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; -import org.jboss.as.controller.operations.validation.IntRangeValidator; -import org.jboss.as.controller.operations.validation.StringLengthValidator; -import org.jboss.as.controller.registry.ManagementResourceRegistration; -import org.jboss.dmr.ModelNode; -import org.jboss.dmr.ModelType; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Defines attributes and operations for a secure-deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -abstract class AbstractAdapterConfigurationDefinition extends SimpleResourceDefinition { - - protected static final SimpleAttributeDefinition REALM = - new SimpleAttributeDefinitionBuilder("realm", ModelType.STRING, true) - .setXmlName("realm") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition RESOURCE = - new SimpleAttributeDefinitionBuilder("resource", ModelType.STRING, true) - .setXmlName("resource") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition USE_RESOURCE_ROLE_MAPPINGS = - new SimpleAttributeDefinitionBuilder("use-resource-role-mappings", ModelType.BOOLEAN, true) - .setXmlName("use-resource-role-mappings") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition BEARER_ONLY = - new SimpleAttributeDefinitionBuilder("bearer-only", ModelType.BOOLEAN, true) - .setXmlName("bearer-only") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition ENABLE_BASIC_AUTH = - new SimpleAttributeDefinitionBuilder("enable-basic-auth", ModelType.BOOLEAN, true) - .setXmlName("enable-basic-auth") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition PUBLIC_CLIENT = - new SimpleAttributeDefinitionBuilder("public-client", ModelType.BOOLEAN, true) - .setXmlName("public-client") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition TURN_OFF_CHANGE_SESSION = - new SimpleAttributeDefinitionBuilder("turn-off-change-session-id-on-login", ModelType.BOOLEAN, true) - .setXmlName("turn-off-change-session-id-on-login") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition TOKEN_MINIMUM_TIME_TO_LIVE = - new SimpleAttributeDefinitionBuilder("token-minimum-time-to-live", ModelType.INT, true) - .setXmlName("token-minimum-time-to-live") - .setValidator(new IntRangeValidator(-1, true)) - .setAllowExpression(true) - .build(); - protected static final SimpleAttributeDefinition MIN_TIME_BETWEEN_JWKS_REQUESTS = - new SimpleAttributeDefinitionBuilder("min-time-between-jwks-requests", ModelType.INT, true) - .setXmlName("min-time-between-jwks-requests") - .setValidator(new IntRangeValidator(-1, true)) - .setAllowExpression(true) - .build(); - protected static final SimpleAttributeDefinition PUBLIC_KEY_CACHE_TTL = - new SimpleAttributeDefinitionBuilder("public-key-cache-ttl", ModelType.INT, true) - .setXmlName("public-key-cache-ttl") - .setAllowExpression(true) - .setValidator(new IntRangeValidator(-1, true)) - .build(); - protected static final SimpleAttributeDefinition ADAPTER_STATE_COOKIE_PATH = - new SimpleAttributeDefinitionBuilder("adapter-state-cookie-path", ModelType.STRING, true) - .setXmlName("adapter-state-cookie-path") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - - static final List DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList(); - - static { - DEPLOYMENT_ONLY_ATTRIBUTES.add(REALM); - DEPLOYMENT_ONLY_ATTRIBUTES.add(RESOURCE); - DEPLOYMENT_ONLY_ATTRIBUTES.add(USE_RESOURCE_ROLE_MAPPINGS); - DEPLOYMENT_ONLY_ATTRIBUTES.add(BEARER_ONLY); - DEPLOYMENT_ONLY_ATTRIBUTES.add(ENABLE_BASIC_AUTH); - DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_CLIENT); - DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION); - DEPLOYMENT_ONLY_ATTRIBUTES.add(TOKEN_MINIMUM_TIME_TO_LIVE); - DEPLOYMENT_ONLY_ATTRIBUTES.add(MIN_TIME_BETWEEN_JWKS_REQUESTS); - DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_KEY_CACHE_TTL); - DEPLOYMENT_ONLY_ATTRIBUTES.add(ADAPTER_STATE_COOKIE_PATH); - } - - static final List ALL_ATTRIBUTES = new ArrayList(); - - static { - ALL_ATTRIBUTES.addAll(DEPLOYMENT_ONLY_ATTRIBUTES); - ALL_ATTRIBUTES.addAll(SharedAttributeDefinitons.ATTRIBUTES); - } - - static final Map XML_ATTRIBUTES = new HashMap(); - - static { - for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { - XML_ATTRIBUTES.put(def.getXmlName(), def); - } - } - - private static final Map DEFINITION_LOOKUP = new HashMap(); - static { - for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { - DEFINITION_LOOKUP.put(def.getXmlName(), def); - } - } - - private final AbstractAdapterConfigurationWriteAttributeHandler attrWriteHandler; - private final List attributes; - - protected AbstractAdapterConfigurationDefinition(String name, List attributes, AbstractAdapterConfigurationAddHandler addHandler, AbstractAdapterConfigurationRemoveHandler removeHandler, AbstractAdapterConfigurationWriteAttributeHandler attrWriteHandler) { - super(PathElement.pathElement(name), - KeycloakExtension.getResourceDescriptionResolver(name), - addHandler, - removeHandler); - this.attributes = attributes; - this.attrWriteHandler = attrWriteHandler; - } - - @Override - public void registerOperations(ManagementResourceRegistration resourceRegistration) { - super.registerOperations(resourceRegistration); - resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); - } - - @Override - public void registerAttributes(ManagementResourceRegistration resourceRegistration) { - super.registerAttributes(resourceRegistration); - for (AttributeDefinition attrDef : this.attributes) { - resourceRegistration.registerReadWriteAttribute(attrDef, null, this.attrWriteHandler); - } - } - - public static SimpleAttributeDefinition lookup(String name) { - return DEFINITION_LOOKUP.get(name); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationRemoveHandler.java deleted file mode 100644 index 2066aed3f48f..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationRemoveHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractRemoveStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Remove a secure-deployment from a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -abstract class AbstractAdapterConfigurationRemoveHandler extends AbstractRemoveStepHandler { - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.removeSecureDeployment(operation); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationWriteAttributeHandler.java deleted file mode 100755 index 127ea538f1b4..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/AbstractAdapterConfigurationWriteAttributeHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractWriteAttributeHandler; -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.dmr.ModelNode; - -import java.util.List; - -/** - * Update an attribute on a secure-deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -abstract class AbstractAdapterConfigurationWriteAttributeHandler extends AbstractWriteAttributeHandler { - - AbstractAdapterConfigurationWriteAttributeHandler(List definitions) { - super(definitions.toArray(new AttributeDefinition[definitions.size()])); - } - - @Override - protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode resolvedValue, ModelNode currentValue, HandbackHolder hh) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - hh.setHandback(ckService); - ckService.updateSecureDeployment(operation, attributeName, resolvedValue); - return false; - } - - @Override - protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { - ckService.updateSecureDeployment(operation, attributeName, valueToRestore); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialAddHandler.java deleted file mode 100755 index 0b87563fbef6..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialAddHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractAddStepHandler; -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Add a credential to a deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public class CredentialAddHandler extends AbstractAddStepHandler { - - public CredentialAddHandler(AttributeDefinition... attributes) { - super(attributes); - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.addCredential(operation, context.resolveExpressions(model)); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialDefinition.java deleted file mode 100755 index 318f47f3db57..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialDefinition.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; -import org.jboss.as.controller.operations.validation.StringLengthValidator; -import org.jboss.as.controller.registry.ManagementResourceRegistration; -import org.jboss.dmr.ModelType; - -/** - * Defines attributes and operations for a credential. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class CredentialDefinition extends SimpleResourceDefinition { - - public static final String TAG_NAME = "credential"; - - protected static final AttributeDefinition VALUE = - new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false) - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) - .build(); - - public CredentialDefinition() { - super(PathElement.pathElement(TAG_NAME), - KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), - new CredentialAddHandler(VALUE), - CredentialRemoveHandler.INSTANCE); - } - - @Override - public void registerOperations(ManagementResourceRegistration resourceRegistration) { - super.registerOperations(resourceRegistration); - resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); - } - - @Override - public void registerAttributes(ManagementResourceRegistration resourceRegistration) { - super.registerAttributes(resourceRegistration); - resourceRegistration.registerReadWriteAttribute(VALUE, null, new CredentialReadWriteAttributeHandler()); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialReadWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialReadWriteAttributeHandler.java deleted file mode 100644 index 2013dbaeee74..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialReadWriteAttributeHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractWriteAttributeHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Update a credential value. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public class CredentialReadWriteAttributeHandler extends AbstractWriteAttributeHandler { - - @Override - protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder hh) throws OperationFailedException { - - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.updateCredential(operation, attributeName, resolvedValue); - - hh.setHandback(ckService); - - return false; - } - - @Override - protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { - ckService.updateCredential(operation, attributeName, valueToRestore); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialRemoveHandler.java deleted file mode 100644 index 3dc2b3a56d85..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialRemoveHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractRemoveStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Remove a credential from a deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public final class CredentialRemoveHandler extends AbstractRemoveStepHandler { - - public static CredentialRemoveHandler INSTANCE = new CredentialRemoveHandler(); - - private CredentialRemoveHandler() {} - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.removeCredential(operation); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/Elytron.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/Elytron.java deleted file mode 100644 index 385e4f8a39a5..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/Elytron.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2018 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.subsystem.adapter.extension; - -import org.jboss.as.server.deployment.DeploymentPhaseContext; -import org.jboss.as.server.deployment.DeploymentUnit; -import org.jboss.as.web.common.WarMetaData; -import org.jboss.metadata.web.jboss.JBossWebMetaData; -import org.jboss.msc.service.ServiceName; - -/** - * Utility class for Elytron integration - * - * @author Pedro Igor - */ -public final class Elytron { - - private static final String DEFAULT_SECURITY_DOMAIN = "other"; - private static final String UNDERTOW_APPLICATION_SECURITY_DOMAIN = "org.wildfly.undertow.application-security-domain."; - - static boolean isElytronEnabled(DeploymentPhaseContext phaseContext) { - String securityDomain = getSecurityDomain(phaseContext.getDeploymentUnit()); - ServiceName serviceName = ServiceName.parse(new StringBuilder(UNDERTOW_APPLICATION_SECURITY_DOMAIN).append(securityDomain).toString()); - return phaseContext.getServiceRegistry().getService(serviceName) != null; - } - - private static String getSecurityDomain(DeploymentUnit deploymentUnit) { - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - - if (warMetaData != null) { - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - - if (webMetaData != null) { - String configuredSecurityDomain = webMetaData.getSecurityDomain(); - - if (configuredSecurityDomain != null) { - return configuredSecurityDomain; - } - } - } - - return DEFAULT_SECURITY_DOMAIN; - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigDeploymentProcessor.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigDeploymentProcessor.java deleted file mode 100755 index 20e9500440d5..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigDeploymentProcessor.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import static org.keycloak.subsystem.adapter.extension.Elytron.isElytronEnabled; - -import org.jboss.as.server.deployment.DeploymentPhaseContext; -import org.jboss.as.server.deployment.DeploymentUnit; -import org.jboss.as.server.deployment.DeploymentUnitProcessingException; -import org.jboss.as.server.deployment.DeploymentUnitProcessor; -import org.jboss.as.web.common.WarMetaData; -import org.jboss.logging.Logger; -import org.jboss.metadata.javaee.spec.ParamValueMetaData; -import org.jboss.metadata.web.jboss.JBossWebMetaData; -import org.jboss.metadata.web.spec.ListenerMetaData; -import org.jboss.metadata.web.spec.LoginConfigMetaData; -import org.keycloak.adapters.elytron.KeycloakConfigurationServletListener; -import org.keycloak.subsystem.adapter.logging.KeycloakLogger; - -import java.util.ArrayList; -import java.util.List; - -/** - * Pass authentication data (keycloak.json) as a servlet context param so it can be read by the KeycloakServletExtension. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitProcessor { - protected Logger log = Logger.getLogger(KeycloakAdapterConfigDeploymentProcessor.class); - - // This param name is defined again in Keycloak Undertow Integration class - // org.keycloak.adapters.undertow.KeycloakServletExtension. We have this value in - // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration. - public static final String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; - - // not sure if we need this yet, keeping here just in case - protected void addSecurityDomain(DeploymentUnit deploymentUnit, KeycloakAdapterConfigService service) { - if (!service.isSecureDeployment(deploymentUnit)) { - return; - } - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - if (warMetaData == null) return; - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) return; - - LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); - if (loginConfig == null || !loginConfig.getAuthMethod().equalsIgnoreCase("KEYCLOAK")) { - return; - } - - webMetaData.setSecurityDomain("keycloak"); - } - - @Override - public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { - DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); - - KeycloakAdapterConfigService service = KeycloakAdapterConfigService.getInstance(); - if (service.isSecureDeployment(deploymentUnit) && service.isDeploymentConfigured(deploymentUnit)) { - addKeycloakAuthData(phaseContext, service); - } - - addConfigurationListener(phaseContext); - - // FYI, Undertow Extension will find deployments that have auth-method set to KEYCLOAK - - // todo notsure if we need this - // addSecurityDomain(deploymentUnit, service); - } - - private void addKeycloakAuthData(DeploymentPhaseContext phaseContext, KeycloakAdapterConfigService service) throws DeploymentUnitProcessingException { - DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - if (warMetaData == null) { - throw new DeploymentUnitProcessingException("WarMetaData not found for " + deploymentUnit.getName() + ". Make sure you have specified a WAR as your secure-deployment in the Keycloak subsystem."); - } - - addJSONData(service.getJSON(deploymentUnit), warMetaData); - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) { - webMetaData = new JBossWebMetaData(); - warMetaData.setMergedJBossWebMetaData(webMetaData); - } - - LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); - if (loginConfig == null) { - loginConfig = new LoginConfigMetaData(); - webMetaData.setLoginConfig(loginConfig); - } - loginConfig.setAuthMethod("KEYCLOAK"); - loginConfig.setRealmName(service.getRealmName(deploymentUnit)); - KeycloakLogger.ROOT_LOGGER.deploymentSecured(deploymentUnit.getName()); - } - - private void addJSONData(String json, WarMetaData warMetaData) { - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) { - webMetaData = new JBossWebMetaData(); - warMetaData.setMergedJBossWebMetaData(webMetaData); - } - - List contextParams = webMetaData.getContextParams(); - if (contextParams == null) { - contextParams = new ArrayList(); - } - - ParamValueMetaData param = new ParamValueMetaData(); - param.setParamName(AUTH_DATA_PARAM_NAME); - param.setParamValue(json); - contextParams.add(param); - - webMetaData.setContextParams(contextParams); - } - - private void addConfigurationListener(DeploymentPhaseContext phaseContext) { - DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - if (warMetaData == null) { - return; - } - - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) { - webMetaData = new JBossWebMetaData(); - warMetaData.setMergedJBossWebMetaData(webMetaData); - } - - LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); - if (loginConfig == null) { - return; - } - if (!loginConfig.getAuthMethod().equals("KEYCLOAK")) { - return; - } - - if (isElytronEnabled(phaseContext)) { - ListenerMetaData listenerMetaData = new ListenerMetaData(); - - listenerMetaData.setListenerClass(KeycloakConfigurationServletListener.class.getName()); - - webMetaData.getListeners().add(listenerMetaData); - } - } - - @Override - public void undeploy(DeploymentUnit du) { - - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java deleted file mode 100755 index bebf486c4e9c..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.server.deployment.DeploymentUnit; -import org.jboss.as.web.common.WarMetaData; -import org.jboss.dmr.ModelNode; -import org.jboss.dmr.Property; -import org.jboss.metadata.web.jboss.JBossWebMetaData; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; - -/** - * This service keeps track of the entire Keycloak management model so as to provide - * adapter configuration to each deployment at deploy time. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public final class KeycloakAdapterConfigService { - - private static final String CREDENTIALS_JSON_NAME = "credentials"; - - private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rules"; - - private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService(); - - public static KeycloakAdapterConfigService getInstance() { - return INSTANCE; - } - - private final Map realms = new HashMap(); - - // keycloak-secured deployments - private final Map secureDeployments = new HashMap(); - private final Set elytronEnabledDeployments = new HashSet<>(); - - - private KeycloakAdapterConfigService() { - } - - public void addRealm(ModelNode operation, ModelNode model) { - this.realms.put(realmNameFromOp(operation), model.clone()); - } - - public void updateRealm(ModelNode operation, String attrName, ModelNode resolvedValue) { - ModelNode realm = this.realms.get(realmNameFromOp(operation)); - realm.get(attrName).set(resolvedValue); - } - - public void removeRealm(ModelNode operation) { - this.realms.remove(realmNameFromOp(operation)); - } - - public void addSecureDeployment(ModelNode operation, ModelNode model, boolean elytronEnabled) { - ModelNode deployment = model.clone(); - String name = deploymentNameFromOp(operation); - this.secureDeployments.put(name, deployment); - if (elytronEnabled) { - elytronEnabledDeployments.add(name); - } - } - - public void updateSecureDeployment(ModelNode operation, String attrName, ModelNode resolvedValue) { - ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); - deployment.get(attrName).set(resolvedValue); - } - - public void removeSecureDeployment(ModelNode operation) { - String name = deploymentNameFromOp(operation); - this.secureDeployments.remove(name); - elytronEnabledDeployments.remove(name); - } - - public void addCredential(ModelNode operation, ModelNode model) { - ModelNode credentials = credentialsFromOp(operation); - if (!credentials.isDefined()) { - credentials = new ModelNode(); - } - - String credentialName = credentialNameFromOp(operation); - if (!credentialName.contains(".")) { - credentials.get(credentialName).set(model.get("value").asString()); - } else { - String[] parts = credentialName.split("\\."); - String provider = parts[0]; - String property = parts[1]; - ModelNode credential = credentials.get(provider); - if (!credential.isDefined()) { - credential = new ModelNode(); - } - credential.get(property).set(model.get("value").asString()); - credentials.set(provider, credential); - } - - ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); - deployment.get(CREDENTIALS_JSON_NAME).set(credentials); - } - - public void removeCredential(ModelNode operation) { - ModelNode credentials = credentialsFromOp(operation); - if (!credentials.isDefined()) { - throw new RuntimeException("Can not remove credential. No credential defined for deployment in op " + operation.toString()); - } - - String credentialName = credentialNameFromOp(operation); - credentials.remove(credentialName); - } - - public void updateCredential(ModelNode operation, String attrName, ModelNode resolvedValue) { - ModelNode credentials = credentialsFromOp(operation); - if (!credentials.isDefined()) { - throw new RuntimeException("Can not update credential. No credential defined for deployment in op " + operation.toString()); - } - - String credentialName = credentialNameFromOp(operation); - credentials.get(credentialName).set(resolvedValue); - } - - private ModelNode credentialsFromOp(ModelNode operation) { - ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); - return deployment.get(CREDENTIALS_JSON_NAME); - } - - public void addRedirectRewriteRule(ModelNode operation, ModelNode model) { - ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); - if (!redirectRewritesRules.isDefined()) { - redirectRewritesRules = new ModelNode(); - } - String redirectRewriteRuleName = redirectRewriteRule(operation); - redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString()); - - ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); - deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules); - } - - public void removeRedirectRewriteRule(ModelNode operation) { - ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); - if (!redirectRewritesRules.isDefined()) { - throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString()); - } - - String ruleName = credentialNameFromOp(operation); - redirectRewritesRules.remove(ruleName); - } - - public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) { - ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); - if (!redirectRewritesRules.isDefined()) { - throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString()); - } - - String ruleName = credentialNameFromOp(operation); - redirectRewritesRules.get(ruleName).set(resolvedValue); - } - - private ModelNode redirectRewriteRuleFromOp(ModelNode operation) { - ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); - return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME); - } - - private String realmNameFromOp(ModelNode operation) { - return valueFromOpAddress(RealmDefinition.TAG_NAME, operation); - } - - private String deploymentNameFromOp(ModelNode operation) { - String deploymentName = valueFromOpAddress(SecureDeploymentDefinition.TAG_NAME, operation); - - if (deploymentName == null) { - deploymentName = valueFromOpAddress(KeycloakHttpServerAuthenticationMechanismFactoryDefinition.TAG_NAME, operation); - } - - if (deploymentName == null) { - deploymentName = valueFromOpAddress(SecureServerDefinition.TAG_NAME, operation); - } - - if (deploymentName == null) throw new RuntimeException("Can't find deployment name in address " + operation); - - return deploymentName; - } - - private String credentialNameFromOp(ModelNode operation) { - return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation); - } - - private String redirectRewriteRule(ModelNode operation) { - return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation); - } - - private String valueFromOpAddress(String addrElement, ModelNode operation) { - return getValueOfAddrElement(operation.get(ADDRESS), addrElement); - } - - private String getValueOfAddrElement(ModelNode address, String elementName) { - for (ModelNode element : address.asList()) { - if (element.has(elementName)) return element.get(elementName).asString(); - } - - return null; - } - - public String getRealmName(DeploymentUnit deploymentUnit) { - ModelNode deployment = getSecureDeployment(deploymentUnit); - return deployment.get(RealmDefinition.TAG_NAME).asString(); - - } - - protected boolean isDeploymentConfigured(DeploymentUnit deploymentUnit) { - ModelNode deployment = getSecureDeployment(deploymentUnit); - if (! deployment.isDefined()) { - return false; - } - ModelNode resource = deployment.get(SecureDeploymentDefinition.RESOURCE.getName()); - return resource.isDefined(); - } - - public String getJSON(DeploymentUnit deploymentUnit) { - ModelNode deployment = getSecureDeployment(deploymentUnit); - String realmName = deployment.get(RealmDefinition.TAG_NAME).asString(); - ModelNode realm = this.realms.get(realmName); - - ModelNode json = new ModelNode(); - json.get(RealmDefinition.TAG_NAME).set(realmName); - - // Realm values set first. Some can be overridden by deployment values. - if (realm != null) setJSONValues(json, realm); - setJSONValues(json, deployment); - return json.toJSONString(true); - } - - public String getJSON(String deploymentName) { - ModelNode deployment = this.secureDeployments.get(deploymentName); - String realmName = deployment.get(RealmDefinition.TAG_NAME).asString(); - ModelNode realm = this.realms.get(realmName); - - ModelNode json = new ModelNode(); - json.get(RealmDefinition.TAG_NAME).set(realmName); - - // Realm values set first. Some can be overridden by deployment values. - if (realm != null) setJSONValues(json, realm); - setJSONValues(json, deployment); - return json.toJSONString(true); - } - - private void setJSONValues(ModelNode json, ModelNode values) { - synchronized (values) { - for (Property prop : new ArrayList<>(values.asPropertyList())) { - String name = prop.getName(); - ModelNode value = prop.getValue(); - if (value.isDefined()) { - json.get(name).set(value); - } - } - } - } - - public boolean isSecureDeployment(DeploymentUnit deploymentUnit) { - //log.info("********* CHECK KEYCLOAK DEPLOYMENT: deployments.size()" + deployments.size()); - - String deploymentName = preferredDeploymentName(deploymentUnit); - return this.secureDeployments.containsKey(deploymentName); - } - - public boolean isElytronEnabled(DeploymentUnit deploymentUnit) { - return elytronEnabledDeployments.contains(preferredDeploymentName(deploymentUnit)); - } - - private ModelNode getSecureDeployment(DeploymentUnit deploymentUnit) { - String deploymentName = preferredDeploymentName(deploymentUnit); - return this.secureDeployments.containsKey(deploymentName) - ? this.secureDeployments.get(deploymentName) - : new ModelNode(); - } - - // KEYCLOAK-3273: prefer module name if available - private String preferredDeploymentName(DeploymentUnit deploymentUnit) { - String deploymentName = deploymentUnit.getName(); - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - if (warMetaData == null) { - return deploymentName; - } - - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) { - return deploymentName; - } - - String moduleName = webMetaData.getModuleName(); - if (moduleName != null) return moduleName + ".war"; - - return deploymentName; - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessor.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessor.java deleted file mode 100755 index b4c6a6578a73..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 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.subsystem.adapter.extension; - -import org.jboss.as.server.deployment.Attachments; -import org.jboss.as.server.deployment.DeploymentPhaseContext; -import org.jboss.as.server.deployment.DeploymentUnit; -import org.jboss.as.server.deployment.DeploymentUnitProcessingException; -import org.jboss.as.server.deployment.DeploymentUnitProcessor; -import org.jboss.as.server.deployment.module.ModuleDependency; -import org.jboss.as.server.deployment.module.ModuleSpecification; -import org.jboss.as.web.common.WarMetaData; -import org.jboss.metadata.web.jboss.JBossWebMetaData; -import org.jboss.metadata.web.spec.LoginConfigMetaData; -import org.jboss.modules.Module; -import org.jboss.modules.ModuleIdentifier; -import org.jboss.modules.ModuleLoader; - -/** - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public abstract class KeycloakDependencyProcessor implements DeploymentUnitProcessor { - - private static final ModuleIdentifier KEYCLOAK_JBOSS_CORE_ADAPTER = KeycloakSubsystemDefinition.KEYCLOAK_JBOSS_CORE_ADAPTER; - private static final ModuleIdentifier KEYCLOAK_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-adapter-core"); - private static final ModuleIdentifier KEYCLOAK_CORE = ModuleIdentifier.create("org.keycloak.keycloak-core"); - private static final ModuleIdentifier KEYCLOAK_COMMON = ModuleIdentifier.create("org.keycloak.keycloak-common"); - - @Override - public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { - final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); - if (!KeycloakAdapterConfigService.getInstance().isSecureDeployment(deploymentUnit)) { - WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); - if (warMetaData == null) { - return; - } - JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); - if (webMetaData == null) { - return; - } - LoginConfigMetaData loginConfig = webMetaData.getLoginConfig(); - if (loginConfig == null) return; - if (loginConfig.getAuthMethod() == null) return; - if (!loginConfig.getAuthMethod().equals("KEYCLOAK")) return; - } - - final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION); - final ModuleLoader moduleLoader = Module.getBootModuleLoader(); - addCommonModules(moduleSpecification, moduleLoader); - addPlatformSpecificModules(phaseContext, moduleSpecification, moduleLoader); - } - - private void addCommonModules(ModuleSpecification moduleSpecification, ModuleLoader moduleLoader) { - // ModuleDependency(ModuleLoader moduleLoader, ModuleIdentifier identifier, boolean optional, boolean export, boolean importServices, boolean userSpecified) - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_JBOSS_CORE_ADAPTER, false, false, false, false)); - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_ADAPTER, false, false, false, false)); - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE, false, false, false, false)); - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_COMMON, false, false, false, false)); - } - - abstract protected void addPlatformSpecificModules(DeploymentPhaseContext phaseContext, ModuleSpecification moduleSpecification, ModuleLoader moduleLoader); - - @Override - public void undeploy(DeploymentUnit du) { - - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessorWildFly.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessorWildFly.java deleted file mode 100755 index 3189123152a5..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessorWildFly.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import static org.keycloak.subsystem.adapter.extension.Elytron.isElytronEnabled; - -import org.jboss.as.server.deployment.DeploymentPhaseContext; -import org.jboss.as.server.deployment.module.ModuleDependency; -import org.jboss.as.server.deployment.module.ModuleSpecification; -import org.jboss.modules.ModuleIdentifier; -import org.jboss.modules.ModuleLoader; - -/** - * Add platform-specific modules for WildFly. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public class KeycloakDependencyProcessorWildFly extends KeycloakDependencyProcessor { - - private static final ModuleIdentifier KEYCLOAK_ELYTRON_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-wildfly-elytron-oidc-adapter"); - - @Override - protected void addPlatformSpecificModules(DeploymentPhaseContext phaseContext, ModuleSpecification moduleSpecification, ModuleLoader moduleLoader) { - if (isElytronEnabled(phaseContext)) { - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_ELYTRON_ADAPTER, true, false, true, false)); - } else { - throw new RuntimeException("Legacy WildFly security layer is no longer supported by the Keycloak WildFly adapter"); - } - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java deleted file mode 100755 index 9af118973a84..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.Extension; -import org.jboss.as.controller.ExtensionContext; -import org.jboss.as.controller.ModelVersion; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.ResourceDefinition; -import org.jboss.as.controller.SubsystemRegistration; -import org.jboss.as.controller.descriptions.StandardResourceDescriptionResolver; -import org.jboss.as.controller.parsing.ExtensionParsingContext; -import org.jboss.as.controller.registry.ManagementResourceRegistration; -import org.keycloak.subsystem.adapter.logging.KeycloakLogger; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; - - -/** - * Main Extension class for the subsystem. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class KeycloakExtension implements Extension { - - public static final String SUBSYSTEM_NAME = "keycloak"; - public static final String NAMESPACE_1_1 = "urn:jboss:domain:keycloak:1.1"; - public static final String NAMESPACE_1_2 = "urn:jboss:domain:keycloak:1.2"; - public static final String CURRENT_NAMESPACE = NAMESPACE_1_2; - private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser(); - static final PathElement PATH_SUBSYSTEM = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); - private static final String RESOURCE_NAME = KeycloakExtension.class.getPackage().getName() + ".LocalDescriptions"; - private static final ModelVersion MGMT_API_VERSION = ModelVersion.create(1,1,0); - static final PathElement SUBSYSTEM_PATH = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); - private static final ResourceDefinition KEYCLOAK_SUBSYSTEM_RESOURCE = new KeycloakSubsystemDefinition(); - static final RealmDefinition REALM_DEFINITION = new RealmDefinition(); - static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition(); - static final SecureServerDefinition SECURE_SERVER_DEFINITION = new SecureServerDefinition(); - static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition(); - static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition(); - - public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) { - StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME); - for (String kp : keyPrefix) { - prefix.append('.').append(kp); - } - return new StandardResourceDescriptionResolver(prefix.toString(), RESOURCE_NAME, KeycloakExtension.class.getClassLoader(), true, false); - } - - /** - * {@inheritDoc} - */ - @Override - public void initializeParsers(final ExtensionParsingContext context) { - context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakExtension.NAMESPACE_1_1, PARSER); - context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakExtension.NAMESPACE_1_2, PARSER); - } - - /** - * {@inheritDoc} - */ - @Override - public void initialize(final ExtensionContext context) { - KeycloakLogger.ROOT_LOGGER.debug("Activating Keycloak Extension"); - final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME, MGMT_API_VERSION); - - ManagementResourceRegistration registration = subsystem.registerSubsystemModel(KEYCLOAK_SUBSYSTEM_RESOURCE); - registration.registerSubModel(REALM_DEFINITION); - ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION); - secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION); - secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON); - - ManagementResourceRegistration secureServerRegistration = registration.registerSubModel(SECURE_SERVER_DEFINITION); - secureServerRegistration.registerSubModel(CREDENTIAL_DEFINITION); - secureServerRegistration.registerSubModel(REDIRECT_RULE_DEFINITON); - - subsystem.registerXMLElementWriter(PARSER); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpAuthenticationFactoryService.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpAuthenticationFactoryService.java deleted file mode 100644 index 94fb8e566d10..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpAuthenticationFactoryService.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.subsystem.adapter.extension; - -import org.jboss.msc.service.Service; -import org.jboss.msc.service.StartContext; -import org.jboss.msc.service.StartException; -import org.jboss.msc.service.StopContext; -import org.jboss.msc.value.InjectedValue; -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.elytron.KeycloakHttpServerAuthenticationMechanismFactory; -import org.wildfly.security.auth.server.SecurityDomain; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; -import org.wildfly.security.http.util.SetMechanismInformationMechanismFactory; - -import java.io.ByteArrayInputStream; - -/** - * @author Pedro Igor - */ -public class KeycloakHttpAuthenticationFactoryService implements Service { - - private final String factoryName; - private HttpServerAuthenticationMechanismFactory httpAuthenticationFactory; - - public KeycloakHttpAuthenticationFactoryService(String factoryName) { - this.factoryName = factoryName; - } - - @Override - public void start(StartContext context) throws StartException { - KeycloakAdapterConfigService adapterConfigService = KeycloakAdapterConfigService.getInstance(); - String config = adapterConfigService.getJSON(this.factoryName); - this.httpAuthenticationFactory = new KeycloakHttpServerAuthenticationMechanismFactory(createDeploymentContext(config.getBytes())); - } - - @Override - public void stop(StopContext context) { - this.httpAuthenticationFactory = null; - } - - @Override - public HttpServerAuthenticationMechanismFactory getValue() throws IllegalStateException, IllegalArgumentException { - return new SetMechanismInformationMechanismFactory(this.httpAuthenticationFactory); - } - - private AdapterDeploymentContext createDeploymentContext(byte[] config) { - return new AdapterDeploymentContext(KeycloakDeploymentBuilder.build(new ByteArrayInputStream(config))); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpServerAuthenticationMechanismFactoryDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpServerAuthenticationMechanismFactoryDefinition.java deleted file mode 100644 index 1e177a49f4f4..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakHttpServerAuthenticationMechanismFactoryDefinition.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.subsystem.adapter.extension; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; -import static org.keycloak.subsystem.adapter.extension.KeycloakHttpServerAuthenticationMechanismFactoryDefinition.KeycloakHttpServerAuthenticationMechanismFactoryAddHandler.HTTP_SERVER_AUTHENTICATION_CAPABILITY; - -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.as.controller.PathAddress; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.capability.RuntimeCapability; -import org.jboss.dmr.ModelNode; -import org.jboss.msc.service.ServiceController; -import org.jboss.msc.service.ServiceName; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; - -/** - * A {@link SimpleResourceDefinition} that can be used to configure a {@link org.keycloak.adapters.elytron.KeycloakHttpServerAuthenticationMechanismFactory} - * and expose it as a capability for other subsystems. - * - * @author Pedro Igor - */ -class KeycloakHttpServerAuthenticationMechanismFactoryDefinition extends AbstractAdapterConfigurationDefinition { - - static final String TAG_NAME = "http-server-mechanism-factory"; - - KeycloakHttpServerAuthenticationMechanismFactoryDefinition() { - this(TAG_NAME); - } - - KeycloakHttpServerAuthenticationMechanismFactoryDefinition(String tagName) { - super(tagName, ALL_ATTRIBUTES, new KeycloakHttpServerAuthenticationMechanismFactoryAddHandler(), new KeycloakHttpServerAuthenticationMechanismFactoryRemoveHandler(), new KeycloakHttpServerAuthenticationMechanismFactoryWriteHandler()); - } - - /** - * A {@link AbstractAdapterConfigurationAddHandler} that exposes a {@link KeycloakHttpServerAuthenticationMechanismFactoryDefinition} - * as a capability through the installation of a {@link KeycloakHttpAuthenticationFactoryService}. - * - * @author Pedro Igor - */ - static final class KeycloakHttpServerAuthenticationMechanismFactoryAddHandler extends AbstractAdapterConfigurationAddHandler { - - static final String HTTP_SERVER_AUTHENTICATION_CAPABILITY = "org.wildfly.security.http-server-mechanism-factory"; - static final RuntimeCapability HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY = RuntimeCapability - .Builder.of(HTTP_SERVER_AUTHENTICATION_CAPABILITY, true, HttpServerAuthenticationMechanismFactory.class) - .build(); - - KeycloakHttpServerAuthenticationMechanismFactoryAddHandler() { - super(HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY, ALL_ATTRIBUTES); - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.performRuntime(context, operation, model); - installCapability(context, operation); - } - - static void installCapability(OperationContext context, ModelNode operation) { - PathAddress pathAddress = PathAddress.pathAddress(operation.get(OP_ADDR)); - String factoryName = pathAddress.getLastElement().getValue(); - ServiceName serviceName = context.getCapabilityServiceName(HTTP_SERVER_AUTHENTICATION_CAPABILITY, factoryName, HttpServerAuthenticationMechanismFactory.class); - KeycloakHttpAuthenticationFactoryService service = new KeycloakHttpAuthenticationFactoryService(factoryName); - context.getServiceTarget().addService(serviceName, service).setInitialMode(ServiceController.Mode.ACTIVE).install(); - } - } - - /** - * A {@link AbstractAdapterConfigurationRemoveHandler} that handles the removal of {@link KeycloakHttpServerAuthenticationMechanismFactoryDefinition}. - * - * @author Pedro Igor - */ - static final class KeycloakHttpServerAuthenticationMechanismFactoryRemoveHandler extends AbstractAdapterConfigurationRemoveHandler { - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.performRuntime(context, operation, model); - PathAddress pathAddress = PathAddress.pathAddress(operation.get(OP_ADDR)); - String factoryName = pathAddress.getLastElement().getValue(); - ServiceName serviceName = context.getCapabilityServiceName(HTTP_SERVER_AUTHENTICATION_CAPABILITY, factoryName, HttpServerAuthenticationMechanismFactory.class); - - context.removeService(serviceName); - } - - @Override - protected void recoverServices(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.recoverServices(context, operation, model); - KeycloakHttpServerAuthenticationMechanismFactoryAddHandler.installCapability(context, operation); - } - } - - /** - * A {@link AbstractAdapterConfigurationWriteAttributeHandler} that updates attributes on a {@link KeycloakHttpServerAuthenticationMechanismFactoryDefinition}. - * - * @author Pedro Igor - */ - static final class KeycloakHttpServerAuthenticationMechanismFactoryWriteHandler extends AbstractAdapterConfigurationWriteAttributeHandler { - KeycloakHttpServerAuthenticationMechanismFactoryWriteHandler() { - super(ALL_ATTRIBUTES); - } - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemAdd.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemAdd.java deleted file mode 100755 index 8fbee2bd58e7..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemAdd.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - - -import org.jboss.as.controller.AbstractBoottimeAddStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.server.AbstractDeploymentChainStep; -import org.jboss.as.server.DeploymentProcessorTarget; -import org.jboss.as.server.deployment.DeploymentUnitProcessor; -import org.jboss.as.server.deployment.Phase; -import org.jboss.dmr.ModelNode; - -/** - * The Keycloak subsystem add update handler. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { - - static final KeycloakSubsystemAdd INSTANCE = new KeycloakSubsystemAdd(); - - @Override - protected void performBoottime(final OperationContext context, ModelNode operation, final ModelNode model) { - context.addStep(new AbstractDeploymentChainStep() { - @Override - protected void execute(DeploymentProcessorTarget processorTarget) { - processorTarget.addDeploymentProcessor(KeycloakExtension.SUBSYSTEM_NAME, Phase.DEPENDENCIES, 0, chooseDependencyProcessor()); - processorTarget.addDeploymentProcessor(KeycloakExtension.SUBSYSTEM_NAME, - Phase.POST_MODULE, // PHASE - Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY - chooseConfigDeploymentProcessor()); - } - }, OperationContext.Stage.RUNTIME); - } - - private DeploymentUnitProcessor chooseDependencyProcessor() { - return new KeycloakDependencyProcessorWildFly(); - } - - private DeploymentUnitProcessor chooseConfigDeploymentProcessor() { - return new KeycloakAdapterConfigDeploymentProcessor(); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemDefinition.java deleted file mode 100644 index 04b6c6417fe6..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemDefinition.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 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.subsystem.adapter.extension; - -import org.jboss.as.controller.ReloadRequiredRemoveStepHandler; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; -import org.jboss.as.controller.registry.ManagementResourceRegistration; -import org.jboss.as.controller.registry.RuntimePackageDependency; -import org.jboss.modules.ModuleIdentifier; - -/** - * Definition of subsystem=keycloak. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class KeycloakSubsystemDefinition extends SimpleResourceDefinition { - - static final ModuleIdentifier KEYCLOAK_JBOSS_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-jboss-adapter-core"); - - protected KeycloakSubsystemDefinition() { - super(KeycloakExtension.SUBSYSTEM_PATH, - KeycloakExtension.getResourceDescriptionResolver("subsystem"), - KeycloakSubsystemAdd.INSTANCE, - ReloadRequiredRemoveStepHandler.INSTANCE - ); - } - - @Override - public void registerOperations(ManagementResourceRegistration resourceRegistration) { - super.registerOperations(resourceRegistration); - resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); - } - - @Override - public void registerAdditionalRuntimePackages(ManagementResourceRegistration resourceRegistration) { - // This module is required by deployment but not referenced by JBoss modules - resourceRegistration.registerAdditionalRuntimePackages( - RuntimePackageDependency.required(KEYCLOAK_JBOSS_CORE_ADAPTER.getName())); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java deleted file mode 100755 index 81215a2d0341..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.PathAddress; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.as.controller.descriptions.ModelDescriptionConstants; -import org.jboss.as.controller.operations.common.Util; -import org.jboss.as.controller.parsing.ParseUtils; -import org.jboss.as.controller.persistence.SubsystemMarshallingContext; -import org.jboss.dmr.ModelNode; -import org.jboss.dmr.Property; -import org.jboss.staxmapper.XMLElementReader; -import org.jboss.staxmapper.XMLElementWriter; -import org.jboss.staxmapper.XMLExtendedStreamReader; -import org.jboss.staxmapper.XMLExtendedStreamWriter; - -import javax.xml.stream.XMLStreamConstants; -import javax.xml.stream.XMLStreamException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * The subsystem parser, which uses stax to read and write to and from xml - */ -class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader>, XMLElementWriter { - - /** - * {@inheritDoc} - */ - @Override - public void readElement(final XMLExtendedStreamReader reader, final List list) throws XMLStreamException { - // Require no attributes - ParseUtils.requireNoAttributes(reader); - ModelNode addKeycloakSub = Util.createAddOperation(PathAddress.pathAddress(KeycloakExtension.PATH_SUBSYSTEM)); - list.add(addKeycloakSub); - - while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { - if (reader.getLocalName().equals(RealmDefinition.TAG_NAME)) { - readRealm(reader, list); - } - else if (reader.getLocalName().equals(SecureDeploymentDefinition.TAG_NAME)) { - readDeployment(reader, list); - } - else if (reader.getLocalName().equals(SecureServerDefinition.TAG_NAME)) { - readSecureServer(reader, list); - } - } - } - - // used for debugging - private int nextTag(XMLExtendedStreamReader reader) throws XMLStreamException { - return reader.nextTag(); - } - - private void readRealm(XMLExtendedStreamReader reader, List list) throws XMLStreamException { - String realmName = readNameAttribute(reader); - ModelNode addRealm = new ModelNode(); - addRealm.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); - PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), - PathElement.pathElement(RealmDefinition.TAG_NAME, realmName)); - addRealm.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - - while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { - String tagName = reader.getLocalName(); - SimpleAttributeDefinition def = RealmDefinition.lookup(tagName); - if (def == null) throw new XMLStreamException("Unknown realm tag " + tagName); - def.parseAndSetParameter(reader.getElementText(), addRealm, reader); - } - - list.add(addRealm); - } - - private void readDeployment(XMLExtendedStreamReader reader, List resourcesToAdd) throws XMLStreamException { - readSecureResource(KeycloakExtension.SECURE_DEPLOYMENT_DEFINITION.TAG_NAME, KeycloakExtension.SECURE_DEPLOYMENT_DEFINITION, reader, resourcesToAdd); - } - - private void readSecureServer(XMLExtendedStreamReader reader, List resourcesToAdd) throws XMLStreamException { - readSecureResource(KeycloakExtension.SECURE_SERVER_DEFINITION.TAG_NAME, KeycloakExtension.SECURE_SERVER_DEFINITION, reader, resourcesToAdd); - } - - private void readSecureResource(String tagName, AbstractAdapterConfigurationDefinition resource, XMLExtendedStreamReader reader, List resourcesToAdd) throws XMLStreamException { - String name = readNameAttribute(reader); - ModelNode addSecureDeployment = new ModelNode(); - addSecureDeployment.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); - PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), - PathElement.pathElement(tagName, name)); - addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - List credentialsToAdd = new ArrayList(); - List redirectRulesToAdd = new ArrayList(); - while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { - String localName = reader.getLocalName(); - if (localName.equals(CredentialDefinition.TAG_NAME)) { - readCredential(reader, addr, credentialsToAdd); - continue; - } - if (localName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) { - readRewriteRule(reader, addr, redirectRulesToAdd); - continue; - } - - SimpleAttributeDefinition def = resource.lookup(localName); - if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + localName); - def.parseAndSetParameter(reader.getElementText(), addSecureDeployment, reader); - } - - // Must add credentials after the deployment is added. - resourcesToAdd.add(addSecureDeployment); - resourcesToAdd.addAll(credentialsToAdd); - resourcesToAdd.addAll(redirectRulesToAdd); - } - - public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List credentialsToAdd) throws XMLStreamException { - String name = readNameAttribute(reader); - - Map values = new HashMap<>(); - String textValue = null; - while (reader.hasNext()) { - int next = reader.next(); - if (next == CHARACTERS) { - // text value of credential element (like for "secret" ) - String text = reader.getText(); - if (text == null || text.trim().isEmpty()) { - continue; - } - textValue = text; - } else if (next == START_ELEMENT) { - String key = reader.getLocalName(); - reader.next(); - String value = reader.getText(); - reader.next(); - - values.put(key, value); - } else if (next == END_ELEMENT) { - break; - } - } - - if (textValue != null) { - ModelNode addCredential = getCredentialToAdd(parent, name, textValue); - credentialsToAdd.add(addCredential); - } else { - for (Map.Entry entry : values.entrySet()) { - ModelNode addCredential = getCredentialToAdd(parent, name + "." + entry.getKey(), entry.getValue()); - credentialsToAdd.add(addCredential); - } - } - } - - public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List rewriteRuleToToAdd) throws XMLStreamException { - String name = readNameAttribute(reader); - - Map values = new HashMap<>(); - String textValue = null; - while (reader.hasNext()) { - int next = reader.next(); - if (next == CHARACTERS) { - // text value of redirect rule element - String text = reader.getText(); - if (text == null || text.trim().isEmpty()) { - continue; - } - textValue = text; - } else if (next == START_ELEMENT) { - String key = reader.getLocalName(); - reader.next(); - String value = reader.getText(); - reader.next(); - - values.put(key, value); - } else if (next == END_ELEMENT) { - break; - } - } - - if (textValue != null) { - ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue); - rewriteRuleToToAdd.add(addRedirectRule); - } else { - for (Map.Entry entry : values.entrySet()) { - ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue()); - rewriteRuleToToAdd.add(addRedirectRule); - } - } - } - - private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) { - ModelNode addCredential = new ModelNode(); - addCredential.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); - PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(CredentialDefinition.TAG_NAME, name)); - addCredential.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - addCredential.get(CredentialDefinition.VALUE.getName()).set(value); - return addCredential; - } - - private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) { - ModelNode addRedirectRule = new ModelNode(); - addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); - PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name)); - addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value); - return addRedirectRule; - } - - // expects that the current tag will have one single attribute called "name" - private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException { - String name = null; - for (int i = 0; i < reader.getAttributeCount(); i++) { - String attr = reader.getAttributeLocalName(i); - if (attr.equals("name")) { - name = reader.getAttributeValue(i); - continue; - } - throw ParseUtils.unexpectedAttribute(reader, i); - } - if (name == null) { - throw ParseUtils.missingRequired(reader, Collections.singleton("name")); - } - return name; - } - - /** - * {@inheritDoc} - */ - @Override - public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException { - context.startSubsystemElement(KeycloakExtension.CURRENT_NAMESPACE, false); - writeRealms(writer, context); - writeSecureDeployments(writer, context); - writeSecureServers(writer, context); - writer.writeEndElement(); - } - - private void writeRealms(XMLExtendedStreamWriter writer, SubsystemMarshallingContext context) throws XMLStreamException { - if (!context.getModelNode().get(RealmDefinition.TAG_NAME).isDefined()) { - return; - } - for (Property realm : context.getModelNode().get(RealmDefinition.TAG_NAME).asPropertyList()) { - writer.writeStartElement(RealmDefinition.TAG_NAME); - writer.writeAttribute("name", realm.getName()); - ModelNode realmElements = realm.getValue(); - for (AttributeDefinition element : RealmDefinition.ALL_ATTRIBUTES) { - element.marshallAsElement(realmElements, writer); - } - - writer.writeEndElement(); - } - } - - private void writeSecureDeployments(XMLExtendedStreamWriter writer, SubsystemMarshallingContext context) throws XMLStreamException { - writeSecureResource(SecureDeploymentDefinition.TAG_NAME, SecureDeploymentDefinition.ALL_ATTRIBUTES, writer, context); - } - - private void writeSecureServers(XMLExtendedStreamWriter writer, SubsystemMarshallingContext context) throws XMLStreamException { - writeSecureResource(SecureServerDefinition.TAG_NAME, SecureServerDefinition.ALL_ATTRIBUTES, writer, context); - } - - private void writeSecureResource(String tagName, List attributes, XMLExtendedStreamWriter writer, SubsystemMarshallingContext context) throws XMLStreamException { - if (!context.getModelNode().get(tagName).isDefined()) { - return; - } - for (Property deployment : context.getModelNode().get(tagName).asPropertyList()) { - writer.writeStartElement(tagName); - writer.writeAttribute("name", deployment.getName()); - ModelNode deploymentElements = deployment.getValue(); - for (AttributeDefinition element : attributes) { - element.marshallAsElement(deploymentElements, writer); - } - - ModelNode credentials = deploymentElements.get(CredentialDefinition.TAG_NAME); - if (credentials.isDefined()) { - writeCredentials(writer, credentials); - } - - ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME); - if (redirectRewriteRule.isDefined()) { - writeRedirectRules(writer, redirectRewriteRule); - } - - writer.writeEndElement(); - } - } - - private void writeCredentials(XMLExtendedStreamWriter writer, ModelNode credentials) throws XMLStreamException { - Map parsed = new LinkedHashMap<>(); - for (Property credential : credentials.asPropertyList()) { - String credName = credential.getName(); - String credValue = credential.getValue().get(CredentialDefinition.VALUE.getName()).asString(); - - if (credName.contains(".")) { - String[] parts = credName.split("\\."); - String provider = parts[0]; - String propKey = parts[1]; - - Map currentProviderMap = (Map) parsed.get(provider); - if (currentProviderMap == null) { - currentProviderMap = new LinkedHashMap<>(); - parsed.put(provider, currentProviderMap); - } - currentProviderMap.put(propKey, credValue); - } else { - parsed.put(credName, credValue); - } - } - - for (Map.Entry entry : parsed.entrySet()) { - writer.writeStartElement(CredentialDefinition.TAG_NAME); - writer.writeAttribute("name", entry.getKey()); - - Object value = entry.getValue(); - if (value instanceof String) { - writeCharacters(writer, (String) value); - } else { - Map credentialProps = (Map) value; - for (Map.Entry prop : credentialProps.entrySet()) { - writer.writeStartElement(prop.getKey()); - writeCharacters(writer, prop.getValue()); - writer.writeEndElement(); - } - } - - writer.writeEndElement(); - } - } - - private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException { - Map parsed = new LinkedHashMap<>(); - for (Property redirectRule : redirectRules.asPropertyList()) { - String ruleName = redirectRule.getName(); - String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString(); - parsed.put(ruleName, ruleValue); - } - - for (Map.Entry entry : parsed.entrySet()) { - writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME); - writer.writeAttribute("name", entry.getKey()); - - Object value = entry.getValue(); - if (value instanceof String) { - writeCharacters(writer, (String) value); - } else { - Map redirectRulesProps = (Map) value; - for (Map.Entry prop : redirectRulesProps.entrySet()) { - writer.writeStartElement(prop.getKey()); - writeCharacters(writer, prop.getValue()); - writer.writeEndElement(); - } - } - - writer.writeEndElement(); - } - } - - // code taken from org.jboss.as.controller.AttributeMarshaller - private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException { - if (content.indexOf('\n') > -1) { - // Multiline content. Use the overloaded variant that staxmapper will format - writer.writeCharacters(content); - } else { - // Staxmapper will just output the chars without adding newlines if this is used - char[] chars = content.toCharArray(); - writer.writeCharacters(chars, 0, chars.length); - } - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmAddHandler.java deleted file mode 100755 index 0f59c4e18c81..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmAddHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractAddStepHandler; -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; - -/** - * Add a new realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public final class RealmAddHandler extends AbstractAddStepHandler { - - public static RealmAddHandler INSTANCE = new RealmAddHandler(); - - private RealmAddHandler() {} - - @Override - protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { - // TODO: localize exception. get id number - if (!operation.get(OP).asString().equals(ADD)) { - throw new OperationFailedException("Unexpected operation for add realm. operation=" + operation.toString()); - } - - for (AttributeDefinition attrib : RealmDefinition.ALL_ATTRIBUTES) { - attrib.validateAndSet(operation, model); - } - - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.addRealm(operation, context.resolveExpressions(model)); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmDefinition.java deleted file mode 100755 index 5ab7bf274c76..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmDefinition.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; -import org.jboss.as.controller.registry.ManagementResourceRegistration; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Defines attributes and operations for the Realm - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class RealmDefinition extends SimpleResourceDefinition { - - public static final String TAG_NAME = "realm"; - - - protected static final List REALM_ONLY_ATTRIBUTES = new ArrayList(); - static { - } - - protected static final List ALL_ATTRIBUTES = new ArrayList(); - static { - ALL_ATTRIBUTES.addAll(REALM_ONLY_ATTRIBUTES); - ALL_ATTRIBUTES.addAll(SharedAttributeDefinitons.ATTRIBUTES); - } - - private static final Map DEFINITION_LOOKUP = new HashMap(); - static { - for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { - DEFINITION_LOOKUP.put(def.getXmlName(), def); - } - } - - private static final RealmWriteAttributeHandler realmAttrHandler = new RealmWriteAttributeHandler(ALL_ATTRIBUTES.toArray(new SimpleAttributeDefinition[0])); - - public RealmDefinition() { - super(PathElement.pathElement("realm"), - KeycloakExtension.getResourceDescriptionResolver("realm"), - RealmAddHandler.INSTANCE, - RealmRemoveHandler.INSTANCE); - } - - @Override - public void registerOperations(ManagementResourceRegistration resourceRegistration) { - super.registerOperations(resourceRegistration); - resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); - } - - @Override - public void registerAttributes(ManagementResourceRegistration resourceRegistration) { - super.registerAttributes(resourceRegistration); - - for (AttributeDefinition attrDef : ALL_ATTRIBUTES) { - //TODO: use subclass of realmAttrHandler that can call RealmDefinition.validateTruststoreSetIfRequired - resourceRegistration.registerReadWriteAttribute(attrDef, null, realmAttrHandler); - } - } - - - public static SimpleAttributeDefinition lookup(String name) { - return DEFINITION_LOOKUP.get(name); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmRemoveHandler.java deleted file mode 100644 index 471ff8fd9184..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmRemoveHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractRemoveStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Remove a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public final class RealmRemoveHandler extends AbstractRemoveStepHandler { - - public static RealmRemoveHandler INSTANCE = new RealmRemoveHandler(); - - private RealmRemoveHandler() {} - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.removeRealm(operation); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmWriteAttributeHandler.java deleted file mode 100755 index ee8d78a256cc..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmWriteAttributeHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractWriteAttributeHandler; -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -/** - * Update an attribute on a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class RealmWriteAttributeHandler extends AbstractWriteAttributeHandler { - - public RealmWriteAttributeHandler(AttributeDefinition... definitions) { - super(definitions); - } - - @Override - protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode resolvedValue, ModelNode currentValue, HandbackHolder hh) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.updateRealm(operation, attributeName, resolvedValue); - - hh.setHandback(ckService); - - return false; - } - - @Override - protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { - ckService.updateRealm(operation, attributeName, valueToRestore); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java deleted file mode 100644 index a9095c7d8e3f..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; -import org.jboss.as.controller.SimpleResourceDefinition; -import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; -import org.jboss.as.controller.operations.validation.StringLengthValidator; -import org.jboss.as.controller.registry.ManagementResourceRegistration; -import org.jboss.dmr.ModelType; - -/** - * - * @author sblanc - */ -public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition { - - public static final String TAG_NAME = "redirect-rewrite-rule"; - - protected static final AttributeDefinition VALUE = - new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false) - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) - .build(); - - public RedirecRewritetRuleDefinition() { - super(PathElement.pathElement(TAG_NAME), - KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), - new RedirectRewriteRuleAddHandler(VALUE), - RedirectRewriteRuleRemoveHandler.INSTANCE); - } - - @Override - public void registerOperations(ManagementResourceRegistration resourceRegistration) { - super.registerOperations(resourceRegistration); - resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); - } - - @Override - public void registerAttributes(ManagementResourceRegistration resourceRegistration) { - super.registerAttributes(resourceRegistration); - resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler()); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java deleted file mode 100644 index 2fc25f7df737..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractAddStepHandler; -import org.jboss.as.controller.AttributeDefinition; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler { - - public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) { - super(attributes); - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model)); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java deleted file mode 100644 index 171e7555bcd0..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractWriteAttributeHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler { - - @Override - protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder hh) throws OperationFailedException { - - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue); - - hh.setHandback(ckService); - - return false; - } - - @Override - protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, - ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { - ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java deleted file mode 100644 index de17c9666e69..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.AbstractRemoveStepHandler; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.dmr.ModelNode; - -public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler { - - public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler(); - - private RedirectRewriteRuleRemoveHandler() {} - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); - ckService.removeRedirectRewriteRule(operation); - } - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentDefinition.java deleted file mode 100755 index 5a09b9a2f2c8..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentDefinition.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; - -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.as.controller.PathAddress; -import org.jboss.as.controller.capability.RuntimeCapability; -import org.jboss.dmr.ModelNode; -import org.jboss.msc.service.ServiceController; -import org.jboss.msc.service.ServiceName; -import org.jboss.msc.service.ServiceTarget; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; - -/** - * Defines attributes and operations for a secure-deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -final class SecureDeploymentDefinition extends AbstractAdapterConfigurationDefinition { - - static final String TAG_NAME = "secure-deployment"; - - public SecureDeploymentDefinition() { - super(TAG_NAME, ALL_ATTRIBUTES, new SecureDeploymentAddHandler(), new SecureDeploymentRemoveHandler(), new SecureDeploymentWriteAttributeHandler()); - } - - /** - * Add a deployment to a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ - static final class SecureDeploymentAddHandler extends AbstractAdapterConfigurationAddHandler { - - static final String HTTP_SERVER_AUTHENTICATION_CAPABILITY = "org.wildfly.security.http-server-mechanism-factory"; - static RuntimeCapability HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY; - - static { - try { - HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY = RuntimeCapability - .Builder.of(HTTP_SERVER_AUTHENTICATION_CAPABILITY, true, HttpServerAuthenticationMechanismFactory.class) - .build(); - } catch (NoClassDefFoundError ncfe) { - // ignore, Elytron not present thus no capability will be published by this resource definition - } - } - - SecureDeploymentAddHandler() { - super(HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY, ALL_ATTRIBUTES); - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.performRuntime(context, operation, model); - if (HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY != null) { - installCapability(context, operation); - } - } - - static void installCapability(OperationContext context, ModelNode operation) { - PathAddress pathAddress = PathAddress.pathAddress(operation.get(OP_ADDR)); - String factoryName = pathAddress.getLastElement().getValue(); - ServiceName serviceName = context.getCapabilityServiceName(HTTP_SERVER_AUTHENTICATION_CAPABILITY, factoryName, HttpServerAuthenticationMechanismFactory.class); - KeycloakHttpAuthenticationFactoryService service = new KeycloakHttpAuthenticationFactoryService(factoryName); - ServiceTarget serviceTarget = context.getServiceTarget(); - serviceTarget.addService(serviceName, service).setInitialMode(ServiceController.Mode.ACTIVE).install(); - } - } - - /** - * Remove a secure-deployment from a realm. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ - static final class SecureDeploymentRemoveHandler extends AbstractAdapterConfigurationRemoveHandler {} - - /** - * Update an attribute on a secure-deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ - static final class SecureDeploymentWriteAttributeHandler extends AbstractAdapterConfigurationWriteAttributeHandler { - SecureDeploymentWriteAttributeHandler() { - super(ALL_ATTRIBUTES); - } - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureServerDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureServerDefinition.java deleted file mode 100755 index 7d8fd05dfdbe..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureServerDefinition.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; -import static org.keycloak.subsystem.adapter.extension.KeycloakHttpServerAuthenticationMechanismFactoryDefinition.KeycloakHttpServerAuthenticationMechanismFactoryAddHandler.HTTP_SERVER_AUTHENTICATION_CAPABILITY; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import io.undertow.io.IoCallback; -import io.undertow.io.Sender; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.resource.Resource; -import io.undertow.server.handlers.resource.ResourceChangeListener; -import io.undertow.server.handlers.resource.ResourceManager; -import io.undertow.util.ETag; -import io.undertow.util.MimeMappings; -import org.jboss.as.controller.OperationContext; -import org.jboss.as.controller.OperationFailedException; -import org.jboss.as.controller.PathAddress; -import org.jboss.as.controller.capability.RuntimeCapability; -import org.jboss.as.server.mgmt.domain.ExtensibleHttpManagement; -import org.jboss.dmr.ModelNode; -import org.jboss.msc.service.Service; -import org.jboss.msc.service.ServiceController.Mode; -import org.jboss.msc.service.ServiceName; -import org.jboss.msc.service.ServiceTarget; -import org.jboss.msc.service.StartContext; -import org.jboss.msc.service.StartException; -import org.jboss.msc.service.StopContext; -import org.jboss.msc.value.InjectedValue; -import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; - -/** - * Defines attributes and operations for a secure-deployment. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -final class SecureServerDefinition extends AbstractAdapterConfigurationDefinition { - - public static final String TAG_NAME = "secure-server"; - - SecureServerDefinition() { - super(TAG_NAME, ALL_ATTRIBUTES, new SecureServerAddHandler(), new SecureServerRemoveHandler(), new SecureServerWriteHandler()); - } - - /** - * A {@link AbstractAdapterConfigurationAddHandler} that exposes a {@link SecureServerDefinition} - * as a capability through the installation of a {@link KeycloakHttpAuthenticationFactoryService}. - * - * @author Pedro Igor - */ - static final class SecureServerAddHandler extends AbstractAdapterConfigurationAddHandler { - - static final String HTTP_SERVER_AUTHENTICATION_CAPABILITY = "org.wildfly.security.http-server-mechanism-factory"; - static final String HTTP_MANAGEMENT_HTTP_EXTENSIBLE_CAPABILITY = "org.wildfly.management.http.extensible"; - static RuntimeCapability HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY; - - static { - try { - HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY = RuntimeCapability - .Builder.of(HTTP_SERVER_AUTHENTICATION_CAPABILITY, true, HttpServerAuthenticationMechanismFactory.class) - .build(); - } catch (NoClassDefFoundError ncfe) { - // ignore, Elytron not present thus no capability will be published by this resource definition - } - } - - SecureServerAddHandler() { - super(HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY, ALL_ATTRIBUTES); - } - - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.performRuntime(context, operation, model); - if (HTTP_SERVER_AUTHENTICATION_RUNTIME_CAPABILITY != null) { - installCapability(context, operation); - } - } - - static void installCapability(OperationContext context, ModelNode operation) throws OperationFailedException { - PathAddress pathAddress = PathAddress.pathAddress(operation.get(OP_ADDR)); - String factoryName = pathAddress.getLastElement().getValue(); - ServiceName serviceName = context.getCapabilityServiceName(HTTP_SERVER_AUTHENTICATION_CAPABILITY, factoryName, HttpServerAuthenticationMechanismFactory.class); - boolean publicClient = SecureServerDefinition.PUBLIC_CLIENT.resolveModelAttribute(context, operation).asBoolean(false); - - if (!publicClient) { - throw new OperationFailedException("Only public clients are allowed to have their configuration exposed through the management interface"); - } - - KeycloakHttpAuthenticationFactoryService service = new KeycloakHttpAuthenticationFactoryService(factoryName); - ServiceTarget serviceTarget = context.getServiceTarget(); - InjectedValue injectedValue = new InjectedValue<>(); - serviceTarget.addService(serviceName.append("http-management-context"), createHttpManagementConfigContextService(factoryName, injectedValue)) - .addDependency(context.getCapabilityServiceName(HTTP_MANAGEMENT_HTTP_EXTENSIBLE_CAPABILITY, ExtensibleHttpManagement.class), ExtensibleHttpManagement.class, injectedValue).setInitialMode(Mode.ACTIVE).install(); - serviceTarget.addService(serviceName, service).setInitialMode(Mode.ACTIVE).install(); - } - } - - /** - * A {@link AbstractAdapterConfigurationRemoveHandler} that handles the removal of {@link SecureServerDefinition}. - * - * @author Pedro Igor - */ - static final class SecureServerRemoveHandler extends AbstractAdapterConfigurationRemoveHandler { - @Override - protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.performRuntime(context, operation, model); - PathAddress pathAddress = PathAddress.pathAddress(operation.get(OP_ADDR)); - String factoryName = pathAddress.getLastElement().getValue(); - ServiceName serviceName = context.getCapabilityServiceName(HTTP_SERVER_AUTHENTICATION_CAPABILITY, factoryName, HttpServerAuthenticationMechanismFactory.class); - context.removeService(serviceName); - } - - @Override - protected void recoverServices(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { - super.recoverServices(context, operation, model); - SecureServerDefinition.SecureServerAddHandler.installCapability(context, operation); - } - } - - /** - * A {@link AbstractAdapterConfigurationWriteAttributeHandler} that updates attributes on a {@link SecureServerDefinition}. - * - * @author Pedro Igor - */ - static final class SecureServerWriteHandler extends AbstractAdapterConfigurationWriteAttributeHandler { - SecureServerWriteHandler() { - super(ALL_ATTRIBUTES); - } - } - - private static Service createHttpManagementConfigContextService(final String factoryName, final InjectedValue httpConfigContext) { - final String contextName = "/keycloak/adapter/" + factoryName + "/"; - return new Service() { - public void start(StartContext startContext) throws StartException { - ExtensibleHttpManagement extensibleHttpManagement = (ExtensibleHttpManagement)httpConfigContext.getValue(); - extensibleHttpManagement.addStaticContext(contextName, new ResourceManager() { - public Resource getResource(final String path) throws IOException { - KeycloakAdapterConfigService adapterConfigService = KeycloakAdapterConfigService.getInstance(); - final String config = adapterConfigService.getJSON(factoryName); - - if (config == null) { - return null; - } - - return new Resource() { - public String getPath() { - return null; - } - - public Date getLastModified() { - return null; - } - - public String getLastModifiedString() { - return null; - } - - public ETag getETag() { - return null; - } - - public String getName() { - return null; - } - - public boolean isDirectory() { - return false; - } - - public List list() { - return Collections.emptyList(); - } - - public String getContentType(MimeMappings mimeMappings) { - return "application/json"; - } - - public void serve(Sender sender, HttpServerExchange exchange, IoCallback completionCallback) { - sender.send(config); - } - - public Long getContentLength() { - return Long.valueOf((long)config.length()); - } - - public String getCacheKey() { - return null; - } - - public File getFile() { - return null; - } - - public Path getFilePath() { - return null; - } - - public File getResourceManagerRoot() { - return null; - } - - public Path getResourceManagerRootPath() { - return null; - } - - public URL getUrl() { - return null; - } - }; - } - - public boolean isResourceChangeListenerSupported() { - return false; - } - - public void registerResourceChangeListener(ResourceChangeListener listener) { - } - - public void removeResourceChangeListener(ResourceChangeListener listener) { - } - - public void close() throws IOException { - } - }); - } - - public void stop(StopContext stopContext) { - ((ExtensibleHttpManagement)httpConfigContext.getValue()).removeContext(contextName); - } - - public Void getValue() throws IllegalStateException, IllegalArgumentException { - return null; - } - }; - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java deleted file mode 100755 index ab75a5a7cca6..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.jboss.as.controller.SimpleAttributeDefinition; -import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; -import org.jboss.as.controller.operations.validation.IntRangeValidator; -import org.jboss.as.controller.operations.validation.LongRangeValidator; -import org.jboss.as.controller.operations.validation.StringLengthValidator; -import org.jboss.dmr.ModelNode; -import org.jboss.dmr.ModelType; - -import java.util.ArrayList; -import java.util.List; - -/** - * Defines attributes that can be present in both a realm and an application (secure-deployment). - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -public class SharedAttributeDefinitons { - - protected static final SimpleAttributeDefinition REALM_PUBLIC_KEY = - new SimpleAttributeDefinitionBuilder("realm-public-key", ModelType.STRING, true) - .setXmlName("realm-public-key") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition AUTH_SERVER_URL = - new SimpleAttributeDefinitionBuilder("auth-server-url", ModelType.STRING, true) - .setXmlName("auth-server-url") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition SSL_REQUIRED = - new SimpleAttributeDefinitionBuilder("ssl-required", ModelType.STRING, true) - .setXmlName("ssl-required") - .setAllowExpression(true) - .setDefaultValue(new ModelNode("external")) - .build(); - protected static final SimpleAttributeDefinition ALLOW_ANY_HOSTNAME = - new SimpleAttributeDefinitionBuilder("allow-any-hostname", ModelType.BOOLEAN, true) - .setXmlName("allow-any-hostname") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition DISABLE_TRUST_MANAGER = - new SimpleAttributeDefinitionBuilder("disable-trust-manager", ModelType.BOOLEAN, true) - .setXmlName("disable-trust-manager") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition TRUSTSTORE = - new SimpleAttributeDefinitionBuilder("truststore", ModelType.STRING, true) - .setXmlName("truststore") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition TRUSTSTORE_PASSWORD = - new SimpleAttributeDefinitionBuilder("truststore-password", ModelType.STRING, true) - .setXmlName("truststore-password") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CONNECTION_POOL_SIZE = - new SimpleAttributeDefinitionBuilder("connection-pool-size", ModelType.INT, true) - .setXmlName("connection-pool-size") - .setAllowExpression(true) - .setValidator(new IntRangeValidator(0, true)) - .build(); - protected static final SimpleAttributeDefinition SOCKET_TIMEOUT = - new SimpleAttributeDefinitionBuilder("socket-timeout-millis", ModelType.LONG, true) - .setXmlName("socket-timeout-millis") - .setAllowExpression(true) - .setValidator(new LongRangeValidator(-1L, true)) - .build(); - protected static final SimpleAttributeDefinition CONNECTION_TTL = - new SimpleAttributeDefinitionBuilder("connection-ttl-millis", ModelType.LONG, true) - .setXmlName("connection-ttl-millis") - .setAllowExpression(true) - .setValidator(new LongRangeValidator(-1L, true)) - .build(); - protected static final SimpleAttributeDefinition CONNECTION_TIMEOUT = - new SimpleAttributeDefinitionBuilder("connection-timeout-millis", ModelType.LONG, true) - .setXmlName("connection-timeout-millis") - .setAllowExpression(true) - .setValidator(new LongRangeValidator(-1L, true)) - .build(); - - protected static final SimpleAttributeDefinition ENABLE_CORS = - new SimpleAttributeDefinitionBuilder("enable-cors", ModelType.BOOLEAN, true) - .setXmlName("enable-cors") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition CLIENT_KEYSTORE = - new SimpleAttributeDefinitionBuilder("client-keystore", ModelType.STRING, true) - .setXmlName("client-keystore") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CLIENT_KEYSTORE_PASSWORD = - new SimpleAttributeDefinitionBuilder("client-keystore-password", ModelType.STRING, true) - .setXmlName("client-keystore-password") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CLIENT_KEY_PASSWORD = - new SimpleAttributeDefinitionBuilder("client-key-password", ModelType.STRING, true) - .setXmlName("client-key-password") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CORS_MAX_AGE = - new SimpleAttributeDefinitionBuilder("cors-max-age", ModelType.INT, true) - .setXmlName("cors-max-age") - .setAllowExpression(true) - .setValidator(new IntRangeValidator(-1, true)) - .build(); - protected static final SimpleAttributeDefinition CORS_ALLOWED_HEADERS = - new SimpleAttributeDefinitionBuilder("cors-allowed-headers", ModelType.STRING, true) - .setXmlName("cors-allowed-headers") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CORS_ALLOWED_METHODS = - new SimpleAttributeDefinitionBuilder("cors-allowed-methods", ModelType.STRING, true) - .setXmlName("cors-allowed-methods") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition CORS_EXPOSED_HEADERS = - new SimpleAttributeDefinitionBuilder("cors-exposed-headers", ModelType.STRING, true) - .setXmlName("cors-exposed-headers") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition EXPOSE_TOKEN = - new SimpleAttributeDefinitionBuilder("expose-token", ModelType.BOOLEAN, true) - .setXmlName("expose-token") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition AUTH_SERVER_URL_FOR_BACKEND_REQUESTS = - new SimpleAttributeDefinitionBuilder("auth-server-url-for-backend-requests", ModelType.STRING, true) - .setXmlName("auth-server-url-for-backend-requests") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition ALWAYS_REFRESH_TOKEN = - new SimpleAttributeDefinitionBuilder("always-refresh-token", ModelType.BOOLEAN, true) - .setXmlName("always-refresh-token") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition REGISTER_NODE_AT_STARTUP = - new SimpleAttributeDefinitionBuilder("register-node-at-startup", ModelType.BOOLEAN, true) - .setXmlName("register-node-at-startup") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - protected static final SimpleAttributeDefinition REGISTER_NODE_PERIOD = - new SimpleAttributeDefinitionBuilder("register-node-period", ModelType.INT, true) - .setXmlName("register-node-period") - .setAllowExpression(true) - .setValidator(new IntRangeValidator(-1, true)) - .build(); - protected static final SimpleAttributeDefinition TOKEN_STORE = - new SimpleAttributeDefinitionBuilder("token-store", ModelType.STRING, true) - .setXmlName("token-store") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition PRINCIPAL_ATTRIBUTE = - new SimpleAttributeDefinitionBuilder("principal-attribute", ModelType.STRING, true) - .setXmlName("principal-attribute") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - protected static final SimpleAttributeDefinition AUTODETECT_BEARER_ONLY = - new SimpleAttributeDefinitionBuilder("autodetect-bearer-only", ModelType.BOOLEAN, true) - .setXmlName("autodetect-bearer-only") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - - protected static final SimpleAttributeDefinition IGNORE_OAUTH_QUERY_PARAMETER = - new SimpleAttributeDefinitionBuilder("ignore-oauth-query-parameter", ModelType.BOOLEAN, true) - .setXmlName("ignore-oauth-query-parameter") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - - protected static final SimpleAttributeDefinition CONFIDENTIAL_PORT = - new SimpleAttributeDefinitionBuilder("confidential-port", ModelType.INT, true) - .setXmlName("confidential-port") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(8443)) - .build(); - - protected static final SimpleAttributeDefinition PROXY_URL = - new SimpleAttributeDefinitionBuilder("proxy-url", ModelType.STRING, true) - .setXmlName("proxy-url") - .setAllowExpression(true) - .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) - .build(); - - protected static final SimpleAttributeDefinition VERIFY_TOKEN_AUDIENCE = - new SimpleAttributeDefinitionBuilder("verify-token-audience", ModelType.BOOLEAN, true) - .setXmlName("verify-token-audience") - .setAllowExpression(true) - .setDefaultValue(new ModelNode(false)) - .build(); - - - protected static final List ATTRIBUTES = new ArrayList(); - static { - ATTRIBUTES.add(REALM_PUBLIC_KEY); - ATTRIBUTES.add(AUTH_SERVER_URL); - ATTRIBUTES.add(TRUSTSTORE); - ATTRIBUTES.add(TRUSTSTORE_PASSWORD); - ATTRIBUTES.add(SSL_REQUIRED); - ATTRIBUTES.add(CONFIDENTIAL_PORT); - ATTRIBUTES.add(ALLOW_ANY_HOSTNAME); - ATTRIBUTES.add(DISABLE_TRUST_MANAGER); - ATTRIBUTES.add(CONNECTION_POOL_SIZE); - ATTRIBUTES.add(SOCKET_TIMEOUT); - ATTRIBUTES.add(CONNECTION_TTL); - ATTRIBUTES.add(CONNECTION_TIMEOUT); - ATTRIBUTES.add(ENABLE_CORS); - ATTRIBUTES.add(CLIENT_KEYSTORE); - ATTRIBUTES.add(CLIENT_KEYSTORE_PASSWORD); - ATTRIBUTES.add(CLIENT_KEY_PASSWORD); - ATTRIBUTES.add(CORS_MAX_AGE); - ATTRIBUTES.add(CORS_ALLOWED_HEADERS); - ATTRIBUTES.add(CORS_ALLOWED_METHODS); - ATTRIBUTES.add(CORS_EXPOSED_HEADERS); - ATTRIBUTES.add(EXPOSE_TOKEN); - ATTRIBUTES.add(AUTH_SERVER_URL_FOR_BACKEND_REQUESTS); - ATTRIBUTES.add(ALWAYS_REFRESH_TOKEN); - ATTRIBUTES.add(REGISTER_NODE_AT_STARTUP); - ATTRIBUTES.add(REGISTER_NODE_PERIOD); - ATTRIBUTES.add(TOKEN_STORE); - ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE); - ATTRIBUTES.add(AUTODETECT_BEARER_ONLY); - ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER); - ATTRIBUTES.add(PROXY_URL); - ATTRIBUTES.add(VERIFY_TOKEN_AUDIENCE); - } - - private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) { - ModelNode attribute = attributes.get(def.getName()); - - if (def.getType() == ModelType.BOOLEAN) { - return attribute.isDefined() && attribute.asBoolean(); - } - - return attribute.isDefined() && !attribute.asString().isEmpty(); - } - - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakLogger.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakLogger.java deleted file mode 100755 index 7020ff5459fb..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakLogger.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.logging; - -import org.jboss.logging.BasicLogger; -import org.jboss.logging.Logger; -import org.jboss.logging.annotations.LogMessage; -import org.jboss.logging.annotations.Message; -import org.jboss.logging.annotations.MessageLogger; - -import static org.jboss.logging.Logger.Level.INFO; - -/** - * This interface to be fleshed out later when error messages are fully externalized. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. - */ -@MessageLogger(projectCode = "KEYCLOAK") -public interface KeycloakLogger extends BasicLogger { - - /** - * A logger with a category of the package name. - */ - KeycloakLogger ROOT_LOGGER = Logger.getMessageLogger(KeycloakLogger.class, "org.jboss.keycloak"); - - @LogMessage(level = INFO) - @Message(value = "Keycloak subsystem override for deployment %s") - void deploymentSecured(String deployment); - - -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakMessages.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakMessages.java deleted file mode 100755 index 00757edb9c6e..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakMessages.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.logging; - -import org.jboss.logging.Messages; -import org.jboss.logging.annotations.MessageBundle; - -/** - * This interface to be fleshed out later when error messages are fully externalized. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc. - */ -@MessageBundle(projectCode = "KEYCLOAK") -public interface KeycloakMessages { - - /** - * The messages - */ - KeycloakMessages MESSAGES = Messages.getBundle(KeycloakMessages.class); -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension deleted file mode 100644 index 67bfed970e9b..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2016 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. -# - -org.keycloak.subsystem.adapter.extension.KeycloakExtension diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties deleted file mode 100755 index c0ac12f7629c..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties +++ /dev/null @@ -1,168 +0,0 @@ -# -# Copyright 2016 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. -# - -keycloak.subsystem=Keycloak adapter subsystem -keycloak.subsystem.add=Operation Adds Keycloak adapter subsystem -keycloak.subsystem.remove=Operation removes Keycloak adapter subsystem -keycloak.subsystem.realm=A Keycloak realm. -keycloak.subsystem.secure-deployment=A deployment secured by Keycloak. -keycloak.subsystem.secure-server=A configuration exposed to the server. -keycloak.subsystem.http-server-mechanism-factory=A http-server-mechanism-factory exposed to the server. - -keycloak.realm=A Keycloak realm. -keycloak.realm.add=Add a realm definition to the subsystem. -keycloak.realm.remove=Remove a realm from the subsystem. -keycloak.realm.realm-public-key=Public key of the realm -keycloak.realm.auth-server-url=Base URL of the Realm Auth Server -keycloak.realm.disable-trust-manager=Adapter will not use a trust manager when making adapter HTTPS requests -keycloak.realm.ssl-required=Specify if SSL is required (valid values are all, external and none) -keycloak.realm.confidential-port=Specify the confidential port (SSL/TLS) used by the Realm Auth Server -keycloak.realm.allow-any-hostname=SSL Setting -keycloak.realm.truststore=Truststore used for adapter client HTTPS requests -keycloak.realm.truststore-password=Password of the Truststore -keycloak.realm.connection-pool-size=Connection pool size for the client used by the adapter -keycloak.realm.socket-timeout-millis=Timeout for socket waiting for data in milliseconds -keycloak.realm.connection-ttl-millis=Connection time to live in milliseconds -keycloak.realm.connection-timeout-millis=Timeout for establishing the connection with the remote host in milliseconds -keycloak.realm.enable-cors=Enable Keycloak CORS support -keycloak.realm.client-keystore=n/a -keycloak.realm.client-keystore-password=n/a -keycloak.realm.client-key-password=n/a -keycloak.realm.cors-max-age=CORS max-age header -keycloak.realm.cors-allowed-headers=CORS allowed headers -keycloak.realm.cors-allowed-methods=CORS allowed methods -keycloak.realm.cors-exposed-headers=CORS exposed headers -keycloak.realm.expose-token=Enable secure URL that exposes access token -keycloak.realm.auth-server-url-for-backend-requests=URL to use to make background calls to auth server -keycloak.realm.always-refresh-token=Refresh token on every single web request -keycloak.realm.register-node-at-startup=Cluster setting -keycloak.realm.register-node-period=how often to re-register node -keycloak.realm.token-store=cookie or session storage for auth session data -keycloak.realm.principal-attribute=token attribute to use to set Principal name -keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests -keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token -keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used. -keycloak.realm.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience - -keycloak.secure-deployment=A deployment secured by Keycloak -keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak -keycloak.secure-deployment.realm=Keycloak realm -keycloak.secure-deployment.remove=Remove a deployment to be secured by Keycloak -keycloak.secure-deployment.realm-public-key=Public key of the realm -keycloak.secure-deployment.auth-server-url=Base URL of the Realm Auth Server -keycloak.secure-deployment.disable-trust-manager=Adapter will not use a trust manager when making adapter HTTPS requests -keycloak.secure-deployment.ssl-required=Specify if SSL is required (valid values are all, external and none) -keycloak.secure-deployment.confidential-port=Specify the confidential port (SSL/TLS) used by the Realm Auth Server -keycloak.secure-deployment.allow-any-hostname=SSL Setting -keycloak.secure-deployment.truststore=Truststore used for adapter client HTTPS requests -keycloak.secure-deployment.truststore-password=Password of the Truststore -keycloak.secure-deployment.connection-pool-size=Connection pool size for the client used by the adapter -keycloak.secure-deployment.socket-timeout-millis=Timeout for socket waiting for data in milliseconds -keycloak.secure-deployment.connection-ttl-millis=Connection time to live in milliseconds -keycloak.secure-deployment.connection-timeout-millis=Timeout for establishing the connection with the remote host in milliseconds -keycloak.secure-deployment.resource=Application name -keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token -keycloak.secure-deployment.credentials=Adapter credentials -keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI -keycloak.secure-deployment.bearer-only=Bearer Token Auth only -keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication -keycloak.secure-deployment.public-client=Public client -keycloak.secure-deployment.enable-cors=Enable Keycloak CORS support -keycloak.secure-deployment.autodetect-bearer-only=autodetect bearer-only requests -keycloak.secure-deployment.client-keystore=n/a -keycloak.secure-deployment.client-keystore-password=n/a -keycloak.secure-deployment.client-key-password=n/a -keycloak.secure-deployment.cors-max-age=CORS max-age header -keycloak.secure-deployment.cors-allowed-headers=CORS allowed headers -keycloak.secure-deployment.cors-allowed-methods=CORS allowed methods -keycloak.secure-deployment.cors-exposed-headers=CORS exposed headers -keycloak.secure-deployment.expose-token=Enable secure URL that exposes access token -keycloak.secure-deployment.auth-server-url-for-backend-requests=URL to use to make background calls to auth server -keycloak.secure-deployment.always-refresh-token=Refresh token on every single web request -keycloak.secure-deployment.register-node-at-startup=Cluster setting -keycloak.secure-deployment.register-node-period=how often to re-register node -keycloak.secure-deployment.token-store=cookie or session storage for auth session data -keycloak.secure-deployment.principal-attribute=token attribute to use to set Principal name -keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off -keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less -keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds -keycloak.secure-deployment.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server -keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token -keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used. -keycloak.secure-deployment.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience -keycloak.secure-deployment.adapter-state-cookie-path=If set, defines the path used in cookies set by the adapter. Useful when deploying the application in the root context path. - -keycloak.secure-server=A deployment secured by Keycloak -keycloak.secure-server.add=Add a deployment to be secured by Keycloak -keycloak.secure-server.realm=Keycloak realm -keycloak.secure-server.remove=Remove a deployment to be secured by Keycloak -keycloak.secure-server.realm-public-key=Public key of the realm -keycloak.secure-server.auth-server-url=Base URL of the Realm Auth Server -keycloak.secure-server.disable-trust-manager=Adapter will not use a trust manager when making adapter HTTPS requests -keycloak.secure-server.ssl-required=Specify if SSL is required (valid values are all, external and none) -keycloak.secure-server.confidential-port=Specify the confidential port (SSL/TLS) used by the Realm Auth Server -keycloak.secure-server.allow-any-hostname=SSL Setting -keycloak.secure-server.truststore=Truststore used for adapter client HTTPS requests -keycloak.secure-server.truststore-password=Password of the Truststore -keycloak.secure-server.connection-pool-size=Connection pool size for the client used by the adapter -keycloak.secure-server.socket-timeout-millis=Timeout for socket waiting for data in milliseconds -keycloak.secure-server.connection-ttl-millis=Connection time to live in milliseconds -keycloak.secure-server.connection-timeout-millis=Timeout for establishing the connection with the remote host in milliseconds -keycloak.secure-server.resource=Application name -keycloak.secure-server.use-resource-role-mappings=Use resource level permissions from token -keycloak.secure-server.credentials=Adapter credentials -keycloak.secure-server.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI -keycloak.secure-server.bearer-only=Bearer Token Auth only -keycloak.secure-server.enable-basic-auth=Enable Basic Authentication -keycloak.secure-server.public-client=Public client -keycloak.secure-server.enable-cors=Enable Keycloak CORS support -keycloak.secure-server.autodetect-bearer-only=autodetect bearer-only requests -keycloak.secure-server.client-keystore=n/a -keycloak.secure-server.client-keystore-password=n/a -keycloak.secure-server.client-key-password=n/a -keycloak.secure-server.cors-max-age=CORS max-age header -keycloak.secure-server.cors-allowed-headers=CORS allowed headers -keycloak.secure-server.cors-allowed-methods=CORS allowed methods -keycloak.secure-server.cors-exposed-headers=CORS exposed headers -keycloak.secure-server.expose-token=Enable secure URL that exposes access token -keycloak.secure-server.auth-server-url-for-backend-requests=URL to use to make background calls to auth server -keycloak.secure-server.always-refresh-token=Refresh token on every single web request -keycloak.secure-server.register-node-at-startup=Cluster setting -keycloak.secure-server.register-node-period=how often to re-register node -keycloak.secure-server.token-store=cookie or session storage for auth session data -keycloak.secure-server.principal-attribute=token attribute to use to set Principal name -keycloak.secure-server.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off -keycloak.secure-server.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less -keycloak.secure-server.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds -keycloak.secure-server.public-key-cache-ttl=Maximum time the downloaded public keys are considered valid. When this time reach, the adapter is forced to download public keys from keycloak server -keycloak.secure-server.ignore-oauth-query-parameter=disable query parameter parsing for access_token -keycloak.secure-server.proxy-url=The URL for the HTTP proxy if one is used. -keycloak.secure-server.verify-token-audience=If true, then during bearer-only authentication, the adapter will verify if token contains this client name (resource) as an audience -keycloak.secure-server.adapter-state-cookie-path=If set, defines the path used in cookies set by the adapter. Useful when deploying the application in the root context path. - -keycloak.secure-deployment.credential=Credential value -keycloak.secure-server.credential=Credential value - -keycloak.credential=Credential -keycloak.credential.value=Credential value -keycloak.credential.add=Credential add -keycloak.credential.remove=Credential remove - -keycloak.redirect-rewrite-rule=redirect-rewrite-rule -keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value -keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add -keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove \ No newline at end of file diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd deleted file mode 100755 index 339499495eb9..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The name of the realm. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The name of the realm. - - - - - - - - - - - - - - - - - diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_2.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_2.xsd deleted file mode 100755 index dd8eefcea7fa..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_2.xsd +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The name of the realm. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The name of the realm. - - - - - - - - - - - - - - - - - diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-adapter.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-adapter.xml deleted file mode 100644 index d895973d0418..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-adapter.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - org.keycloak.keycloak-adapter-subsystem - - - diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/extension/SubsystemParsingTestCase.java b/adapters/oidc/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/extension/SubsystemParsingTestCase.java deleted file mode 100755 index 5fd33c920a30..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/extension/SubsystemParsingTestCase.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2016 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.subsystem.adapter.extension; - -import org.hamcrest.CoreMatchers; -import org.jboss.as.controller.PathAddress; -import org.jboss.as.controller.PathElement; -import org.jboss.as.controller.descriptions.ModelDescriptionConstants; -import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest; -import org.jboss.as.subsystem.test.KernelServices; -import org.jboss.dmr.ModelNode; -import org.junit.Assert; -import org.junit.Test; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Map; - -import static org.hamcrest.MatcherAssert.assertThat; - - -/** - * Tests all management expects for subsystem, parsing, marshaling, model definition and other - * Here is an example that allows you a fine grained controller over what is tested and how. So it can give you ideas what can be done and tested. - * If you have no need for advanced testing of subsystem you look at {@link SubsystemBaseParsingTestCase} that testes same stuff but most of the code - * is hidden inside of test harness - * - * @author Kabir Khan - * @author Tomaz Cerar - * @author Marko Strukelj - */ -public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest { - - public SubsystemParsingTestCase() { - super(KeycloakExtension.SUBSYSTEM_NAME, new KeycloakExtension()); - } - - @Test - public void testJson() throws Exception { - ModelNode node = new ModelNode(); - node.get("realm").set("demo"); - node.get("resource").set("customer-portal"); - node.get("realm-public-key").set("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"); - node.get("auth-url").set("http://localhost:8080/auth-server/rest/realms/demo/protocol/openid-connect/login"); - node.get("code-url").set("http://localhost:8080/auth-server/rest/realms/demo/protocol/openid-connect/access/codes"); - node.get("ssl-required").set("external"); - node.get("confidential-port").set(443); - node.get("expose-token").set(true); - - ModelNode jwtCredential = new ModelNode(); - jwtCredential.get("client-keystore-file").set("/tmp/keystore.jks"); - jwtCredential.get("client-keystore-password").set("changeit"); - ModelNode credential = new ModelNode(); - credential.get("jwt").set(jwtCredential); - node.get("credentials").set(credential); - - System.out.println("json=" + node.toJSONString(false)); - } - - @Test - public void testJsonFromSignedJWTCredentials() { - KeycloakAdapterConfigService service = KeycloakAdapterConfigService.getInstance(); - - PathAddress addr = PathAddress.pathAddress(PathElement.pathElement("subsystem", "keycloak"), PathElement.pathElement("secure-deployment", "foo")); - ModelNode deploymentOp = new ModelNode(); - deploymentOp.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - ModelNode deployment = new ModelNode(); - deployment.get("realm").set("demo"); - deployment.get("resource").set("customer-portal"); - service.addSecureDeployment(deploymentOp, deployment, false); - - addCredential(addr, service, "secret", "secret1"); - addCredential(addr, service, "jwt.client-keystore-file", "/tmp/foo.jks"); - addCredential(addr, service, "jwt.token-timeout", "10"); - } - - private void addCredential(PathAddress parent, KeycloakAdapterConfigService service, String key, String value) { - PathAddress credAddr = PathAddress.pathAddress(parent, PathElement.pathElement("credential", key)); - ModelNode credOp = new ModelNode(); - credOp.get(ModelDescriptionConstants.OP_ADDR).set(credAddr.toModelNode()); - ModelNode credential = new ModelNode(); - credential.get("value").set(value); - service.addCredential(credOp, credential); - } - - @Override - protected String getSubsystemXml() throws IOException { - return readResource("keycloak-1.2.xml"); - } - - @Override - protected String getSubsystemXsdPath() throws Exception { - return "schema/wildfly-keycloak_1_2.xsd"; - } - - @Override - protected String[] getSubsystemTemplatePaths() throws IOException { - return new String[]{ - "/subsystem-templates/keycloak-adapter.xml" - }; - } - - /** - * Checks if the subsystem is still capable of reading a configuration that uses version 1.1 of the schema. - * - * @throws Exception if an error occurs while running the test. - */ - @Test - public void testSubsystem1_1() throws Exception { - KernelServices servicesA = super.createKernelServicesBuilder(createAdditionalInitialization()) - .setSubsystemXml(readResource("keycloak-1.1.xml")).build(); - Assert.assertTrue("Subsystem boot failed!", servicesA.isSuccessfulBoot()); - ModelNode modelA = servicesA.readWholeModel(); - super.validateModel(modelA); - } - - /** - * Tests a subsystem configuration that contains a {@code redirect-rewrite-rule}, checking that the resulting JSON - * can be properly used to create an {@link AdapterConfig}. - * - * Added as part of the fix for {@code KEYCLOAK-18302}. - */ - @Test - public void testJsonFromRedirectRewriteRuleConfiguration() { - KeycloakAdapterConfigService service = KeycloakAdapterConfigService.getInstance(); - - // add a secure deployment with a redirect-rewrite-rule - PathAddress addr = PathAddress.pathAddress(PathElement.pathElement("subsystem", "keycloak"), PathElement.pathElement("secure-deployment", "foo")); - ModelNode deploymentOp = new ModelNode(); - deploymentOp.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - ModelNode deployment = new ModelNode(); - deployment.get("realm").set("demo"); - deployment.get("resource").set("customer-portal"); - service.addSecureDeployment(deploymentOp, deployment, false); - this.addRedirectRewriteRule(addr, service, "^/wsmaster/api/(.*)$", "api/$1"); - - // get the subsystem config as JSON - String jsonConfig = service.getJSON("foo"); - - // attempt to create an adapter config instance from the subsystem JSON config - AdapterConfig config = KeycloakDeploymentBuilder.loadAdapterConfig(new ByteArrayInputStream(jsonConfig.getBytes())); - Assert.assertNotNull(config); - - // assert that the config has the configured rule - Map redirectRewriteRules = config.getRedirectRewriteRules(); - Assert.assertNotNull(redirectRewriteRules); - Map.Entry entry = redirectRewriteRules.entrySet().iterator().next(); - Assert.assertEquals("^/wsmaster/api/(.*)$", entry.getKey()); - Assert.assertEquals("api/$1", entry.getValue()); - } - - @Test - public void testJsonHttpClientAttributes() { - KeycloakAdapterConfigService service = KeycloakAdapterConfigService.getInstance(); - - // add a secure deployment - PathAddress addr = PathAddress.pathAddress(PathElement.pathElement("subsystem", "keycloak"), PathElement.pathElement("secure-deployment", "foo")); - ModelNode deploymentOp = new ModelNode(); - deploymentOp.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); - - ModelNode deployment = new ModelNode(); - deployment.get("realm").set("demo"); - deployment.get("resource").set("customer-portal"); - - deployment.get(SharedAttributeDefinitons.SOCKET_TIMEOUT.getName()).set(3000L); - deployment.get(SharedAttributeDefinitons.CONNECTION_TIMEOUT.getName()).set(5000L); - deployment.get(SharedAttributeDefinitons.CONNECTION_TTL.getName()).set(1000L); - - service.addSecureDeployment(deploymentOp, deployment, false); - - // get the subsystem config as JSON - String jsonConfig = service.getJSON("foo"); - - // attempt to create an adapter config instance from the subsystem JSON config - AdapterConfig config = KeycloakDeploymentBuilder.loadAdapterConfig(new ByteArrayInputStream(jsonConfig.getBytes())); - assertThat(config, CoreMatchers.notNullValue()); - - assertThat(config.getSocketTimeout(), CoreMatchers.notNullValue()); - assertThat(config.getSocketTimeout(), CoreMatchers.is(3000L)); - - assertThat(config.getConnectionTimeout(), CoreMatchers.notNullValue()); - assertThat(config.getConnectionTimeout(), CoreMatchers.is(5000L)); - - assertThat(config.getConnectionTTL(), CoreMatchers.notNullValue()); - assertThat(config.getConnectionTTL(), CoreMatchers.is(1000L)); - } - - private void addRedirectRewriteRule(PathAddress parent, KeycloakAdapterConfigService service, String key, String value) { - PathAddress redirectRewriteAddr = PathAddress.pathAddress(parent, PathElement.pathElement("redirect-rewrite-rule", key)); - ModelNode redirectRewriteOp = new ModelNode(); - redirectRewriteOp.get(ModelDescriptionConstants.OP_ADDR).set(redirectRewriteAddr.toModelNode()); - ModelNode rule = new ModelNode(); - rule.get("value").set(value); - service.addRedirectRewriteRule(redirectRewriteOp, rule); - } -} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml deleted file mode 100755 index 8810c99983d9..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - http://localhost:8080/auth - truststore.jks - secret - EXTERNAL - 443 - false - true - 20 - true - keys.jks - secret - secret - 600 - X-Custom - PUT,POST,DELETE,GET - false - http://127.0.0.2:8080/auth - false - true - 60 - session - sub - http://localhost:9000 - - - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqKoq+a9MgXepmsPJDmo45qswuChW9pWjanX68oIBuI4hGvhQxFHryCow230A+sr7tFdMQMt8f1l/ysmV/fYAuW29WaoY4kI4Ou1yYPuwywKSsxT6PooTs83hKyZ1h4LZMj5DkLGDDDyVRHob2WmPaYg9RGVRw3iGGsD/p+Yb+L/gnBYQnZZ7lYqmN7h36p5CkzzlgXQA1Ha8sQxL+rJNH8+sZm0vBrKsoII3Of7TqHGsm1RwFV3XCuGJ7S61AbjJMXL5DQgJl9Z5scvxGAyoRLKC294UgMnQdzyBTMPw2GybxkRKmiK2KjQKmcopmrJp/Bt6fBR6ZkGSs9qUlxGHgwIDAQAB - http://localhost:8180/auth - - - master - web-console - true - false - 10 - 20 - 3600 - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - - http://localhost:8080/auth - EXTERNAL - 443 - http://localhost:9000 - true - 0aa31d98-e0aa-404c-b6e0-e771dba1e798 - api/$1/ - - - master - http-endpoint - true - / - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - - http://localhost:8080/auth - EXTERNAL - - /tmp/keystore.jks - - /api/$1/ - - - jboss-infra - wildfly-management - true - EXTERNAL - preferred_username - - - jboss-infra - wildfly-console - true - / - EXTERNAL - 443 - http://localhost:9000 - - \ No newline at end of file diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.2.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.2.xml deleted file mode 100755 index 5e6356df7b3c..000000000000 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.2.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - http://localhost:8080/auth - truststore.jks - secret - EXTERNAL - 443 - false - true - 20 - 2000 - 5000 - 3000 - true - keys.jks - secret - secret - 600 - X-Custom - PUT,POST,DELETE,GET - false - http://127.0.0.2:8080/auth - false - true - 60 - session - sub - http://localhost:9000 - - - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqKoq+a9MgXepmsPJDmo45qswuChW9pWjanX68oIBuI4hGvhQxFHryCow230A+sr7tFdMQMt8f1l/ysmV/fYAuW29WaoY4kI4Ou1yYPuwywKSsxT6PooTs83hKyZ1h4LZMj5DkLGDDDyVRHob2WmPaYg9RGVRw3iGGsD/p+Yb+L/gnBYQnZZ7lYqmN7h36p5CkzzlgXQA1Ha8sQxL+rJNH8+sZm0vBrKsoII3Of7TqHGsm1RwFV3XCuGJ7S61AbjJMXL5DQgJl9Z5scvxGAyoRLKC294UgMnQdzyBTMPw2GybxkRKmiK2KjQKmcopmrJp/Bt6fBR6ZkGSs9qUlxGHgwIDAQAB - http://localhost:8180/auth - - - master - web-console - true - false - 10 - 20 - 3600 - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - - http://localhost:8080/auth - EXTERNAL - 443 - http://localhost:9000 - true - 0aa31d98-e0aa-404c-b6e0-e771dba1e798 - api/$1/ - - - master - http-endpoint - true - / - - MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB - - http://localhost:8080/auth - EXTERNAL - - /tmp/keystore.jks - - /api/$1/ - - - jboss-infra - wildfly-management - true - EXTERNAL - 10000 - 40000 - 50000 - preferred_username - - - jboss-infra - wildfly-console - true - / - EXTERNAL - 443 - http://localhost:9000 - - \ No newline at end of file diff --git a/adapters/pom.xml b/adapters/pom.xml index fac0534171c2..a82799bcd314 100755 --- a/adapters/pom.xml +++ b/adapters/pom.xml @@ -33,6 +33,5 @@ spi saml - oidc diff --git a/adapters/saml/core-jakarta/pom.xml b/adapters/saml/core-jakarta/pom.xml deleted file mode 100644 index 2b4f82342130..000000000000 --- a/adapters/saml/core-jakarta/pom.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-saml-adapter-core-jakarta - Keycloak SAML Client Adapter Core Jakarta - - - ${maven.build.timestamp} - yyyy-MM-dd HH:mm - - ${project.basedir}/../core/src - ${project.basedir}/src - - - - - org.keycloak - keycloak-saml-core-public - provided - - - org.keycloak - keycloak-saml-core - provided - - - org.keycloak - keycloak-adapter-spi - provided - - - org.keycloak - keycloak-saml-adapter-api-public - provided - - - org.keycloak - keycloak-common - provided - - - org.keycloak - keycloak-crypto-default - test - - - org.jboss.logging - jboss-logging - provided - - - junit - junit - test - - - org.hamcrest - hamcrest - test - - - org.apache.httpcomponents - httpclient - provided - - - - - - - maven-antrun-plugin - 3.1.0 - - - transform - initialize - - run - - - - - - - - - - - - - - - - - - - org.eclipse.transformer - org.eclipse.transformer.cli - 0.2.0 - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - - - - - \ No newline at end of file diff --git a/adapters/saml/core-public/pom.xml b/adapters/saml/core-public/pom.xml index a5e4e0aa3f9b..94d354283e18 100755 --- a/adapters/saml/core-public/pom.xml +++ b/adapters/saml/core-public/pom.xml @@ -32,6 +32,8 @@ + + 11 ${maven.build.timestamp} yyyy-MM-dd HH:mm diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml index d72de7b0ee37..0abc99412056 100755 --- a/adapters/saml/core/pom.xml +++ b/adapters/saml/core/pom.xml @@ -62,15 +62,25 @@ provided - org.keycloak - keycloak-crypto-default - test + org.bouncycastle + bcprov-jdk18on + provided + + + org.bouncycastle + bcpkix-jdk18on + provided org.jboss.logging jboss-logging provided + + org.jboss.logging + commons-logging-jboss-logging + provided + junit junit diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java index 248f4c30eb8a..adaecdcba0b7 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java @@ -40,10 +40,16 @@ public abstract class AbstractInitiateLogin implements AuthChallenge { protected SamlDeployment deployment; protected SamlSessionStore sessionStore; + protected boolean saveRequestUri; public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { + this(deployment, sessionStore, true); + } + + public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore, boolean saveRequestUri) { this.deployment = deployment; this.sessionStore = sessionStore; + this.saveRequestUri = saveRequestUri; } @Override @@ -56,7 +62,9 @@ public boolean challenge(HttpFacade httpFacade) { try { SAML2AuthnRequestBuilder authnRequestBuilder = buildSaml2AuthnRequestBuilder(deployment); BaseSAML2BindingBuilder binding = createSaml2Binding(deployment); - sessionStore.saveRequest(); + if (saveRequestUri) { + sessionStore.saveRequest(); + } sendAuthnRequest(httpFacade, authnRequestBuilder, binding); sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN); diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java index 3646ed455411..9901bf23bcfe 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java @@ -25,4 +25,5 @@ public class AdapterConstants { public static final String AUTH_DATA_PARAM_NAME="org.keycloak.saml.xml.adapterConfig"; public static final String REPLICATION_CONFIG_CONTAINER_PARAM_NAME = "org.keycloak.saml.replication.container"; public static final String REPLICATION_CONFIG_SSO_CACHE_PARAM_NAME = "org.keycloak.saml.replication.cache.sso"; + public static final String AUTHENTICATION_EXPIRED_MESSAGE = "authentication_expired"; } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java index 8848598d770e..3a76a1743e6a 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java @@ -60,7 +60,7 @@ * The {@code properties} file can contain both roles and principals as keys, and a list of zero or more roles separated by comma * as values. When the {@code {@link #map(String, Set)}} method is called, the implementation iterates through the set of roles * that were extracted from the assertion and checks, for eache role, if a mapping exists. If the role maps to an empty role, - * it is discarded. If it maps to a set of one ore more different roles, then these roles are set in the result set. If no + * it is discarded. If it maps to a set of one or more different roles, then these roles are set in the result set. If no * mapping is found for the role then it is included as is in the result set. * * Once the roles have been processed, the implementation checks if the principal extracted from the assertion contains an entry diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java index 5aeee06d26b8..e48b47621d53 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java @@ -94,19 +94,19 @@ public interface IDP { public interface SingleSignOnService { /** * Returns {@code true} if the requests to IdP need to be signed by SP key. - * @return see dscription + * @return see description */ boolean signRequest(); /** * Returns {@code true} if the complete response message from IdP should * be checked for valid signature. - * @return see dscription + * @return see description */ boolean validateResponseSignature(); /** * Returns {@code true} if individual assertions in response from IdP should * be checked for valid signature. - * @return see dscription + * @return see description */ boolean validateAssertionSignature(); Binding getRequestBinding(); diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/PemUtils.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/PemUtils.java new file mode 100644 index 000000000000..8b9b0f3cec96 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/PemUtils.java @@ -0,0 +1,162 @@ +/* + * 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.adapters.saml.config; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jboss.logging.Logger; +import org.keycloak.common.crypto.CryptoConstants; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.PemException; + +/** + * Fork of the PemUtils from common module to avoid dependency on keycloak-crypto-default + */ +public class PemUtils { + + private static final Logger log = Logger.getLogger(PemUtils.class); + + static { + Provider existingBc = Security.getProvider(CryptoConstants.BC_PROVIDER_ID); + Provider bcProvider = existingBc == null ? new BouncyCastleProvider() : existingBc; + + if (existingBc == null) { + Security.addProvider(bcProvider); + log.debugv("Loaded {0} security provider", bcProvider.getClass().getName()); + } else { + log.debugv("Security provider {0} already loaded", bcProvider.getClass().getName()); + } + } + + /** + * Decode a X509 Certificate from a PEM string + * + * @param cert + * @return + * @throws Exception + */ + public static X509Certificate decodeCertificate(String cert) { + if (cert == null) { + return null; + } + + try { + byte[] der = pemToDer(cert); + ByteArrayInputStream bis = new ByteArrayInputStream(der); + return decodeCertificate(bis); + } catch (Exception e) { + throw new PemException(e); + } + } + + + /** + * Decode a Public Key from a PEM string + * + * @param pem + * @return + * @throws Exception + */ + public static PublicKey decodePublicKey(String pem) { + if (pem == null) { + return null; + } + + try { + byte[] der = pemToDer(pem); + return decodePublicKey(der, "RSA"); + } catch (Exception e) { + throw new PemException(e); + } + } + + /** + * Decode a Private Key from a PEM string + * + * @param pem + * @return + * @throws Exception + */ + public static PrivateKey decodePrivateKey(String pem){ + if (pem == null) { + return null; + } + + try { + byte[] der = pemToDer(pem); + return decodePrivateKey(der); + } catch (Exception e) { + throw new PemException(e); + } + } + + private static byte[] pemToDer(String pem) { + try { + pem = removeBeginEnd(pem); + return Base64.decode(pem); + } catch (IOException ioe) { + throw new PemException(ioe); + } + } + + private static String removeBeginEnd(String pem) { + pem = pem.replaceAll("-----BEGIN (.*)-----", ""); + pem = pem.replaceAll("-----END (.*)----", ""); + pem = pem.replaceAll("\r\n", ""); + pem = pem.replaceAll("\n", ""); + return pem.trim(); + } + + private static PrivateKey decodePrivateKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + PKCS8EncodedKeySpec spec = + new PKCS8EncodedKeySpec(der); + KeyFactory kf = KeyFactory.getInstance("RSA", CryptoConstants.BC_PROVIDER_ID); + return kf.generatePrivate(spec); + } + + private static X509Certificate decodeCertificate(InputStream is) throws Exception { + CertificateFactory cf = CertificateFactory.getInstance("X.509", CryptoConstants.BC_PROVIDER_ID); + X509Certificate cert = (X509Certificate) cf.generateCertificate(is); + is.close(); + return cert; + } + + private static PublicKey decodePublicKey(byte[] der, String type) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + X509EncodedKeySpec spec = + new X509EncodedKeySpec(der); + KeyFactory kf = KeyFactory.getInstance("RSA", CryptoConstants.BC_PROVIDER_ID); + return kf.generatePublic(spec); + } +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java index 27336b5cd896..298d0b2b8d0f 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java @@ -24,10 +24,9 @@ import org.keycloak.adapters.saml.config.IDP; import org.keycloak.adapters.saml.config.Key; import org.keycloak.adapters.saml.config.KeycloakSamlAdapter; +import org.keycloak.adapters.saml.config.PemUtils; import org.keycloak.adapters.saml.config.SP; -import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.enums.SslRequired; -import org.keycloak.common.util.PemUtils; import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.common.exceptions.ParsingException; @@ -58,7 +57,6 @@ public class DeploymentBuilder { protected static Logger log = Logger.getLogger(DeploymentBuilder.class); public SamlDeployment build(InputStream xml, ResourceLoader resourceLoader) throws ParsingException { - CryptoIntegration.init(DeploymentBuilder.class.getClassLoader()); DefaultSamlDeployment deployment = new DefaultSamlDeployment(); DefaultSamlDeployment.DefaultIDP defaultIDP = new DefaultSamlDeployment.DefaultIDP(); DefaultSamlDeployment.DefaultSingleSignOnService sso = new DefaultSamlDeployment.DefaultSingleSignOnService(); diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java index d6b59f5a12b1..ed9767ca3832 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java @@ -19,6 +19,7 @@ import org.keycloak.adapters.saml.config.Key; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.util.StaxParserUtil; @@ -60,20 +61,24 @@ protected void processSubElement(XMLEventReader xmlEventReader, Key target, Keyc case CERTIFICATE_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setCertificatePem(StringPropertyReplacer.replaceProperties(value)); + target.setCertificatePem(replaceProperties(value)); break; case PUBLIC_KEY_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setPublicKeyPem(StringPropertyReplacer.replaceProperties(value)); + target.setPublicKeyPem(replaceProperties(value)); break; case PRIVATE_KEY_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setPrivateKeyPem(StringPropertyReplacer.replaceProperties(value)); + target.setPrivateKeyPem(replaceProperties(value)); break; } } + + private String replaceProperties(String value) { + return StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty); + } } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java index ab5463473cbd..585545224f3f 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -32,6 +33,7 @@ import javax.xml.namespace.QName; import org.jboss.logging.Logger; import org.keycloak.adapters.saml.AbstractInitiateLogin; +import org.keycloak.adapters.saml.AdapterConstants; import org.keycloak.adapters.saml.OnSessionCreated; import org.keycloak.adapters.saml.SamlAuthenticationError; import org.keycloak.adapters.saml.SamlDeployment; @@ -148,7 +150,7 @@ public AuthOutcome doHandle(SamlInvocationContext context, OnSessionCreated onCr log.debug("AUTHENTICATED: was cached"); return handleRequest(); } - return initiateLogin(); + return initiateLogin(true); } protected AuthOutcome handleRequest() { @@ -361,7 +363,10 @@ protected AuthOutcome handleLoginResponse(SAMLDocumentHolder responseHolder, boo final ResponseType responseType = (ResponseType) responseHolder.getSamlObject(); AssertionType assertion = null; - if (! isSuccessfulSamlResponse(responseType) || responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) { + if (isRetrayableSamlResponse(responseType)) { + // initiate the login but do not save the request cos it's /saml + return initiateLogin(false); + } else if (!isSuccessfulSamlResponse(responseType) || responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) { return failed(createAuthChallenge403(responseType)); } try { @@ -378,7 +383,8 @@ protected AuthOutcome handleLoginResponse(SAMLDocumentHolder responseHolder, boo // warning has been already emitted in DeploymentBuilder } if (! cvb.build().isValid()) { - return initiateLogin(); + // initiate the login but do not save the request cos it's /saml + return initiateLogin(false); } } catch (Exception e) { log.error("Error extracting SAML assertion: " + e.getMessage()); @@ -523,6 +529,21 @@ private boolean isSuccessfulSamlResponse(ResponseType responseType) { && Objects.equals(responseType.getStatus().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get()); } + private boolean isRetrayableSamlResponse(ResponseType responseType) { + if (responseType == null || responseType.getStatus() == null) { + return false; + } + + StatusType status = responseType.getStatus(); + return status.getStatusCode() != null + && AdapterConstants.AUTHENTICATION_EXPIRED_MESSAGE.equals(status.getStatusMessage()) + && status.getStatusCode().getValue() != null + && Objects.equals(status.getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) + && status.getStatusCode().getStatusCode() != null + && status.getStatusCode().getStatusCode().getValue() != null + && Objects.equals(status.getStatusCode().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_AUTHNFAILED.get()); + } + private Element getAssertionFromResponse(final SAMLDocumentHolder responseHolder) throws ConfigurationException, ProcessingException { Element encryptedAssertion = DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get())); if (encryptedAssertion != null) { @@ -601,13 +622,13 @@ protected SAMLDocumentHolder extractPostBindingResponse(String response) { } - protected AuthOutcome initiateLogin() { - challenge = createChallenge(); + protected AuthOutcome initiateLogin(boolean saveRequestUri) { + challenge = createChallenge(saveRequestUri); return AuthOutcome.NOT_ATTEMPTED; } - protected AbstractInitiateLogin createChallenge() { - return new AbstractInitiateLogin(deployment, sessionStore) { + protected AbstractInitiateLogin createChallenge(boolean saveRequestUri) { + return new AbstractInitiateLogin(deployment, sessionStore, saveRequestUri) { @Override protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException { if (isAutodetectedBearerOnly(httpFacade.getRequest())) { @@ -669,7 +690,7 @@ private void verifyRedirectBindingSignature(String paramKey, KeyLocator keyLocat try { //byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature); byte[] decodedSignature = Base64.decode(signature); - byte[] rawQueryBytes = rawQuery.getBytes("UTF-8"); + byte[] rawQueryBytes = rawQuery.getBytes(StandardCharsets.UTF_8); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm); diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java index 7f40e5aed3cc..78b5d9eea1cb 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java @@ -36,13 +36,13 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; -import javax.xml.soap.MessageFactory; -import javax.xml.soap.SOAPBody; -import javax.xml.soap.SOAPEnvelope; -import javax.xml.soap.SOAPException; -import javax.xml.soap.SOAPHeader; -import javax.xml.soap.SOAPHeaderElement; -import javax.xml.soap.SOAPMessage; +import jakarta.xml.soap.MessageFactory; +import jakarta.xml.soap.SOAPBody; +import jakarta.xml.soap.SOAPEnvelope; +import jakarta.xml.soap.SOAPException; +import jakarta.xml.soap.SOAPHeader; +import jakarta.xml.soap.SOAPHeaderElement; +import jakarta.xml.soap.SOAPMessage; /** * @author Pedro Igor @@ -105,8 +105,8 @@ public AuthOutcome handle(OnSessionCreated onCreateSession) { } @Override - protected AbstractInitiateLogin createChallenge() { - return new AbstractInitiateLogin(deployment, sessionStore) { + protected AbstractInitiateLogin createChallenge(boolean saveChallenge) { + return new AbstractInitiateLogin(deployment, sessionStore, saveChallenge) { @Override protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) { try { @@ -168,4 +168,4 @@ private String getResponseConsumerUrl() { } }; } -} \ No newline at end of file +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java index d8f9d7356c33..febaee398da7 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java @@ -24,7 +24,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import javax.security.auth.x500.X500Principal; import javax.xml.crypto.dsig.keyinfo.KeyInfo; @@ -34,6 +33,7 @@ import org.keycloak.adapters.cloned.HttpAdapterUtils; import org.keycloak.adapters.cloned.HttpClientAdapterException; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.Time; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.rotation.KeyLocator; @@ -179,7 +179,7 @@ private synchronized Key refreshCertificateCacheAndGet(T key, Map ca this.publicKeyCacheByKey.put(new KeyHash(x509certificate.getPublicKey()), x509certificate.getPublicKey()); } else { final X500Principal principal = x509certificate.getSubjectX500Principal(); - String name = (principal == null ? "unnamed" : principal.getName()) + "@" + x509certificate.getSerialNumber() + "$" + UUID.randomUUID(); + String name = (principal == null ? "unnamed" : principal.getName()) + "@" + x509certificate.getSerialNumber() + "$" + SecretGenerator.getInstance().generateSecureID(); this.publicKeyCacheByName.put(name, x509certificate.getPublicKey()); this.publicKeyCacheByKey.put(new KeyHash(x509certificate.getPublicKey()), x509certificate.getPublicKey()); LOG.tracef("Adding certificate %s without a specific key name: %s", name, x509certificate); diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_10.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_10.xsd index 5c3b9e16680e..3b1e7b6188a0 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_10.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_10.xsd @@ -450,7 +450,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_11.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_11.xsd index df961db25a8a..b791526d59fa 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_11.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_11.xsd @@ -455,7 +455,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd index 1b1245d52da6..1fbb7dcdcd27 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd @@ -503,7 +503,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_13.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_13.xsd index b01494469854..1b988b3b9b53 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_13.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_13.xsd @@ -503,7 +503,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd index d791682c2f7b..ea6b5ad99131 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd @@ -435,7 +435,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd index a5169d3c351e..61ef72221a45 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd @@ -440,7 +440,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd index cf9c05c4a701..8d287ba57d7b 100644 --- a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd @@ -445,7 +445,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/DeploymentBuilderTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/DeploymentBuilderTest.java new file mode 100644 index 000000000000..993b71a11dc4 --- /dev/null +++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/DeploymentBuilderTest.java @@ -0,0 +1,47 @@ +/* + * 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.adapters.saml; + +import java.io.InputStream; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; + +/** + * @author Marek Posolda + */ +public class DeploymentBuilderTest { + + @Test + public void testPropertiesBasedRoleMapper() throws Exception { + InputStream is = getClass().getResourceAsStream("config/parsers/keycloak-saml-pem-keys.xml"); + SamlDeployment deployment = new DeploymentBuilder().build(is, new ResourceLoader() { + @Override + public InputStream getResourceAsStream(String resource) { + return this.getClass().getClassLoader().getResourceAsStream(resource); + } + }); + Assert.assertNotNull(deployment); + Assert.assertNotNull(deployment.getSigningKeyPair().getPrivate()); + Assert.assertNotNull(deployment.getSigningKeyPair().getPublic()); + } +} diff --git a/adapters/saml/core/src/test/resources/keystores/keystore.jks b/adapters/saml/core/src/test/resources/keystores/keystore.jks new file mode 100755 index 000000000000..5554f13323d6 Binary files /dev/null and b/adapters/saml/core/src/test/resources/keystores/keystore.jks differ diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-pem-keys.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-pem-keys.xml new file mode 100644 index 000000000000..1a843cffe5ef --- /dev/null +++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-pem-keys.xml @@ -0,0 +1,67 @@ + + + + + + + + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+bNiR4IasHerrZihVxg47K79VL9hJAXmOlVZA36e4M04FYeftmyaY/g3qlH0uM2OaXQAm5FRLWYVnf6HvsF/S5XBT8L+VmC/LF+CxKTEsJDq6RecH/btAr7v/tC5pgNIyOkt0mIfKgp+b+n6eVXCVMn69SWvmAVK/Zsc8z60iHRiLg9b+gRcpo8Uj336gN3NfElzUfBzcroKfU40nXThWsFs96xHSM0myAIa+iGf+c1kEev1784522kFoi2GMM7VHCWyjJUpLp23oqb2FYK/RyDVaAUN43ljQhbNDD7kAygZamXl2KFAI6LIp1HiqfoNZTKcpk4q0FEBooGZz4Xi5AgMBAAECggEAQAE92epp2bhEmdLAg/QKHIFb0jo+rGs+fFpdn3iNWzCDbPO3jPm1Q39BFjPKz5ieReg0gN4GJz1zxZH00CesTaqo0s381z9L8SuZbnK2AGw9ARc1zE3QfrGSsyPQ5c1S2WcWrZ4HJl45X6gWnwmAyeUrDFx9U9XmBkd5eEslmm0wfF782sGPwhVrscZngLZIo9bXmdZTbJtwuh3xtq3nbRWltIK9lLqRniDmYE/DcpxuVDSXfa0+uht/6MQExVohKugKZjUhmXESw+hbWJ6QxDaOyHtNQ35oN5ae2DcZcO5Lj+fURDv/H5ifMi80gWCjVFEsUEaSJJT4kOBdpUutsQKBgQDqkcj/Gctu3Z0Y8plnNR+gzVvo7kcTzPUk4aOIYronAWgrKMXhWbgpB/iP4+zV45BF5oVRK/ngayosPVGOHqFzM/oPHhYQH5b+YEU01DwhA3TpeamNCm3z/wrvvCzM1gvjKoPgQh6yuehYX1k6zI8kKz4RvqTvcPj9OskB/Iu7UwKBgQDP0pgyYcawS/dD1xDldLHorAeruKy5EieR1YEF4GTMUBAjsTHPLlVmYrEfPIeJtbv4AhSXCrPCgdSBJ0Z/sLXWWoq5iVd4G8NQAPEd/pz82mtvN5K63JGih2TXKFtxNdsjoIqyBrXaiSifNXcG0gVxz0/juvKrT0vTxsU7xXaGQwKBgQDnoYZlwkcM93JGTGoHbIIK/D8iSQmPF/mLrfUanMNN+SmwVNbyrPIaMnDVRjF9FPZG0Fgdy9s4LRq8DOEYAk9Tv6PSgdcvnMIx90bf4CRwRUWRuD4htIbXRqa6DYv/ye57KGSJc0F1I/e4LI+kbJN9F+Z3B1c/ysNU7FPJzmT9WQKBgQCBLuVQnBrHx9DiKLPmDg3xFc6G3frv5+sU6eST5JKDtljx9tmBccnAJST4x8VwwrkfRxvJb+uhwtZ3mhRml0/Q+OM2xbrLfGaCOrOm83hebN9PePoKkcUthIAYhoug6dtYYBkW5LjyKURJAxED+lVME5QTeUgTWO1HrU05BFvSxQKBgQDM3mkN40xPcYzp4ZI/DpD/I5ynIM66GbxIDS3WyNBHD249WUR9ybhOIPGXpCFqWmIM9DE5FWJTgxLMdeSAPByCpPlxO5jDiG7S/FWKDDdsi9fdct1AXXg0tQfDHOarThxPTJSWyPFmGghfkwM9/hu/Zzmxr+l7EwaTI6Q2dTKGVQ== + + + MIIDATCCAemgAwIBAgIIOQ5fb1mWXb4wDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjaHR0cDovL2xvY2FsaG9zdDo4MDgwL2VtcGxveWVlLXNpZy8wIBcNMjQwNjIyMTAwMjI3WhgPMjEyNDA1MjkxMDAyMjdaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1zaWcvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmzYkeCGrB3q62YoVcYOOyu/VS/YSQF5jpVWQN+nuDNOBWHn7ZsmmP4N6pR9LjNjml0AJuRUS1mFZ3+h77Bf0uVwU/C/lZgvyxfgsSkxLCQ6ukXnB/27QK+7/7QuaYDSMjpLdJiHyoKfm/p+nlVwlTJ+vUlr5gFSv2bHPM+tIh0Yi4PW/oEXKaPFI99+oDdzXxJc1Hwc3K6Cn1ONJ104VrBbPesR0jNJsgCGvohn/nNZBHr9e/OOdtpBaIthjDO1RwlsoyVKS6dt6Km9hWCv0cg1WgFDeN5Y0IWzQw+5AMoGWpl5dihQCOiyKdR4qn6DWUynKZOKtBRAaKBmc+F4uQIDAQABoyEwHzAdBgNVHQ4EFgQUyPCcw2DKgLMQKLpHfIwjjG+yXsAwDQYJKoZIhvcNAQELBQADggEBAFwjt6JAPc3EQt4S0AjrDlzO6Mt/JuDPaJclrgwjFCQQhdonwpdX3gwSlABGOA337/DZv+lQLeunZlt94ORsBMt2RWWmhVXPF1baBaxpJodyC8k5FHyrNepoNKhqoiSsFiNH3929kN8DCk+SV+z5y55wJ9iIsi9pPYS3yO7kRYZqyZRRtY8iVPoHPCIYsKLGRFBL7iF6QEJx7C9Qml2sOnU5HmMlsDSfrOm+D0BcjBizcqPbt/vdYZlEQT76TCUHWIf+HHXTFquHjORRgb4Z6lFEE+MzO3HgduzM6NncrcS57cLkxirOIDZ5v1bnc/x18VIEy/RupXFRmG9bUCvkcBQ= + + + + + + + + + + + + + + + + + + + + + diff --git a/adapters/saml/jakarta-servlet-filter/pom.xml b/adapters/saml/jakarta-servlet-filter/pom.xml deleted file mode 100755 index 03b33d337b4c..000000000000 --- a/adapters/saml/jakarta-servlet-filter/pom.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-saml-jakarta-servlet-filter-adapter - Keycloak SAML Jakarta Servlet Filter - - - - - ${project.basedir}/../servlet-filter/src - ${project.basedir}/src - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-common - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-jakarta-servlet-adapter-spi - - - org.bouncycastle - bcprov-jdk18on - - - org.keycloak - keycloak-saml-core - - - org.keycloak - keycloak-saml-adapter-api-public - - - org.keycloak - keycloak-saml-adapter-core-jakarta - - - org.keycloak - keycloak-crypto-default - - - jakarta.servlet - jakarta.servlet-api - provided - - - junit - junit - test - - - - - - - maven-antrun-plugin - 3.0.0 - - - transform - initialize - - run - - - - - - - - - - - - - - - - - - - org.eclipse.transformer - org.eclipse.transformer.cli - 0.2.0 - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - - - - - diff --git a/adapters/saml/jetty/jetty-core/pom.xml b/adapters/saml/jetty/jetty-core/pom.xml deleted file mode 100755 index 7ad707523966..000000000000 --- a/adapters/saml/jetty/jetty-core/pom.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - 4.0.0 - - keycloak-saml-jetty-adapter-core - Keycloak Jetty Core SAML Integration - - 8.1.17.v20150415 - - org.keycloak.adapters.jetty.core.* - - - org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional, - javax.servlet.*;version="[2.5,4)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-jetty-adapter-spi - - - org.keycloak - keycloak-saml-core - - - org.keycloak - keycloak-saml-adapter-api-public - - - org.keycloak - keycloak-saml-adapter-core - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - org.eclipse.jetty - jetty-server - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-util - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-security - ${jetty9.version} - provided - - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java deleted file mode 100755 index 415c9deaf255..000000000000 --- a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.security.DefaultUserIdentity; -import org.eclipse.jetty.security.IdentityService; -import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.ServerAuthException; -import org.eclipse.jetty.security.UserAuthentication; -import org.eclipse.jetty.security.authentication.DeferredAuthentication; -import org.eclipse.jetty.security.authentication.FormAuthenticator; -import org.eclipse.jetty.security.authentication.LoginAuthenticator; -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.jboss.logging.Logger; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; -import org.keycloak.adapters.saml.AdapterConstants; -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlConfigResolver; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.BrowserHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.SamlEndpoint; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.saml.common.exceptions.ParsingException; - -import javax.security.auth.Subject; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractSamlAuthenticator extends LoginAuthenticator { - public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; - protected static final Logger log = Logger.getLogger(AbstractSamlAuthenticator.class); - protected SamlDeploymentContext deploymentContext; - protected SamlConfigResolver configResolver; - protected String errorPage; - protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); - - public AbstractSamlAuthenticator() { - super(); - } - - private static InputStream getJSONFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (json == null) { - return null; - } - return new ByteArrayInputStream(json.getBytes()); - } - - public JettySamlSessionStore getTokenStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - JettySamlSessionStore store = (JettySamlSessionStore) request.getAttribute(TOKEN_STORE_NOTE); - if (store != null) { - return store; - } - store = createJettySamlSessionStore(request, facade, resolvedDeployment); - - request.setAttribute(TOKEN_STORE_NOTE, store); - return store; - } - - protected JettySamlSessionStore createJettySamlSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - JettySamlSessionStore store; - store = new JettySamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, createSessionManagement(request), resolvedDeployment); - return store; - } - - public abstract AdapterSessionStore createSessionTokenStore(Request request, SamlDeployment resolvedDeployment); - - public abstract JettyUserSessionManagement createSessionManagement(Request request); - - public void logoutCurrent(Request request) { - JettyHttpFacade facade = new JettyHttpFacade(request, null); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - JettySamlSessionStore tokenStore = getTokenStore(request, facade, deployment); - tokenStore.logoutAccount(); - } - - private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:"); - - protected void forwardToLogoutPage(Request request, HttpServletResponse response, SamlDeployment deployment) { - final String location = deployment.getLogoutPage(); - - try { - //make sure the login page is never cached - response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - response.setHeader("Pragma", "no-cache"); - response.setHeader("Expires", "0"); - - if (location == null) { - log.warn("Logout page not set."); - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } else if (PROTOCOL_PATTERN.matcher(location).find()) { - response.sendRedirect(response.encodeRedirectURL(location)); - } else { - RequestDispatcher disp = request.getRequestDispatcher(location); - - disp.forward(request, response); - } - } catch (ServletException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - - } - - private static class DummyLoginService implements LoginService { - @Override - public String getName() { - return null; - } - - @Override - public UserIdentity login(String username, Object credentials) { - return null; - } - - @Override - public boolean validate(UserIdentity user) { - return false; - } - - @Override - public IdentityService getIdentityService() { - return null; - } - - @Override - public void setIdentityService(IdentityService service) { - - } - - @Override - public void logout(UserIdentity user) { - - } - } - - - - @Override - public void setConfiguration(AuthConfiguration configuration) { - //super.setConfiguration(configuration); - initializeKeycloak(); - // need this so that getUserPrincipal does not throw NPE - _loginService = new DummyLoginService(); - String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); - setErrorPage(error); - } - - private void setErrorPage(String path) { - if (path == null || path.trim().length() == 0) { - } else { - if (!path.startsWith("/")) { - path = "/" + path; - } - errorPage = path; - - if (errorPage.indexOf('?') > 0) - errorPage = errorPage.substring(0, errorPage.indexOf('?')); - } - } - - @Override - public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException { - return true; - } - - - - public SamlConfigResolver getConfigResolver() { - return configResolver; - } - - public void setConfigResolver(SamlConfigResolver configResolver) { - this.configResolver = configResolver; - } - - @SuppressWarnings("UseSpecificCatch") - public void initializeKeycloak() { - - ServletContext theServletContext = null; - ContextHandler.Context currentContext = ContextHandler.getCurrentContext(); - if (currentContext != null) { - String contextPath = currentContext.getContextPath(); - - if ("".equals(contextPath)) { - // This could be the case in osgi environment when deploying apps through pax whiteboard extension. - theServletContext = currentContext; - } else { - theServletContext = currentContext.getContext(contextPath); - } - } - - // Jetty 9.1.x servlet context will be null :( - if (configResolver == null && theServletContext != null) { - String configResolverClass = theServletContext.getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - configResolver = (SamlConfigResolver) ContextHandler.getCurrentContext().getClassLoader().loadClass(configResolverClass).newInstance(); - log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.infov("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); - } - } - } - - if (configResolver != null) { - //deploymentContext = new AdapterDeploymentContext(configResolver); - } else if (theServletContext != null) { - InputStream configInputStream = getConfigInputStream(theServletContext); - if (configInputStream != null) { - final ServletContext servletContext = theServletContext; - SamlDeployment deployment = null; - try { - deployment = new DeploymentBuilder().build(configInputStream, new ResourceLoader() { - @Override - public InputStream getResourceAsStream(String resource) { - return servletContext.getResourceAsStream(resource); - } - }); - } catch (ParsingException e) { - throw new RuntimeException(e); - } - deploymentContext = new SamlDeploymentContext(deployment); - } - } - if (theServletContext != null) - theServletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); - } - - private InputStream getConfigInputStream(ServletContext servletContext) { - InputStream is = getJSONFromServletContext(servletContext); - if (is == null) { - String path = servletContext.getInitParameter("keycloak.config.file"); - if (path == null) { - is = servletContext.getResourceAsStream("/WEB-INF/keycloak-saml.xml"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } - return is; - } - - @Override - public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException { - if (log.isTraceEnabled()) { - log.trace("*** authenticate"); - } - Request request = resolveRequest(req); - JettyHttpFacade facade = new JettyHttpFacade(request, (HttpServletResponse) res); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - log.debug("*** deployment isn't configured return false"); - return Authentication.UNAUTHENTICATED; - } - boolean isEndpoint = request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml"); - if (!mandatory && !isEndpoint) - return new DeferredAuthentication(this); - JettySamlSessionStore tokenStore = getTokenStore(request, facade, deployment); - - SamlAuthenticator authenticator = null; - if (isEndpoint) { - authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { - @Override - protected void completeAuthentication(SamlSession account) { - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new SamlEndpoint(facade, deployment, sessionStore); - } - }; - - } else { - authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { - @Override - protected void completeAuthentication(SamlSession account) { - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new BrowserHandler(facade, deployment, sessionStore); - } - }; - } - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - if (facade.isEnded()) { - return Authentication.SEND_SUCCESS; - } - SamlSession samlSession = tokenStore.getAccount(); - Authentication authentication = register(request, samlSession); - return authentication; - - } - if (outcome == AuthOutcome.LOGGED_OUT) { - logoutCurrent(request); - if (deployment.getLogoutPage() != null) { - forwardToLogoutPage(request, (HttpServletResponse)res, deployment); - - } - return Authentication.SEND_CONTINUE; - } - - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - challenge.challenge(facade); - } - return Authentication.SEND_CONTINUE; - } - - - protected abstract Request resolveRequest(ServletRequest req); - - @Override - public String getAuthMethod() { - return "KEYCLOAK-SAML"; - } - - public static UserIdentity createIdentity(SamlSession samlSession) { - Set roles = samlSession.getRoles(); - if (roles == null) { - roles = new HashSet(); - } - Subject theSubject = new Subject(); - String[] theRoles = new String[roles.size()]; - roles.toArray(theRoles); - - return new DefaultUserIdentity(theSubject, samlSession.getPrincipal(), theRoles); - } - public Authentication register(Request request, SamlSession samlSession) { - Authentication authentication = request.getAuthentication(); - if (!(authentication instanceof KeycloakAuthentication)) { - UserIdentity userIdentity = createIdentity(samlSession); - authentication = createAuthentication(userIdentity, request); - request.setAuthentication(authentication); - } - return authentication; - } - - public abstract Authentication createAuthentication(UserIdentity userIdentity, Request request); - - public static abstract class KeycloakAuthentication extends UserAuthentication { - public KeycloakAuthentication(String method, UserIdentity userIdentity) { - super(method, userIdentity); - } - - } -} diff --git a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java deleted file mode 100755 index ad5bd30b1bf0..000000000000 --- a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.server.Request; -import org.jboss.logging.Logger; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.SamlUtil; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.common.util.KeycloakUriBuilder; - -import javax.servlet.http.HttpSession; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettySamlSessionStore implements SamlSessionStore { - public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI"; - private static final Logger log = Logger.getLogger(JettySamlSessionStore.class); - protected Request request; - protected AdapterSessionStore sessionStore; - protected HttpFacade facade; - protected SessionIdMapper idMapper; - protected JettyUserSessionManagement sessionManagement; - protected final SamlDeployment deployment; - - public JettySamlSessionStore(Request request, AdapterSessionStore sessionStore, HttpFacade facade, - SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement, SamlDeployment deployment) { - this.request = request; - this.sessionStore = sessionStore; - this.facade = facade; - this.idMapper = idMapper; - this.sessionManagement = sessionManagement; - this.deployment = deployment; - } - - @Override - public void setCurrentAction(CurrentAction action) { - if (action == CurrentAction.NONE && request.getSession(false) == null) return; - request.getSession().setAttribute(CURRENT_ACTION, action); - } - - @Override - public boolean isLoggingIn() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_IN; - } - - @Override - public boolean isLoggingOut() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_OUT; - } - - @Override - public void logoutAccount() { - HttpSession session = request.getSession(false); - if (session != null) { - SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - if (samlSession != null) { - if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(session.getId()); - } - session.removeAttribute(SamlSession.class.getName()); - } - session.removeAttribute(SAML_REDIRECT_URI); - } - } - - @Override - public void logoutByPrincipal(String principal) { - Set sessions = idMapper.getUserSessions(principal); - if (sessions != null) { - List ids = new LinkedList(); - ids.addAll(sessions); - logoutSessionIds(ids); - for (String id : ids) { - idMapper.removeSession(id); - } - } - - } - - @Override - public void logoutBySsoId(List ssoIds) { - if (ssoIds == null) return; - List sessionIds = new LinkedList(); - for (String id : ssoIds) { - String sessionId = idMapper.getSessionFromSSO(id); - if (sessionId != null) { - sessionIds.add(sessionId); - idMapper.removeSession(sessionId); - } - - } - logoutSessionIds(sessionIds); - } - - protected void logoutSessionIds(List sessionIds) { - if (sessionIds == null || sessionIds.isEmpty()) return; - sessionManagement.logoutHttpSessions(sessionIds); - } - - @Override - public boolean isLoggedIn() { - HttpSession session = request.getSession(false); - if (session == null) { - log.debug("session was null, returning false"); - return false; - } - SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment); - if (samlSession == null) { - return false; - } - - restoreRequest(); - return true; - } - - @Override - public void saveAccount(SamlSession account) { - HttpSession session = request.getSession(true); - session.setAttribute(SamlSession.class.getName(), account); - - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), changeSessionId(session)); - - } - - protected String changeSessionId(HttpSession session) { - return session.getId(); - } - - @Override - public SamlSession getAccount() { - HttpSession session = request.getSession(true); - return (SamlSession)session.getAttribute(SamlSession.class.getName()); - } - - @Override - public String getRedirectUri() { - String redirect = (String)request.getSession(true).getAttribute(SAML_REDIRECT_URI); - if (redirect == null) { - String contextPath = request.getContextPath(); - String baseUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString()).replacePath(contextPath).build().toString(); - return SamlUtil.getRedirectTo(facade, contextPath, baseUri); - } - return redirect; - } - - @Override - public void saveRequest() { - sessionStore.saveRequest(); - - request.getSession(true).setAttribute(SAML_REDIRECT_URI, facade.getRequest().getURI()); - - } - - @Override - public boolean restoreRequest() { - return sessionStore.restoreRequest(); - } - -} diff --git a/adapters/saml/jetty/jetty9.4/pom.xml b/adapters/saml/jetty/jetty9.4/pom.xml deleted file mode 100644 index 91e5589f0b4c..000000000000 --- a/adapters/saml/jetty/jetty9.4/pom.xml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - 4.0.0 - - keycloak-saml-jetty94-adapter - Keycloak Jetty 9.4.x SAML Integration - - ${jetty94.version} - - org.keycloak.adapters.jetty.* - - - org.eclipse.jetty.*;resolution:=optional, - javax.servlet.*;version="[3.0,4)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-common - - - org.bouncycastle - bcprov-jdk18on - - - org.keycloak - keycloak-saml-adapter-api-public - - - org.keycloak - keycloak-saml-adapter-core - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-saml-jetty-adapter-core - - - org.eclipse.jetty - jetty-server - - - org.eclipse.jetty - jetty-util - - - org.eclipse.jetty - jetty-security - - - - - org.eclipse.jetty - jetty-server - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-util - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-security - ${jetty9.version} - provided - - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SamlSessionStore.java b/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SamlSessionStore.java deleted file mode 100644 index fa618cb57375..000000000000 --- a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SamlSessionStore.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.server.Request; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.SessionIdMapper; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class Jetty9SamlSessionStore extends JettySamlSessionStore { - public Jetty9SamlSessionStore(Request request, AdapterSessionStore sessionStore, HttpFacade facade, SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement, SamlDeployment deployment) { - super(request, sessionStore, facade, idMapper, sessionManagement, deployment); - } - - @Override - protected String changeSessionId(HttpSession session) { - Request request = this.request; - if (!deployment.turnOffChangeSessionIdOnLogin()) return request.changeSessionId(); - else return session.getId(); - } -} diff --git a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SessionManager.java b/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SessionManager.java deleted file mode 100755 index bd560577b1ee..000000000000 --- a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/Jetty9SessionManager.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.server.session.Session; -import org.eclipse.jetty.server.session.SessionHandler; -import org.keycloak.adapters.jetty.spi.JettySessionManager; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class Jetty9SessionManager implements JettySessionManager { - protected SessionHandler sessionHandler; - - public Jetty9SessionManager(SessionHandler sessionHandler) { - this.sessionHandler = sessionHandler; - } - - @Override - public HttpSession getHttpSession(String extendedId) { - // inlined code from sessionHandler.getHttpSession(extendedId) since the method visibility changed to protected - - String id = sessionHandler.getSessionIdManager().getId(extendedId); - Session session = sessionHandler.getSession(id); - - if (session != null && !session.getExtendedId().equals(extendedId)) { - session.setIdChanged(true); - } - return session; - } -} diff --git a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/JettyAdapterSessionStore.java b/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/JettyAdapterSessionStore.java deleted file mode 100644 index 17246056c679..000000000000 --- a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/JettyAdapterSessionStore.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.security.authentication.FormAuthenticator; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.util.MultiMap; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.common.util.MultivaluedHashMap; - -import javax.servlet.http.HttpSession; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettyAdapterSessionStore implements AdapterSessionStore { - public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS"; - protected Request myRequest; - - public JettyAdapterSessionStore(Request request) { - this.myRequest = request; // for IDE/compilation purposes - } - - protected MultiMap extractFormParameters(Request base_request) { - MultiMap formParameters = new MultiMap(); - base_request.extractFormParameters(formParameters); - return formParameters; - } - protected void restoreFormParameters(MultiMap j_post, Request base_request) { - base_request.setContentParameters(j_post); - } - - public boolean restoreRequest() { - HttpSession session = myRequest.getSession(false); - if (session == null) return false; - synchronized (session) { - String j_uri = (String) session.getAttribute(FormAuthenticator.__J_URI); - if (j_uri != null) { - // check if the request is for the same url as the original and restore - // params if it was a post - StringBuffer buf = myRequest.getRequestURL(); - if (myRequest.getQueryString() != null) - buf.append("?").append(myRequest.getQueryString()); - if (j_uri.equals(buf.toString())) { - String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD); - myRequest.setMethod(method); - MultivaluedHashMap j_post = (MultivaluedHashMap) session.getAttribute(CACHED_FORM_PARAMETERS); - if (j_post != null) { - myRequest.setContentType("application/x-www-form-urlencoded"); - MultiMap map = new MultiMap(); - for (String key : j_post.keySet()) { - for (String val : j_post.getList(key)) { - map.add(key, val); - } - } - restoreFormParameters(map, myRequest); - } - session.removeAttribute(FormAuthenticator.__J_URI); - session.removeAttribute(JettyHttpFacade.__J_METHOD); - session.removeAttribute(FormAuthenticator.__J_POST); - } - return true; - } - } - return false; - } - - public void saveRequest() { - // remember the current URI - HttpSession session = myRequest.getSession(); - synchronized (session) { - // But only if it is not set already, or we save every uri that leads to a login form redirect - if (session.getAttribute(FormAuthenticator.__J_URI) == null) { - StringBuffer buf = myRequest.getRequestURL(); - if (myRequest.getQueryString() != null) - buf.append("?").append(myRequest.getQueryString()); - session.setAttribute(FormAuthenticator.__J_URI, buf.toString()); - session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod()); - - if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) { - MultiMap formParameters = extractFormParameters(myRequest); - MultivaluedHashMap map = new MultivaluedHashMap(); - for (String key : formParameters.keySet()) { - for (Object value : formParameters.getValues(key)) { - map.add(key, (String) value); - } - } - session.setAttribute(CACHED_FORM_PARAMETERS, map); - } - } - } - } - -} diff --git a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/KeycloakSamlAuthenticator.java b/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/KeycloakSamlAuthenticator.java deleted file mode 100644 index 4ec71cd178b4..000000000000 --- a/adapters/saml/jetty/jetty9.4/src/main/java/org/keycloak/adapters/saml/jetty/KeycloakSamlAuthenticator.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.jetty; - -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.UserIdentity; -import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; - -import javax.servlet.ServletRequest; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class KeycloakSamlAuthenticator extends AbstractSamlAuthenticator { - - public KeycloakSamlAuthenticator() { - super(); - } - - - @Override - protected Request resolveRequest(ServletRequest req) { - return Request.getBaseRequest(req); - } - - @Override - public Authentication createAuthentication(UserIdentity userIdentity, final Request request) { - return new KeycloakAuthentication(getAuthMethod(), userIdentity) { - @Override - public Authentication logout(ServletRequest servletRequest) { - logoutCurrent((Request) servletRequest); - return super.logout(servletRequest); - } - }; - } - - @Override - public AdapterSessionStore createSessionTokenStore(Request request, SamlDeployment resolvedDeployment) { - return new JettyAdapterSessionStore(request); - } - - @Override - public JettyUserSessionManagement createSessionManagement(Request request) { - return new JettyUserSessionManagement(new Jetty9SessionManager(request.getSessionHandler())); - } - - @Override - protected JettySamlSessionStore createJettySamlSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - JettySamlSessionStore store; - store = new Jetty9SamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, createSessionManagement(request), resolvedDeployment); - return store; - } -} diff --git a/adapters/saml/jetty/pom.xml b/adapters/saml/jetty/pom.xml deleted file mode 100755 index dda79695de4a..000000000000 --- a/adapters/saml/jetty/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - Keycloak SAML Jetty Integration - - 4.0.0 - - keycloak-saml-jetty-integration-pom - pom - - - jetty-core - jetty9.4 - - diff --git a/adapters/saml/pom.xml b/adapters/saml/pom.xml index 443ad2a468d0..0f46c3a97bfa 100755 --- a/adapters/saml/pom.xml +++ b/adapters/saml/pom.xml @@ -33,14 +33,7 @@ core-public core - core-jakarta - jetty - undertow - tomcat wildfly - servlet-filter - jakarta-servlet-filter wildfly-elytron - wildfly-elytron-jakarta diff --git a/adapters/saml/servlet-filter/pom.xml b/adapters/saml/servlet-filter/pom.xml deleted file mode 100755 index 5fc027042952..000000000000 --- a/adapters/saml/servlet-filter/pom.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-saml-servlet-filter-adapter - Keycloak SAML Servlet Filter - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-common - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-servlet-adapter-spi - - - org.bouncycastle - bcprov-jdk18on - - - org.keycloak - keycloak-saml-core - - - org.keycloak - keycloak-saml-adapter-api-public - - - org.keycloak - keycloak-saml-adapter-core - - - org.keycloak - keycloak-crypto-default - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - junit - junit - test - - - - diff --git a/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java b/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java deleted file mode 100755 index 731bdba26a35..000000000000 --- a/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.servlet; - -import org.jboss.logging.Logger; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.SamlUtil; -import org.keycloak.adapters.servlet.FilterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.common.util.KeycloakUriBuilder; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpSession; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class FilterSamlSessionStore extends FilterSessionStore implements SamlSessionStore { - protected static Logger log = Logger.getLogger(SamlSessionStore.class); - protected final SessionIdMapper idMapper; - private final SamlDeployment deployment; - - public FilterSamlSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, SessionIdMapper idMapper, SamlDeployment deployment) { - super(request, facade, maxBuffer); - this.idMapper = idMapper; - this.deployment = deployment; - } - - @Override - public void setCurrentAction(CurrentAction action) { - if (action == CurrentAction.NONE && request.getSession(false) == null) return; - request.getSession().setAttribute(CURRENT_ACTION, action); - } - - @Override - public boolean isLoggingIn() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_IN; - } - - @Override - public boolean isLoggingOut() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_OUT; - } - - @Override - public void logoutAccount() { - HttpSession session = request.getSession(false); - if (session == null) return; - if (session != null) { - if (idMapper != null) idMapper.removeSession(session.getId()); - SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - if (samlSession != null) { - session.removeAttribute(SamlSession.class.getName()); - } - clearSavedRequest(session); - } - } - - @Override - public void logoutByPrincipal(String principal) { - SamlSession account = getAccount(); - if (account != null && account.getPrincipal().getSamlSubject().equals(principal)) { - logoutAccount(); - } - if (idMapper != null) { - Set sessions = idMapper.getUserSessions(principal); - if (sessions != null) { - List ids = new LinkedList(); - ids.addAll(sessions); - for (String id : ids) { - idMapper.removeSession(id); - } - } - } - - } - - @Override - public void logoutBySsoId(List ssoIds) { - SamlSession account = getAccount(); - for (String ssoId : ssoIds) { - if (account != null && account.getSessionIndex().equals(ssoId)) { - logoutAccount(); - } else if (idMapper != null) { - String sessionId = idMapper.getSessionFromSSO(ssoId); - idMapper.removeSession(sessionId); - } - } - } - - @Override - public boolean isLoggedIn() { - HttpSession session = request.getSession(false); - if (session == null) { - log.debug("session was null, returning false"); - return false; - } - final SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment); - if (samlSession == null) { - log.debug("SamlSession was not in session, returning null"); - return false; - } - if (idMapper != null && !idMapper.hasSession(session.getId())) { - logoutAccount(); - return false; - } - - needRequestRestore = restoreRequest(); - return true; - } - - public HttpServletRequestWrapper getWrap() { - HttpSession session = request.getSession(true); - final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - final KeycloakAccount account = samlSession; - return buildWrapper(session, account); - } - - @Override - public void saveAccount(SamlSession account) { - HttpSession session = request.getSession(true); - session.setAttribute(SamlSession.class.getName(), account); - if (idMapper != null) idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), session.getId()); - } - - @Override - public SamlSession getAccount() { - HttpSession session = request.getSession(false); - if (session == null) return null; - return (SamlSession)session.getAttribute(SamlSession.class.getName()); - } - - @Override - public String getRedirectUri() { - HttpSession session = request.getSession(false); - if (session == null) return null; - String redirect = (String)session.getAttribute(REDIRECT_URI); - if (redirect == null) { - String contextPath = request.getContextPath(); - String baseUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString()).replacePath(contextPath).build().toString(); - return SamlUtil.getRedirectTo(facade, contextPath, baseUri); - } - return redirect; - } - -} diff --git a/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java b/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java deleted file mode 100755 index 093f203d5311..000000000000 --- a/adapters/saml/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.servlet; - -import org.keycloak.adapters.saml.DefaultSamlDeployment; -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlConfigResolver; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.BrowserHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.SamlEndpoint; -import org.keycloak.adapters.servlet.ServletHttpFacade; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.saml.common.exceptions.ParsingException; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SamlFilter implements Filter { - protected SamlDeploymentContext deploymentContext; - protected SessionIdMapper idMapper; - private final static Logger log = Logger.getLogger("" + SamlFilter.class); - private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:"); - - @Override - public void init(final FilterConfig filterConfig) throws ServletException { - deploymentContext = (SamlDeploymentContext)filterConfig.getServletContext().getAttribute(SamlDeploymentContext.class.getName()); - if (deploymentContext != null) { - idMapper = (SessionIdMapper)filterConfig.getServletContext().getAttribute(SessionIdMapper.class.getName()); - return; - } - String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - SamlConfigResolver configResolver = (SamlConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new SamlDeploymentContext(configResolver); - log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.log(Level.WARNING, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() }); - deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment()); - } - } else { - String fp = filterConfig.getInitParameter("keycloak.config.file"); - InputStream is = null; - if (fp != null) { - try { - is = new FileInputStream(fp); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } else { - String path = "/WEB-INF/keycloak-saml.xml"; - String pathParam = filterConfig.getInitParameter("keycloak.config.path"); - if (pathParam != null) - path = pathParam; - is = filterConfig.getServletContext().getResourceAsStream(path); - } - final SamlDeployment deployment; - if (is == null) { - log.info("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - deployment = new DefaultSamlDeployment(); - } else { - try { - ResourceLoader loader = new ResourceLoader() { - @Override - public InputStream getResourceAsStream(String resource) { - return filterConfig.getServletContext().getResourceAsStream(resource); - } - }; - deployment = new DeploymentBuilder().build(is, loader); - } catch (ParsingException e) { - throw new RuntimeException(e); - } - } - deploymentContext = new SamlDeploymentContext(deployment); - log.fine("Keycloak is using a per-deployment configuration."); - } - idMapper = new InMemorySessionIdMapper(); - filterConfig.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); - filterConfig.getServletContext().setAttribute(SessionIdMapper.class.getName(), idMapper); - - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - ServletHttpFacade facade = new ServletHttpFacade(request, response); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - response.sendError(403); - log.fine("deployment not configured"); - return; - } - FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper, deployment); - boolean isEndpoint = request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml"); - SamlAuthenticator authenticator; - if (isEndpoint) { - authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { - @Override - protected void completeAuthentication(SamlSession account) { - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new SamlEndpoint(facade, deployment, sessionStore); - } - }; - - } else { - authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { - @Override - protected void completeAuthentication(SamlSession account) { - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new BrowserHandler(facade, deployment, sessionStore); - } - }; - } - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - log.fine("AUTHENTICATED"); - if (facade.isEnded()) { - return; - } - HttpServletRequestWrapper wrapper = tokenStore.getWrap(); - chain.doFilter(wrapper, res); - return; - } - if (outcome == AuthOutcome.LOGGED_OUT) { - tokenStore.logoutAccount(); - String logoutPage = deployment.getLogoutPage(); - if (logoutPage != null) { - if (PROTOCOL_PATTERN.matcher(logoutPage).find()) { - response.sendRedirect(logoutPage); - log.log(Level.FINE, "Redirected to logout page {0}", logoutPage); - } else { - RequestDispatcher disp = req.getRequestDispatcher(logoutPage); - disp.forward(req, res); - } - return; - } - chain.doFilter(req, res); - return; - } - - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - log.fine("challenge"); - challenge.challenge(facade); - return; - } - - if (deployment.isIsPassive() && outcome == AuthOutcome.NOT_AUTHENTICATED) { - log.fine("PASSIVE_NOT_AUTHENTICATED"); - if (facade.isEnded()) { - return; - } - chain.doFilter(req, res); - return; - } - - if (!facade.isEnded()) { - response.sendError(403); - } - - } - - @Override - public void destroy() { - - } -} diff --git a/adapters/saml/tomcat/pom.xml b/adapters/saml/tomcat/pom.xml deleted file mode 100755 index 70bd25315d01..000000000000 --- a/adapters/saml/tomcat/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - Keycloak SAML Tomcat Integration - - 4.0.0 - - keycloak-saml-tomcat-integration-pom - pom - - - tomcat-core - tomcat - - diff --git a/adapters/saml/tomcat/tomcat-core/pom.xml b/adapters/saml/tomcat/tomcat-core/pom.xml deleted file mode 100755 index 9844e59f090d..000000000000 --- a/adapters/saml/tomcat/tomcat-core/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - keycloak-saml-tomcat-integration-pom - org.keycloak - 999.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-saml-tomcat-adapter-core - Keycloak Tomcat Core SAML Integration - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-common - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-tomcat-adapter-spi - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - org.keycloak - keycloak-saml-core - - - org.keycloak - keycloak-saml-adapter-api-public - - - org.keycloak - keycloak-saml-adapter-core - - - - org.apache.tomcat - tomcat-catalina - ${tomcat8.version} - compile - - - - junit - junit - test - - - - diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java deleted file mode 100755 index a7b8f41fd021..000000000000 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml; - -import org.apache.catalina.Context; -import org.apache.catalina.Lifecycle; -import org.apache.catalina.LifecycleEvent; -import org.apache.catalina.LifecycleListener; -import org.apache.catalina.authenticator.FormAuthenticator; -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.jboss.logging.Logger; - -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.spi.*; -import org.keycloak.adapters.tomcat.CatalinaHttpFacade; -import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; -import org.keycloak.adapters.tomcat.PrincipalFactory; -import org.keycloak.saml.common.exceptions.ParsingException; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.*; -import java.util.regex.Pattern; - -/** - * Keycloak authentication valve - * - * @author Davide Ungari - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator implements LifecycleListener { - - public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; - - private final static Logger log = Logger.getLogger(AbstractSamlAuthenticatorValve.class); - protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); - protected SamlDeploymentContext deploymentContext; - protected SessionIdMapper mapper = new InMemorySessionIdMapper(); - protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT; - - @Override - public void lifecycleEvent(LifecycleEvent event) { - if (Lifecycle.START_EVENT.equals(event.getType())) { - cache = false; - } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { - keycloakInit(); - } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) { - beforeStop(); - } - } - - protected void logoutInternal(Request request) { - CatalinaHttpFacade facade = new CatalinaHttpFacade(null, request); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - SamlSessionStore tokenStore = getSessionStore(request, facade, deployment); - tokenStore.logoutAccount(); - request.setUserPrincipal(null); - } - - @SuppressWarnings("UseSpecificCatch") - public void keycloakInit() { - // Possible scenarios: - // 1) The deployment has a keycloak.config.resolver specified and it exists: - // Outcome: adapter uses the resolver - // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exist, isn't a resolver, ...) : - // Outcome: adapter is left unconfigured - // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent) - // Outcome: adapter uses it - // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent) - // Outcome: adapter is left unconfigured - - String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - SamlConfigResolver configResolver = (SamlConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new SamlDeploymentContext(configResolver); - log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage()); - deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment()); - } - } else { - InputStream is = getConfigInputStream(context); - final SamlDeployment deployment; - if (is == null) { - log.error("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - deployment = new DefaultSamlDeployment(); - } else { - try { - ResourceLoader loader = new ResourceLoader() { - @Override - public InputStream getResourceAsStream(String resource) { - return context.getServletContext().getResourceAsStream(resource); - } - }; - deployment = new DeploymentBuilder().build(is, loader); - } catch (ParsingException e) { - throw new RuntimeException(e); - } - } - deploymentContext = new SamlDeploymentContext(deployment); - log.debug("Keycloak is using a per-deployment configuration."); - } - - context.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); - - addTokenStoreUpdaters(); - } - - protected void beforeStop() { - } - - private static InputStream getConfigFromServletContext(ServletContext servletContext) { - String xml = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (xml == null) { - return null; - } - log.trace("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME); - return new ByteArrayInputStream(xml.getBytes()); - } - - private static InputStream getConfigInputStream(Context context) { - InputStream is = getConfigFromServletContext(context.getServletContext()); - if (is == null) { - String path = context.getServletContext().getInitParameter("keycloak.config.file"); - if (path == null) { - log.trace("**** using /WEB-INF/keycloak-saml.xml"); - is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak-saml.xml"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - log.errorv("NOT FOUND {0}", path); - throw new RuntimeException(e); - } - } - } - return is; - } - - @Override - public void invoke(Request request, Response response) throws IOException, ServletException { - log.trace("*********************** SAML ************"); - CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) { - if (deployment != null && deployment.isConfigured()) { - SamlSessionStore tokenStore = getSessionStore(request, facade, deployment); - SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore); - executeAuthenticator(request, response, facade, deployment, authenticator); - return; - } - - } - - try { - getSessionStore(request, facade, deployment).isLoggedIn(); // sets request UserPrincipal if logged in. we do this so that the UserPrincipal is available on unsecured, unconstrainted URLs - super.invoke(request, response); - } finally { - } - - } - - protected abstract PrincipalFactory createPrincipalFactory(); - protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException; - private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:"); - - protected void forwardToLogoutPage(Request request, HttpServletResponse response, SamlDeployment deployment) { - final String location = deployment.getLogoutPage(); - - try { - //make sure the login page is never cached - response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - response.setHeader("Pragma", "no-cache"); - response.setHeader("Expires", "0"); - - if (location == null) { - log.warn("Logout page not set."); - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } else if (PROTOCOL_PATTERN.matcher(location).find()) { - response.sendRedirect(response.encodeRedirectURL(location)); - } else { - RequestDispatcher disp = request.getRequestDispatcher(location); - - disp.forward(request.getRequest(), response); - } - } catch (ServletException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - - } - - protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { - log.trace("authenticateInternal"); - CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - if (deployment == null || !deployment.isConfigured()) { - log.trace("deployment not configured"); - return false; - } - SamlSessionStore tokenStore = getSessionStore(request, facade, deployment); - - - SamlAuthenticator authenticator = new CatalinaSamlAuthenticator(facade, deployment, tokenStore); - return executeAuthenticator(request, response, facade, deployment, authenticator); - } - - protected boolean executeAuthenticator(Request request, HttpServletResponse response, CatalinaHttpFacade facade, SamlDeployment deployment, SamlAuthenticator authenticator) { - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - log.trace("AUTHENTICATED"); - if (facade.isEnded()) { - return false; - } - return true; - } - if (outcome == AuthOutcome.LOGGED_OUT) { - logoutInternal(request); - if (deployment.getLogoutPage() != null) { - forwardToLogoutPage(request, response, deployment); - - } - log.trace("Logging OUT"); - return false; - } - - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - log.trace("challenge"); - challenge.challenge(facade); - } - return false; - } - - public void keycloakSaveRequest(Request request) throws IOException { - saveRequest(request, request.getSessionInternal(true)); - } - - public boolean keycloakRestoreRequest(Request request) { - try { - return restoreRequest(request, request.getSessionInternal()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected SamlSessionStore getSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - SamlSessionStore store = (SamlSessionStore)request.getNote(TOKEN_STORE_NOTE); - if (store != null) { - return store; - } - - store = createSessionStore(request, facade, resolvedDeployment); - - request.setNote(TOKEN_STORE_NOTE, store); - return store; - } - - protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - SamlSessionStore store; - store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, idMapperUpdater, request, this, facade, resolvedDeployment); - return store; - } - - protected void addTokenStoreUpdaters() { - SessionIdMapperUpdater updater = getIdMapperUpdater(); - - try { - String idMapperSessionUpdaterClasses = context.getServletContext().getInitParameter("keycloak.sessionIdMapperUpdater.classes"); - if (idMapperSessionUpdaterClasses == null) { - return; - } - - for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { - if (! clazz.isEmpty()) { - updater = invokeAddTokenStoreUpdaterMethod(clazz, updater); - } - } - } finally { - setIdMapperUpdater(updater); - } - } - - private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, SessionIdMapperUpdater previousIdMapperUpdater) { - try { - Class clazz = context.getLoader().getClassLoader().loadClass(idMapperSessionUpdaterClass); - Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", Context.class, SessionIdMapper.class, SessionIdMapperUpdater.class); - if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers()) - || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers()) - || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) { - log.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } - - log.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, context, mapper, previousIdMapperUpdater); - } catch (ClassNotFoundException ex) { - log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (NoSuchMethodException ex) { - log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (SecurityException ex) { - log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (IllegalAccessException ex) { - log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (IllegalArgumentException ex) { - log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (InvocationTargetException ex) { - log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } - } - - public SessionIdMapperUpdater getIdMapperUpdater() { - return idMapperUpdater; - } - - public void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) { - this.idMapperUpdater = idMapperUpdater; - } -} diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlAuthenticator.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlAuthenticator.java deleted file mode 100755 index 0f4ec04ed623..000000000000 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlAuthenticator.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml; - -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.BrowserHandler; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaSamlAuthenticator extends SamlAuthenticator { - public CatalinaSamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - super(facade, deployment, sessionStore); - } - - @Override - protected void completeAuthentication(SamlSession account) { - // complete - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new BrowserHandler(facade, deployment, sessionStore); - } - -} diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlEndpoint.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlEndpoint.java deleted file mode 100755 index 36c94a2cf612..000000000000 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlEndpoint.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml; - -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.SamlEndpoint; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaSamlEndpoint extends SamlAuthenticator { - public CatalinaSamlEndpoint(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - super(facade, deployment, sessionStore); - } - - @Override - protected void completeAuthentication(SamlSession account) { - // complete - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new SamlEndpoint(facade, deployment, sessionStore); - } - - -} diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java deleted file mode 100755 index ff122ac5a66c..000000000000 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml; - -import org.apache.catalina.Manager; -import org.apache.catalina.Session; -import org.apache.catalina.connector.Request; -import org.apache.catalina.realm.GenericPrincipal; -import org.jboss.logging.Logger; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapperUpdater; -import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; -import org.keycloak.adapters.tomcat.PrincipalFactory; -import org.keycloak.common.util.KeycloakUriBuilder; - -import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaSamlSessionStore implements SamlSessionStore { - protected static Logger log = Logger.getLogger(SamlSessionStore.class); - public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI"; - - private final CatalinaUserSessionManagement sessionManagement; - protected final PrincipalFactory principalFactory; - private final SessionIdMapper idMapper; - private final SessionIdMapperUpdater idMapperUpdater; - protected final Request request; - protected final AbstractSamlAuthenticatorValve valve; - protected final HttpFacade facade; - protected final SamlDeployment deployment; - - public CatalinaSamlSessionStore(CatalinaUserSessionManagement sessionManagement, PrincipalFactory principalFactory, - SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, - Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, - SamlDeployment deployment) { - this.sessionManagement = sessionManagement; - this.principalFactory = principalFactory; - this.idMapper = idMapper; - this.idMapperUpdater = idMapperUpdater; - this.request = request; - this.valve = valve; - this.facade = facade; - this.deployment = deployment; - } - - @Override - public void setCurrentAction(CurrentAction action) { - if (action == CurrentAction.NONE && request.getSession(false) == null) return; - request.getSession().setAttribute(CURRENT_ACTION, action); - } - - @Override - public boolean isLoggingIn() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_IN; - } - - @Override - public boolean isLoggingOut() { - HttpSession session = request.getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_OUT; - } - - @Override - public void logoutAccount() { - Session sessionInternal = request.getSessionInternal(false); - if (sessionInternal == null) return; - HttpSession session = sessionInternal.getSession(); - List ids = new LinkedList(); - if (session != null) { - SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - if (samlSession != null) { - if (samlSession.getSessionIndex() != null) { - ids.add(session.getId()); - idMapperUpdater.removeSession(idMapper, session.getId()); - } - session.removeAttribute(SamlSession.class.getName()); - } - session.removeAttribute(SAML_REDIRECT_URI); - } - sessionInternal.setPrincipal(null); - sessionInternal.setAuthType(null); - logoutSessionIds(ids); - } - - @Override - public void logoutByPrincipal(String principal) { - Set sessions = idMapper.getUserSessions(principal); - if (sessions != null) { - List ids = new LinkedList(); - ids.addAll(sessions); - logoutSessionIds(ids); - for (String id : ids) { - idMapperUpdater.removeSession(idMapper, id); - } - } - - } - - @Override - public void logoutBySsoId(List ssoIds) { - if (ssoIds == null) return; - List sessionIds = new LinkedList(); - for (String id : ssoIds) { - String sessionId = idMapper.getSessionFromSSO(id); - if (sessionId != null) { - sessionIds.add(sessionId); - idMapperUpdater.removeSession(idMapper, sessionId); - } - - } - logoutSessionIds(sessionIds); - } - - protected void logoutSessionIds(List sessionIds) { - if (sessionIds == null || sessionIds.isEmpty()) return; - Manager sessionManager = request.getContext().getManager(); - sessionManagement.logoutHttpSessions(sessionManager, sessionIds); - } - - @Override - public boolean isLoggedIn() { - Session session = request.getSessionInternal(false); - if (session == null) { - log.debug("session was null, returning null"); - return false; - } - final SamlSession samlSession = SamlUtil.validateSamlSession(session.getSession().getAttribute(SamlSession.class.getName()), deployment); - if (samlSession == null) { - return false; - } - - GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); - // in clustered environment in JBossWeb, principal is not serialized or saved - if (principal == null) { - principal = principalFactory.createPrincipal(request.getContext().getRealm(), samlSession.getPrincipal(), samlSession.getRoles()); - session.setPrincipal(principal); - session.setAuthType("KEYCLOAK-SAML"); - - } - else if (samlSession.getPrincipal().getName().equals(principal.getName())){ - if (!principal.getUserPrincipal().getName().equals(samlSession.getPrincipal().getName())) { - throw new RuntimeException("Unknown State"); - } - log.debug("************principal already in"); - if (log.isDebugEnabled()) { - for (String role : principal.getRoles()) { - log.debug("principal role: " + role); - } - } - - } - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK-SAML"); - restoreRequest(); - return true; - } - - @Override - public void saveAccount(SamlSession account) { - Session session = request.getSessionInternal(true); - session.getSession().setAttribute(SamlSession.class.getName(), account); - GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); - // in clustered environment in JBossWeb, principal is not serialized or saved - if (principal == null) { - principal = principalFactory.createPrincipal(request.getContext().getRealm(), account.getPrincipal(), account.getRoles()); - session.setPrincipal(principal); - session.setAuthType("KEYCLOAK-SAML"); - - } - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK-SAML"); - String newId = changeSessionId(session); - idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId); - - } - - protected String changeSessionId(Session session) { - return session.getId(); - } - - @Override - public SamlSession getAccount() { - HttpSession session = getSession(true); - return (SamlSession)session.getAttribute(SamlSession.class.getName()); - } - - @Override - public String getRedirectUri() { - String redirect = (String)getSession(true).getAttribute(SAML_REDIRECT_URI); - if (redirect == null) { - String contextPath = request.getContextPath(); - String baseUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString()).replacePath(contextPath).build().toString(); - return SamlUtil.getRedirectTo(facade, contextPath, baseUri); - } - return redirect; - } - - @Override - public void saveRequest() { - try { - valve.keycloakSaveRequest(request); - } catch (IOException e) { - throw new RuntimeException(e); - } - - getSession(true).setAttribute(SAML_REDIRECT_URI, facade.getRequest().getURI()); - - } - - @Override - public boolean restoreRequest() { - getSession(true).removeAttribute(SAML_REDIRECT_URI); - return valve.keycloakRestoreRequest(request); - } - - protected HttpSession getSession(boolean create) { - Session session = request.getSessionInternal(create); - if (session == null) return null; - return session.getSession(); - } -} diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java deleted file mode 100644 index 4fc78149fd38..000000000000 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/IdMapperUpdaterSessionListener.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017 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.adapters.saml; - -import org.keycloak.adapters.spi.SessionIdMapper; - -import java.util.Objects; -import javax.servlet.http.*; - -/** - * - * @author hmlnarik - */ -public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener { - - private final SessionIdMapper idMapper; - - public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) { - this.idMapper = idMapper; - } - - @Override - public void sessionCreated(HttpSessionEvent hse) { - HttpSession session = hse.getSession(); - Object value = session.getAttribute(SamlSession.class.getName()); - map(session.getId(), value); - } - - @Override - public void sessionDestroyed(HttpSessionEvent hse) { - HttpSession session = hse.getSession(); - unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); - } - - @Override - public void attributeAdded(HttpSessionBindingEvent hsbe) { - HttpSession session = hsbe.getSession(); - if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { - map(session.getId(), hsbe.getValue()); - } - } - - @Override - public void attributeRemoved(HttpSessionBindingEvent hsbe) { - HttpSession session = hsbe.getSession(); - if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { - unmap(session.getId(), hsbe.getValue()); - } - } - - @Override - public void attributeReplaced(HttpSessionBindingEvent hsbe) { - HttpSession session = hsbe.getSession(); - if (Objects.equals(hsbe.getName(), SamlSession.class.getName())) { - unmap(session.getId(), hsbe.getValue()); - map(session.getId(), session.getAttribute(SamlSession.class.getName())); - } - } - - private void map(String sessionId, Object value) { - if (! (value instanceof SamlSession) || sessionId == null) { - return; - } - SamlSession account = (SamlSession) value; - - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); - } - - private void unmap(String sessionId, Object value) { - if (! (value instanceof SamlSession) || sessionId == null) { - return; - } - - SamlSession samlSession = (SamlSession) value; - if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(sessionId); - } - } -} diff --git a/adapters/saml/tomcat/tomcat/pom.xml b/adapters/saml/tomcat/tomcat/pom.xml deleted file mode 100755 index 0dd64f673b34..000000000000 --- a/adapters/saml/tomcat/tomcat/pom.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - keycloak-saml-tomcat-integration-pom - org.keycloak - 999.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-saml-tomcat-adapter - Keycloak Tomcat SAML Integration - - - - - org.jboss.logging - jboss-logging - - - org.apache.tomcat - tomcat-servlet-api - ${tomcat8.version} - provided - - - org.apache.tomcat - tomcat-catalina - ${tomcat8.version} - provided - - - - org.keycloak - keycloak-saml-tomcat-adapter-core - - - org.apache.tomcat - tomcat-servlet-api - - - org.apache.tomcat - tomcat-catalina - - - org.apache.tomcat - catalina - - - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcprov-jdk18on - - - junit - junit - test - - - - diff --git a/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java deleted file mode 100755 index 58c041091fc1..000000000000 --- a/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.tomcat; - -import org.apache.catalina.authenticator.FormAuthenticator; -import org.apache.catalina.connector.Request; -import org.apache.catalina.core.StandardContext; -import org.apache.catalina.realm.GenericPrincipal; -import org.apache.tomcat.util.descriptor.web.LoginConfig; -import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.tomcat.GenericPrincipalFactory; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.lang.reflect.Method; -import java.security.Principal; -import java.util.List; - -/** - * Keycloak authentication valve - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve { - /** - * Method called by Tomcat < 8.5.5 - */ - @Override - public boolean authenticate(Request request, HttpServletResponse response) throws IOException { - return authenticateInternal(request, response, request.getContext().getLoginConfig()); - } - - /** - * Method called by Tomcat >= 8.5.5 - */ - @Override - protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { - return this.authenticate(request, response); - } - - @Override - protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { - if (loginConfig == null) return false; - LoginConfig config = (LoginConfig)loginConfig; - if (config.getErrorPage() == null) return false; - // had to do this to get around compiler/IDE issues :( - try { - Method method = FormAuthenticator.class.getDeclaredMethod("forwardToErrorPage", Request.class, HttpServletResponse.class, LoginConfig.class); - method.setAccessible(true); - method.invoke(this, request, response, config); - } catch (Exception e) { - throw new RuntimeException(e); - } - return true; - } - - @Override - protected void initInternal() { - StandardContext standardContext = (StandardContext) context; - standardContext.addLifecycleListener(this); - } - - @Override - public void logout(Request request) { - logoutInternal(request); - } - - @Override - protected GenericPrincipalFactory createPrincipalFactory() { - return new GenericPrincipalFactory() { - @Override - protected GenericPrincipal createPrincipal(Principal userPrincipal, List roles) { - return new GenericPrincipal(userPrincipal.getName(), null, roles, userPrincipal, null); - } - }; - } - - @Override - protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) { - SamlSessionStore store; - store = new TomcatSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade, resolvedDeployment); - return store; - } - -} diff --git a/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/TomcatSamlSessionStore.java b/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/TomcatSamlSessionStore.java deleted file mode 100755 index c1e04dd04856..000000000000 --- a/adapters/saml/tomcat/tomcat/src/main/java/org/keycloak/adapters/saml/tomcat/TomcatSamlSessionStore.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.tomcat; - -import org.apache.catalina.Session; -import org.apache.catalina.connector.Request; -import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve; -import org.keycloak.adapters.saml.CatalinaSamlSessionStore; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapperUpdater; -import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement; -import org.keycloak.adapters.tomcat.PrincipalFactory; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class TomcatSamlSessionStore extends CatalinaSamlSessionStore { - public TomcatSamlSessionStore(CatalinaUserSessionManagement sessionManagement, PrincipalFactory principalFactory, SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, SamlDeployment deployment) { - super(sessionManagement, principalFactory, idMapper, SessionIdMapperUpdater.DIRECT, request, valve, facade, deployment); - } - - @Override - protected String changeSessionId(Session session) { - Request request = this.request; - if (!deployment.turnOffChangeSessionIdOnLogin()) return request.changeSessionId(); - else return session.getId(); - } -} diff --git a/adapters/saml/undertow/pom.xml b/adapters/saml/undertow/pom.xml deleted file mode 100755 index b4d5480a78c1..000000000000 --- a/adapters/saml/undertow/pom.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-saml-undertow-adapter - Keycloak Undertow SAML Adapter - - - - - org.keycloak - keycloak-saml-core - provided - - - org.keycloak - keycloak-adapter-spi - provided - - - org.keycloak - keycloak-common - provided - - - org.keycloak - keycloak-saml-adapter-api-public - provided - - - org.keycloak - keycloak-saml-adapter-core - provided - - - org.keycloak - keycloak-undertow-adapter-spi - provided - - - org.jboss.logging - jboss-logging - provided - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - io.undertow - undertow-servlet - provided - - - io.undertow - undertow-core - provided - - - junit - junit - test - - - - diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java deleted file mode 100755 index 34197bc504b4..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.NotificationReceiver; -import io.undertow.security.api.SecurityContext; -import io.undertow.security.api.SecurityNotification; -import io.undertow.server.HttpServerExchange; -import io.undertow.util.AttachmentKey; -import io.undertow.util.Headers; -import io.undertow.util.StatusCodes; -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.undertow.UndertowHttpFacade; -import org.keycloak.adapters.undertow.UndertowUserSessionManagement; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. - */ -public abstract class AbstractSamlAuthMech implements AuthenticationMechanism { - - private static final Logger LOG = Logger.getLogger(AbstractSamlAuthMech.class.getName()); - - public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); - protected SamlDeploymentContext deploymentContext; - protected UndertowUserSessionManagement sessionManagement; - protected String errorPage; - - public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { - this.deploymentContext = deploymentContext; - this.sessionManagement = sessionManagement; - this.errorPage = errorPage; - } - - @Override - public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { - AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY); - if (challenge != null) { - UndertowHttpFacade facade = createFacade(exchange); - if (challenge.challenge(facade)) { - return new ChallengeResult(true, exchange.getResponseCode()); - } - } - return new ChallengeResult(false); - } - - protected Integer servePage(final HttpServerExchange exchange, final String location) { - sendRedirect(exchange, location); - return StatusCodes.TEMPORARY_REDIRECT; - } - - private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:"); - - static void sendRedirect(final HttpServerExchange exchange, final String location) { - if (location == null) { - LOG.log(Level.WARNING, "Logout page not set."); - exchange.setStatusCode(StatusCodes.NOT_FOUND); - exchange.endExchange(); - return; - } - - if (PROTOCOL_PATTERN.matcher(location).find()) { - exchange.getResponseHeaders().put(Headers.LOCATION, location); - } else { - String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; - exchange.getResponseHeaders().put(Headers.LOCATION, loc); - } - } - - protected void registerNotifications(final SecurityContext securityContext) { - - final NotificationReceiver logoutReceiver = new NotificationReceiver() { - @Override - public void handleNotification(SecurityNotification notification) { - if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) - return; - - HttpServerExchange exchange = notification.getExchange(); - UndertowHttpFacade facade = createFacade(exchange); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext); - sessionStore.logoutAccount(); - } - }; - - securityContext.registerNotificationReceiver(logoutReceiver); - } - - /** - * Call this inside your authenticate method. - */ - public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { - UndertowHttpFacade facade = createFacade(exchange); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); - if (!deployment.isConfigured()) { - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext); - SamlAuthenticator authenticator = null; - if (exchange.getRequestPath().endsWith("/saml")) { - authenticator = new UndertowSamlEndpoint(facade, deploymentContext.resolveDeployment(facade), sessionStore); - } else { - authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore); - - } - - AuthOutcome outcome = authenticator.authenticate(); - if (outcome == AuthOutcome.AUTHENTICATED) { - registerNotifications(securityContext); - return AuthenticationMechanismOutcome.AUTHENTICATED; - } - if (outcome == AuthOutcome.NOT_AUTHENTICATED) { - // we are in passive mode and user is not authenticated, let app server to try another auth mechanism - // See KEYCLOAK-2107, AbstractSamlAuthenticationHandler - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - if (outcome == AuthOutcome.LOGGED_OUT) { - securityContext.logout(); - if (deployment.getLogoutPage() != null) { - redirectLogout(deployment, exchange); - } - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - AuthChallenge challenge = authenticator.getChallenge(); - if (challenge != null) { - exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, challenge); - if (authenticator instanceof UndertowSamlEndpoint) { - exchange.getSecurityContext().setAuthenticationRequired(); - } - } - - if (outcome == AuthOutcome.FAILED) { - return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; - } - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - - protected void redirectLogout(SamlDeployment deployment, HttpServerExchange exchange) { - String page = deployment.getLogoutPage(); - sendRedirect(exchange, page); - exchange.setStatusCode(StatusCodes.FOUND); - exchange.endExchange(); - } - - protected UndertowHttpFacade createFacade(HttpServerExchange exchange) { - return new UndertowHttpFacade(exchange); - } - - protected abstract SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext); -} \ No newline at end of file diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java deleted file mode 100644 index 692413e5aabf..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/IdMapperUpdaterSessionListener.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2017 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.adapters.saml.undertow; - -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.spi.SessionIdMapper; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.server.session.SessionListener; -import java.util.Objects; - -/** - * - * @author hmlnarik - */ -public class IdMapperUpdaterSessionListener implements SessionListener { - - private final SessionIdMapper idMapper; - - public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) { - this.idMapper = idMapper; - } - - @Override - public void sessionCreated(Session session, HttpServerExchange exchange) { - Object value = session.getAttribute(SamlSession.class.getName()); - map(session.getId(), value); - } - - @Override - public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { - if (reason != SessionDestroyedReason.UNDEPLOY) { - unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); - } - } - - @Override - public void attributeAdded(Session session, String name, Object value) { - if (Objects.equals(name, SamlSession.class.getName())) { - map(session.getId(), value); - } - } - - @Override - public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { - if (Objects.equals(name, SamlSession.class.getName())) { - unmap(session.getId(), oldValue); - map(session.getId(), newValue); - } - } - - @Override - public void attributeRemoved(Session session, String name, Object oldValue) { - if (Objects.equals(name, SamlSession.class.getName())) { - unmap(session.getId(), oldValue); - } - } - - @Override - public void sessionIdChanged(Session session, String oldSessionId) { - Object value = session.getAttribute(SamlSession.class.getName()); - if (value != null) { - unmap(oldSessionId, value); - map(session.getId(), value); - } - } - - private void map(String sessionId, Object value) { - if (! (value instanceof SamlSession) || sessionId == null) { - return; - } - SamlSession account = (SamlSession) value; - - idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); - } - - private void unmap(String sessionId, Object value) { - if (! (value instanceof SamlSession) || sessionId == null) { - return; - } - - SamlSession samlSession = (SamlSession) value; - if (samlSession.getSessionIndex() != null) { - idMapper.removeSession(sessionId); - } - } - -} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java deleted file mode 100755 index 931ecd7169ea..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.AuthenticationMechanismFactory; -import io.undertow.security.idm.Account; -import io.undertow.security.idm.Credential; -import io.undertow.security.idm.IdentityManager; -import io.undertow.server.handlers.form.FormParserFactory; -import io.undertow.servlet.ServletExtension; -import io.undertow.servlet.api.AuthMethodConfig; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.LoginConfig; -import io.undertow.servlet.api.SecurityConstraint; -import io.undertow.servlet.api.ServletSessionConfig; -import io.undertow.servlet.api.WebResourceCollection; -import org.jboss.logging.Logger; -import org.keycloak.adapters.saml.AdapterConstants; -import org.keycloak.adapters.saml.DefaultSamlDeployment; -import org.keycloak.adapters.saml.SamlConfigResolver; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.undertow.ChangeSessionId; -import org.keycloak.adapters.undertow.UndertowUserSessionManagement; -import org.keycloak.saml.common.exceptions.ParsingException; - -import javax.servlet.ServletContext; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SamlServletExtension implements ServletExtension { - - protected static Logger log = Logger.getLogger(SamlServletExtension.class); - - // todo when this DeploymentInfo method of the same name is fixed. - public boolean isAuthenticationMechanismPresent(DeploymentInfo deploymentInfo, final String mechanismName) { - LoginConfig loginConfig = deploymentInfo.getLoginConfig(); - if (loginConfig != null) { - for (AuthMethodConfig method : loginConfig.getAuthMethods()) { - if (method.getName().equalsIgnoreCase(mechanismName)) { - return true; - } - } - } - return false; - } - - private static InputStream getXMLFromServletContext(ServletContext servletContext) { - String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); - if (json == null) { - return null; - } - return new ByteArrayInputStream(json.getBytes()); - } - - private static InputStream getConfigInputStream(ServletContext context) { - InputStream is = getXMLFromServletContext(context); - if (is == null) { - String path = context.getInitParameter("keycloak.config.file"); - if (path == null) { - log.debug("using /WEB-INF/keycloak-saml.xml"); - is = context.getResourceAsStream("/WEB-INF/keycloak-saml.xml"); - } else { - try { - is = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } - return is; - } - - - @Override - @SuppressWarnings("UseSpecificCatch") - public void handleDeployment(DeploymentInfo deploymentInfo, final ServletContext servletContext) { - if (!isAuthenticationMechanismPresent(deploymentInfo, "KEYCLOAK-SAML")) { - log.debug("auth-method is not keycloak saml!"); - return; - } - log.debug("SamlServletException initialization"); - - // Possible scenarios: - // 1) The deployment has a keycloak.config.resolver specified and it exists: - // Outcome: adapter uses the resolver - // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exist, isn't a resolver, ...) : - // Outcome: adapter is left unconfigured - // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent) - // Outcome: adapter uses it - // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent) - // Outcome: adapter is left unconfigured - - SamlConfigResolver configResolver; - String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver"); - SamlDeploymentContext deploymentContext = null; - if (configResolverClass != null) { - try { - configResolver = (SamlConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new SamlDeploymentContext(configResolver); - log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage()); - deploymentContext = new SamlDeploymentContext(new DefaultSamlDeployment()); - } - } else { - InputStream is = getConfigInputStream(servletContext); - final SamlDeployment deployment; - if (is == null) { - log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); - deployment = new DefaultSamlDeployment(); - } else { - try { - ResourceLoader loader = new ResourceLoader() { - @Override - public InputStream getResourceAsStream(String resource) { - return servletContext.getResourceAsStream(resource); - } - }; - deployment = new DeploymentBuilder().build(is, loader); - } catch (ParsingException e) { - throw new RuntimeException(e); - } - } - deploymentContext = new SamlDeploymentContext(deployment); - log.debug("Keycloak is using a per-deployment configuration."); - } - - servletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); - UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement(); - final ServletSamlAuthMech mech = createAuthMech(deploymentInfo, deploymentContext, userSessionManagement); - mech.addTokenStoreUpdaters(deploymentInfo); - - // setup handlers - - deploymentInfo.addAuthenticationMechanism("KEYCLOAK-SAML", new AuthenticationMechanismFactory() { - @Override - public AuthenticationMechanism create(String s, FormParserFactory formParserFactory, Map stringStringMap) { - return mech; - } - }); // authentication - - deploymentInfo.setIdentityManager(new IdentityManager() { - @Override - public Account verify(Account account) { - return account; - } - - @Override - public Account verify(String id, Credential credential) { - throw new IllegalStateException("Should never be called in Keycloak flow"); - } - - @Override - public Account verify(Credential credential) { - throw new IllegalStateException("Should never be called in Keycloak flow"); - } - }); - - ServletSessionConfig cookieConfig = deploymentInfo.getServletSessionConfig(); - if (cookieConfig == null) { - cookieConfig = new ServletSessionConfig(); - } - if (cookieConfig.getPath() == null) { - log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath()); - cookieConfig.setPath(deploymentInfo.getContextPath()); - deploymentInfo.setServletSessionConfig(cookieConfig); - } - addEndpointConstraint(deploymentInfo); - - ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo); - - } - - /** - * add security constraint to /saml so that the endpoint can be called and auth mechanism pinged. - * @param deploymentInfo - */ - protected void addEndpointConstraint(DeploymentInfo deploymentInfo) { - SecurityConstraint constraint = new SecurityConstraint(); - WebResourceCollection collection = new WebResourceCollection(); - collection.addUrlPattern("/saml"); - constraint.addWebResourceCollection(collection); - deploymentInfo.addSecurityConstraint(constraint); - } - - protected ServletSamlAuthMech createAuthMech(DeploymentInfo deploymentInfo, SamlDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) { - return new ServletSamlAuthMech(deploymentContext, userSessionManagement, getErrorPage(deploymentInfo)); - } - - protected String getErrorPage(DeploymentInfo deploymentInfo) { - LoginConfig loginConfig = deploymentInfo.getLoginConfig(); - String errorPage = null; - if (loginConfig != null) { - errorPage = loginConfig.getErrorPage(); - } - return errorPage; - } -} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java deleted file mode 100755 index df1a471b070b..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlAuthMech.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.util.Headers; - -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.spi.*; -import org.keycloak.adapters.undertow.ServletHttpFacade; -import org.keycloak.adapters.undertow.UndertowHttpFacade; -import org.keycloak.adapters.undertow.UndertowUserSessionManagement; - -import io.undertow.servlet.api.DeploymentInfo; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import java.io.IOException; -import java.lang.reflect.*; -import java.util.Map; -import org.jboss.logging.Logger; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletSamlAuthMech extends AbstractSamlAuthMech { - - private static final Logger LOG = Logger.getLogger(ServletSamlAuthMech.class); - - protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); - protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT; - - public ServletSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { - super(deploymentContext, sessionManagement, errorPage); - } - - public void addTokenStoreUpdaters(DeploymentInfo deploymentInfo) { - deploymentInfo.addSessionListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally - SessionIdMapperUpdater updater = SessionIdMapperUpdater.EXTERNAL; - - try { - Map initParameters = deploymentInfo.getInitParameters(); - String idMapperSessionUpdaterClasses = initParameters == null - ? null - : initParameters.get("keycloak.sessionIdMapperUpdater.classes"); - if (idMapperSessionUpdaterClasses == null) { - return; - } - - for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { - if (! clazz.isEmpty()) { - updater = invokeAddTokenStoreUpdaterMethod(clazz, deploymentInfo, updater); - } - } - } finally { - setIdMapperUpdater(updater); - } - } - - private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, DeploymentInfo deploymentInfo, - SessionIdMapperUpdater previousIdMapperUpdater) { - try { - Class clazz = deploymentInfo.getClassLoader().loadClass(idMapperSessionUpdaterClass); - Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", DeploymentInfo.class, SessionIdMapper.class, SessionIdMapperUpdater.class); - if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers()) - || ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers()) - || ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) { - LOG.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } - - LOG.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, deploymentInfo, idMapper, previousIdMapperUpdater); - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) { - LOG.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - LOG.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass); - return previousIdMapperUpdater; - } - } - - @Override - protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) { - return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, deployment); - } - - @Override - protected UndertowHttpFacade createFacade(HttpServerExchange exchange) { - return new ServletHttpFacade(exchange); - } - - @Override - protected void redirectLogout(SamlDeployment deployment, HttpServerExchange exchange) { - exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache"); - exchange.getResponseHeaders().add(Headers.EXPIRES, "0"); - - super.redirectLogout(deployment, exchange); - } - - @Override - protected Integer servePage(HttpServerExchange exchange, String location) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ServletRequest req = servletRequestContext.getServletRequest(); - ServletResponse resp = servletRequestContext.getServletResponse(); - RequestDispatcher disp = req.getRequestDispatcher(location); - //make sure the login page is never cached - exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache"); - exchange.getResponseHeaders().add(Headers.EXPIRES, "0"); - - - try { - disp.forward(req, resp); - } catch (ServletException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public SessionIdMapperUpdater getIdMapperUpdater() { - return idMapperUpdater; - } - - protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) { - this.idMapperUpdater = idMapperUpdater; - } -} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java deleted file mode 100755 index 449a876c77bc..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.security.idm.Account; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.SessionManager; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.servlet.spec.HttpSessionImpl; -import org.jboss.logging.Logger; - -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.SamlUtil; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapperUpdater; -import org.keycloak.adapters.undertow.ChangeSessionId; -import org.keycloak.adapters.undertow.SavedRequest; -import org.keycloak.adapters.undertow.ServletHttpFacade; -import org.keycloak.adapters.undertow.UndertowUserSessionManagement; -import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.xml.datatype.DatatypeConstants; -import javax.xml.datatype.XMLGregorianCalendar; -import java.security.Principal; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * Session store manipulation methods per single HTTP exchange. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletSamlSessionStore implements SamlSessionStore { - protected static Logger log = Logger.getLogger(SamlSessionStore.class); - public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI"; - - private final HttpServerExchange exchange; - private final UndertowUserSessionManagement sessionManagement; - private final SecurityContext securityContext; - private final SessionIdMapper idMapper; - private final SessionIdMapperUpdater idMapperUpdater; - protected final SamlDeployment deployment; - - - public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement, - SecurityContext securityContext, - SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater, - SamlDeployment deployment) { - this.exchange = exchange; - this.sessionManagement = sessionManagement; - this.securityContext = securityContext; - this.idMapper = idMapper; - this.deployment = deployment; - this.idMapperUpdater = idMapperUpdater; - } - - @Override - public void setCurrentAction(CurrentAction action) { - if (action == CurrentAction.NONE && getRequest().getSession(false) == null) return; - getRequest().getSession().setAttribute(CURRENT_ACTION, action); - } - - @Override - public boolean isLoggingIn() { - HttpSession session = getRequest().getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_IN; - } - - @Override - public boolean isLoggingOut() { - HttpSession session = getRequest().getSession(false); - if (session == null) return false; - CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION); - return action == CurrentAction.LOGGING_OUT; - } - - @Override - public void logoutAccount() { - HttpSession session = getSession(false); - if (session != null) { - SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - if (samlSession != null) { - if (samlSession.getSessionIndex() != null) { - idMapperUpdater.removeSession(idMapper, session.getId()); - } - session.removeAttribute(SamlSession.class.getName()); - } - session.removeAttribute(SAML_REDIRECT_URI); - } - } - - @Override - public void logoutByPrincipal(String principal) { - Set sessions = idMapper.getUserSessions(principal); - if (sessions != null) { - List ids = new LinkedList<>(); - ids.addAll(sessions); - logoutSessionIds(ids); - for (String id : ids) { - idMapperUpdater.removeSession(idMapper, id); - } - } - - } - - @Override - public void logoutBySsoId(List ssoIds) { - if (ssoIds == null) return; - List sessionIds = new LinkedList<>(); - for (String id : ssoIds) { - String sessionId = idMapper.getSessionFromSSO(id); - if (sessionId != null) { - sessionIds.add(sessionId); - idMapperUpdater.removeSession(idMapper, sessionId); - } - - } - logoutSessionIds(sessionIds); - } - - protected void logoutSessionIds(List sessionIds) { - if (sessionIds == null || sessionIds.isEmpty()) return; - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - SessionManager sessionManager = servletRequestContext.getDeployment().getSessionManager(); - sessionManagement.logoutHttpSessions(sessionManager, sessionIds); - } - - @Override - public boolean isLoggedIn() { - HttpSession session = getSession(false); - if (session == null) { - log.debug("Session was not found"); - return false; - } - - if (! idMapper.hasSession(session.getId()) && ! idMapperUpdater.refreshMapping(idMapper, session.getId())) { - log.debugf("Session %s has expired on some other node", session.getId()); - session.removeAttribute(SamlSession.class.getName()); - return false; - } - - final SamlSession samlSession = SamlUtil.validateSamlSession(session.getAttribute(SamlSession.class.getName()), deployment); - if (samlSession == null) { - return false; - } - - Account undertowAccount = new Account() { - @Override - public Principal getPrincipal() { - return samlSession.getPrincipal(); - } - - @Override - public Set getRoles() { - return samlSession.getRoles(); - } - }; - securityContext.authenticationComplete(undertowAccount, "KEYCLOAK-SAML", false); - restoreRequest(); - return true; - } - - @Override - public void saveAccount(SamlSession account) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSession session = getSession(true); - session.setAttribute(SamlSession.class.getName(), account); - sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); - String sessionId = changeSessionId(session); - idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); - } - - protected String changeSessionId(HttpSession session) { - if (!deployment.turnOffChangeSessionIdOnLogin()) return ChangeSessionId.changeSessionId(exchange, false); - else return session.getId(); - } - - @Override - public SamlSession getAccount() { - HttpSession session = getSession(true); - return (SamlSession)session.getAttribute(SamlSession.class.getName()); - } - - @Override - public String getRedirectUri() { - final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSessionImpl session = sc.getCurrentServletContext().getSession(exchange, true); - String redirect = (String)session.getAttribute(SAML_REDIRECT_URI); - if (redirect == null) { - ServletHttpFacade facade = new ServletHttpFacade(exchange); - HttpServletRequest req = (HttpServletRequest)sc.getServletRequest(); - String contextPath = req.getContextPath(); - String baseUri = KeycloakUriBuilder.fromUri(req.getRequestURL().toString()).replacePath(contextPath).build().toString(); - return SamlUtil.getRedirectTo(facade, contextPath, baseUri); - } - return redirect; - } - - @Override - public void saveRequest() { - SavedRequest.trySaveRequest(exchange); - final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSessionImpl session = sc.getCurrentServletContext().getSession(exchange, true); - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getRequestURI()) - .replaceQuery(exchange.getQueryString()); - if (!exchange.isHostIncludedInRequestURI()) uriBuilder.scheme(exchange.getRequestScheme()).host(exchange.getHostAndPort()); - String uri = uriBuilder.buildAsString(); - - session.setAttribute(SAML_REDIRECT_URI, uri); - - } - - @Override - public boolean restoreRequest() { - HttpSession session = getSession(false); - if (session == null) return false; - SavedRequest.tryRestoreRequest(exchange, session); - session.removeAttribute(SAML_REDIRECT_URI); - return false; - } - - protected HttpSession getSession(boolean create) { - HttpServletRequest req = getRequest(); - return req.getSession(create); - } - - private HttpServletResponse getResponse() { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - return (HttpServletResponse)servletRequestContext.getServletResponse(); - - } - - private HttpServletRequest getRequest() { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - return (HttpServletRequest) servletRequestContext.getServletRequest(); - } -} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlAuthenticator.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlAuthenticator.java deleted file mode 100755 index 40fa1d6bada5..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlAuthenticator.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import io.undertow.security.api.SecurityContext; -import io.undertow.security.idm.Account; -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.BrowserHandler; -import org.keycloak.adapters.spi.HttpFacade; - -import java.security.Principal; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowSamlAuthenticator extends SamlAuthenticator { - protected SecurityContext securityContext; - - public UndertowSamlAuthenticator(SecurityContext securityContext, HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - super(facade, deployment, sessionStore); - this.securityContext = securityContext; - } - - @Override - protected void completeAuthentication(final SamlSession samlSession) { - Account undertowAccount = new Account() { - @Override - public Principal getPrincipal() { - return samlSession.getPrincipal(); - } - - @Override - public Set getRoles() { - return samlSession.getRoles(); - } - }; - securityContext.authenticationComplete(undertowAccount, "KEYCLOAK-SAML", false); - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new BrowserHandler(facade, deployment, sessionStore); - } - -} diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlEndpoint.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlEndpoint.java deleted file mode 100755 index 3f5cc1a64992..000000000000 --- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/UndertowSamlEndpoint.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 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.adapters.saml.undertow; - -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; -import org.keycloak.adapters.saml.profile.webbrowsersso.SamlEndpoint; -import org.keycloak.adapters.spi.HttpFacade; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowSamlEndpoint extends SamlAuthenticator { - public UndertowSamlEndpoint(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - super(facade, deployment, sessionStore); - } - - - - @Override - protected void completeAuthentication(SamlSession samlSession) { - - } - - @Override - protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - return new SamlEndpoint(facade, deployment, sessionStore); - } -} diff --git a/adapters/saml/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/adapters/saml/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension deleted file mode 100644 index f385fd032896..000000000000 --- a/adapters/saml/undertow/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2018 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. -# - -org.keycloak.adapters.saml.undertow.SamlServletExtension diff --git a/adapters/saml/wildfly-elytron-jakarta/pom.xml b/adapters/saml/wildfly-elytron-jakarta/pom.xml deleted file mode 100755 index b3a496978ee5..000000000000 --- a/adapters/saml/wildfly-elytron-jakarta/pom.xml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-saml-wildfly-elytron-jakarta-adapter - Keycloak WildFly Elytron Jakarta SAML Adapter - - - - - ${project.basedir}/../wildfly-elytron/src - ${project.basedir}/src - - - - - org.keycloak - keycloak-adapter-core - provided - - - org.keycloak - keycloak-saml-core - provided - - - org.keycloak - keycloak-adapter-spi - provided - - - org.keycloak - keycloak-common - provided - - - org.keycloak - keycloak-saml-adapter-api-public - provided - - - org.keycloak - keycloak-saml-adapter-core-jakarta - provided - - - org.jboss.logging - jboss-logging - provided - - - jakarta.servlet - jakarta.servlet-api - provided - - - org.wildfly.security - wildfly-elytron - - - org.wildfly.security.elytron-web - undertow-server - provided - - - org.infinispan - infinispan-core - - - org.infinispan - infinispan-cachestore-remote - - - junit - junit - test - - - - - - - maven-antrun-plugin - 3.0.0 - - - transform - initialize - - run - - - - - - - - - - - - - - - - - - - org.eclipse.transformer - org.eclipse.transformer.cli - 0.2.0 - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - - - - - diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml index 5dfaf1bb6e32..56c7c05de32a 100755 --- a/adapters/saml/wildfly-elytron/pom.xml +++ b/adapters/saml/wildfly-elytron/pom.xml @@ -31,11 +31,6 @@ - - org.keycloak - keycloak-adapter-core - provided - org.keycloak keycloak-saml-core @@ -67,8 +62,8 @@ provided - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec + jakarta.servlet + jakarta.servlet-api provided diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java index 9926237f22dd..be12df83d80f 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java @@ -22,11 +22,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -226,11 +226,7 @@ public String getMethod() { @Override public String getURI() { if (elyweb163Workaround) { - try { - return URLDecoder.decode(request.getRequestURI().toString(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Failed to decode request URI", e); - } + return URLDecoder.decode(request.getRequestURI().toString(), StandardCharsets.UTF_8); } else { return request.getRequestURI().toString(); } @@ -261,11 +257,7 @@ public String getQueryParamValue(String param) { for (String parameter : parameters) { String[] keyValue = parameter.split("=", 2); if (keyValue[0].equals(param)) { - try { - return URLDecoder.decode(keyValue[1], "UTF-8"); - } catch (IOException e) { - throw new RuntimeException("Failed to decode request URI", e); - } + return URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); } } } diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java index 26dc328477d8..6dd6f0e7e377 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java @@ -31,13 +31,9 @@ import org.keycloak.adapters.spi.SessionIdMapper; import org.keycloak.adapters.spi.SessionIdMapperUpdater; import org.keycloak.common.util.KeycloakUriBuilder; -import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.Scope; -import javax.xml.datatype.DatatypeConstants; -import javax.xml.datatype.XMLGregorianCalendar; - /** * @author Bill Burke * @version $Revision: 1 $ @@ -174,8 +170,12 @@ public void saveAccount(SamlSession account) { } protected String changeSessionId(HttpScope session) { - if (!deployment.turnOffChangeSessionIdOnLogin()) return session.getID(); - else return session.getID(); + if (!deployment.turnOffChangeSessionIdOnLogin()) { + if (!session.supportsChangeID() || !session.changeID()) { + log.debug("Session ID cannot be changed although turnOffChangeSessionIdOnLogin is set to false"); + } + } + return session.getID(); } @Override diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java index d65d74a3085f..91abdb851456 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/IdMapperUpdaterSessionListener.java @@ -20,18 +20,19 @@ import org.keycloak.adapters.spi.SessionIdMapper; import java.util.Objects; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionBindingEvent; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; import org.jboss.logging.Logger; /** * * @author hmlnarik */ -public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener { +public class IdMapperUpdaterSessionListener implements HttpSessionListener, HttpSessionAttributeListener, HttpSessionIdListener { private static final Logger LOG = Logger.getLogger(IdMapperUpdaterSessionListener.class); @@ -56,6 +57,15 @@ public void sessionDestroyed(HttpSessionEvent hse) { unmap(session.getId(), session.getAttribute(SamlSession.class.getName())); } + @Override + public void sessionIdChanged(HttpSessionEvent hse, String oldSessionId) { + LOG.debugf("Session changed ID from %s", oldSessionId); + HttpSession session = hse.getSession(); + Object value = session.getAttribute(SamlSession.class.getName()); + unmap(oldSessionId, value); + map(session.getId(), value); + } + @Override public void attributeAdded(HttpSessionBindingEvent hsbe) { HttpSession session = hsbe.getSession(); diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java index 5ece449657bb..4fc84e870684 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java @@ -22,9 +22,9 @@ import java.io.FileNotFoundException; import java.io.InputStream; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import org.jboss.logging.Logger; import org.keycloak.adapters.saml.AdapterConstants; @@ -156,14 +156,14 @@ private static InputStream getXMLFromServletContext(ServletContext servletContex public void addTokenStoreUpdaters(ServletContext servletContext) { SessionIdMapperUpdater updater = this.idMapperUpdater; + servletContext.addListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally + try { String idMapperSessionUpdaterClasses = servletContext.getInitParameter("keycloak.sessionIdMapperUpdater.classes"); if (idMapperSessionUpdaterClasses == null) { return; } - servletContext.addListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally - updater = SessionIdMapperUpdater.DIRECT; for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) { diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java index 5f9beb653440..2d991dd82dbe 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; import javax.security.auth.callback.CallbackHandler; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.jboss.logging.Logger; import org.keycloak.adapters.saml.SamlAuthenticator; @@ -159,7 +159,7 @@ protected void redirectLogout(SamlDeployment deployment, ElytronHttpFacade excha } private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:"); - + static void sendRedirect(final ElytronHttpFacade exchange, final String location) { if (location == null) { LOGGER.warn("Logout page not set."); diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java index a6ac4fe6cd15..e18b65ed6f62 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/infinispan/InfinispanSessionCacheIdMapperUpdater.java @@ -16,21 +16,21 @@ */ package org.keycloak.adapters.saml.elytron.infinispan; -import org.keycloak.adapters.saml.AdapterConstants; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapperUpdater; - -import javax.naming.InitialContext; -import javax.naming.NamingException; -import javax.servlet.ServletContext; import org.infinispan.Cache; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; +import org.infinispan.factories.ComponentRegistry; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.persistence.manager.PersistenceManager; import org.infinispan.persistence.remote.RemoteStore; import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.AdapterConstants; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapperUpdater; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import jakarta.servlet.ServletContext; import java.util.Set; /** @@ -94,21 +94,19 @@ public static SessionIdMapperUpdater addTokenStoreUpdaters(ServletContext servle } Cache ssoCache = cacheManager.getCache(cacheName, true); - final SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper); + SsoSessionCacheListener listener = new SsoSessionCacheListener(ssoCache, mapper); ssoCache.addListener(listener); addSsoCacheCrossDcListener(ssoCache, listener); LOG.debugv("Added distributed SSO session cache, lookup={0}, cache name={1}", cacheContainerLookup, cacheName); - SsoCacheSessionIdMapperUpdater updater = new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater) { + return new SsoCacheSessionIdMapperUpdater(ssoCache, previousIdMapperUpdater) { @Override - public void close() throws Exception { + public void close() { ssoCache.stop(); } }; - - return updater; } catch (NamingException ex) { LOG.warnv("Failed to obtain distributed session cache container, lookup={0}", cacheContainerLookup); return previousIdMapperUpdater; @@ -137,7 +135,7 @@ private static void addSsoCacheCrossDcListener(Cache ssoCache, return; } - final Set stores = getRemoteStores(ssoCache); + Set stores = getRemoteStores(ssoCache); if (stores == null || stores.isEmpty()) { return; } @@ -149,7 +147,7 @@ private static void addSsoCacheCrossDcListener(Cache ssoCache, } } - public static Set getRemoteStores(Cache ispnCache) { - return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class); + public static Set getRemoteStores(Cache ispnCache) { + return ComponentRegistry.componentOf(ispnCache, PersistenceManager.class).getStores(RemoteStore.class); } } diff --git a/adapters/saml/wildfly/pom.xml b/adapters/saml/wildfly/pom.xml index 02f49d830727..f1b3048e711c 100755 --- a/adapters/saml/wildfly/pom.xml +++ b/adapters/saml/wildfly/pom.xml @@ -32,6 +32,5 @@ wildfly-subsystem - wildfly-jakarta-subsystem diff --git a/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml b/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml deleted file mode 100755 index 15e52a641515..000000000000 --- a/adapters/saml/wildfly/wildfly-jakarta-subsystem/pom.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - 4.0.0 - - - org.keycloak - keycloak-parent - 999.0.0-SNAPSHOT - ../../../../pom.xml - - - keycloak-saml-wildfly-jakarta-subsystem - Keycloak Wildfly Jakarta SAML Adapter Subsystem - - jar - - - - ${project.basedir}/../wildfly-subsystem/src - ${project.basedir}/src - - - - - - maven-antrun-plugin - 3.0.0 - - - transform - initialize - - run - - - - - - - - - - - - - - - - - - - org.eclipse.transformer - org.eclipse.transformer.cli - 0.2.0 - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - true - - - jboss.home - ${jboss.home} - - - - **/*TestCase.java - - - - - - - - - org.wildfly.core - wildfly-controller - provided - - - org.wildfly.core - wildfly-server - provided - - - ${ee.maven.groupId} - wildfly-web-common - provided - - - org.jboss.logging - jboss-logging-annotations - - provided - true - - - - org.jboss.logging - jboss-logging-processor - - provided - true - - - - org.wildfly.common - wildfly-common - ${wildfly.common.wildfly.aligned.version} - - - - org.wildfly.core - wildfly-subsystem-test-framework - test - - - junit - junit - test - - - org.keycloak - keycloak-saml-adapter-core-jakarta - ${project.version} - - - org.keycloak - keycloak-saml-wildfly-elytron-jakarta-adapter - ${project.version} - - - diff --git a/adapters/saml/wildfly/wildfly-subsystem/.gitignore b/adapters/saml/wildfly/wildfly-subsystem/.gitignore new file mode 100644 index 000000000000..00d2ab71ddbd --- /dev/null +++ b/adapters/saml/wildfly/wildfly-subsystem/.gitignore @@ -0,0 +1,2 @@ +/.apt_generated/ +/.apt_generated_tests/ diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessor.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessor.java index 5fc44267b252..c361e9ac2f0d 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessor.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessor.java @@ -63,7 +63,10 @@ public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitPro // Next phase, need to detect if this is a Keycloak deployment. If not, don't add the modules. final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION); - final ModuleLoader moduleLoader = Module.getBootModuleLoader(); + ModuleLoader moduleLoader = Module.getCallerModuleLoader(); + if (moduleLoader == null) { + moduleLoader = Module.getSystemModuleLoader(); + } addCoreModules(moduleSpecification, moduleLoader); addCommonModules(moduleSpecification, moduleLoader); diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessorWildFly.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessorWildFly.java index de0b39b482a2..f535ee078cef 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessorWildFly.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessorWildFly.java @@ -33,27 +33,18 @@ */ public class KeycloakDependencyProcessorWildFly extends KeycloakDependencyProcessor { - private static final ModuleIdentifier KEYCLOAK_CORE_JAKARTA_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-saml-adapter-core-jakarta"); + private static final ModuleIdentifier KEYCLOAK_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-saml-adapter-core"); private static final ModuleIdentifier KEYCLOAK_ELYTRON_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-saml-wildfly-elytron-adapter"); - private static final ModuleIdentifier KEYCLOAK_ELYTRON_JAKARTA_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-saml-wildfly-elytron-jakarta-adapter"); @Override protected void addCoreModules(ModuleSpecification moduleSpecification, ModuleLoader moduleLoader) { - if (isJakarta()) { - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_JAKARTA_ADAPTER, false, false, false, false)); - } else { - super.addCoreModules(moduleSpecification, moduleLoader); - } + moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_ADAPTER, false, false, false, false)); } @Override protected void addPlatformSpecificModules(DeploymentPhaseContext phaseContext, ModuleSpecification moduleSpecification, ModuleLoader moduleLoader) { if (isElytronEnabled(phaseContext)) { - if (isJakarta()) { - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_ELYTRON_JAKARTA_ADAPTER, true, false, false, false)); - } else { - moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_ELYTRON_ADAPTER, true, false, false, false)); - } + moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_ELYTRON_ADAPTER, true, false, false, false)); } else { throw new RuntimeException("Legacy WildFly security layer is no longer supported by the Keycloak WildFly adapter"); } diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_3.xsd b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_3.xsd index 48562b66b657..be4a28b58b0e 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_3.xsd +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_3.xsd @@ -531,7 +531,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_4.xsd b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_4.xsd index 9150f7a62fd4..52defbf2dc21 100644 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_4.xsd +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_4.xsd @@ -531,7 +531,7 @@ The value is the file path to a keystore file. If you prefix the path with classpath:, then the truststore will be obtained from the deployment's classpath instead. Used for outgoing HTTPS communications to the IDP server. Client making HTTPS requests need - a way to verify the host of the server they are talking to. This is what the trustore does. + a way to verify the host of the server they are talking to. This is what the truststore does. The keystore contains one or more trusted host certificates or certificate authorities. You can create this truststore by extracting the public certificate of the IDP's SSL keystore. diff --git a/adapters/spi/adapter-spi/pom.xml b/adapters/spi/adapter-spi/pom.xml index aab344d284be..8c2206c57dd9 100755 --- a/adapters/spi/adapter-spi/pom.xml +++ b/adapters/spi/adapter-spi/pom.xml @@ -31,13 +31,8 @@ - - org.keycloak.adapters.spi.* - - - org.keycloak.*;version="${project.version}", - *;resolution:=optional - + + 11 @@ -57,41 +52,4 @@ test - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - diff --git a/adapters/spi/jakarta-servlet-adapter-spi/pom.xml b/adapters/spi/jakarta-servlet-adapter-spi/pom.xml deleted file mode 100755 index a5d8d48e4729..000000000000 --- a/adapters/spi/jakarta-servlet-adapter-spi/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-jakarta-servlet-adapter-spi - Keycloak Jakarta Servlet Integration - - - - - org.keycloak.adapters.servlet.* - - - *;resolution:=optional - - ${project.groupId}.keycloak-jakarta-servlet-filter-adapter - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-common - - - jakarta.servlet - jakarta.servlet-api - provided - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - ${keycloak.osgi.fragment} - - - - - - - diff --git a/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java b/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java deleted file mode 100755 index 2de0245eb91c..000000000000 --- a/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.common.util.Encode; -import org.keycloak.common.util.MultivaluedHashMap; - -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletInputStream; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; -import jakarta.servlet.http.HttpSession; -import jakarta.servlet.ReadListener; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.Principal; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class FilterSessionStore implements AdapterSessionStore { - public static final String REDIRECT_URI = "__REDIRECT_URI"; - public static final String SAVED_METHOD = "__SAVED_METHOD"; - public static final String SAVED_HEADERS = "__SAVED_HEADERS"; - public static final String SAVED_BODY = "__SAVED_BODY"; - protected final HttpServletRequest request; - protected final HttpFacade facade; - protected final int maxBuffer; - protected byte[] restoredBuffer = null; - protected boolean needRequestRestore; - - public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) { - this.request = request; - this.facade = facade; - this.maxBuffer = maxBuffer; - } - - public void clearSavedRequest(HttpSession session) { - session.removeAttribute(REDIRECT_URI); - session.removeAttribute(SAVED_METHOD); - session.removeAttribute(SAVED_HEADERS); - session.removeAttribute(SAVED_BODY); - } - - public void servletRequestLogout() { - - } - - public static String getCharsetFromContentType(String contentType) { - - if (contentType == null) - return (null); - int start = contentType.indexOf("charset="); - if (start < 0) - return (null); - String encoding = contentType.substring(start + 8); - int end = encoding.indexOf(';'); - if (end >= 0) - encoding = encoding.substring(0, end); - encoding = encoding.trim(); - if ((encoding.length() > 2) && (encoding.startsWith("\"")) - && (encoding.endsWith("\""))) - encoding = encoding.substring(1, encoding.length() - 1); - return (encoding.trim()); - - } - - - public HttpServletRequestWrapper buildWrapper(HttpSession session, final KeycloakAccount account) { - if (needRequestRestore) { - final String method = (String)session.getAttribute(SAVED_METHOD); - final byte[] body = (byte[])session.getAttribute(SAVED_BODY); - final MultivaluedHashMap headers = (MultivaluedHashMap)session.getAttribute(SAVED_HEADERS); - clearSavedRequest(session); - HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { - protected MultivaluedHashMap parameters; - - MultivaluedHashMap getParams() { - if (parameters != null) return parameters; - - if (body == null) return new MultivaluedHashMap(); - - String contentType = getContentType(); - if (contentType != null && contentType.toLowerCase().startsWith("application/x-www-form-urlencoded")) { - ByteArrayInputStream is = new ByteArrayInputStream(body); - try { - parameters = parseForm(is); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return parameters; - - } - @Override - public boolean isUserInRole(String role) { - return account.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - return account.getPrincipal(); - } - - @Override - public String getMethod() { - if (needRequestRestore) { - return method; - } else { - return super.getMethod(); - - } - } - - @Override - public String getHeader(String name) { - if (needRequestRestore && headers != null) { - return headers.getFirst(name.toLowerCase()); - } - return super.getHeader(name); - } - - @Override - public Enumeration getHeaders(String name) { - if (needRequestRestore && headers != null) { - List values = headers.getList(name.toLowerCase()); - if (values == null) return Collections.emptyEnumeration(); - else return Collections.enumeration(values); - } - return super.getHeaders(name); - } - - @Override - public Enumeration getHeaderNames() { - if (needRequestRestore && headers != null) { - return Collections.enumeration(headers.keySet()); - } - return super.getHeaderNames(); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - if (needRequestRestore && body != null) { - final ByteArrayInputStream is = new ByteArrayInputStream(body); - return new ServletInputStream() { - @Override - public int read() throws IOException { - return is.read(); - } - - @Override - public boolean isFinished() { - return is.available() == 0; // Check if the underlying stream has data available. - } - - @Override - public boolean isReady() { - return true; // Return true to indicate that the data is always ready to be read. - } - - @Override - public void setReadListener(ReadListener readListener) { - throw new UnsupportedOperationException(); - } - }; - } - return super.getInputStream(); - } - - @Override - public void logout() throws ServletException { - servletRequestLogout(); - } - - @Override - public long getDateHeader(String name) { - if (!needRequestRestore) return super.getDateHeader(name); - return -1; - } - - @Override - public int getIntHeader(String name) { - if (!needRequestRestore) return super.getIntHeader(name); - String value = getHeader(name); - if (value == null) return -1; - return Integer.valueOf(value); - - } - - @Override - public String[] getParameterValues(String name) { - if (!needRequestRestore) return super.getParameterValues(name); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterValues(name); - } - String[] values = request.getParameterValues(name); - List list = new LinkedList<>(); - if (values != null) { - for (String val : values) list.add(val); - } - List vals = formParams.get(name); - if (vals != null) list.addAll(vals); - return list.toArray(new String[list.size()]); - } - - @Override - public Enumeration getParameterNames() { - if (!needRequestRestore) return super.getParameterNames(); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterNames(); - } - Set names = new HashSet<>(); - Enumeration qnames = super.getParameterNames(); - while (qnames.hasMoreElements()) names.add(qnames.nextElement()); - names.addAll(formParams.keySet()); - return Collections.enumeration(names); - - } - - @Override - public Map getParameterMap() { - if (!needRequestRestore) return super.getParameterMap(); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterMap(); - } - Map map = new HashMap<>(); - Enumeration names = getParameterNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - String[] values = getParameterValues(name); - if (values != null) { - map.put(name, values); - } - } - return map; - } - - @Override - public String getParameter(String name) { - if (!needRequestRestore) return super.getParameter(name); - String param = super.getParameter(name); - if (param != null) return param; - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return null; - } - return formParams.getFirst(name); - - } - - @Override - public BufferedReader getReader() throws IOException { - if (!needRequestRestore) return super.getReader(); - return new BufferedReader(new InputStreamReader(getInputStream())); - } - - @Override - public int getContentLength() { - if (!needRequestRestore) return super.getContentLength(); - String header = getHeader("content-length"); - if (header == null) return -1; - return Integer.valueOf(header); - } - - @Override - public String getContentType() { - if (!needRequestRestore) return super.getContentType(); - return getHeader("content-type"); - } - - @Override - public String getCharacterEncoding() { - if (!needRequestRestore) return super.getCharacterEncoding(); - return getCharsetFromContentType(getContentType()); - } - - }; - return wrapper; - } else { - return new HttpServletRequestWrapper(request) { - @Override - public boolean isUserInRole(String role) { - return account.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - if (account == null) return null; - return account.getPrincipal(); - } - - @Override - public void logout() throws ServletException { - servletRequestLogout(); - } - - - }; - } - } - - public String getRedirectUri() { - HttpSession session = request.getSession(true); - return (String)session.getAttribute(REDIRECT_URI); - } - - @Override - public boolean restoreRequest() { - HttpSession session = request.getSession(false); - if (session == null) return false; - return session.getAttribute(REDIRECT_URI) != null; - } - - public static MultivaluedHashMap parseForm(InputStream entityStream) - throws IOException - { - char[] buffer = new char[100]; - StringBuffer buf = new StringBuffer(); - BufferedReader reader = new BufferedReader(new InputStreamReader(entityStream)); - - int wasRead = 0; - do - { - wasRead = reader.read(buffer, 0, 100); - if (wasRead > 0) buf.append(buffer, 0, wasRead); - } while (wasRead > -1); - - String form = buf.toString(); - - MultivaluedHashMap formData = new MultivaluedHashMap(); - if ("".equals(form)) return formData; - - String[] params = form.split("&"); - - for (String param : params) - { - if (param.indexOf('=') >= 0) - { - String[] nv = param.split("="); - String val = nv.length > 1 ? nv[1] : ""; - formData.add(Encode.decode(nv[0]), Encode.decode(val)); - } - else - { - formData.add(Encode.decode(param), ""); - } - } - return formData; - } - - - - @Override - public void saveRequest() { - HttpSession session = request.getSession(true); - session.setAttribute(REDIRECT_URI, facade.getRequest().getURI()); - session.setAttribute(SAVED_METHOD, request.getMethod()); - MultivaluedHashMap headers = new MultivaluedHashMap<>(); - Enumeration names = request.getHeaderNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - Enumeration values = request.getHeaders(name); - while (values.hasMoreElements()) { - headers.add(name.toLowerCase(), values.nextElement()); - } - } - session.setAttribute(SAVED_HEADERS, headers); - if (request.getMethod().equalsIgnoreCase("GET")) { - return; - } - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - byte[] buffer = new byte[4096]; - int bytesRead; - int totalRead = 0; - try { - InputStream is = request.getInputStream(); - - while ( (bytesRead = is.read(buffer) ) >= 0) { - os.write(buffer, 0, bytesRead); - totalRead += bytesRead; - if (totalRead > maxBuffer) { - throw new RuntimeException("max buffer reached on a saved request"); - } - - } - } catch (IOException e) { - throw new RuntimeException(e); - } - byte[] body = os.toByteArray(); - // Only save the request body if there is something to save - if (body.length > 0) { - session.setAttribute(SAVED_BODY, body); - } - - - } - -} diff --git a/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java deleted file mode 100755 index b3d30f45b0cb..000000000000 --- a/adapters/spi/jakarta-servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.ServerCookie; -import org.keycloak.common.util.UriUtils; - -import javax.security.cert.X509Certificate; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Enumeration; -import java.util.LinkedList; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletHttpFacade implements HttpFacade { - protected final RequestFacade requestFacade = new RequestFacade(); - protected final ResponseFacade responseFacade = new ResponseFacade(); - protected HttpServletRequest request; - protected HttpServletResponse response; - protected MultivaluedHashMap queryParameters; - - public ServletHttpFacade(HttpServletRequest request, HttpServletResponse response) { - this.request = request; - this.response = response; - } - - protected class RequestFacade implements Request { - - private InputStream inputStream; - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getURI() { - StringBuffer buf = request.getRequestURL(); - if (request.getQueryString() != null) { - buf.append('?').append(request.getQueryString()); - } - return buf.toString(); - } - - @Override - public String getRelativePath() { - String uri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String servletPath = uri.substring(uri.indexOf(contextPath) + contextPath.length()); - - if ("".equals(servletPath)) { - servletPath = "/"; - } - - return servletPath; - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public String getQueryParamValue(String param) { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters.getFirst(param); - } - - public MultivaluedHashMap getQueryParameters() { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters; - } - - @Override - public Cookie getCookie(String cookieName) { - if (request.getCookies() == null) return null; - jakarta.servlet.http.Cookie cookie = null; - for (jakarta.servlet.http.Cookie c : request.getCookies()) { - if (c.getName().equals(cookieName)) { - cookie = c; - break; - } - } - if (cookie == null) return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public List getHeaders(String name) { - Enumeration values = request.getHeaders(name); - List list = new LinkedList<>(); - while (values.hasMoreElements()) list.add(values.nextElement()); - return list; - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - } - public boolean isEnded() { - return responseFacade.isEnded(); - } - - protected class ResponseFacade implements Response { - protected boolean ended; - - @Override - public void setStatus(int status) { - response.setStatus(status); - } - - @Override - public void addHeader(String name, String value) { - response.addHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public void resetCookie(String name, String path) { - setCookie(name, "", path, null, 0, false, false); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - StringBuilder cookieBuf = new StringBuilder(); - ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null); - String cookie = cookieBuf.toString(); - response.addHeader("Set-Cookie", cookie); - } - - @Override - public OutputStream getOutputStream() { - try { - return response.getOutputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void end() { - ended = true; - } - - public boolean isEnded() { - return ended; - } - } - - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - throw new IllegalStateException("Not supported yet"); - } -} diff --git a/adapters/spi/jboss-adapter-core/pom.xml b/adapters/spi/jboss-adapter-core/pom.xml index a184ae5e0d8a..6978a6d20c9b 100755 --- a/adapters/spi/jboss-adapter-core/pom.xml +++ b/adapters/spi/jboss-adapter-core/pom.xml @@ -30,6 +30,11 @@ Common JBoss/Wildfly Core Classes + + + 11 + + org.jboss.logging diff --git a/adapters/spi/jetty-adapter-spi/pom.xml b/adapters/spi/jetty-adapter-spi/pom.xml deleted file mode 100755 index 17a0654fd008..000000000000 --- a/adapters/spi/jetty-adapter-spi/pom.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-jetty-adapter-spi - Keycloak Jetty Adapter SPI - - 8.1.17.v20150415 - - org.keycloak.adapters.jetty.spi.* - - - org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional, - javax.servlet.*;version="[2.5,4)";resolution:=optional, - org.keycloak.*;version="${project.version}", - *;resolution:=optional - - - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-common - - - org.keycloak - keycloak-adapter-spi - - - org.eclipse.jetty - jetty-server - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-util - ${jetty9.version} - provided - - - - org.eclipse.jetty - jetty-security - ${jetty9.version} - provided - - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - - - - diff --git a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java deleted file mode 100755 index 5e822edfce3b..000000000000 --- a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.spi; - -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.UriUtils; - -import javax.security.cert.X509Certificate; -import javax.servlet.http.HttpServletResponse; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettyHttpFacade implements HttpFacade { - public final static String __J_METHOD = "org.eclipse.jetty.security.HTTP_METHOD"; - protected org.eclipse.jetty.server.Request request; - protected HttpServletResponse response; - protected RequestFacade requestFacade = new RequestFacade(); - protected ResponseFacade responseFacade = new ResponseFacade(); - protected MultivaluedHashMap queryParameters; - - public JettyHttpFacade(org.eclipse.jetty.server.Request request, HttpServletResponse response) { - this.request = request; - this.response = response; - } - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - throw new IllegalStateException("Not supported yet"); - } - - public boolean isEnded() { - return responseFacade.isEnded(); - } - - protected class RequestFacade implements Request { - - private InputStream inputStream; - - @Override - public String getURI() { - StringBuffer buf = request.getRequestURL(); - if (request.getQueryString() != null) { - buf.append('?').append(request.getQueryString()); - } - return buf.toString(); - } - - @Override - public String getRelativePath() { - return request.getServletPath() + (request.getPathInfo() != null ? request.getPathInfo() : ""); - } - - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getQueryParamValue(String paramName) { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters.getFirst(paramName); - } - - @Override - public Cookie getCookie(String cookieName) { - if (request.getCookies() == null) return null; - javax.servlet.http.Cookie cookie = null; - for (javax.servlet.http.Cookie c : request.getCookies()) { - if (c.getName().equals(cookieName)) { - cookie = c; - break; - } - } - if (cookie == null) return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public List getHeaders(String name) { - Enumeration headers = request.getHeaders(name); - if (headers == null) return null; - List list = new ArrayList(); - while (headers.hasMoreElements()) { - list.add(headers.nextElement()); - } - return list; - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - - } - - protected class ResponseFacade implements Response { - protected boolean ended; - - @Override - public void setStatus(int status) { - response.setStatus(status); - } - - @Override - public void addHeader(String name, String value) { - response.addHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public void resetCookie(String name, String path) { - setCookie(name, "", path, null, 0, false, false); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(name, value); - if (domain != null) cookie.setDomain(domain); - if (path != null) cookie.setPath(path); - if (secure) cookie.setSecure(true); - if (httpOnly) cookie.setHttpOnly(httpOnly); - cookie.setMaxAge(maxAge); - response.addCookie(cookie); - } - - @Override - public OutputStream getOutputStream() { - try { - return response.getOutputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void end() { - ended = true; - } - - public boolean isEnded() { - return ended; - } - } -} diff --git a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettySessionManager.java b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettySessionManager.java deleted file mode 100644 index 645ce1f48c79..000000000000 --- a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettySessionManager.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.spi; - -import javax.servlet.http.HttpSession; - -/** - * @author Marek Posolda - */ -public interface JettySessionManager { - - public HttpSession getHttpSession(String id); -} diff --git a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyUserSessionManagement.java b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyUserSessionManagement.java deleted file mode 100755 index 55ebd32c9281..000000000000 --- a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyUserSessionManagement.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.spi; - -import org.jboss.logging.Logger; -import org.keycloak.adapters.spi.UserSessionManagement; - -import javax.servlet.http.HttpSession; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class JettyUserSessionManagement implements UserSessionManagement { - private static final org.jboss.logging.Logger log = Logger.getLogger(JettyUserSessionManagement.class); - protected JettySessionManager sessionManager; - - public JettyUserSessionManagement(JettySessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - @Override - public void logoutAll() { - // todo not implemented yet - } - - @Override - public void logoutHttpSessions(List ids) { - log.trace("---> logoutHttpSessions"); - for (String id : ids) { - HttpSession httpSession = sessionManager.getHttpSession(id); - if (httpSession != null) httpSession.invalidate(); - } - - } -} diff --git a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/WrappingSessionHandler.java b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/WrappingSessionHandler.java deleted file mode 100644 index 17dda1bdaaa5..000000000000 --- a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/WrappingSessionHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 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.adapters.jetty.spi; - -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.SessionManager; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.server.session.SessionHandler; - -/** - * @author Marek Posolda - */ -public class WrappingSessionHandler extends SessionHandler { - - public WrappingSessionHandler() { - super(); - } - - public WrappingSessionHandler(SessionManager mgr) { - super(mgr); - } - - @Override - public void setHandler(Handler handler) { - if (getHandler() != null && getHandler() instanceof HandlerWrapper) { - HandlerWrapper wrappedHandler = (HandlerWrapper) getHandler(); - wrappedHandler.setHandler(handler); - } else { - super.setHandler(handler); - } - } -} diff --git a/adapters/spi/pom.xml b/adapters/spi/pom.xml index 6bd05d9b412a..cdfe1b127ff3 100755 --- a/adapters/spi/pom.xml +++ b/adapters/spi/pom.xml @@ -32,11 +32,6 @@ adapter-spi - tomcat-adapter-spi - undertow-adapter-spi - servlet-adapter-spi - jakarta-servlet-adapter-spi jboss-adapter-core - jetty-adapter-spi diff --git a/adapters/spi/servlet-adapter-spi/pom.xml b/adapters/spi/servlet-adapter-spi/pom.xml deleted file mode 100755 index 2d8f0ca1f6dd..000000000000 --- a/adapters/spi/servlet-adapter-spi/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-servlet-adapter-spi - Keycloak Servlet Integration - - - - - org.keycloak.adapters.servlet.* - - - *;resolution:=optional - - ${project.groupId}.keycloak-servlet-filter-adapter - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-common - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - ${keycloak.osgi.fragment} - - - - - - - diff --git a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java deleted file mode 100755 index f2b5a47a34f2..000000000000 --- a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.adapters.spi.AdapterSessionStore; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.KeycloakAccount; -import org.keycloak.common.util.Encode; -import org.keycloak.common.util.MultivaluedHashMap; - -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpSession; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.Principal; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class FilterSessionStore implements AdapterSessionStore { - public static final String REDIRECT_URI = "__REDIRECT_URI"; - public static final String SAVED_METHOD = "__SAVED_METHOD"; - public static final String SAVED_HEADERS = "__SAVED_HEADERS"; - public static final String SAVED_BODY = "__SAVED_BODY"; - protected final HttpServletRequest request; - protected final HttpFacade facade; - protected final int maxBuffer; - protected byte[] restoredBuffer = null; - protected boolean needRequestRestore; - - public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) { - this.request = request; - this.facade = facade; - this.maxBuffer = maxBuffer; - } - - public void clearSavedRequest(HttpSession session) { - session.removeAttribute(REDIRECT_URI); - session.removeAttribute(SAVED_METHOD); - session.removeAttribute(SAVED_HEADERS); - session.removeAttribute(SAVED_BODY); - } - - public void servletRequestLogout() { - - } - - public static String getCharsetFromContentType(String contentType) { - - if (contentType == null) - return (null); - int start = contentType.indexOf("charset="); - if (start < 0) - return (null); - String encoding = contentType.substring(start + 8); - int end = encoding.indexOf(';'); - if (end >= 0) - encoding = encoding.substring(0, end); - encoding = encoding.trim(); - if ((encoding.length() > 2) && (encoding.startsWith("\"")) - && (encoding.endsWith("\""))) - encoding = encoding.substring(1, encoding.length() - 1); - return (encoding.trim()); - - } - - - public HttpServletRequestWrapper buildWrapper(HttpSession session, final KeycloakAccount account) { - if (needRequestRestore) { - final String method = (String)session.getAttribute(SAVED_METHOD); - final byte[] body = (byte[])session.getAttribute(SAVED_BODY); - final MultivaluedHashMap headers = (MultivaluedHashMap)session.getAttribute(SAVED_HEADERS); - clearSavedRequest(session); - HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { - protected MultivaluedHashMap parameters; - - MultivaluedHashMap getParams() { - if (parameters != null) return parameters; - - if (body == null) return new MultivaluedHashMap(); - - String contentType = getContentType(); - if (contentType != null && contentType.toLowerCase().startsWith("application/x-www-form-urlencoded")) { - ByteArrayInputStream is = new ByteArrayInputStream(body); - try { - parameters = parseForm(is); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return parameters; - - } - @Override - public boolean isUserInRole(String role) { - return account.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - return account.getPrincipal(); - } - - @Override - public String getMethod() { - if (needRequestRestore) { - return method; - } else { - return super.getMethod(); - - } - } - - @Override - public String getHeader(String name) { - if (needRequestRestore && headers != null) { - return headers.getFirst(name.toLowerCase()); - } - return super.getHeader(name); - } - - @Override - public Enumeration getHeaders(String name) { - if (needRequestRestore && headers != null) { - List values = headers.getList(name.toLowerCase()); - if (values == null) return Collections.emptyEnumeration(); - else return Collections.enumeration(values); - } - return super.getHeaders(name); - } - - @Override - public Enumeration getHeaderNames() { - if (needRequestRestore && headers != null) { - return Collections.enumeration(headers.keySet()); - } - return super.getHeaderNames(); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - - if (needRequestRestore && body != null) { - final ByteArrayInputStream is = new ByteArrayInputStream(body); - return new ServletInputStream() { - @Override - public int read() throws IOException { - return is.read(); - } - }; - } - return super.getInputStream(); - } - - @Override - public void logout() throws ServletException { - servletRequestLogout(); - } - - @Override - public long getDateHeader(String name) { - if (!needRequestRestore) return super.getDateHeader(name); - return -1; - } - - @Override - public int getIntHeader(String name) { - if (!needRequestRestore) return super.getIntHeader(name); - String value = getHeader(name); - if (value == null) return -1; - return Integer.valueOf(value); - - } - - @Override - public String[] getParameterValues(String name) { - if (!needRequestRestore) return super.getParameterValues(name); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterValues(name); - } - String[] values = request.getParameterValues(name); - List list = new LinkedList<>(); - if (values != null) { - for (String val : values) list.add(val); - } - List vals = formParams.get(name); - if (vals != null) list.addAll(vals); - return list.toArray(new String[list.size()]); - } - - @Override - public Enumeration getParameterNames() { - if (!needRequestRestore) return super.getParameterNames(); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterNames(); - } - Set names = new HashSet<>(); - Enumeration qnames = super.getParameterNames(); - while (qnames.hasMoreElements()) names.add(qnames.nextElement()); - names.addAll(formParams.keySet()); - return Collections.enumeration(names); - - } - - @Override - public Map getParameterMap() { - if (!needRequestRestore) return super.getParameterMap(); - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return super.getParameterMap(); - } - Map map = new HashMap<>(); - Enumeration names = getParameterNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - String[] values = getParameterValues(name); - if (values != null) { - map.put(name, values); - } - } - return map; - } - - @Override - public String getParameter(String name) { - if (!needRequestRestore) return super.getParameter(name); - String param = super.getParameter(name); - if (param != null) return param; - MultivaluedHashMap formParams = getParams(); - if (formParams == null) { - return null; - } - return formParams.getFirst(name); - - } - - @Override - public BufferedReader getReader() throws IOException { - if (!needRequestRestore) return super.getReader(); - return new BufferedReader(new InputStreamReader(getInputStream())); - } - - @Override - public int getContentLength() { - if (!needRequestRestore) return super.getContentLength(); - String header = getHeader("content-length"); - if (header == null) return -1; - return Integer.valueOf(header); - } - - @Override - public String getContentType() { - if (!needRequestRestore) return super.getContentType(); - return getHeader("content-type"); - } - - @Override - public String getCharacterEncoding() { - if (!needRequestRestore) return super.getCharacterEncoding(); - return getCharsetFromContentType(getContentType()); - } - - }; - return wrapper; - } else { - return new HttpServletRequestWrapper(request) { - @Override - public boolean isUserInRole(String role) { - return account.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - if (account == null) return null; - return account.getPrincipal(); - } - - @Override - public void logout() throws ServletException { - servletRequestLogout(); - } - - - }; - } - } - - public String getRedirectUri() { - HttpSession session = request.getSession(true); - return (String)session.getAttribute(REDIRECT_URI); - } - - @Override - public boolean restoreRequest() { - HttpSession session = request.getSession(false); - if (session == null) return false; - return session.getAttribute(REDIRECT_URI) != null; - } - - public static MultivaluedHashMap parseForm(InputStream entityStream) - throws IOException - { - char[] buffer = new char[100]; - StringBuffer buf = new StringBuffer(); - BufferedReader reader = new BufferedReader(new InputStreamReader(entityStream)); - - int wasRead = 0; - do - { - wasRead = reader.read(buffer, 0, 100); - if (wasRead > 0) buf.append(buffer, 0, wasRead); - } while (wasRead > -1); - - String form = buf.toString(); - - MultivaluedHashMap formData = new MultivaluedHashMap(); - if ("".equals(form)) return formData; - - String[] params = form.split("&"); - - for (String param : params) - { - if (param.indexOf('=') >= 0) - { - String[] nv = param.split("="); - String val = nv.length > 1 ? nv[1] : ""; - formData.add(Encode.decode(nv[0]), Encode.decode(val)); - } - else - { - formData.add(Encode.decode(param), ""); - } - } - return formData; - } - - - - @Override - public void saveRequest() { - HttpSession session = request.getSession(true); - session.setAttribute(REDIRECT_URI, facade.getRequest().getURI()); - session.setAttribute(SAVED_METHOD, request.getMethod()); - MultivaluedHashMap headers = new MultivaluedHashMap<>(); - Enumeration names = request.getHeaderNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - Enumeration values = request.getHeaders(name); - while (values.hasMoreElements()) { - headers.add(name.toLowerCase(), values.nextElement()); - } - } - session.setAttribute(SAVED_HEADERS, headers); - if (request.getMethod().equalsIgnoreCase("GET")) { - return; - } - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - byte[] buffer = new byte[4096]; - int bytesRead; - int totalRead = 0; - try { - InputStream is = request.getInputStream(); - - while ( (bytesRead = is.read(buffer) ) >= 0) { - os.write(buffer, 0, bytesRead); - totalRead += bytesRead; - if (totalRead > maxBuffer) { - throw new RuntimeException("max buffer reached on a saved request"); - } - - } - } catch (IOException e) { - throw new RuntimeException(e); - } - byte[] body = os.toByteArray(); - // Only save the request body if there is something to save - if (body.length > 0) { - session.setAttribute(SAVED_BODY, body); - } - - - } - -} diff --git a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java deleted file mode 100755 index add26e1c04c6..000000000000 --- a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2016 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.adapters.servlet; - -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.ServerCookie; -import org.keycloak.common.util.UriUtils; - -import javax.security.cert.X509Certificate; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Enumeration; -import java.util.LinkedList; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletHttpFacade implements HttpFacade { - protected final RequestFacade requestFacade = new RequestFacade(); - protected final ResponseFacade responseFacade = new ResponseFacade(); - protected HttpServletRequest request; - protected HttpServletResponse response; - protected MultivaluedHashMap queryParameters; - - public ServletHttpFacade(HttpServletRequest request, HttpServletResponse response) { - this.request = request; - this.response = response; - } - - protected class RequestFacade implements Request { - - private InputStream inputStream; - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getURI() { - StringBuffer buf = request.getRequestURL(); - if (request.getQueryString() != null) { - buf.append('?').append(request.getQueryString()); - } - return buf.toString(); - } - - @Override - public String getRelativePath() { - String uri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String servletPath = uri.substring(uri.indexOf(contextPath) + contextPath.length()); - - if ("".equals(servletPath)) { - servletPath = "/"; - } - - return servletPath; - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public String getQueryParamValue(String param) { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters.getFirst(param); - } - - public MultivaluedHashMap getQueryParameters() { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters; - } - - @Override - public Cookie getCookie(String cookieName) { - if (request.getCookies() == null) return null; - javax.servlet.http.Cookie cookie = null; - for (javax.servlet.http.Cookie c : request.getCookies()) { - if (c.getName().equals(cookieName)) { - cookie = c; - break; - } - } - if (cookie == null) return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public List getHeaders(String name) { - Enumeration values = request.getHeaders(name); - List list = new LinkedList<>(); - while (values.hasMoreElements()) list.add(values.nextElement()); - return list; - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - } - public boolean isEnded() { - return responseFacade.isEnded(); - } - - protected class ResponseFacade implements Response { - protected boolean ended; - - @Override - public void setStatus(int status) { - response.setStatus(status); - } - - @Override - public void addHeader(String name, String value) { - response.addHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public void resetCookie(String name, String path) { - setCookie(name, "", path, null, 0, false, false); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - StringBuilder cookieBuf = new StringBuilder(); - ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null); - String cookie = cookieBuf.toString(); - response.addHeader("Set-Cookie", cookie); - } - - @Override - public OutputStream getOutputStream() { - try { - return response.getOutputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void end() { - ended = true; - } - - public boolean isEnded() { - return ended; - } - } - - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - throw new IllegalStateException("Not supported yet"); - } -} diff --git a/adapters/spi/tomcat-adapter-spi/pom.xml b/adapters/spi/tomcat-adapter-spi/pom.xml deleted file mode 100755 index 0588a9826061..000000000000 --- a/adapters/spi/tomcat-adapter-spi/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-tomcat-adapter-spi - Keycloak Tomcat Adapter SPI - - - - - org.jboss.logging - jboss-logging - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-common - - - - org.apache.tomcat - tomcat-catalina - ${tomcat8.version} - compile - - - - junit - junit - test - - - diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java deleted file mode 100755 index 7ff1fbceb635..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.ServerCookie; -import org.keycloak.common.util.UriUtils; - -import javax.security.cert.X509Certificate; -import javax.servlet.http.HttpServletResponse; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaHttpFacade implements HttpFacade { - protected org.apache.catalina.connector.Request request; - protected HttpServletResponse response; - protected RequestFacade requestFacade = new RequestFacade(); - protected ResponseFacade responseFacade = new ResponseFacade(); - protected MultivaluedHashMap queryParameters; - - public CatalinaHttpFacade(HttpServletResponse response, org.apache.catalina.connector.Request request) { - this.response = response; - this.request = request; - } - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - throw new IllegalStateException("Not supported yet"); - } - - public boolean isEnded() { - return responseFacade.isEnded(); - } - - protected class RequestFacade implements Request { - - private InputStream inputStream; - - @Override - public String getURI() { - StringBuffer buf = request.getRequestURL(); - if (request.getQueryString() != null) { - buf.append('?').append(request.getQueryString()); - } - return buf.toString(); - } - - @Override - public String getRelativePath() { - String uri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String servletPath = uri.substring(uri.indexOf(contextPath) + contextPath.length()); - - if ("".equals(servletPath)) { - servletPath = "/"; - } - - return servletPath; - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public String getQueryParamValue(String paramName) { - if (queryParameters == null) { - queryParameters = UriUtils.decodeQueryString(request.getQueryString()); - } - return queryParameters.getFirst(paramName); - } - - @Override - public Cookie getCookie(String cookieName) { - if (request.getCookies() == null) return null; - javax.servlet.http.Cookie cookie = null; - for (javax.servlet.http.Cookie c : request.getCookies()) { - if (c.getName().equals(cookieName)) { - cookie = c; - break; - } - } - if (cookie == null) return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public List getHeaders(String name) { - Enumeration headers = request.getHeaders(name); - if (headers == null) return null; - List list = new ArrayList(); - while (headers.hasMoreElements()) { - list.add(headers.nextElement()); - } - return list; - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - - } - - protected class ResponseFacade implements Response { - protected boolean ended; - - @Override - public void setStatus(int status) { - response.setStatus(status); - } - - @Override - public void addHeader(String name, String value) { - response.addHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } - - @Override - public void resetCookie(String name, String path) { - setCookie(name, "", path, null, 0, false, false); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - StringBuilder cookieBuf = new StringBuilder(); - ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null); - String cookie = cookieBuf.toString(); - response.addHeader("Set-Cookie", cookie); - } - - @Override - public OutputStream getOutputStream() { - try { - return response.getOutputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - @Override - public void end() { - ended = true; - } - - public boolean isEnded() { - return ended; - } - } -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java deleted file mode 100755 index 15d2f2bdc3d4..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Manager; -import org.apache.catalina.Session; -import org.apache.catalina.SessionEvent; -import org.apache.catalina.SessionListener; -import org.apache.catalina.realm.GenericPrincipal; -import org.jboss.logging.Logger; - -import java.io.IOException; -import java.util.List; - -/** - * Manages relationship to users and sessions so that forced admin logout can be implemented - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CatalinaUserSessionManagement implements SessionListener { - private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class); - - public void login(Session session) { - session.addSessionListener(this); - } - - public void logoutAll(Manager sessionManager) { - Session[] allSessions = sessionManager.findSessions(); - for (Session session : allSessions) { - logoutSession(session); - } - } - - public void logoutHttpSessions(Manager sessionManager, List sessionIds) { - log.debug("logoutHttpSessions: " + sessionIds); - - for (String sessionId : sessionIds) { - logoutSession(sessionManager, sessionId); - } - } - - protected void logoutSession(Manager manager, String httpSessionId) { - log.debug("logoutHttpSession: " + httpSessionId); - - Session session; - try { - session = manager.findSession(httpSessionId); - } catch (IOException ioe) { - log.warn("IO exception when looking for session " + httpSessionId, ioe); - return; - } - - logoutSession(session); - } - - protected void logoutSession(Session session) { - try { - if (session != null) session.expire(); - } catch (Exception e) { - log.debug("Session not present or already invalidated.", e); - } - } - - public void sessionEvent(SessionEvent event) { - // We only care about session destroyed events - if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())) - return; - - // Look up the single session id associated with this session (if any) - Session session = event.getSession(); - log.debugf("Session %s destroyed", session.getId()); - - GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); - if (principal == null) return; - session.setPrincipal(null); - session.setAuthType(null); - } -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagementWrapper.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagementWrapper.java deleted file mode 100755 index 18e5b43070de..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagementWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Manager; -import org.keycloak.adapters.spi.UserSessionManagement; - -import java.util.List; - -/** - * @author Marek Posolda - */ -public class CatalinaUserSessionManagementWrapper implements UserSessionManagement { - - private final CatalinaUserSessionManagement delegate; - private final Manager sessionManager; - - public CatalinaUserSessionManagementWrapper(CatalinaUserSessionManagement delegate, Manager sessionManager) { - this.delegate = delegate; - this.sessionManager = sessionManager; - } - - @Override - public void logoutAll() { - delegate.logoutAll(sessionManager); - } - - @Override - public void logoutHttpSessions(List ids) { - delegate.logoutHttpSessions(sessionManager, ids); - } -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/GenericPrincipalFactory.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/GenericPrincipalFactory.java deleted file mode 100755 index 4042f4367faa..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/GenericPrincipalFactory.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import org.apache.catalina.Realm; -import org.apache.catalina.realm.GenericPrincipal; - -import javax.security.auth.Subject; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** - * @author Davide Ungari - * @version $Revision: 1 $ - */ -public abstract class GenericPrincipalFactory implements PrincipalFactory { - - @Override - public GenericPrincipal createPrincipal(Realm realm, final Principal identity, final Set roleSet) { - Subject subject = new Subject(); - Set principals = subject.getPrincipals(); - principals.add(identity); - final SimpleGroup[] roleSets = getRoleSets(roleSet); - for (SimpleGroup group : roleSets) { - String name = group.getName(); - SimpleGroup subjectGroup = createGroup(name, principals); - // Copy the group members to the Subject group - Enumeration members = group.members(); - while (members.hasMoreElements()) { - Principal role = members.nextElement(); - subjectGroup.addMember(role); - } - } - return createPrincipal(getPrincipal(subject), new ArrayList<>(roleSet)); - } - - protected abstract GenericPrincipal createPrincipal(Principal userPrincipal, List roles); - - /** - * Get the Principal given the authenticated Subject. Currently the first subject that is not of type {@code Group} is - * considered or the single subject inside the CallerPrincipal group. - * - * @param subject - * @return the authenticated subject - */ - protected Principal getPrincipal(Subject subject) { - Principal principal = null; - if (subject != null) { - Set principals = subject.getPrincipals(); - if (principals != null && !principals.isEmpty()) { - for (Principal p : principals) { - if (!(p instanceof SimpleGroup) && principal == null) { - principal = p; - } - } - } - } - return principal; - } - - protected SimpleGroup createGroup(String name, Set principals) { - SimpleGroup roles = null; - for (final Object next : principals) { - if (!(next instanceof SimpleGroup)) continue; - SimpleGroup grp = (SimpleGroup) next; - if (grp.getName().equals(name)) { - roles = grp; - break; - } - } - // If we did not find a group create one - if (roles == null) { - roles = new SimpleGroup(name); - principals.add(roles); - } - return roles; - } - - protected SimpleGroup[] getRoleSets(Collection roleSet) { - SimpleGroup roles = new SimpleGroup("Roles"); - SimpleGroup[] roleSets = {roles}; - for (String role : roleSet) { - roles.addMember(new SimplePrincipal(role)); - } - return roleSets; - } - -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/PrincipalFactory.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/PrincipalFactory.java deleted file mode 100644 index 7fc301ab6f0d..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/PrincipalFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.adapters.tomcat; - -import org.apache.catalina.Realm; -import org.apache.catalina.realm.GenericPrincipal; - -import java.security.Principal; -import java.util.Set; - -public interface PrincipalFactory { - GenericPrincipal createPrincipal(Realm realm, final Principal identity, final Set roleSet); -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimpleGroup.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimpleGroup.java deleted file mode 100755 index 36fe5766d5a1..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimpleGroup.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import java.security.Principal; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Set; - -public class SimpleGroup extends SimplePrincipal { - private final Set members = new HashSet(); - - /** - * Creates a new group with the given name. - * @param name Group name. - */ - public SimpleGroup(final String name) { - super(name); - } - - public boolean addMember(final Principal user) { - return this.members.add(user); - } - - public boolean isMember(final Principal member) { - return this.members.contains(member); - } - - public Enumeration members() { - return Collections.enumeration(this.members); - } - - public boolean removeMember(final Principal user) { - return this.members.remove(user); - } - - public String toString() { - return super.toString() + ": " + members.toString(); - } - -} diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimplePrincipal.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimplePrincipal.java deleted file mode 100755 index 7d271263f5d5..000000000000 --- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/SimplePrincipal.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 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.adapters.tomcat; - -import java.io.Serializable; -import java.security.Principal; - -/** - * Simple security principal implementation. - * - * @author Marvin S. Addison - * @version $Revision: 22071 $ - * @since 3.1.11 - * - */ -public class SimplePrincipal implements Principal, Serializable { - - /** SimplePrincipal.java */ - - /** The unique identifier for this principal. */ - private final String name; - - /** - * Creates a new principal with the given name. - * @param name Principal name. - */ - public SimplePrincipal(final String name) { - this.name = name; - } - - public final String getName() { - return this.name; - } - - public String toString() { - return getName(); - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } else if (!(o instanceof SimplePrincipal)) { - return false; - } else { - return getName().equals(((SimplePrincipal)o).getName()); - } - } - - public int hashCode() { - return 37 * getName().hashCode(); - } -} \ No newline at end of file diff --git a/adapters/spi/undertow-adapter-spi/pom.xml b/adapters/spi/undertow-adapter-spi/pom.xml deleted file mode 100755 index 884b93c77400..000000000000 --- a/adapters/spi/undertow-adapter-spi/pom.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - 4.0.0 - - keycloak-undertow-adapter-spi - Keycloak Undertow Integration SPI - - - - - org.keycloak.adapters.undertow.* - - - io.undertow.*;version="[1.4,3)", - *;resolution:=optional - - ${project.groupId}.keycloak-undertow-adapter - - - - - org.jboss.logging - jboss-logging - provided - - - org.keycloak - keycloak-adapter-spi - - - org.keycloak - keycloak-common - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - io.undertow - undertow-servlet - provided - - - io.undertow - undertow-core - provided - - - junit - junit - test - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - ${keycloak.osgi.fragment} - - - - - - - diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ChangeSessionId.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ChangeSessionId.java deleted file mode 100755 index 24eef7e0ddcb..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ChangeSessionId.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.servlet.spec.HttpSessionImpl; -import io.undertow.servlet.spec.ServletContextImpl; - -import java.lang.reflect.Method; -import java.security.AccessController; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ChangeSessionId { - /** - * This is a hack to be backward compatible between Undertow 1.3+ and versions lower. In Undertow 1.3, a new - * switch was added setChangeSessionIdOnLogin, this screws up session management for keycloak as after the session id - * is uploaded to Keycloak, undertow changes the session id and it can't be invalidated. - * - * @param deploymentInfo - */ - public static void turnOffChangeSessionIdOnLogin(DeploymentInfo deploymentInfo) { - try { - Method method = DeploymentInfo.class.getMethod("setChangeSessionIdOnLogin", boolean.class); - method.invoke(deploymentInfo, false); - } catch (Exception ignore) { - - } - } - - public static String changeSessionId(HttpServerExchange exchange, boolean create) { - final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ServletContextImpl currentServletContext = sc.getCurrentServletContext(); - HttpSessionImpl session = currentServletContext.getSession(exchange, create); - if (session == null) { - return null; - } - Session underlyingSession; - if(System.getSecurityManager() == null) { - underlyingSession = session.getSession(); - } else { - underlyingSession = AccessController.doPrivileged(new HttpSessionImpl.UnwrapSessionAction(session)); - } - - - return underlyingSession.changeSessionId(exchange, currentServletContext.getSessionConfig()); - } -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SavedRequest.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SavedRequest.java deleted file mode 100755 index 0d4aae71614a..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SavedRequest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.servlet.spec.HttpSessionImpl; - -import javax.servlet.http.HttpSession; -import java.io.Serializable; -import java.security.AccessController; - -/** - * Saved servlet request. - * - * Note bill burke: I had to fork this because Undertow was automatically restoring the request before the code could be - * processed and redirected. - * - * CachedAuthenticatedSessionHandler was restoring the request before the authentication manager could read the code from the URI - * Originally, I copied SavedRequest as is, but there are type mismatches between Undertow 1.1.1 and 1.3.10. - * So, trySaveRequest calls the same undertow version, removes the saved request, stores it in a different session attribute, - * then restores the old attribute later - * - * - * @author Stuart Douglas - */ -public class SavedRequest implements Serializable { - - private static final String SESSION_KEY = SavedRequest.class.getName(); - - public static void trySaveRequest(final HttpServerExchange exchange) { - io.undertow.servlet.util.SavedRequest.trySaveRequest(exchange); - final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSessionImpl session = sc.getCurrentServletContext().getSession(exchange, true); - Session underlyingSession; - if(System.getSecurityManager() == null) { - underlyingSession = session.getSession(); - } else { - underlyingSession = AccessController.doPrivileged(new HttpSessionImpl.UnwrapSessionAction(session)); - } - io.undertow.servlet.util.SavedRequest request = (io.undertow.servlet.util.SavedRequest) underlyingSession.removeAttribute(io.undertow.servlet.util.SavedRequest.class.getName()); - if (request != null) underlyingSession.setAttribute(SESSION_KEY, request); - - - } - - public static void tryRestoreRequest(final HttpServerExchange exchange, HttpSession session) { - if(session instanceof HttpSessionImpl) { - - Session underlyingSession; - if(System.getSecurityManager() == null) { - underlyingSession = ((HttpSessionImpl) session).getSession(); - } else { - underlyingSession = AccessController.doPrivileged(new HttpSessionImpl.UnwrapSessionAction(session)); - } - io.undertow.servlet.util.SavedRequest request = (io.undertow.servlet.util.SavedRequest) underlyingSession.removeAttribute(SESSION_KEY); - if (request != null) { - underlyingSession.setAttribute(io.undertow.servlet.util.SavedRequest.class.getName(), request); - io.undertow.servlet.util.SavedRequest.tryRestoreRequest(exchange, session); - - } - - } - } - -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java deleted file mode 100755 index fbcd6d3219ce..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.servlet.handlers.ServletRequestContext; -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.LogoutError; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ServletHttpFacade extends UndertowHttpFacade { - protected HttpServletRequest request; - protected HttpServletResponse response; - - public ServletHttpFacade(HttpServerExchange exchange) { - super(exchange); - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - request = (HttpServletRequest)servletRequestContext.getServletRequest(); - response = (HttpServletResponse)servletRequestContext.getServletResponse(); - } - - protected class RequestFacade extends UndertowHttpFacade.RequestFacade { - @Override - public String getFirstParam(String param) { - return request.getParameter(param); - } - - @Override - public void setError(AuthenticationError error) { - request.setAttribute(AuthenticationError.class.getName(), error); - - } - - @Override - public void setError(LogoutError error) { - request.setAttribute(LogoutError.class.getName(), error); - } - - - } - - protected class ResponseFacade extends UndertowHttpFacade.ResponseFacade { - // can't call sendError from a challenge. Undertow ends up calling send error. - /* - @Override - public void sendError(int code) { - try { - response.sendError(code); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int code, String message) { - try { - response.sendError(code, message); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - */ - - } - - @Override - public Response getResponse() { - return new ResponseFacade(); - } - - @Override - public Request getRequest() { - return new RequestFacade(); - } -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java deleted file mode 100755 index fcde53737bbc..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.session.SessionManager; -import org.keycloak.adapters.spi.UserSessionManagement; - -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SessionManagementBridge implements UserSessionManagement { - - protected UndertowUserSessionManagement userSessionManagement; - protected SessionManager sessionManager; - - public SessionManagementBridge(UndertowUserSessionManagement userSessionManagement, SessionManager sessionManager) { - this.userSessionManagement = userSessionManagement; - this.sessionManager = sessionManager; - } - - @Override - public void logoutAll() { - userSessionManagement.logoutAll(sessionManager); - } - - @Override - public void logoutHttpSessions(List ids) { - userSessionManagement.logoutHttpSessions(sessionManager, ids); - } - -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java deleted file mode 100755 index c86a54f0a477..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.CookieImpl; -import io.undertow.server.handlers.form.FormData; -import io.undertow.server.handlers.form.FormData.FormValue; -import io.undertow.server.handlers.form.FormDataParser; -import io.undertow.server.handlers.form.FormParserFactory; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.util.AttachmentKey; -import io.undertow.util.Headers; -import io.undertow.util.HttpString; -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.common.util.KeycloakUriBuilder; - -import javax.security.cert.X509Certificate; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.Deque; -import java.util.List; -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowHttpFacade implements HttpFacade { - public static final AttachmentKey AUTH_ERROR_ATTACHMENT_KEY = AttachmentKey.create(AuthenticationError.class); - public static final AttachmentKey LOGOUT_ERROR_ATTACHMENT_KEY = AttachmentKey.create(LogoutError.class); - - protected HttpServerExchange exchange; - protected RequestFacade requestFacade = new RequestFacade(); - protected ResponseFacade responseFacade = new ResponseFacade(); - - public UndertowHttpFacade(HttpServerExchange exchange) { - this.exchange = exchange; - } - - @Override - public Request getRequest() { - return requestFacade; - } - - @Override - public Response getResponse() { - return responseFacade; - } - - @Override - public X509Certificate[] getCertificateChain() { - X509Certificate[] chain = new X509Certificate[0]; - try { - chain = exchange.getConnection().getSslSessionInfo().getPeerCertificateChain(); - } catch (Exception ignore) { - - } - return chain; - } - - protected class RequestFacade implements Request { - - private InputStream inputStream; - private final FormParserFactory formParserFactory = FormParserFactory.builder().build(); - private FormData formData; - - @Override - public String getURI() { - KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getRequestURI()) - .replaceQuery(exchange.getQueryString()); - if (!exchange.isHostIncludedInRequestURI()) uriBuilder.scheme(exchange.getRequestScheme()).host(exchange.getHostAndPort()); - return uriBuilder.buildAsString(); - } - - @Override - public String getRelativePath() { - return exchange.getRelativePath(); - } - - @Override - public boolean isSecure() { - String protocol = exchange.getRequestScheme(); - return protocol.equalsIgnoreCase("https"); - } - - @Override - public String getFirstParam(String param) { - Deque values = exchange.getQueryParameters().get(param); - - if (values != null && !values.isEmpty()) { - return values.getFirst(); - } - - if (formData == null && "post".equalsIgnoreCase(getMethod())) { - FormDataParser parser = formParserFactory.createParser(exchange); - try { - formData = parser.parseBlocking(); - } catch (IOException cause) { - throw new RuntimeException("Failed to parse form parameters", cause); - } - } - - if (formData != null) { - Deque formValues = formData.get(param); - - if (formValues != null && !formValues.isEmpty()) { - FormValue firstValue = formValues.getFirst(); - - if (!firstValue.isFile()) { - return firstValue.getValue(); - } - } - } - - return null; - } - - @Override - public String getQueryParamValue(String param) { - Map> queryParameters = exchange.getQueryParameters(); - if (queryParameters == null) return null; - Deque strings = queryParameters.get(param); - if (strings == null) return null; - return strings.getFirst(); - } - - @Override - public Cookie getCookie(String cookieName) { - Map requestCookies = exchange.getRequestCookies(); - if (requestCookies == null) return null; - io.undertow.server.handlers.Cookie cookie = requestCookies.get(cookieName); - if (cookie == null) return null; - return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); - } - - @Override - public List getHeaders(String name) { - return exchange.getRequestHeaders().get(name); - } - - @Override - public String getMethod() { - return exchange.getRequestMethod().toString(); - } - - - - @Override - public String getHeader(String name) { - return exchange.getRequestHeaders().getFirst(name); - } - - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (!exchange.isBlocking()) exchange.startBlocking(); - - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - ServletRequestContext context = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ServletRequest servletRequest = context.getServletRequest(); - - inputStream = new BufferedInputStream(exchange.getInputStream()); - - context.setServletRequest(UndertowHttpServletRequest.setupServletInputStream(servletRequest, inputStream)); - return inputStream; - } - - return exchange.getInputStream(); - } - - @Override - public String getRemoteAddr() { - InetSocketAddress sourceAddress = exchange.getSourceAddress(); - if (sourceAddress == null) { - return ""; - } - InetAddress address = sourceAddress.getAddress(); - if (address == null) { - // this is unresolved, so we just return the host name not exactly spec, but if the name should be - // resolved then a PeerNameResolvingHandler should be used and this is probably better than just - // returning null - return sourceAddress.getHostString(); - } - return address.getHostAddress(); - } - - @Override - public void setError(AuthenticationError error) { - exchange.putAttachment(AUTH_ERROR_ATTACHMENT_KEY, error); - } - - @Override - public void setError(LogoutError error) { - exchange.putAttachment(LOGOUT_ERROR_ATTACHMENT_KEY, error); - - } - } - - protected class ResponseFacade implements Response { - @Override - public void setStatus(int status) { - exchange.setResponseCode(status); - } - - @Override - public void addHeader(String name, String value) { - exchange.getResponseHeaders().add(new HttpString(name), value); - } - - @Override - public void setHeader(String name, String value) { - exchange.getResponseHeaders().put(new HttpString(name), value); - } - - @Override - public void resetCookie(String name, String path) { - CookieImpl cookie = new CookieImpl(name, ""); - cookie.setMaxAge(0); - cookie.setPath(path); - exchange.setResponseCookie(cookie); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - CookieImpl cookie = new CookieImpl(name, value); - cookie.setPath(path); - cookie.setDomain(domain); - cookie.setMaxAge(maxAge); - cookie.setSecure(secure); - cookie.setHttpOnly(httpOnly); - exchange.setResponseCookie(cookie); - } - - @Override - public OutputStream getOutputStream() { - if (!exchange.isBlocking()) exchange.startBlocking(); - return exchange.getOutputStream(); - } - - @Override - public void sendError(int code) { - exchange.setResponseCode(code); - } - - @Override - public void sendError(int code, String message) { - exchange.setResponseCode(code); - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); - try { - exchange.getOutputStream().write(message.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - exchange.endExchange(); - } - - - @Override - public void end() { - exchange.endExchange(); - } - } -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpServletRequest.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpServletRequest.java deleted file mode 100644 index 338f66e22acf..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpServletRequest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.adapters.undertow; - -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import java.io.IOException; -import java.io.InputStream; - -public class UndertowHttpServletRequest { - - public static HttpServletRequestWrapper setupServletInputStream(ServletRequest servletRequest, final InputStream inputStream) { - return new HttpServletRequestWrapper((HttpServletRequest) servletRequest) { - @Override - public ServletInputStream getInputStream() { - inputStream.mark(0); - return new ServletInputStream() { - @Override - public int read() throws IOException { - return inputStream.read(); - } - }; - } - }; - } -} diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java deleted file mode 100755 index 953080c214c0..000000000000 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2016 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.adapters.undertow; - -import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; -import io.undertow.server.session.SessionConfig; -import io.undertow.server.session.SessionListener; -import io.undertow.server.session.SessionManager; -import org.jboss.logging.Logger; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Manages relationship to users and sessions so that forced admin logout can be implemented - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowUserSessionManagement implements SessionListener { - private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class); - protected volatile boolean registered; - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - - public void login(SessionManager manager) { - if (!registered) { - manager.registerSessionListener(this); - registered = true; - } - } - - /** - * This method runs the given runnable in the current thread if the session manager does not use distributed sessions, - * or in a separate thread if it does. This is to work around: - *

-     *   org.infinispan.util.concurrent.TimeoutException: ISPN000299: Unable to acquire lock after 15 seconds for key SessionCreationMetaDataKey
-     * 
- * See https://issues.jboss.org/browse/KEYCLOAK-9822 - * @param r - */ - private void workaroundIspnDeadlock(final SessionManager manager, Runnable r) { - if (manager.getClass().getName().equals("org.wildfly.clustering.web.undertow.session.DistributableSessionManager")) { - executor.submit(r); - } else { - r.run(); - } - } - - public void logoutAll(final SessionManager manager) { - final Set allSessions = manager.getAllSessions(); - workaroundIspnDeadlock(manager, new Runnable() { - @Override - public void run() { - for (String sessionId : allSessions) logoutSession(manager, sessionId); - } - }); - } - - public void logoutHttpSessions(final SessionManager manager, final List sessionIds) { - log.debugf("logoutHttpSessions: %s", sessionIds); - - workaroundIspnDeadlock(manager, new Runnable() { - @Override - public void run() { - for (String sessionId : sessionIds) { - logoutSession(manager, sessionId); - } - } - }); - } - - protected void logoutSession(SessionManager manager, String httpSessionId) { - log.debugf("logoutHttpSession: %s", httpSessionId); - Session session = getSessionById(manager, httpSessionId); - try { - if (session != null) session.invalidate(null); - } catch (Exception e) { - log.warnf("Session %s not present or already invalidated.", httpSessionId); - } - } - - protected Session getSessionById(SessionManager manager, final String sessionId) { - // TODO: Workaround for WFLY-3345. Remove this once we move to wildfly 8.2 - if (manager.getClass().getName().equals("org.wildfly.clustering.web.undertow.session.DistributableSessionManager")) { - return manager.getSession(null, new SessionConfig() { - - @Override - public void setSessionId(HttpServerExchange exchange, String sessionId) { - } - - @Override - public void clearSession(HttpServerExchange exchange, String sessionId) { - } - - @Override - public String findSessionId(HttpServerExchange exchange) { - return sessionId; - } - - @Override - public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { - return null; - } - - @Override - public String rewriteUrl(String originalUrl, String sessionId) { - return null; - } - - }); - - } else { - return manager.getSession(sessionId); - } - } - - - @Override - public void sessionCreated(Session session, HttpServerExchange exchange) { - } - - @Override - public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { - } - - - @Override - public void sessionIdChanged(Session session, String oldSessionId) { - } - - @Override - public void attributeAdded(Session session, String name, Object value) { - } - - @Override - public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { - } - - @Override - public void attributeRemoved(Session session, String name, Object oldValue) { - } - -} diff --git a/authz/client/pom.xml b/authz/client/pom.xml index d410042e7ec0..05889a050411 100644 --- a/authz/client/pom.xml +++ b/authz/client/pom.xml @@ -11,30 +11,17 @@ ../pom.xml - keycloak-authz-client + keycloak-authz-client-tests jar Keycloak Authz: Client API - KeyCloak AuthZ: Client API + Keycloak Authz: Client API. This module is supposed to be used just in the Keycloak repository for the testsuite. It is NOT supposed to be used by the 3rd party applications. + For the use by 3rd party applications, please use `org.keycloak:keycloak-authz-client` module. - - org.keycloak.authorization.client.* - - - org.keycloak.*;version="${project.version}", - org.apache.http.auth.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.client.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.conn.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.cookie.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.auth.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.client.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.conn.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.cookie.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.impl.execchain.*;version=${apache.httpcomponents.fuse.version}, - org.apache.http.*;version=${apache.httpcomponents.httpcore.fuse.version}, - *;resolution:=optional - + 8 + 8 + 8 @@ -46,7 +33,11 @@ org.jboss.logging jboss-logging - + + org.jboss.logging + commons-logging-jboss-logging + runtime + org.apache.httpcomponents httpclient @@ -67,43 +58,36 @@ jackson-annotations provided + + + junit + junit + test + + + org.hamcrest + hamcrest + test + - - maven-jar-plugin + org.apache.maven.plugins + maven-javadoc-plugin - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - + true - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - + org.apache.maven.plugins + maven-deploy-plugin - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - + true - + diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java index cd06d2a4b808..58706b7e2de8 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Objects; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,9 +29,9 @@ import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.authorization.client.util.Http; import org.keycloak.authorization.client.util.TokenCallable; +import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.util.SystemPropertiesJsonParserFactory; /** *

This is class serves as an entry point for clients looking for access to Keycloak Authorization Services. @@ -91,6 +90,7 @@ public static AuthzClient create(InputStream configStream) throws RuntimeExcepti * @return a new instance */ public static AuthzClient create(Configuration configuration) { + CryptoIntegration.init(AuthzClient.class.getClassLoader()); return new AuthzClient(configuration); } @@ -242,7 +242,7 @@ private AuthzClient(Configuration configuration) { throw new IllegalArgumentException("Configuration URL can not be null."); } - configurationUrl = KeycloakUriBuilder.fromUri(configurationUrl).clone().path(AUTHZ_DISCOVERY_URL).build(configuration.getRealm()).toString(); + configurationUrl = KeycloakUriBuilder.fromUri(configurationUrl).clone().path(AUTHZ_DISCOVERY_URL).build(configuration.getRealm()).toString(); this.configuration = configuration; this.http = new Http(configuration, configuration.getClientCredentialsProvider()); @@ -256,14 +256,14 @@ private AuthzClient(Configuration configuration) { } } - private TokenCallable createPatSupplier(String userName, String password) { + public TokenCallable createPatSupplier(String userName, String password) { if (patSupplier == null) { patSupplier = createRefreshableAccessTokenSupplier(userName, password); } return patSupplier; } - private TokenCallable createPatSupplier() { + public TokenCallable createPatSupplier() { return createPatSupplier(null, null); } @@ -275,4 +275,4 @@ private TokenCallable createRefreshableAccessTokenSupplier(final String userName final String scope) { return new TokenCallable(userName, password, scope, http, configuration, serverConfiguration); } -} \ No newline at end of file +} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ResourceNotFoundException.java b/authz/client/src/main/java/org/keycloak/authorization/client/ResourceNotFoundException.java new file mode 100644 index 000000000000..c418ba9ac29d --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/ResourceNotFoundException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 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.authorization.client; + +/** + * @author Pedro Igor + */ +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java b/authz/client/src/main/java/org/keycloak/authorization/client/SystemPropertiesJsonParserFactory.java similarity index 92% rename from core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java rename to authz/client/src/main/java/org/keycloak/authorization/client/SystemPropertiesJsonParserFactory.java index 10481fb28f06..339b35f45b0c 100644 --- a/core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/SystemPropertiesJsonParserFactory.java @@ -15,7 +15,11 @@ * limitations under the License. */ -package org.keycloak.util; +package org.keycloak.authorization.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.io.IOContext; @@ -24,19 +28,12 @@ import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.SystemEnvProperties; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.util.Properties; - /** * Provides replacing of system properties for parsed values * * @author Marek Posolda */ -public class SystemPropertiesJsonParserFactory extends MappingJsonFactory { - - private static final Properties properties = new SystemEnvProperties(); +class SystemPropertiesJsonParserFactory extends MappingJsonFactory { @Override protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException { @@ -71,7 +68,7 @@ public SystemPropertiesAwareJsonParser(JsonParser d) { @Override public String getText() throws IOException { String orig = super.getText(); - return StringPropertyReplacer.replaceProperties(orig, properties); + return StringPropertyReplacer.replaceProperties(orig, SystemEnvProperties.UNFILTERED::getProperty); } } } 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..c613b4855a2e 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 @@ -71,7 +71,7 @@ public AuthorizationResponse authorize() throws AuthorizationDeniedException { * @throws AuthorizationDeniedException in case the request was denied by the server */ public AuthorizationResponse authorize(final AuthorizationRequest request) throws AuthorizationDeniedException { - return invoke(request); + return invoke(request, new TypeReference(){}); } /** @@ -86,16 +86,17 @@ public List getPermissions(final AuthorizationRequest request) throw if (request.getMetadata() == null) { metadata = new AuthorizationRequest.Metadata(); + request.setMetadata(metadata); } else { metadata = request.getMetadata(); } metadata.setResponseMode("permissions"); - return invoke(request); + return (List) invoke(request, new TypeReference>(){}); } - private T invoke(AuthorizationRequest request) { + private T invoke(AuthorizationRequest request, TypeReference responseType) { if (request == null) { throw new IllegalArgumentException("Authorization request must not be null"); } @@ -116,13 +117,8 @@ public T call() throws Exception { HttpMethodResponse response = method .authentication() .uma(request) - .response(); - - if (request.getMetadata() != null && "permissions".equals(request.getMetadata().getResponseMode())) { - response = response.json(new TypeReference(){}); - } else { - response = response.json((Class) AuthorizationResponse.class); - } + .response() + .json(responseType); return response.execute(); } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java index 8f0f3c0d2f49..c3b086165280 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java @@ -17,7 +17,7 @@ */ package org.keycloak.authorization.client.util; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -184,11 +184,7 @@ protected void preExecute(RequestBuilder builder) { } } - try { - builder.setEntity(new UrlEncodedFormEntity(formparams, "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Error creating form parameters"); - } + builder.setEntity(new UrlEncodedFormEntity(formparams, StandardCharsets.UTF_8)); } } }; diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java index 016daf28787b..00121f42fcfe 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java @@ -19,6 +19,7 @@ import java.util.concurrent.Callable; import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.authorization.client.ResourceNotFoundException; import org.keycloak.authorization.client.representation.TokenIntrospectionResponse; /** @@ -85,6 +86,8 @@ public static V retryAndWrapExceptionIfNecessary(Callable callable, Token } throw handleWrapException(message, cause); + } else if (httpe.getStatusCode() == 400 && new String(httpe.getBytes()).contains("invalid_resource_id")) { + throw new ResourceNotFoundException(message, cause); } } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java index 383db674b4a1..dca3d673b735 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java @@ -106,7 +106,7 @@ private AccessTokenResponse tryRefreshToken() { } public boolean isTokenTimeToLiveSufficient(AccessToken token) { - return token != null && (token.getExpiration() - getConfiguration().getTokenMinimumTimeToLive()) > Time.currentTime(); + return token != null && (token.getExp() - getConfiguration().getTokenMinimumTimeToLive()) > Time.currentTime(); } /** diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java new file mode 100644 index 000000000000..01ea3bc41910 --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java @@ -0,0 +1,203 @@ +/* + * 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.authorization.client.util.crypto; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author rmartinc + */ +class ASN1Decoder { + + private final ByteArrayInputStream is; + private final int limit; + private int count; + + ASN1Decoder(byte[] bytes) { + is = new ByteArrayInputStream(bytes); + count = 0; + limit = bytes.length; + } + + public static ASN1Decoder create(byte[] bytes) { + return new ASN1Decoder(bytes); + } + + public List readSequence() throws IOException { + int tag = readTag(); + int tagNo = readTagNumber(tag); + if (tagNo != ASN1Encoder.SEQUENCE) { + throw new IOException("Invalid Sequence tag " + tagNo); + } + int length = readLength(); + List result = new ArrayList<>(); + while (length > 0) { + byte[] bytes = readNext(); + result.add(bytes); + length = length - bytes.length; + } + return result; + } + + public BigInteger readInteger() throws IOException { + int tag = readTag(); + int tagNo = readTagNumber(tag); + if (tagNo != ASN1Encoder.INTEGER) { + throw new IOException("Invalid Integer tag " + tagNo); + } + int length = readLength(); + byte[] bytes = read(length); + return new BigInteger(bytes); + } + + byte[] readNext() throws IOException { + mark(); + int tag = readTag(); + readTagNumber(tag); + int length = readLength(); + length += reset(); + return read(length); + } + + int readTag() throws IOException { + int tag = read(); + if (tag < 0) { + throw new EOFException("EOF found inside tag value."); + } + return tag; + } + + int readTagNumber(int tag) throws IOException { + int tagNo = tag & 0x1f; + + // + // with tagged object tag number is bottom 5 bits, or stored at the start of the content + // + if (tagNo == 0x1f) { + tagNo = 0; + + int b = read(); + + // X.690-0207 8.1.2.4.2 + // "c) bits 7 to 1 of the first subsequent octet shall not all be zero." + if ((b & 0x7f) == 0) // Note: -1 will pass + { + throw new IOException("corrupted stream - invalid high tag number found"); + } + + while ((b >= 0) && ((b & 0x80) != 0)) { + tagNo |= (b & 0x7f); + tagNo <<= 7; + b = read(); + } + + if (b < 0) { + throw new EOFException("EOF found inside tag value."); + } + + tagNo |= (b & 0x7f); + } + + return tagNo; + } + + int readLength() throws IOException { + int length = read(); + if (length < 0) { + throw new EOFException("EOF found when length expected"); + } + + if (length == 0x80) { + return -1; // indefinite-length encoding + } + + if (length > 127) { + int size = length & 0x7f; + + // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here + if (size > 4) { + throw new IOException("DER length more than 4 bytes: " + size); + } + + length = 0; + for (int i = 0; i < size; i++) { + int next = read(); + + if (next < 0) { + throw new EOFException("EOF found reading length"); + } + + length = (length << 8) + next; + } + + if (length < 0) { + throw new IOException("corrupted stream - negative length found"); + } + + if (length >= limit) // after all we must have read at least 1 byte + { + throw new IOException("corrupted stream - out of bounds length found"); + } + } + + return length; + } + + byte[] read(int length) throws IOException { + byte[] bytes = new byte[length]; + int totalBytesRead = 0; + + while (totalBytesRead < length) { + int bytesRead = is.read(bytes, totalBytesRead, length - totalBytesRead); + if (bytesRead == -1) { + throw new IOException(String.format("EOF found reading %d bytes", length)); + } + totalBytesRead += bytesRead; + } + count += length; + return bytes; + } + + void mark() { + count = 0; + is.mark(is.available()); + } + + int reset() { + int tmp = count; + is.reset(); + return tmp; + } + + int read() { + int tmp = is.read(); + if (tmp >= 0) { + count++; + } + return tmp; + } +} + diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java new file mode 100644 index 000000000000..2bfda3a76bd5 --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java @@ -0,0 +1,101 @@ +/* + * 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.authorization.client.util.crypto; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; + +/** + * + * @author rmartinc + */ +class ASN1Encoder { + + static final int INTEGER = 0x02; + static final int SEQUENCE = 0x10; + static final int CONSTRUCTED = 0x20; + + private final ByteArrayOutputStream os; + + private ASN1Encoder() { + this.os = new ByteArrayOutputStream(); + } + + static public ASN1Encoder create() { + return new ASN1Encoder(); + } + + public ASN1Encoder write(BigInteger value) throws IOException { + writeEncoded(INTEGER, value.toByteArray()); + return this; + } + + public ASN1Encoder writeDerSeq(ASN1Encoder... objects) throws IOException { + writeEncoded(CONSTRUCTED | SEQUENCE, concatenate(objects)); + return this; + } + + public byte[] toByteArray() { + return os.toByteArray(); + } + + void writeEncoded(int tag, byte[] bytes) throws IOException { + write(tag); + writeLength(bytes.length); + write(bytes); + } + + void writeLength(int length) throws IOException { + if (length > 127) { + int size = 1; + int val = length; + + while ((val >>>= 8) != 0) { + size++; + } + + write((byte) (size | 0x80)); + + for (int i = (size - 1) * 8; i >= 0; i -= 8) { + write((byte) (length >> i)); + } + } else { + write((byte) length); + } + } + + void write(byte[] bytes) throws IOException { + os.write(bytes); + } + + void write(int b) throws IOException { + os.write(b); + } + + byte[] concatenate(ASN1Encoder... objects) throws IOException { + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + for (ASN1Encoder object : objects) { + tmp.write(object.toByteArray()); + } + return tmp.toByteArray(); + } +} + diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java new file mode 100644 index 000000000000..90c9c813826a --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java @@ -0,0 +1,226 @@ +/* + * 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.authorization.client.util.crypto; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Signature; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKeyFactory; +import javax.net.ssl.SSLSocketFactory; +import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.keycloak.common.crypto.CryptoProvider; +import org.keycloak.common.crypto.ECDSACryptoProvider; +import org.keycloak.common.crypto.PemUtilsProvider; +import org.keycloak.common.crypto.UserIdentityExtractorProvider; +import org.keycloak.common.util.KeystoreUtil; + +/** + *

Simple crypto provider to be used with the authz-client.

+ * + * @author rmartinc + */ +public class AuthzClientCryptoProvider implements CryptoProvider { + + @Override + public Provider getBouncyCastleProvider() { + try { + return KeyStore.getInstance(KeyStore.getDefaultType()).getProvider(); + } catch (KeyStoreException e) { + throw new IllegalStateException(e); + } + } + + @Override + public int order() { + return 100; + } + + @Override + public T getAlgorithmProvider(Class clazz, String algorithm) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CertificateUtilsProvider getCertificateUtils() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public PemUtilsProvider getPemUtils() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public T getOCSPProver(Class clazz) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public UserIdentityExtractorProvider getIdentityExtractorProvider() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ECDSACryptoProvider getEcdsaCryptoProvider() { + return new ECDSACryptoProvider() { + @Override + public byte[] concatenatedRSToASN1DER(byte[] signature, int signLength) throws IOException { + int len = signLength / 2; + int arraySize = len + 1; + + byte[] r = new byte[arraySize]; + byte[] s = new byte[arraySize]; + System.arraycopy(signature, 0, r, 1, len); + System.arraycopy(signature, len, s, 1, len); + BigInteger rBigInteger = new BigInteger(r); + BigInteger sBigInteger = new BigInteger(s); + + ASN1Encoder.create().write(rBigInteger); + ASN1Encoder.create().write(sBigInteger); + + return ASN1Encoder.create() + .writeDerSeq( + ASN1Encoder.create().write(rBigInteger), + ASN1Encoder.create().write(sBigInteger)) + .toByteArray(); + } + + @Override + public byte[] asn1derToConcatenatedRS(byte[] derEncodedSignatureValue, int signLength) throws IOException { + int len = signLength / 2; + + List seq = ASN1Decoder.create(derEncodedSignatureValue).readSequence(); + if (seq.size() != 2) { + throw new IOException("Invalid sequence with size different to 2"); + } + + BigInteger rBigInteger = ASN1Decoder.create(seq.get(0)).readInteger(); + BigInteger sBigInteger = ASN1Decoder.create(seq.get(1)).readInteger(); + + byte[] r = integerToBytes(rBigInteger, len); + byte[] s = integerToBytes(sBigInteger, len); + + byte[] concatenatedSignatureValue = new byte[signLength]; + System.arraycopy(r, 0, concatenatedSignatureValue, 0, len); + System.arraycopy(s, 0, concatenatedSignatureValue, len, len); + + return concatenatedSignatureValue; + } + + @Override + public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) { + throw new UnsupportedOperationException("Not supported yet."); + } + + private byte[] integerToBytes(BigInteger s, int qLength) { + byte[] bytes = s.toByteArray(); + if (qLength < bytes.length) { + byte[] tmp = new byte[qLength]; + System.arraycopy(bytes, bytes.length - tmp.length, tmp, 0, tmp.length); + return tmp; + } else if (qLength > bytes.length) { + byte[] tmp = new byte[qLength]; + System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length); + return tmp; + } + return bytes; + } + }; + } + + @Override + public ECParameterSpec createECParams(String curveName) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public KeyPairGenerator getKeyPairGen(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Cipher getAesCbcCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public KeyStore getKeyStore(KeystoreUtil.KeystoreFormat format) throws KeyStoreException, NoSuchProviderException { + return KeyStore.getInstance(format.name()); + } + + @Override + public CertificateFactory getX509CertFactory() throws CertificateException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CertStore getCertStore(CollectionCertStoreParameters collectionCertStoreParameters) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider b/authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider new file mode 100644 index 000000000000..41f99728b41e --- /dev/null +++ b/authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider @@ -0,0 +1,20 @@ +# +# 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. +# +# + +org.keycloak.authorization.client.util.crypto.AuthzClientCryptoProvider diff --git a/authz/client/src/test/java/org/keycloak/authorization/client/JsonParserTest.java b/authz/client/src/test/java/org/keycloak/authorization/client/JsonParserTest.java new file mode 100755 index 000000000000..90d6170c08cf --- /dev/null +++ b/authz/client/src/test/java/org/keycloak/authorization/client/JsonParserTest.java @@ -0,0 +1,57 @@ +/* + * 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.authorization.client; + +import java.io.IOException; +import java.io.InputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.adapters.config.AdapterConfig; + +/** + * @author Marek Posolda + */ +public class JsonParserTest { + + @Test + public void testParsingSystemProps() throws IOException { + System.setProperty("my.host", "foo"); + System.setProperty("con.pool.size", "200"); + System.setProperty("allow.any.hostname", "true"); + System.setProperty("socket.timeout.millis", "6000"); + System.setProperty("connection.timeout.millis", "7000"); + System.setProperty("connection.ttl.millis", "500"); + + InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json"); + + ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); + AdapterConfig config = mapper.readValue(is, AdapterConfig.class); + Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl()); + Assert.assertEquals("external", config.getSslRequired()); + Assert.assertEquals("angular-product${non.existing}", config.getResource()); + Assert.assertTrue(config.isPublicClient()); + Assert.assertTrue(config.isAllowAnyHostname()); + Assert.assertEquals(100, config.getCorsMaxAge()); + Assert.assertEquals(200, config.getConnectionPoolSize()); + Assert.assertEquals(6000L, config.getSocketTimeout()); + Assert.assertEquals(7000L, config.getConnectionTimeout()); + Assert.assertEquals(500L, config.getConnectionTTL()); + } +} diff --git a/authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java b/authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java new file mode 100644 index 000000000000..01d8a7ce6e46 --- /dev/null +++ b/authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java @@ -0,0 +1,74 @@ +/* + * 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.authorization.client.test; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Signature; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.authorization.client.util.crypto.AuthzClientCryptoProvider; +import org.keycloak.crypto.ECDSAAlgorithm; +import org.keycloak.crypto.JavaAlgorithm; + +/** + * + * @author rmartinc + */ +public class ECDSAAlgorithmTest { + + private final KeyPair keyPair; + + public ECDSAAlgorithmTest() throws Exception { + keyPair = KeyPairGenerator.getInstance("EC").genKeyPair(); + } + + + private void test(ECDSAAlgorithm algorithm) throws Exception { + AuthzClientCryptoProvider prov = new AuthzClientCryptoProvider(); + byte[] data = "Something to sign".getBytes(StandardCharsets.UTF_8); + Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm.name())); + signature.initSign(keyPair.getPrivate()); + signature.update(data); + byte[] sign = signature.sign(); + byte[] rsConcat = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(sign, algorithm.getSignatureLength()); + byte[] asn1Des = prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat, algorithm.getSignatureLength()); + byte[] rsConcat2 = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(asn1Des, algorithm.getSignatureLength()); + Assert.assertArrayEquals(rsConcat, rsConcat2); + } + + @Test + public void testES256() throws Exception { + test(ECDSAAlgorithm.ES256); + } + + @Test + public void testES384() throws Exception { + test(ECDSAAlgorithm.ES384); + } + + @Test + public void testES512() throws Exception { + test(ECDSAAlgorithm.ES512); + } +} + diff --git a/authz/client/src/test/resources/keycloak.json b/authz/client/src/test/resources/keycloak.json new file mode 100644 index 000000000000..4b9279960dd6 --- /dev/null +++ b/authz/client/src/test/resources/keycloak.json @@ -0,0 +1,12 @@ +{ + "auth-server-url" : "http://${my.host}:8080/auth", + "ssl-required" : "external", + "resource" : "angular-product${non.existing}", + "public-client" : true, + "allow-any-hostname": "${allow.any.hostname}", + "cors-max-age": 100, + "connection-pool-size": "${con.pool.size}", + "socket-timeout-millis": "${socket.timeout.millis}", + "connection-timeout-millis": "${connection.timeout.millis}", + "connection-ttl-millis": "${connection.ttl.millis}" +} \ No newline at end of file diff --git a/authz/policy-enforcer/pom.xml b/authz/policy-enforcer/pom.xml deleted file mode 100755 index 767283ce988b..000000000000 --- a/authz/policy-enforcer/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - org.keycloak - keycloak-authz-parent - 999.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-policy-enforcer - Keycloak Authz: Policy Enforcer - jar - - - 2.0.0.Final - - - - - org.keycloak - keycloak-authz-client - - - - - jakarta.servlet - jakarta.servlet-api - true - - - org.wildfly.security - wildfly-elytron-http-oidc - ${wildfly-elytron.version} - true - - - - diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java deleted file mode 100644 index 8aa66a961311..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2016 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.adapters.authorization; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.LockSupport; - -import org.keycloak.common.util.Time; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; - -/** - * A simple LRU cache implementation supporting expiration and maximum number of entries. - * - * @author Pedro Igor - */ -public class PathCache { - - /** - * The load factor. - */ - private static final float DEFAULT_LOAD_FACTOR = 0.75f; - - private final Map cache; - - private final AtomicBoolean writing = new AtomicBoolean(false); - - private final long maxAge; - private final boolean enabled; - private final Map paths; - - /** - * Creates a new instance. - * @param maxEntries the maximum number of entries to keep in the cache - * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire - * @param paths the pre-configured paths - */ - PathCache(final int maxEntries, long maxAge, - Map paths) { - cache = Collections.synchronizedMap(new LinkedHashMap(16, DEFAULT_LOAD_FACTOR, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return cache.size() > maxEntries; - } - }); - this.maxAge = maxAge; - this.enabled = ! (maxAge < -1 || (maxAge > -1 && maxAge <= 0)); - this.paths = paths; - } - - public void put(String uri, PathConfig newValue) { - if (!enabled) { - if (newValue != null) { - // if disabled we also remove from the pre-defined paths map - markForInvalidation(newValue); - } - return; - } - - try { - if (parkForWriteAndCheckInterrupt()) { - return; - } - - CacheEntry cacheEntry = cache.get(uri); - - if (cacheEntry == null) { - cache.put(uri, new CacheEntry(uri, newValue, maxAge)); - } - } finally { - writing.lazySet(false); - } - } - - private void markForInvalidation(PathConfig newValue) { - PathConfig pathConfig = paths.get(newValue.getPath()); - - if (pathConfig != null && !pathConfig.isStatic()) { - // invalidate the configuration so that the path config is reload based on latest changes on the server - pathConfig.invalidate(); - } - } - - public boolean containsKey(String uri) { - return cache.containsKey(uri); - } - - public PathConfig get(String uri) { - if (parkForReadAndCheckInterrupt()) { - return null; - } - - CacheEntry cached = cache.get(uri); - - if (cached != null) { - return removeIfExpired(cached); - } - - return null; - } - - public void remove(String key) { - try { - if (parkForWriteAndCheckInterrupt()) { - return; - } - - cache.remove(key); - } finally { - writing.lazySet(false); - } - } - - private PathConfig removeIfExpired(CacheEntry cached) { - if (cached == null) { - return null; - } - - PathConfig config = cached.value(); - - if (cached.isExpired()) { - remove(cached.key()); - - if (config != null && config.getPath() != null) { - // also remove from pre-defined paths map so that changes on the server are properly reflected - markForInvalidation(config); - } - return null; - } - - return config; - } - - private boolean parkForWriteAndCheckInterrupt() { - while (!writing.compareAndSet(false, true)) { - LockSupport.parkNanos(1L); - if (Thread.interrupted()) { - return true; - } - } - return false; - } - - private boolean parkForReadAndCheckInterrupt() { - while (writing.get()) { - LockSupport.parkNanos(1L); - if (Thread.interrupted()) { - return true; - } - } - return false; - } - - public int size() { - return cache.size(); - } - - private static final class CacheEntry { - - final String key; - final PathConfig value; - final long expiration; - - CacheEntry(String key, PathConfig value, long maxAge) { - this.key = key; - this.value = value; - if(maxAge == -1) { - expiration = -1; - } else { - expiration = Time.currentTimeMillis() + maxAge; - } - } - - String key() { - return key; - } - - PathConfig value() { - return value; - } - - boolean isExpired() { - return expiration != -1 ? Time.currentTimeMillis() > expiration : false; - } - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java deleted file mode 100644 index 2e1b75a3fc2d..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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.adapters.authorization; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.jboss.logging.Logger; -import org.keycloak.authorization.client.AuthzClient; -import org.keycloak.authorization.client.resource.ProtectedResource; -import org.keycloak.common.util.PathMatcher; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; - -/** - * @author Pedro Igor - */ -public class PathConfigMatcher extends PathMatcher { - - private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class); - - private final Map paths; - private final PathCache pathCache; - private final AuthzClient authzClient; - private final PolicyEnforcerConfig enforcerConfig; - - PathConfigMatcher(PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) { - this.enforcerConfig = enforcerConfig; - PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig(); - - if (cacheConfig == null) { - cacheConfig = new PathCacheConfig(); - } - - this.authzClient = authzClient; - this.paths = configurePaths(); - this.pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Initialization complete. Path configuration:"); - for (PathConfig pathConfig : this.paths.values()) { - LOGGER.debug(pathConfig); - } - } - } - - @Override - public PathConfig matches(String targetUri) { - PathConfig pathConfig = pathCache.get(targetUri); - - if (pathCache.containsKey(targetUri) || pathConfig != null) { - return pathConfig; - } - - pathConfig = super.matches(targetUri); - - if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) { - if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) { - try { - List matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri); - - if (matchingResources.isEmpty()) { - // if this config is invalidated (e.g.: due to cache expiration) we remove and return null - if (pathConfig != null && pathConfig.isInvalidated()) { - paths.remove(targetUri); - return null; - } - } else { - Map> cipConfig = null; - PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING; - ResourceRepresentation targetResource = matchingResources.get(0); - List methodConfig = null; - boolean isStatic = false; - - if (pathConfig != null) { - cipConfig = pathConfig.getClaimInformationPointConfig(); - enforcementMode = pathConfig.getEnforcementMode(); - methodConfig = pathConfig.getMethods(); - isStatic = pathConfig.isStatic(); - } else { - for (PathConfig existingPath : paths.values()) { - if (targetResource.getId().equals(existingPath.getId()) - && existingPath.isStatic() - && !org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) { - return null; - } - } - } - - pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next(); - - if (cipConfig != null) { - pathConfig.setClaimInformationPointConfig(cipConfig); - } - - if (methodConfig != null) { - pathConfig.setMethods(methodConfig); - } - - pathConfig.setStatic(isStatic); - pathConfig.setEnforcementMode(enforcementMode); - } - } catch (Exception cause) { - LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server"); - return null; - } - } - } - - pathCache.put(targetUri, pathConfig); - - return pathConfig; - } - - @Override - protected String getPath(PathConfig entry) { - return entry.getPath(); - } - - @Override - protected Collection getPaths() { - return paths.values(); - } - - public PathCache getPathCache() { - return pathCache; - } - - @Override - protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) { - if (originalConfig.hasPattern()) { - ProtectedResource resource = authzClient.protection().resource(); - - // search by an exact match - List search = resource.findByUri(path); - - // if exact match not found, try to obtain from current path the parent path. - // if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1 - // this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource, - // so any permission granted to parent also applies to sub resources - if (search.isEmpty()) { - search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true)); - } - - if (!search.isEmpty()) { - ResourceRepresentation targetResource = search.get(0); - PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next(); - - config.setScopes(originalConfig.getScopes()); - config.setMethods(originalConfig.getMethods()); - config.setParentConfig(originalConfig); - config.setEnforcementMode(originalConfig.getEnforcementMode()); - config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig()); - - return config; - } - } - - return null; - } - - public void removeFromCache(String pathConfig) { - pathCache.remove(pathConfig); - } - - public Map getPathConfig() { - return paths; - } - - private Map configurePaths() { - ProtectedResource protectedResource = this.authzClient.protection().resource(); - boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths(); - - for (PathConfig pathConfig : enforcerConfig.getPaths()) { - if (!org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - loadPathsFromServer = false; - break; - } - } - - if (loadPathsFromServer) { - LOGGER.info("No path provided in configuration."); - Map paths = configureAllPathsForResourceServer(protectedResource); - - paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig)); - - return paths; - } else { - LOGGER.info("Paths provided in configuration."); - return configureDefinedPaths(protectedResource, enforcerConfig); - } - } - - private Map configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) { - Map paths = Collections.synchronizedMap(new LinkedHashMap()); - - for (PathConfig pathConfig : enforcerConfig.getPaths()) { - ResourceRepresentation resource; - String resourceName = pathConfig.getName(); - String path = pathConfig.getPath(); - - if (resourceName != null) { - LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path); - resource = protectedResource.findByName(resourceName); - } else { - LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path); - List resources = protectedResource.findByUri(path); - - if (resources.isEmpty()) { - resources = protectedResource.findByMatchingUri(path); - } - - if (resources.size() == 1) { - resource = resources.get(0); - } else if (resources.size() > 1) { - throw new RuntimeException("Multiple resources found with the same uri"); - } else { - resource = null; - } - } - - if (resource != null) { - pathConfig.setId(resource.getId()); - // if the resource is statically bound to a resource it means the config can not be invalidated - if (resourceName != null) { - pathConfig.setStatic(true); - } - } - - if (org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - pathConfig.setStatic(true); - } - - PathConfig existingPath = null; - - for (PathConfig current : paths.values()) { - if (current.getPath().equals(pathConfig.getPath())) { - existingPath = current; - break; - } - } - - if (existingPath == null) { - paths.put(pathConfig.getPath(), pathConfig); - } else { - existingPath.getMethods().addAll(pathConfig.getMethods()); - existingPath.getScopes().addAll(pathConfig.getScopes()); - } - } - - return paths; - } - - private Map configureAllPathsForResourceServer(ProtectedResource protectedResource) { - LOGGER.info("Querying the server for all resources associated with this application."); - Map paths = Collections.synchronizedMap(new HashMap()); - - if (!enforcerConfig.getLazyLoadPaths()) { - for (String id : protectedResource.findAll()) { - ResourceRepresentation resourceDescription = protectedResource.findById(id); - - if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) { - for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) { - paths.put(pathConfig.getPath(), pathConfig); - } - } - } - } - - return paths; - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java deleted file mode 100644 index 6c5bc73f894c..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java +++ /dev/null @@ -1,669 +0,0 @@ -/* - * Copyright 2016 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.adapters.authorization; - -import static org.keycloak.adapters.authorization.util.JsonUtils.asAccessToken; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.ServiceLoader; -import java.util.Set; - -import org.apache.http.client.HttpClient; -import org.jboss.logging.Logger; -import org.keycloak.AuthorizationContext; -import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.adapters.authorization.spi.HttpResponse; -import org.keycloak.authorization.client.AuthorizationDeniedException; -import org.keycloak.authorization.client.AuthzClient; -import org.keycloak.authorization.client.ClientAuthorizationContext; -import org.keycloak.authorization.client.Configuration; -import org.keycloak.authorization.client.resource.PermissionResource; -import org.keycloak.authorization.client.resource.ProtectionResource; -import org.keycloak.common.util.Base64; -import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessToken.Authorization; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode; -import org.keycloak.representations.idm.authorization.AuthorizationRequest; -import org.keycloak.representations.idm.authorization.AuthorizationResponse; -import org.keycloak.representations.idm.authorization.Permission; -import org.keycloak.representations.idm.authorization.PermissionRequest; -import org.keycloak.util.JsonSerialization; - -/** - *

A Policy Enforcement Point (PEP) that requests and enforces authorization decisions from Keycloak. - * - * @author Pedro Igor - */ -public class PolicyEnforcer { - - private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class); - private static final String HTTP_METHOD_DELETE = "DELETE"; - - public static Builder builder() { - return new Builder(); - } - - private final AuthzClient authzClient; - private final Map paths; - private final PathConfigMatcher pathMatcher; - private final HttpClient httpClient; - private final PolicyEnforcerConfig enforcerConfig; - - private final Map claimInformationPointProviderFactories = new HashMap<>(); - - protected PolicyEnforcer(Builder builder) { - enforcerConfig = builder.getEnforcerConfig(); - Configuration authzClientConfig = builder.authzClientConfig; - - if (authzClientConfig.getRealm() == null) { - authzClientConfig.setRealm(enforcerConfig.getRealm()); - } - - if (authzClientConfig.getAuthServerUrl() == null) { - authzClientConfig.setAuthServerUrl(enforcerConfig.getAuthServerUrl()); - } - - if (authzClientConfig.getCredentials() == null || authzClientConfig.getCredentials().isEmpty()) { - authzClientConfig.setCredentials(enforcerConfig.getCredentials()); - } - - if (authzClientConfig.getResource() == null) { - authzClientConfig.setResource(enforcerConfig.getResource()); - } - - authzClient = AuthzClient.create(authzClientConfig); - httpClient = authzClient.getConfiguration().getHttpClient(); - pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient); - paths = pathMatcher.getPathConfig(); - - loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader())); - loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader())); - } - - public AuthorizationContext enforce(HttpRequest request, HttpResponse response) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", request.getURI()); - } - - AuthorizationContext context = authorize(request, response); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED"); - LOGGER.debugv("Returning authorization context with permissions:"); - for (Permission permission : context.getPermissions()) { - LOGGER.debug(permission); - } - } - - return context; - } - - public HttpClient getHttpClient() { - return httpClient; - } - - public AuthzClient getAuthzClient() { - return authzClient; - } - - public Map getPaths() { - return Collections.unmodifiableMap(paths); - } - - public Map getClaimInformationPointProviderFactories() { - return claimInformationPointProviderFactories; - } - - public PathConfigMatcher getPathMatcher() { - return pathMatcher; - } - - private AuthorizationContext authorize(HttpRequest request, HttpResponse response) { - EnforcementMode enforcementMode = enforcerConfig.getEnforcementMode(); - TokenPrincipal principal = request.getPrincipal(); - boolean anonymous = principal == null || principal.getRawToken() == null; - - if (EnforcementMode.DISABLED.equals(enforcementMode)) { - if (anonymous) { - response.sendError(401, "Invalid bearer"); - } - return createEmptyAuthorizationContext(true); - } - - PathConfig pathConfig = getPathConfig(request); - - if (anonymous) { - if (!isDefaultAccessDeniedUri(request)) { - if (pathConfig != null) { - if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - return createEmptyAuthorizationContext(true); - } else { - challenge(pathConfig, getRequiredScopes(pathConfig, request), request, response); - } - } else { - handleAccessDenied(response); - } - } - return createEmptyAuthorizationContext(false); - } - - AccessToken accessToken = principal.getToken(); - - if (accessToken != null) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig); - } - - if (pathConfig == null) { - if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) { - return createAuthorizationContext(accessToken, null); - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request)); - } - - if (isDefaultAccessDeniedUri(request)) { - return createAuthorizationContext(accessToken, null); - } - - handleAccessDenied(response); - - return createEmptyAuthorizationContext(false); - } - - if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - return createAuthorizationContext(accessToken, pathConfig); - } - - MethodConfig methodConfig = getRequiredScopes(pathConfig, request); - Map> claims = resolveClaims(pathConfig, request); - - if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) { - try { - return createAuthorizationContext(accessToken, pathConfig); - } catch (Exception e) { - throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e); - } - } - - AccessToken original = accessToken; - - accessToken = requestAuthorizationToken(pathConfig, methodConfig, request, claims); - - if (accessToken != null) { - AccessToken.Authorization authorization = original.getAuthorization(); - - if (authorization == null) { - authorization = new AccessToken.Authorization(); - authorization.setPermissions(new ArrayList()); - } - - AccessToken.Authorization newAuthorization = accessToken.getAuthorization(); - - if (newAuthorization != null) { - Collection grantedPermissions = authorization.getPermissions(); - Collection newPermissions = newAuthorization.getPermissions(); - - for (Permission newPermission : newPermissions) { - if (!grantedPermissions.contains(newPermission)) { - grantedPermissions.add(newPermission); - } - } - } - - original.setAuthorization(authorization); - - if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) { - try { - return createAuthorizationContext(accessToken, pathConfig); - } catch (Exception e) { - throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e); - } - } - } - - if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) { - return createEmptyAuthorizationContext(true); - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig); - } - - if (!challenge(pathConfig, methodConfig, request, response)) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig); - } - handleAccessDenied(response); - } - } - - return createEmptyAuthorizationContext(false); - } - - protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, HttpRequest request, Map> claims) { - if (isDefaultAccessDeniedUri(request)) { - return true; - } - - Authorization authorization = accessToken.getAuthorization(); - - if (authorization == null) { - return false; - } - - boolean hasPermission = false; - Collection grantedPermissions = authorization.getPermissions(); - - for (Permission permission : grantedPermissions) { - if (permission.getResourceId() != null) { - if (isResourcePermission(actualPathConfig, permission)) { - hasPermission = true; - - if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) { - continue; - } - - if (hasResourceScopePermission(methodConfig, permission)) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions); - } - if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) { - pathMatcher.removeFromCache(getPath(request)); - } - - return hasValidClaims(permission, claims); - } - } - } else { - if (hasResourceScopePermission(methodConfig, permission)) { - return true; - } - } - } - - if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) { - return true; - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions); - } - - return false; - } - - protected Map> resolveClaims(PathConfig pathConfig, HttpRequest request) { - Map> claims = new HashMap<>(); - - resolveClaims(claims, enforcerConfig.getClaimInformationPointConfig(), request); - resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), request); - - return claims; - } - - protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, HttpResponse response) { - if (isBearerAuthorization(request)) { - String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, request); - - if (ticket != null) { - response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"") - .append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString()); - response.sendError(401); - } else { - response.sendError(403); - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Sending challenge"); - } - - return true; - } - - handleAccessDenied(response); - - return true; - } - - protected void handleAccessDenied(HttpResponse response) { - String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo(); - - if (accessDeniedPath != null) { - response.setHeader("Location", accessDeniedPath); - response.sendError(302); - } else { - response.sendError(403); - } - } - - private boolean hasValidClaims(Permission permission, Map> claims) { - Map> grantedClaims = permission.getClaims(); - - if (grantedClaims != null) { - if (claims.isEmpty()) { - return false; - } - - for (Entry> entry : grantedClaims.entrySet()) { - List requestClaims = claims.get(entry.getKey()); - - if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) { - return false; - } - } - } - - return true; - } - - private boolean isDefaultAccessDeniedUri(HttpRequest request) { - String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo(); - return accessDeniedPath != null && request.getURI().contains(accessDeniedPath); - } - - private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) { - List requiredScopes = methodConfig.getScopes(); - Set allowedScopes = permission.getScopes(); - - if (allowedScopes.isEmpty()) { - return true; - } - - PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode(); - - if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) { - return allowedScopes.containsAll(requiredScopes); - } - - if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) { - for (String requiredScope : requiredScopes) { - if (allowedScopes.contains(requiredScope)) { - return true; - } - } - } - - return requiredScopes.isEmpty(); - } - - private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) { - return new ClientAuthorizationContext(authzClient) { - @Override - public boolean hasPermission(String resourceName, String scopeName) { - return granted; - } - - @Override - public boolean hasResourcePermission(String resourceName) { - return granted; - } - - @Override - public boolean hasScopePermission(String scopeName) { - return granted; - } - - @Override - public List getPermissions() { - return Collections.EMPTY_LIST; - } - - @Override - public boolean isGranted() { - return granted; - } - }; - } - - private String getPath(HttpRequest request) { - return request.getRelativePath(); - } - - private MethodConfig getRequiredScopes(PathConfig pathConfig, HttpRequest request) { - String method = request.getMethod(); - - for (MethodConfig methodConfig : pathConfig.getMethods()) { - if (methodConfig.getMethod().equals(method)) { - return methodConfig; - } - } - - MethodConfig methodConfig = new MethodConfig(); - - methodConfig.setMethod(request.getMethod()); - List scopes = new ArrayList<>(); - - if (Boolean.TRUE.equals(enforcerConfig.getHttpMethodAsScope())) { - scopes.add(request.getMethod()); - } else { - scopes.addAll(pathConfig.getScopes()); - } - - methodConfig.setScopes(scopes); - methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY); - - return methodConfig; - } - - private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) { - return new ClientAuthorizationContext(accessToken, pathConfig, authzClient); - } - - private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) { - // first we try a match using resource id - boolean resourceMatch = matchResourcePermission(actualPathConfig, permission); - - // as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission - if (!resourceMatch && actualPathConfig.isInstance()) { - resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission); - } - - return resourceMatch; - } - - private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) { - return permission.getResourceId().equals(actualPathConfig.getId()); - } - - private PathConfig getPathConfig(HttpRequest request) { - return isDefaultAccessDeniedUri(request) ? null : pathMatcher.matches(getPath(request)); - } - - private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, Map> claims) { - if (enforcerConfig.getUserManagedAccess() != null) { - return null; - } - - try { - TokenPrincipal principal = request.getPrincipal(); - String accessTokenString = principal.getRawToken(); - AccessToken accessToken = principal.getToken(); - AuthorizationRequest authzRequest = new AuthorizationRequest(); - - if (isBearerAuthorization(request) || accessToken.getAuthorization() != null) { - authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes()); - } - - if (!claims.isEmpty()) { - authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt"); - authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims))); - } - - if (accessToken.getAuthorization() != null) { - authzRequest.setRpt(accessTokenString); - } - - LOGGER.debug("Obtaining authorization for authenticated user."); - AuthorizationResponse authzResponse; - - if (isBearerAuthorization(request)) { - authzRequest.setSubjectToken(accessTokenString); - authzResponse = authzClient.authorization().authorize(authzRequest); - } else { - authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest); - } - - if (authzResponse != null) { - return asAccessToken(authzResponse.getToken()); - } - } catch (AuthorizationDeniedException ignore) { - LOGGER.debug("Authorization denied", ignore); - } catch (Exception e) { - LOGGER.debug("Authorization failed", e); - } - - return null; - } - - private String getPermissionTicket(PathConfig pathConfig, MethodConfig methodConfig, AuthzClient authzClient, HttpRequest httpFacade) { - if (enforcerConfig.getUserManagedAccess() != null) { - ProtectionResource protection = authzClient.protection(); - PermissionResource permission = protection.permission(); - PermissionRequest permissionRequest = new PermissionRequest(); - - permissionRequest.setResourceId(pathConfig.getId()); - permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes())); - - Map> claims = resolveClaims(pathConfig, httpFacade); - - if (!claims.isEmpty()) { - permissionRequest.setClaims(claims); - } - - return permission.create(permissionRequest).getTicket(); - } - - return null; - } - - private boolean isBearerAuthorization(HttpRequest request) { - List authHeaders = request.getHeaders("Authorization"); - - if (authHeaders != null) { - for (String authHeader : authHeaders) { - String[] split = authHeader.trim().split("\\s+"); - if (split == null || split.length != 2) continue; - if (!split[0].equalsIgnoreCase("Bearer")) continue; - return true; - } - } - - return authzClient.getConfiguration().isBearerOnly(); - } - - private void loadClaimInformationPointProviders(ServiceLoader loader) { - for (ClaimInformationPointProviderFactory factory : loader) { - factory.init(this); - - claimInformationPointProviderFactories.put(factory.getName(), factory); - } - } - - private void resolveClaims(Map> claims, Map> claimInformationPointConfig, HttpRequest request) { - if (claimInformationPointConfig != null) { - for (Entry> claimDef : claimInformationPointConfig.entrySet()) { - ClaimInformationPointProviderFactory factory = claimInformationPointProviderFactories.get(claimDef.getKey()); - - if (factory != null) { - claims.putAll(factory.create(claimDef.getValue()).resolve(request)); - } - } - } - } - - public static class Builder { - - Configuration authzClientConfig = new Configuration(); - - private Builder() { - } - - public Builder authServerUrl(String authServerUrl) { - authzClientConfig.setAuthServerUrl(authServerUrl); - return this; - } - - public Builder realm(String realm) { - authzClientConfig.setRealm(realm); - return this; - } - - public Builder clientId(String clientId) { - authzClientConfig.setResource(clientId); - return this; - } - - public Builder bearerOnly(boolean bearerOnly) { - authzClientConfig.setBearerOnly(bearerOnly); - return this; - } - - public Builder credentials(Map credentials) { - authzClientConfig.setCredentials(credentials); - return this; - } - - public Builder enforcerConfig(PolicyEnforcerConfig enforcerConfig) { - authzClientConfig.setPolicyEnforcerConfig(enforcerConfig); - return this; - } - - public Builder enforcerConfig(InputStream is) { - try { - enforcerConfig(JsonSerialization.readValue(is, PolicyEnforcerConfig.class)); - } catch (Exception cause) { - throw new RuntimeException("Failed to read configuration", cause); - } - return this; - } - - public Builder httpClient(HttpClient httpClient) { - authzClientConfig.setHttpClient(httpClient); - return this; - } - - public Builder credentialProvider(ClientCredentialsProvider credentialsProvider) { - authzClientConfig.setClientCredentialsProvider(credentialsProvider); - return this; - } - - public PolicyEnforcer build() { - return new PolicyEnforcer(this); - } - - PolicyEnforcerConfig getEnforcerConfig() { - return authzClientConfig.getPolicyEnforcerConfig(); - } - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java deleted file mode 100644 index d47434382b50..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.adapters.authorization; - -import java.security.Principal; - -import org.keycloak.adapters.authorization.util.JsonUtils; -import org.keycloak.representations.AccessToken; - -/** - * A {@link Principal} backed by a token representing the entity requesting permissions. - * - * @author Pedro Igor - */ -public interface TokenPrincipal extends Principal { - - /** - * The token in its raw format. - * - * @return the token in its raw format. - */ - String getRawToken(); - - /** - * The {@link AccessToken} representation of {@link TokenPrincipal#getRawToken()}. - * - * @return the access token representation - */ - default AccessToken getToken() { - return JsonUtils.asAccessToken(getRawToken()); - } - - /** - * The name of the entity represented by the token. - * - * @return the name of the principal - */ - default String getName() { - return getToken().getPreferredUsername(); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java deleted file mode 100644 index 00eab0622336..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.cip; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.adapters.authorization.util.PlaceHolders; - -/** - * @author Pedro Igor - */ -public class ClaimsInformationPointProvider implements ClaimInformationPointProvider { - - private final Map config; - - public ClaimsInformationPointProvider(Map config) { - this.config = config; - } - - @Override - public Map> resolve(HttpRequest request) { - Map> claims = new HashMap<>(); - - for (Entry configEntry : config.entrySet()) { - String claimName = configEntry.getKey(); - Object claimValue = configEntry.getValue(); - List values = new ArrayList<>(); - - if (claimValue instanceof String) { - values = getValues(claimValue.toString(), request); - } else if (claimValue instanceof Collection) { - - for (Object value : Collection.class.cast(claimValue)) { - List resolvedValues = getValues(value.toString(), request); - - if (!resolvedValues.isEmpty()) { - values.addAll(resolvedValues); - } - } - } - - if (!values.isEmpty()) { - claims.put(claimName, values); - } - } - - return claims; - } - - private List getValues(String value, HttpRequest httpFacade) { - return PlaceHolders.resolve(value, httpFacade); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java deleted file mode 100644 index 7c1d82f0c66f..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.cip; - -import java.util.Map; - -import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; - -/** - * @author Pedro Igor - */ -public class ClaimsInformationPointProviderFactory implements ClaimInformationPointProviderFactory { - - @Override - public String getName() { - return "claims"; - } - - @Override - public ClaimsInformationPointProvider create(Map config) { - return new ClaimsInformationPointProvider(config); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java deleted file mode 100644 index 958df5477914..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.cip; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.fasterxml.jackson.databind.JsonNode; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.RequestBuilder; -import org.apache.http.util.EntityUtils; -import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.adapters.authorization.util.JsonUtils; -import org.keycloak.adapters.authorization.util.PlaceHolders; -import org.keycloak.authorization.client.util.HttpResponseException; -import org.keycloak.common.util.StreamUtil; -import org.keycloak.util.JsonSerialization; - -/** - * @author Pedro Igor - */ -public class HttpClaimInformationPointProvider implements ClaimInformationPointProvider { - - private final Map config; - private final HttpClient httpClient; - - public HttpClaimInformationPointProvider(Map config, HttpClient httpClient) { - this.config = config; - this.httpClient = httpClient; - } - - @Override - public Map> resolve(HttpRequest request) { - try { - InputStream responseStream = executeRequest(request); - - try (InputStream inputStream = new BufferedInputStream(responseStream)) { - JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream); - Map> claims = new HashMap<>(); - Map claimsDef = (Map) config.get("claims"); - - if (claimsDef == null) { - Iterator nodeNames = jsonNode.fieldNames(); - - while (nodeNames.hasNext()) { - String nodeName = nodeNames.next(); - claims.put(nodeName, JsonUtils.getValues(jsonNode.get(nodeName))); - } - } else { - for (Entry claimDef : claimsDef.entrySet()) { - List jsonPaths = new ArrayList<>(); - - if (claimDef.getValue() instanceof Collection) { - jsonPaths.addAll(Collection.class.cast(claimDef.getValue())); - } else { - jsonPaths.add(claimDef.getValue().toString()); - } - - List claimValues = new ArrayList<>(); - - for (String path : jsonPaths) { - claimValues.addAll(JsonUtils.getValues(jsonNode, path)); - } - - claims.put(claimDef.getKey(), claimValues); - } - } - - return claims; - } - } catch (IOException cause) { - throw new RuntimeException("Could not obtain claims from http claim information point [" + config.get("url") + "] response", cause); - } - } - - private InputStream executeRequest(HttpRequest request) { - String method = config.get("method").toString(); - - if (method == null) { - method = "GET"; - } - - RequestBuilder builder = null; - - if ("GET".equalsIgnoreCase(method)) { - builder = RequestBuilder.get(); - } else { - builder = RequestBuilder.post(); - } - - builder.setUri(config.get("url").toString()); - - byte[] bytes = new byte[0]; - - try { - setParameters(builder, request); - - if (config.containsKey("headers")) { - setHeaders(builder, request); - } - - HttpResponse response = httpClient.execute(builder.build()); - HttpEntity entity = response.getEntity(); - - if (entity != null) { - bytes = EntityUtils.toByteArray(entity); - } - - StatusLine statusLine = response.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - - if (statusCode < 200 || statusCode >= 300) { - throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes); - } - - return new ByteArrayInputStream(bytes); - } catch (Exception cause) { - try { - throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + StreamUtil.readString(new ByteArrayInputStream(bytes), Charset.forName("UTF-8")), cause); - } catch (Exception e) { - throw new RuntimeException("Error executing http method [" + builder + "]", cause); - } - } - } - - private void setHeaders(RequestBuilder builder, HttpRequest request) { - Object headersDef = config.get("headers"); - - if (headersDef != null) { - Map headers = Map.class.cast(headersDef); - - for (Entry header : headers.entrySet()) { - Object value = header.getValue(); - List headerValues = new ArrayList<>(); - - if (value instanceof Collection) { - Collection values = Collection.class.cast(value); - - for (Object item : values) { - headerValues.addAll(PlaceHolders.resolve(item.toString(), request)); - } - } else { - headerValues.addAll(PlaceHolders.resolve(value.toString(), request)); - } - - for (String headerValue : headerValues) { - builder.addHeader(header.getKey(), headerValue); - } - } - } - } - - private void setParameters(RequestBuilder builder, HttpRequest request) { - Object config = this.config.get("parameters"); - - if (config != null) { - Map paramsDef = Map.class.cast(config); - - for (Entry paramDef : paramsDef.entrySet()) { - Object value = paramDef.getValue(); - List paramValues = new ArrayList<>(); - - if (value instanceof Collection) { - Collection values = Collection.class.cast(value); - - for (Object item : values) { - paramValues.addAll(PlaceHolders.resolve(item.toString(), request)); - } - } else { - paramValues.addAll(PlaceHolders.resolve(value.toString(), request)); - } - - for (String paramValue : paramValues) { - builder.addParameter(paramDef.getKey(), paramValue); - } - } - } - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java deleted file mode 100644 index 3c9a4ec264da..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.cip; - -import java.util.Map; - -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; - -/** - * @author Pedro Igor - */ -public class HttpClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory { - - private PolicyEnforcer policyEnforcer; - - @Override - public String getName() { - return "http"; - } - - @Override - public void init(PolicyEnforcer policyEnforcer) { - this.policyEnforcer = policyEnforcer; - } - - @Override - public HttpClaimInformationPointProvider create(Map config) { - return new HttpClaimInformationPointProvider(config, policyEnforcer.getHttpClient()); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java deleted file mode 100644 index 43325a8c834c..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.adapters.authorization.cip.spi; - -import java.util.List; -import java.util.Map; - -import org.keycloak.adapters.authorization.spi.HttpRequest; - -/** - * @author Pedro Igor - */ -public interface ClaimInformationPointProvider { - - Map> resolve(HttpRequest request); -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java deleted file mode 100644 index f9e1e305a756..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.adapters.authorization.cip.spi; - -import java.util.Map; - -import org.keycloak.adapters.authorization.PolicyEnforcer; - -/** - * @author Pedro Igor - */ -public interface ClaimInformationPointProviderFactory { - - String getName(); - - default void init(PolicyEnforcer policyEnforcer) { - - } - - C create(Map config); -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ElytronPolicyEnforcerFilter.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ElytronPolicyEnforcerFilter.java deleted file mode 100644 index 99c42912e4ad..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ElytronPolicyEnforcerFilter.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.keycloak.adapters.authorization.integration.elytron; - -import java.security.Principal; - -import jakarta.servlet.http.HttpServletRequest; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.authorization.integration.jakarta.ServletPolicyEnforcerFilter; -import org.keycloak.adapters.authorization.spi.ConfigurationResolver; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.wildfly.security.http.oidc.OidcClientConfiguration; -import org.wildfly.security.http.oidc.OidcPrincipal; -import org.wildfly.security.http.oidc.RefreshableOidcSecurityContext; - -public class ElytronPolicyEnforcerFilter extends ServletPolicyEnforcerFilter { - - public ElytronPolicyEnforcerFilter(ConfigurationResolver configResolver) { - super(configResolver); - } - - @Override - protected String extractBearerToken(HttpServletRequest request) { - Principal principal = request.getUserPrincipal(); - - if (principal == null) { - return null; - } - - OidcPrincipal oidcPrincipal = (OidcPrincipal) principal; - RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) oidcPrincipal.getOidcSecurityContext(); - - if (securityContext == null) { - return null; - } - - return securityContext.getTokenString(); - } - - @Override - protected PolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, PolicyEnforcerConfig enforcerConfig) { - RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) ((OidcPrincipal) servletRequest.getUserPrincipal()).getOidcSecurityContext(); - OidcClientConfiguration configuration = securityContext.getOidcClientConfiguration(); - String authServerUrl = configuration.getAuthServerBaseUrl(); - - return PolicyEnforcer.builder() - .authServerUrl(authServerUrl) - .realm(configuration.getRealm()) - .clientId(configuration.getClientId()) - .credentials(configuration.getResourceCredentials()) - .bearerOnly(false) - .enforcerConfig(enforcerConfig) - .httpClient(configuration.getClient()).build(); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java deleted file mode 100644 index d5bec3b955b9..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.keycloak.adapters.authorization.integration.elytron; - -import java.io.IOException; -import java.io.InputStream; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.ServiceLoader; - -import jakarta.servlet.DispatcherType; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletContextEvent; -import jakarta.servlet.ServletContextListener; -import jakarta.servlet.annotation.WebListener; -import org.jboss.logging.Logger; -import org.keycloak.adapters.authorization.spi.ConfigurationResolver; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.keycloak.util.JsonSerialization; - -/** - * A {@link ServletContextListener} to programmatically configure the {@link ServletContext} in order to - * enable the policy enforcer.

- * - * By default, the policy enforcer configuration is loaded from a file at {@code WEB-INF/policy-enforcer.json}.

- * - * Applications can also dynamically resolve the configuration by implementing the {@link ConfigurationResolver} SPI. For that, - * make sure to create a {@link META-INF/services/org.keycloak.adapters.authorization.spi.ConfigurationResolver} to register - * the implementation. - * - * @author Pedro Igor - */ -@WebListener -public class PolicyEnforcerServletContextListener implements ServletContextListener { - - private final Logger logger = Logger.getLogger(getClass()); - - @Override - public void contextInitialized(ServletContextEvent sce) { - ServletContext servletContext = sce.getServletContext(); - Iterator configResolvers = ServiceLoader.load(ConfigurationResolver.class).iterator(); - ConfigurationResolver configResolver; - - if (configResolvers.hasNext()) { - configResolver = configResolvers.next(); - - if (configResolvers.hasNext()) { - throw new IllegalStateException("Multiple " + ConfigurationResolver.class.getName() + " implementations found"); - } - - logger.debugf("Configuration resolver found from classpath: %s", configResolver); - } else { - String enforcerConfigLocation = "WEB-INF/policy-enforcer.json"; - InputStream config = servletContext.getResourceAsStream(enforcerConfigLocation); - - if (config == null) { - logger.debugf("Could not find the policy enforcer configuration file: %s", enforcerConfigLocation); - return; - } - - try { - configResolver = createDefaultConfigurationResolver(JsonSerialization.readValue(config, PolicyEnforcerConfig.class)); - } catch (IOException e) { - throw new RuntimeException("Failed to parse policy enforcer configuration: " + enforcerConfigLocation); - } - } - - logger.debug("Policy enforcement filter is enabled."); - - servletContext.addFilter("keycloak-policy-enforcer", new ElytronPolicyEnforcerFilter(configResolver)) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*"); - } - - private ConfigurationResolver createDefaultConfigurationResolver(PolicyEnforcerConfig enforcerConfig) { - return new ConfigurationResolver() { - @Override - public PolicyEnforcerConfig resolve(HttpRequest request) { - return enforcerConfig; - } - }; - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java deleted file mode 100644 index 6b645fce5e54..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.adapters.authorization.integration.elytron; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import org.keycloak.adapters.authorization.TokenPrincipal; -import org.keycloak.adapters.authorization.spi.HttpRequest; - -/** - * @author Pedro Igor - */ -public class ServletHttpRequest implements HttpRequest { - - private final HttpServletRequest request; - private final TokenPrincipal tokenPrincipal; - private InputStream inputStream; - - public ServletHttpRequest(HttpServletRequest request, TokenPrincipal tokenPrincipal) { - this.request = request; - this.tokenPrincipal = tokenPrincipal; - } - - @Override - public String getRelativePath() { - String uri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String servletPath = uri.substring(uri.indexOf(contextPath) + contextPath.length()); - - if ("".equals(servletPath)) { - servletPath = "/"; - } - - return servletPath; - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getURI() { - return request.getRequestURI(); - } - - @Override - public List getHeaders(String name) { - return Collections.list(request.getHeaders(name)); - } - - @Override - public String getFirstParam(String name) { - Map parameters = request.getParameterMap(); - String[] values = parameters.get(name); - - if (values == null || values.length == 0) { - return null; - } - - return values[0]; - } - - @Override - public String getCookieValue(String name) { - Cookie[] cookies = request.getCookies(); - - for (Cookie cookie : cookies) { - if (cookie.getName().equals(name)) { - return cookie.getValue(); - } - } - - return null; - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public boolean isSecure() { - return request.isSecure(); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public InputStream getInputStream(boolean buffered) { - if (inputStream != null) { - return inputStream; - } - - if (buffered) { - try { - return inputStream = new BufferedInputStream(request.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - return request.getInputStream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public TokenPrincipal getPrincipal() { - return tokenPrincipal; - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java deleted file mode 100644 index 9528224790b5..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.adapters.authorization.integration.elytron; - -import java.io.IOException; - -import jakarta.servlet.http.HttpServletResponse; -import org.keycloak.adapters.authorization.spi.HttpResponse; - -/** - * @author Pedro Igor - */ -public class ServletHttpResponse implements HttpResponse { - - private HttpServletResponse response; - - public ServletHttpResponse(HttpServletResponse response) { - this.response = response; - } - - @Override - public void sendError(int status) { - try { - response.sendError(status); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void sendError(int status, String reason) { - try { - response.sendError(status, reason); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void setHeader(String name, String value) { - response.setHeader(name, value); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/jakarta/ServletPolicyEnforcerFilter.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/jakarta/ServletPolicyEnforcerFilter.java deleted file mode 100644 index ee0931044baf..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/jakarta/ServletPolicyEnforcerFilter.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.keycloak.adapters.authorization.integration.jakarta; - -import java.io.IOException; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletContextAttributeListener; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import org.jboss.logging.Logger; -import org.keycloak.AuthorizationContext; -import org.keycloak.adapters.authorization.PolicyEnforcer; -import org.keycloak.adapters.authorization.TokenPrincipal; -import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest; -import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse; -import org.keycloak.adapters.authorization.spi.ConfigurationResolver; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import org.wildfly.security.http.oidc.OidcClientConfiguration; -import org.wildfly.security.http.oidc.OidcPrincipal; -import org.wildfly.security.http.oidc.RefreshableOidcSecurityContext; - -/** - * A Jakarta Servlet {@link Filter} acting as a policy enforcer. This filter does not enforce access for anonymous subjects.

- * - * For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if - * the request should continue.

- * - * If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly - * respond to client. - * - * @author Pedro Igor - */ -public class ServletPolicyEnforcerFilter implements Filter, ServletContextAttributeListener { - - private final Logger logger = Logger.getLogger(getClass()); - private final Map policyEnforcer; - private final ConfigurationResolver configResolver; - - public ServletPolicyEnforcerFilter(ConfigurationResolver configResolver) { - this.configResolver = configResolver; - this.policyEnforcer = Collections.synchronizedMap(new HashMap<>()); - } - - @Override - public void init(FilterConfig filterConfig) { - // no-init - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - ServletHttpRequest httpRequest = new ServletHttpRequest(request, new TokenPrincipal() { - @Override - public String getRawToken() { - return extractBearerToken(request); - } - }); - - PolicyEnforcer policyEnforcer = getOrCreatePolicyEnforcer(request, httpRequest); - AuthorizationContext authzContext = policyEnforcer.enforce(httpRequest, new ServletHttpResponse(response)); - - request.setAttribute(AuthorizationContext.class.getName(), authzContext); - - if (authzContext.isGranted()) { - logger.debug("Request authorized, continuing the filter chain"); - filterChain.doFilter(servletRequest, servletResponse); - } else { - logger.debugf("Unauthorized request to path [%s], aborting the filter chain", request.getRequestURI()); - } - } - - protected String extractBearerToken(HttpServletRequest request) { - Enumeration authorizationHeaderValues = request.getHeaders("Authorization"); - - while (authorizationHeaderValues.hasMoreElements()) { - String value = authorizationHeaderValues.nextElement(); - String[] parts = value.trim().split("\\s+"); - - if (parts.length != 2) { - continue; - } - - String bearer = parts[0]; - - if (bearer.equalsIgnoreCase("Bearer")) { - return parts[1]; - } - } - - return null; - } - - private PolicyEnforcer getOrCreatePolicyEnforcer(HttpServletRequest servletRequest, HttpRequest request) { - return policyEnforcer.computeIfAbsent(configResolver.resolve(request), new Function() { - @Override - public PolicyEnforcer apply(PolicyEnforcerConfig enforcerConfig) { - return createPolicyEnforcer(servletRequest, enforcerConfig); - } - }); - } - - protected PolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, PolicyEnforcerConfig enforcerConfig) { - String authServerUrl = enforcerConfig.getAuthServerUrl(); - - return PolicyEnforcer.builder() - .authServerUrl(authServerUrl) - .realm(enforcerConfig.getRealm()) - .clientId(enforcerConfig.getResource()) - .credentials(enforcerConfig.getCredentials()) - .bearerOnly(false) - .enforcerConfig(enforcerConfig).build(); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java deleted file mode 100644 index 158d3f0922a1..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.adapters.authorization.spi; - -import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; - -/** - * Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}.

- * - * @author Pedro Igor - */ -public interface ConfigurationResolver { - - /** - * Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}. - * - * @param request the request - * @return the policy enforcer configuration for the given request - */ - PolicyEnforcerConfig resolve(HttpRequest request); -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java deleted file mode 100644 index 8fd07224886f..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.adapters.authorization.spi; - -import java.io.InputStream; -import java.util.List; - -import org.keycloak.adapters.authorization.TokenPrincipal; - -/** - * Represents an incoming HTTP request and the contract to manipulate it. - * - * @author Pedro Igor - */ -public interface HttpRequest { - - /** - * Get the request path. This is the path relative to the context path. - * E.g.: for a HTTP GET request to http://my.appserver.com/my-application/path/sub-path this method is going to return /path/sub-path. - - * @return the relative path - */ - String getRelativePath(); - - /** - * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT. - * - * @return a {@code String} specifying the name of the method with which this request was made - */ - String getMethod(); - - /** - * Get the URI representation for the current request. - * - * @return a {@code String} representation for the current request - */ - String getURI(); - - /** - * Get a list of all of the values set for the specified header within the HTTP request. - * - * @param name the header name - * @return a list of the values set for this header, if the header is not set on the request then null should be returned - */ - List getHeaders(String name); - - /** - * Get the first value for a parameter with the given {@code name} - * - * @param name the parameter name - * @return the value of the parameter - */ - String getFirstParam(String name); - - /** - * Get the first value for a cookie with the given {@code name}. - * - * @param name the parameter name - * @return the value of the cookie - */ - String getCookieValue(String name); - - /** - * Returns the client address. - * - * @return the client address. - */ - String getRemoteAddr(); - - /** - * Indicates if the request is coming from a secure channel through HTTPS. - * - * @return {@code true} if the HTTP scheme is set to 'https'. Otherwise, {@code false} - */ - boolean isSecure(); - - /** - * Get the first value for a HEADER with the given {@code name}. - * - * @param name the HEADER name - * @return the value of the HEADER - */ - String getHeader(String name); - - /** - * Returns the request input stream - * - * @param buffered if the input stream should be buffered and support for multiple reads - * @return the request input stream - */ - InputStream getInputStream(boolean buffered); - - /** - * Returns a {@link TokenPrincipal} associated with the request. - * - * @return the principal - */ - TokenPrincipal getPrincipal(); -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java deleted file mode 100644 index dcd41a5f99c7..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.adapters.authorization.spi; - -/** - * Represents an outgoing HTTP response and the contract to manipulate it. - * - * @author Pedro Igor - */ -public interface HttpResponse { - - /** - * Send an error with the given {@code statusCode}. - * - * @param statusCode the status to set in the response - */ - void sendError(int statusCode); - - /** - * Send an error with the given {@code statusCode} and {@code reason} message. - * - * @param statusCode the status to set in the response - */ - void sendError(int statusCode, String reason); - - /** - * Set a header with the given {@code name} and {@code value}. - * - * @param name the header name - * @param value the header value - */ - void setHeader(String name, String value); -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java deleted file mode 100644 index 5c06ca60b21e..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.util; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.representations.AccessToken; -import org.keycloak.util.JsonSerialization; - -/** - * Utility methods to manipulate JSON data - * - * @author Pedro Igor - */ -public class JsonUtils { - - public static List getValues(JsonNode jsonNode, String path) { - return getValues(jsonNode.at(path)); - } - - public static List getValues(JsonNode jsonNode) { - List values = new ArrayList<>(); - - if (jsonNode.isArray()) { - for (JsonNode node : jsonNode) { - String value; - - if (node.isObject()) { - try { - value = JsonSerialization.writeValueAsString(node); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - value = node.asText(); - } - - if (value != null) { - values.add(value); - } - } - } else { - String value = jsonNode.asText(); - - if (value != null) { - values.add(value); - } - } - - return values; - } - - public static AccessToken asAccessToken(String rawToken) { - try { - return new JWSInput(rawToken).readJsonContent(AccessToken.class); - } catch (Exception cause) { - throw new RuntimeException("Failed to decode token", cause); - } - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java deleted file mode 100644 index b5f824ad9a30..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.util; - -import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter; - -import java.util.Arrays; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; -import org.keycloak.adapters.authorization.TokenPrincipal; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.util.JsonSerialization; - -/** - * @author Pedro Igor - */ -public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderResolver { - - public static final String NAME = "keycloak"; - - @Override - public List resolve(String placeHolder, HttpRequest request) { - String source = placeHolder.substring(placeHolder.indexOf('.') + 1); - TokenPrincipal principal = request.getPrincipal(); - - if (source.endsWith("access_token")) { - return Arrays.asList(principal.getRawToken()); - } - - JsonNode jsonNode; - - if (source.startsWith("access_token[")) { - jsonNode = JsonSerialization.mapper.valueToTree(principal.getToken()); - } else { - throw new RuntimeException("Invalid placeholder [" + placeHolder + "]"); - } - - return JsonUtils.getValues(jsonNode, getParameter(source, "Invalid placeholder [" + placeHolder + "]")); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java deleted file mode 100644 index 935fc47db14d..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.util; - -import java.util.List; - -import org.keycloak.adapters.authorization.spi.HttpRequest; - -/** - * @author Pedro Igor - */ -public interface PlaceHolderResolver { - - List resolve(String placeHolder, HttpRequest httpFacade); - -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java deleted file mode 100644 index d7a4f5f4790f..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.util; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.keycloak.adapters.authorization.spi.HttpRequest; - -/** - * @author Pedro Igor - */ -public class PlaceHolders { - - private static Map resolvers = new HashMap<>(); - - static { - resolvers.put(RequestPlaceHolderResolver.NAME, new RequestPlaceHolderResolver()); - resolvers.put(KeycloakSecurityContextPlaceHolderResolver.NAME, new KeycloakSecurityContextPlaceHolderResolver()); - } - - private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}"); - private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]"); - - public static List resolve(String value, HttpRequest httpFacade) { - Map> placeHolders = parsePlaceHolders(value, httpFacade); - - if (!placeHolders.isEmpty()) { - value = formatPlaceHolder(value); - - for (Entry> entry : placeHolders.entrySet()) { - List values = entry.getValue(); - - if (values.isEmpty() || values.size() > 1) { - return values; - } - - value = value.replaceAll(entry.getKey(), values.get(0)).trim(); - } - } - - return Arrays.asList(value); - } - - static String getParameter(String source, String messageIfNotFound) { - Matcher matcher = PLACEHOLDER_PARAM_PATTERN.matcher(source); - - while (matcher.find()) { - return matcher.group(1).replaceAll("'", ""); - } - - if (messageIfNotFound != null) { - throw new RuntimeException(messageIfNotFound); - } - - return null; - } - - private static Map> parsePlaceHolders(String value, HttpRequest httpFacade) { - Map> placeHolders = Collections.emptyMap(); - Matcher matcher = PLACEHOLDER_PATTERN.matcher(value); - boolean found = matcher.find(); - - if (found) { - placeHolders = new HashMap<>(); - do { - String placeHolder = matcher.group(1); - int resolverNameIdx = placeHolder.indexOf('.'); - - if (resolverNameIdx == -1) { - throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name."); - } - - PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx)); - - if (resolver != null) { - List resolved = resolver.resolve(placeHolder, httpFacade); - - if (resolved != null) { - placeHolders.put(formatPlaceHolder(placeHolder), resolved); - } - } - } while (matcher.find()); - } - - return placeHolders; - } - - private static String formatPlaceHolder(String placeHolder) { - return placeHolder.replaceAll("\\{", "").replace("}", "").replace("[", "").replace("]", "").replace("[", "").replace("]", ""); - } -} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java deleted file mode 100644 index 5d49625e990b..000000000000 --- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2018 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.adapters.authorization.util; - -import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; -import org.keycloak.adapters.authorization.spi.HttpRequest; -import org.keycloak.util.JsonSerialization; - -/** - * @author Pedro Igor - */ -public class RequestPlaceHolderResolver implements PlaceHolderResolver { - - static String NAME = "request"; - - @Override - public List resolve(String placeHolder, HttpRequest request) { - String source = placeHolder.substring(placeHolder.indexOf('.') + 1); - - if (source.startsWith("parameter")) { - String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]"); - String parameterValue = request.getFirstParam(parameterName); - - if (parameterValue != null) { - return Arrays.asList(parameterValue); - } - } else if (source.startsWith("header")) { - String headerName = getParameter(source, "Could not obtain header name from placeholder [" + source + "]"); - List headerValue = request.getHeaders(headerName); - - if (headerValue != null) { - return headerValue; - } - } else if (source.startsWith("cookie")) { - String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]"); - String cookieValue = request.getCookieValue(cookieName); - - if (cookieValue != null) { - return Arrays.asList(cookieValue); - } - } else if (source.startsWith("remoteAddr")) { - String value = request.getRemoteAddr(); - - if (value != null) { - return Arrays.asList(value); - } - } else if (source.startsWith("method")) { - String value = request.getMethod(); - - if (value != null) { - return Arrays.asList(value); - } - } else if (source.startsWith("uri")) { - String value = request.getURI(); - - if (value != null) { - return Arrays.asList(value); - } - } else if (source.startsWith("relativePath")) { - String value = request.getRelativePath(); - - if (value != null) { - return Arrays.asList(value); - } - } else if (source.startsWith("secure")) { - return Arrays.asList(String.valueOf(request.isSecure())); - } else if (source.startsWith("body")) { - String contentType = request.getHeader("Content-Type"); - - if (contentType == null) { - contentType = ""; - } else if (contentType.indexOf(';') != -1){ - contentType = contentType.substring(0, contentType.indexOf(';')).trim(); - } - - InputStream body = request.getInputStream(true); - - try { - if (body == null || body.available() == 0) { - return Collections.emptyList(); - } - } catch (IOException cause) { - throw new RuntimeException("Failed to check available bytes in request input stream", cause); - } - - if (body.markSupported()) { - body.mark(0); - } - - List values = new ArrayList<>(); - - try { - switch (contentType) { - case "application/json": - try { - JsonNode jsonNode = JsonSerialization.mapper.readTree(new BufferedInputStream(body) { - @Override - public void close() { - // we can't close the stream because it may be used later by the application - } - }); - String path = getParameter(source, null); - - if (path == null) { - values.addAll(JsonUtils.getValues(jsonNode)); - } else { - values.addAll(JsonUtils.getValues(jsonNode, path)); - } - } catch (IOException cause) { - throw new RuntimeException("Could not extract claim from request JSON body", cause); - } - break; - default: - StringBuilder value = new StringBuilder(); - BufferedReader reader = new BufferedReader(new InputStreamReader(body)); - - try { - int ch; - - while ((ch = reader.read()) != -1) { - value.append((char) ch); - } - } catch (IOException cause) { - throw new RuntimeException("Could not extract claim from request body", cause); - } - - values.add(value.toString()); - } - } finally { - if (body.markSupported()) { - try { - body.reset(); - } catch (IOException cause) { - throw new RuntimeException("Failed to reset request input stream", cause); - } - } - } - - return values; - } - - return Collections.emptyList(); - } -} diff --git a/authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory b/authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory deleted file mode 100644 index f40afedf2679..000000000000 --- a/authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory +++ /dev/null @@ -1,19 +0,0 @@ -# -# * Copyright 2018 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. -# - -org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory -org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory \ No newline at end of file diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml index d5b13c0cff82..eb1e18fbe74d 100644 --- a/authz/policy/common/pom.xml +++ b/authz/policy/common/pom.xml @@ -51,6 +51,16 @@ keycloak-server-spi-private provided + + io.quarkus.resteasy.reactive + resteasy-reactive-common + provided + + + org.jboss.logging + jboss-logging + provided + \ No newline at end of file diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java index c64734273306..6ed937aa1cb0 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java @@ -17,11 +17,10 @@ */ package org.keycloak.authorization.policy.provider.aggregated; -import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision; import org.keycloak.authorization.model.Policy; @@ -36,9 +35,11 @@ * @author Pedro Igor */ public class AggregatePolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(AggregatePolicyProvider.class); @Override public void evaluate(Evaluation evaluation) { + logger.debugf("Aggregate policy %s evaluating using parent class", evaluation.getPolicy().getName()); DecisionResultCollector decision = new DecisionResultCollector() { @Override protected void onComplete(Result result) { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java index 476d1823aee5..eb8d07e9f712 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java @@ -54,7 +54,7 @@ public PolicyProvider create(AuthorizationProvider authorization) { @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java index 54feda645c35..87680a34a655 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java @@ -2,6 +2,7 @@ import java.util.function.BiFunction; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; @@ -13,6 +14,7 @@ public class ClientPolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(ClientPolicyProvider.class); private final BiFunction representationFunction; public ClientPolicyProvider(BiFunction representationFunction) { @@ -28,10 +30,12 @@ public void evaluate(Evaluation evaluation) { for (String client : representation.getClients()) { ClientModel clientModel = realm.getClientById(client); - - if (context.getAttributes().containsValue("kc.client.id", clientModel.getClientId())) { - evaluation.grant(); - return; + if (clientModel != null) { + if (context.getAttributes().containsValue("kc.client.id", clientModel.getClientId())) { + evaluation.grant(); + logger.debugf("Client policy %s matched with client %s and was granted", evaluation.getPolicy().getName(), clientModel.getClientId()); + return; + } } } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java index e4740349f168..15ada99cd943 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java @@ -108,7 +108,7 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override @@ -140,7 +140,7 @@ public void postInit(KeycloakSessionFactory factory) { try { if (clients.isEmpty()) { - policyStore.delete(removedClient.getRealm(), policy.getId()); + policyStore.delete(policy.getId()); } else { policy.putConfig("clients", JsonSerialization.writeValueAsString(clients)); } @@ -166,7 +166,7 @@ public String getId() { private void updateClients(Policy policy, Set clients, AuthorizationProvider authorization) { RealmModel realm = authorization.getRealm(); - if (clients == null || clients.isEmpty()) { + if (clients == null) { throw new RuntimeException("No client provided."); } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProvider.java index 13b48062214e..7cc555923bf8 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProvider.java @@ -20,6 +20,7 @@ import java.util.Set; import java.util.function.BiFunction; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.model.Policy; @@ -34,6 +35,7 @@ */ public class ClientScopePolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(ClientScopePolicyProvider.class); private final BiFunction representationFunction; public ClientScopePolicyProvider( @@ -68,6 +70,7 @@ public void evaluate(Evaluation evaluation) { } } } + logger.debugf("Client Scope Policy %s evaluated to %s", policy.getName(), evaluation.getEffect()); } private boolean hasClientScope(Identity identity, ClientScopeModel clientScope) { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProviderFactory.java index 4e431d26ff94..8faa18d72aef 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/clientscope/ClientScopePolicyProviderFactory.java @@ -69,13 +69,12 @@ public void postInit(KeycloakSessionFactory factory) { StoreFactory storeFactory = provider.getStoreFactory(); PolicyStore policyStore = storeFactory.getPolicyStore(); ClientScopeModel removedClientScope = ((ClientScopeRemovedEvent) event).getClientScope(); - RealmModel realm = ((ClientScopeRemovedEvent) event).getClientScope().getRealm(); Map filters = new HashMap<>(); filters.put(Policy.FilterOption.TYPE, new String[] { getId() }); - policyStore.find(realm, null, filters, null, null).forEach(new Consumer() { + policyStore.find(null, filters, null, null).forEach(new Consumer() { @Override public void accept(Policy policy) { @@ -94,7 +93,7 @@ public void accept(Policy policy) { } if (clientScopes.isEmpty()) { - policyStore.delete(realm, policy.getId()); + policyStore.delete(policy.getId()); } else { try { policy.putConfig("clientScopes", JsonSerialization.writeValueAsString(clientScopes)); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java index fa76a6dc2f3a..9813acc1252a 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java @@ -20,22 +20,35 @@ import java.util.List; import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.attribute.Attributes; import org.keycloak.authorization.attribute.Attributes.Entry; import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.policy.evaluation.Evaluation; +import org.keycloak.authorization.fgap.evaluation.partial.PartialEvaluationPolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceType; /** * @author Pedro Igor */ -public class GroupPolicyProvider implements PolicyProvider { +public class GroupPolicyProvider implements PolicyProvider, PartialEvaluationPolicyProvider { + private static final Logger logger = Logger.getLogger(GroupPolicyProvider.class); private final BiFunction representationFunction; public GroupPolicyProvider(BiFunction representationFunction) { @@ -54,31 +67,71 @@ public void evaluate(Evaluation evaluation) { groupsClaim = new Entry(policy.getGroupsClaim(), userGroups); } + if (isGranted(realm, policy, groupsClaim)) { + evaluation.grant(); + } + + logger.debugf("Groups policy %s evaluated to %s with identity groups %s", policy.getName(), evaluation.getEffect(), groupsClaim); + } + + private boolean isGranted(RealmModel realm, GroupPolicyRepresentation policy, Attributes.Entry groupsClaim) { for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) { GroupModel allowedGroup = realm.getGroupById(definition.getId()); + if (allowedGroup == null) { + continue; + } + for (int i = 0; i < groupsClaim.size(); i++) { String group = groupsClaim.asString(i); if (group.indexOf('/') != -1) { String allowedGroupPath = buildGroupPath(allowedGroup); if (group.equals(allowedGroupPath) || (definition.isExtendChildren() && group.startsWith(allowedGroupPath))) { - evaluation.grant(); - return; + return true; } } // in case the group from the claim does not represent a path, we just check an exact name match if (group.equals(allowedGroup.getName())) { - evaluation.grant(); - return; + return true; } } } + + return false; + } + + @Override + public Stream getPermissions(KeycloakSession session, ResourceType resourceType, UserModel user) { + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + RealmModel realm = session.getContext().getRealm(); + ClientModel adminPermissionsClient = realm.getAdminPermissionsClient(); + StoreFactory storeFactory = provider.getStoreFactory(); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(adminPermissionsClient); + PolicyStore policyStore = storeFactory.getPolicyStore(); + List groupIds = user.getGroupsStream().map(GroupModel::getId).toList(); + + return policyStore.findDependentPolicies(resourceServer, resourceType.getType(), GroupPolicyProviderFactory.ID, "groups", groupIds); + } + + @Override + public boolean evaluate(KeycloakSession session, Policy policy, UserModel subject) { + RealmModel realm = session.getContext().getRealm(); + AuthorizationProvider authorizationProvider = session.getProvider(AuthorizationProvider.class); + GroupPolicyRepresentation groupPolicy = representationFunction.apply(policy, authorizationProvider); + List userGroups = subject.getGroupsStream().map(ModelToRepresentation::buildGroupPath) + .collect(Collectors.toList()); + return isGranted(realm, groupPolicy, new Entry(groupPolicy.getGroupsClaim(), userGroups)); + } + + @Override + public boolean supports(Policy policy) { + return GroupPolicyProviderFactory.ID.equals(policy.getType()); } @Override public void close() { } -} \ No newline at end of file +} diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java index 77e7e3247b5e..b636ddca2a9c 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java @@ -21,7 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,10 +33,13 @@ import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.models.GroupModel; +import org.keycloak.models.GroupProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation; +import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation.GroupDefinition; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.util.JsonSerialization; @@ -45,11 +48,13 @@ */ public class GroupPolicyProviderFactory implements PolicyProviderFactory { + public static final String ID = "group"; + private GroupPolicyProvider provider = new GroupPolicyProvider(this::toRepresentation); @Override public String getId() { - return "group"; + return ID; } @Override @@ -79,7 +84,7 @@ public GroupPolicyRepresentation toRepresentation(Policy policy, AuthorizationPr representation.setGroupsClaim(policy.getConfig().get("groupsClaim")); try { - representation.setGroups(getGroupsDefinition(policy.getConfig())); + representation.setGroups(getGroupsDefinition(policy.getConfig(), authorization)); } catch (IOException cause) { throw new RuntimeException("Failed to deserialize groups", cause); } @@ -104,7 +109,7 @@ public void onUpdate(Policy policy, GroupPolicyRepresentation representation, Au @Override public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { try { - updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig()), authorization); + updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig(), authorization), authorization); } catch (IOException cause) { throw new RuntimeException("Failed to deserialize groups", cause); } @@ -118,6 +123,11 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori for (GroupPolicyRepresentation.GroupDefinition definition: groups) { GroupModel group = authorization.getRealm().getGroupById(definition.getId()); + + if (group == null) { + continue; + } + definition.setId(null); definition.setPath(ModelToRepresentation.buildGroupPath(group)); } @@ -144,8 +154,6 @@ public void init(Config.Scope config) { @Override public void postInit(KeycloakSessionFactory factory) { - factory.register(event -> { - }); } @Override @@ -154,7 +162,7 @@ public void close() { } private void updatePolicy(Policy policy, String groupsClaim, Set groups, AuthorizationProvider authorization) { - if (groups == null || groups.isEmpty()) { + if (groups == null) { throw new RuntimeException("You must provide at least one group"); } @@ -164,41 +172,11 @@ private void updatePolicy(Policy policy, String groupsClaim, Set topLevelGroups = authorization.getKeycloakSession().groups().getTopLevelGroupsStream(authorization.getRealm()).collect(Collectors.toList()); - for (GroupPolicyRepresentation.GroupDefinition definition : groups) { - GroupModel group = null; - - if (definition.getId() != null) { - group = authorization.getRealm().getGroupById(definition.getId()); - } - - String path = definition.getPath(); - - if (group == null && path != null) { - String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path; - - if (canonicalPath != null) { - String[] parts = canonicalPath.split("/"); - GroupModel parent = null; - - for (String part : parts) { - if (parent == null) { - parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found")); - } else { - group = parent.getSubGroupsStream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found")); - parent = group; - } - } - - if (parts.length == 1) { - group = parent; - } - } - } + GroupModel group = getGroup(authorization, definition); if (group == null) { - throw new RuntimeException("Group with id [" + definition.getId() + "] not found"); + continue; } definition.setId(group.getId()); @@ -214,13 +192,56 @@ private void updatePolicy(Policy policy, String groupsClaim, Set getGroupsDefinition(Map config) throws IOException { + private GroupModel getGroup(AuthorizationProvider authorization, GroupDefinition definition) { + RealmModel realm = authorization.getRealm(); + KeycloakSession session = authorization.getKeycloakSession(); + GroupProvider groups = session.groups(); + + if (definition.getId() != null) { + return realm.getGroupById(definition.getId()); + } + + GroupModel group = null; + String path = definition.getPath(); + + if (path != null) { + String canonicalPath = path.startsWith("/") ? path.substring(1) : path; + String[] parts = canonicalPath.split("/"); + GroupModel parent = null; + + for (String part : parts) { + if (parent == null) { + parent = groups.getGroupByName(realm, null, part); + if (parent == null) { + return null; + } + } else { + group = groups.getGroupByName(realm, parent, part); + if (group == null) { + return null; + } + parent = group; + } + } + + if (parts.length == 1) { + group = parent; + } + } + + return group; + } + + private Set getGroupsDefinition(Map config, AuthorizationProvider authorization) throws IOException { String groups = config.get("groups"); if (groups == null) { return Collections.emptySet(); } - return new HashSet<>(Arrays.asList(JsonSerialization.readValue(groups, GroupPolicyRepresentation.GroupDefinition[].class))); + return Arrays.stream(JsonSerialization.readValue(groups, GroupPolicyRepresentation.GroupDefinition[].class)) + .filter(d -> getGroup(authorization, d) != null) + .sorted() + .collect(Collectors.toCollection(LinkedHashSet::new)); } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java index 9457836d288d..97c3512ea36e 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java @@ -22,6 +22,7 @@ import javax.script.ScriptContext; import javax.script.SimpleScriptContext; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; @@ -33,6 +34,8 @@ */ class JSPolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(JSPolicyProvider.class); + private final BiFunction evaluatableScript; JSPolicyProvider(final BiFunction evaluatableScript) { @@ -51,6 +54,7 @@ public void evaluate(Evaluation evaluation) { context.setAttribute("$evaluation", evaluation, ScriptContext.ENGINE_SCOPE); adapter.eval(context); + logger.debugf("JS Policy %s evaluated to status %s", policy.getName(), evaluation.getEffect()); } catch (Exception e) { throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java index 5da3bb78d948..7a901e7b7db4 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java @@ -5,7 +5,6 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; -import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; @@ -40,7 +39,7 @@ public PolicyProvider create(AuthorizationProvider authorization) { @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java index f2e91d827945..425b1851f391 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java @@ -16,8 +16,12 @@ */ package org.keycloak.authorization.policy.provider.permission; +import java.util.HashSet; +import java.util.Set; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision; +import org.keycloak.authorization.Decision.Effect; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; @@ -32,6 +36,8 @@ */ public abstract class AbstractPermissionProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(AbstractPermissionProvider.class); + @Override public void evaluate(Evaluation evaluation) { AuthorizationProvider authorization = evaluation.getAuthorizationProvider(); @@ -48,11 +54,11 @@ public void evaluate(Evaluation evaluation) { if (effect == null) { PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); - + if (policyProvider == null) { throw new RuntimeException("No policy provider found for policy [" + associatedPolicy.getType() + "]"); } - + policyProvider.evaluate(defaultEvaluation); evaluation.denyIfNoEffect(); decisions.put(permission, defaultEvaluation.getEffect()); @@ -60,6 +66,7 @@ public void evaluate(Evaluation evaluation) { defaultEvaluation.setEffect(effect); } } + logger.debugf("Policy %s was evaluated with status %s in %s mode after processing %s associated policies: %s", policy.getName(), evaluation.getEffect(), policy.getDecisionStrategy(), policy.getAssociatedPolicies().size(), policy.getAssociatedPolicies()); } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java index 348dd8bd9918..b23d7a9d131c 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java @@ -16,6 +16,7 @@ */ package org.keycloak.authorization.policy.provider.permission; +import org.jboss.logging.Logger; import org.keycloak.authorization.Decision; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.permission.ResourcePermission; @@ -30,8 +31,11 @@ */ public class ResourcePolicyProvider extends AbstractPermissionProvider { + private static final Logger logger = Logger.getLogger(ResourcePolicyProvider.class); + @Override public void evaluate(Evaluation evaluation) { + logger.debugf("Resource policy %s evaluating using parent class", evaluation.getPolicy().getName()); DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation); Map> decisionCache = defaultEvaluation.getDecisionCache(); Policy policy = defaultEvaluation.getParentPolicy(); @@ -43,7 +47,6 @@ public void evaluate(Evaluation evaluation) { defaultEvaluation.setEffect(effect); return; } - super.evaluate(evaluation); decisions.put(permission.getResource(), defaultEvaluation.getEffect()); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java index 77b6ec17ef31..51f45c463320 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java @@ -64,7 +64,7 @@ public ResourcePermissionRepresentation toRepresentation(Policy policy, Authoriz @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java index c4d4b53ffb42..1e1e3f662599 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java @@ -16,9 +16,9 @@ */ package org.keycloak.authorization.policy.provider.permission; +import org.jboss.logging.Logger; import org.keycloak.authorization.Decision; import org.keycloak.authorization.model.Policy; -import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; import org.keycloak.authorization.policy.evaluation.Evaluation; @@ -31,8 +31,11 @@ */ public class ScopePolicyProvider extends AbstractPermissionProvider { + private static final Logger logger = Logger.getLogger(ScopePolicyProvider.class); + @Override public void evaluate(Evaluation evaluation) { + logger.debugf("Scope policy %s evaluating using parent class", evaluation.getPolicy().getName()); DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation); Map> decisionCache = defaultEvaluation.getDecisionCache(); Policy policy = defaultEvaluation.getParentPolicy(); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java index 0be56fcfa556..4dc3eb6ccd74 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.authorization.policy.provider.permission; import org.keycloak.Config; +import org.keycloak.authorization.fgap.AdminPermissionsSchema; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.provider.PolicyProvider; @@ -33,7 +34,8 @@ */ public class ScopePolicyProviderFactory implements PolicyProviderFactory { - private ScopePolicyProvider provider = new ScopePolicyProvider(); + public static final String ID = "scope"; + private final ScopePolicyProvider provider = new ScopePolicyProvider(); @Override public String getName() { @@ -52,7 +54,7 @@ public PolicyProvider create(AuthorizationProvider authorization) { @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override @@ -77,9 +79,14 @@ public void onUpdate(Policy policy, ScopePermissionRepresentation representation updateResourceType(policy, representation); } + @Override + public void onRemove(Policy policy, AuthorizationProvider authorization) { + AdminPermissionsSchema.SCHEMA.removeOrphanResources(policy, authorization); + } + private void updateResourceType(Policy policy, ScopePermissionRepresentation representation) { if (representation != null) { - Map config = new HashMap(policy.getConfig()); + Map config = new HashMap<>(policy.getConfig()); config.compute("defaultResourceType", (key, value) -> { String resourceType = representation.getResourceType(); @@ -107,6 +114,6 @@ public void close() { @Override public String getId() { - return "scope"; + return ID; } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java index e20295dab1fd..8aff7d9fc743 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java @@ -16,6 +16,7 @@ */ package org.keycloak.authorization.policy.provider.permission; +import org.jboss.logging.Logger; import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.permission.ResourcePermission; @@ -26,8 +27,11 @@ */ public class UMAPolicyProvider extends AbstractPermissionProvider { + private static final Logger logger = Logger.getLogger(UMAPolicyProvider.class); + @Override public void evaluate(Evaluation evaluation) { + logger.debugf("UMA policy %s evaluating using parent class", evaluation.getPolicy().getName()); ResourcePermission permission = evaluation.getPermission(); Resource resource = permission.getResource(); @@ -36,6 +40,7 @@ public void evaluate(Evaluation evaluation) { // no need to evaluate UMA permissions to resource owner resources if (resource.getOwner().equals(identity.getId())) { + logger.debugv("UMA resource is owned by the current user, bypassing evaluation"); evaluation.grant(); return; } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java index 86280fd18cdd..c72de5a3793a 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java @@ -125,7 +125,6 @@ public void onCreate(Policy policy, UmaPermissionRepresentation representation, public void onUpdate(Policy policy, UmaPermissionRepresentation representation, AuthorizationProvider authorization) { PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); Set associatedPolicies = policy.getAssociatedPolicies(); - RealmModel realm = policy.getResourceServer().getRealm(); for (Policy associatedPolicy : associatedPolicies) { AbstractPolicyRepresentation associatedRep = ModelToRepresentation.toRepresentation(associatedPolicy, authorization, false, false); @@ -144,7 +143,7 @@ public void onUpdate(Policy policy, UmaPermissionRepresentation representation, } if (rep.getRoles().isEmpty()) { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } else { RepresentationToModel.toModel(rep, authorization, associatedPolicy); } @@ -155,7 +154,7 @@ public void onUpdate(Policy policy, UmaPermissionRepresentation representation, rep.setType(representation.getCondition()); RepresentationToModel.toModel(rep, authorization, associatedPolicy); } else { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } } else if ("group".equals(associatedRep.getType())) { GroupPolicyRepresentation rep = GroupPolicyRepresentation.class.cast(associatedRep); @@ -171,7 +170,7 @@ public void onUpdate(Policy policy, UmaPermissionRepresentation representation, } if (rep.getGroups().isEmpty()) { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } else { RepresentationToModel.toModel(rep, authorization, associatedPolicy); } @@ -189,7 +188,7 @@ public void onUpdate(Policy policy, UmaPermissionRepresentation representation, } if (rep.getClients().isEmpty()) { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } else { RepresentationToModel.toModel(rep, authorization, associatedPolicy); } @@ -207,7 +206,7 @@ public void onUpdate(Policy policy, UmaPermissionRepresentation representation, } if (rep.getUsers().isEmpty()) { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } else { RepresentationToModel.toModel(rep, authorization, associatedPolicy); } @@ -365,10 +364,9 @@ public Class getRepresentationType() { @Override public void onRemove(Policy policy, AuthorizationProvider authorization) { PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); - RealmModel realm = policy.getResourceServer().getRealm(); for (Policy associatedPolicy : policy.getAssociatedPolicies()) { - policyStore.delete(realm, associatedPolicy.getId()); + policyStore.delete(associatedPolicy.getId()); } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProvider.java index ea9ec644aa25..cb0b4ea790ec 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProvider.java @@ -28,6 +28,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.attribute.Attributes; import org.keycloak.authorization.model.Policy; @@ -42,6 +43,7 @@ */ public class RegexPolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(RegexPolicyProvider.class); private final BiFunction representationFunction; public RegexPolicyProvider(BiFunction representationFunction) { @@ -66,11 +68,14 @@ public void evaluate(Evaluation evaluation) { Matcher matcher = pattern.matcher(value); if (matcher.matches()) { evaluation.grant(); + logger.debugf("policy %s evaluated with status %s on identity %s and claim value %s", policy.getName(), evaluation.getEffect(), evaluation.getContext().getIdentity().getId(), getClaimValue(evaluation, policy)); } } private String getClaimValue(Evaluation evaluation, RegexPolicyRepresentation policy) { - Attributes attributes = evaluation.getContext().getIdentity().getAttributes(); + Attributes attributes = policy.isTargetContextAttributes() + ? evaluation.getContext().getAttributes() + : evaluation.getContext().getIdentity().getAttributes(); String targetClaim = policy.getTargetClaim(); try { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProviderFactory.java index 3ab77816cf55..e5104a0cdf84 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/regex/RegexPolicyProviderFactory.java @@ -81,6 +81,7 @@ public RegexPolicyRepresentation toRepresentation(Policy policy, AuthorizationPr representation.setTargetClaim(config.get("targetClaim")); representation.setPattern(config.get("pattern")); + representation.setTargetContextAttributes(Boolean.parseBoolean(config.get("targetContextAttributes"))); return representation; } @@ -110,6 +111,7 @@ private void updatePolicy(Policy policy, RegexPolicyRepresentation representatio config.put("targetClaim", representation.getTargetClaim()); config.put("pattern", representation.getPattern()); + config.put("targetContextAttributes", String.valueOf(representation.isTargetContextAttributes())); policy.setConfig(config); } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java index 2435ba85c8cc..4d0342555761 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java @@ -17,26 +17,44 @@ */ package org.keycloak.authorization.policy.provider.role; +import java.util.List; import java.util.Set; import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.attribute.Attributes.Entry; import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.identity.UserModelIdentity; import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.policy.evaluation.Evaluation; +import org.keycloak.authorization.fgap.evaluation.partial.PartialEvaluationPolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.idm.authorization.ResourceType; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; +import static org.keycloak.models.utils.RoleUtils.getDeepUserRoleMappings; + /** * @author Pedro Igor */ -public class RolePolicyProvider implements PolicyProvider { +public class RolePolicyProvider implements PolicyProvider, PartialEvaluationPolicyProvider { private final BiFunction representationFunction; + private static final Logger logger = Logger.getLogger(RolePolicyProvider.class); + public RolePolicyProvider(BiFunction representationFunction) { this.representationFunction = representationFunction; } @@ -44,28 +62,45 @@ public RolePolicyProvider(BiFunction roleIds = representationFunction.apply(policy, evaluation.getAuthorizationProvider()).getRoles(); + RolePolicyRepresentation policyRep = representationFunction.apply(policy, evaluation.getAuthorizationProvider()); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm(); Identity identity = evaluation.getContext().getIdentity(); + if (isGranted(realm, authorizationProvider, policyRep, identity)) { + evaluation.grant(); + } + + logger.debugf("policy %s evaluated with status %s on identity %s", policy.getName(), evaluation.getEffect(), identity.getId()); + } + + private boolean isGranted(RealmModel realm, AuthorizationProvider authorizationProvider, RolePolicyRepresentation policyRep, Identity identity) { + Set roleIds = policyRep.getRoles(); + boolean granted = false; + for (RolePolicyRepresentation.RoleDefinition roleDefinition : roleIds) { RoleModel role = realm.getRoleById(roleDefinition.getId()); if (role != null) { - boolean hasRole = hasRole(identity, role, realm); + boolean isFetchRoles = policyRep.isFetchRoles() != null && policyRep.isFetchRoles(); + boolean hasRole = hasRole(identity, role, realm, authorizationProvider, isFetchRoles); - if (!hasRole && roleDefinition.isRequired()) { - evaluation.deny(); - return; + if (!hasRole && roleDefinition.isRequired() != null && roleDefinition.isRequired()) { + return false; } else if (hasRole) { - evaluation.grant(); + granted = true; } } } + + return granted; } - private boolean hasRole(Identity identity, RoleModel role, RealmModel realm) { + private boolean hasRole(Identity identity, RoleModel role, RealmModel realm, AuthorizationProvider authorizationProvider, boolean fetchRoles) { + if (fetchRoles) { + UserModel subject = getSubject(identity, realm, authorizationProvider); + return subject != null && subject.hasRole(role); + } String roleName = role.getName(); if (role.isClientRole()) { ClientModel clientModel = realm.getClientById(role.getContainerId()); @@ -74,8 +109,52 @@ private boolean hasRole(Identity identity, RoleModel role, RealmModel realm) { return identity.hasRealmRole(roleName); } + private UserModel getSubject(Identity identity, RealmModel realm, AuthorizationProvider authorizationProvider) { + KeycloakSession session = authorizationProvider.getKeycloakSession(); + UserProvider users = session.users(); + UserModel user = users.getUserById(realm, identity.getId()); + + if (user == null) { + Entry sub = identity.getAttributes().getValue(JsonWebToken.SUBJECT); + + if (sub == null || sub.isEmpty()) { + return null; + } + + return users.getUserById(realm, sub.asString(0)); + } + + return user; + } + @Override public void close() { } -} \ No newline at end of file + + @Override + public Stream getPermissions(KeycloakSession session, ResourceType resourceType, UserModel subject) { + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + RealmModel realm = session.getContext().getRealm(); + ClientModel adminPermissionsClient = realm.getAdminPermissionsClient(); + StoreFactory storeFactory = provider.getStoreFactory(); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(adminPermissionsClient); + PolicyStore policyStore = storeFactory.getPolicyStore(); + List roleIds = getDeepUserRoleMappings(subject).stream().map(RoleModel::getId).toList(); + Stream policies = Stream.of(); + + return Stream.concat(policies, policyStore.findDependentPolicies(resourceServer, resourceType.getType(), RolePolicyProviderFactory.ID, "roles", roleIds)); + } + + @Override + public boolean evaluate(KeycloakSession session, Policy policy, UserModel adminUser) { + RealmModel realm = session.getContext().getRealm(); + AuthorizationProvider authorizationProvider = session.getProvider(AuthorizationProvider.class); + return isGranted(realm, authorizationProvider, representationFunction.apply(policy, authorizationProvider), new UserModelIdentity(realm, adminUser)); + } + + @Override + public boolean supports(Policy policy) { + return RolePolicyProviderFactory.ID.equals(policy.getType()); + } +} diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java index 98d07ece0491..bd66d0b478c2 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java @@ -20,38 +20,37 @@ import org.keycloak.Config; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; -import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; -import org.keycloak.authorization.store.PolicyStore; -import org.keycloak.authorization.store.ResourceServerStore; -import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.authorization.policy.provider.util.PolicyValidationException; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleContainerModel; -import org.keycloak.models.RoleContainerModel.RoleRemovedEvent; import org.keycloak.models.RoleModel; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; +import org.keycloak.representations.idm.authorization.RolePolicyRepresentation.RoleDefinition; import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.StringUtil; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * @author Pedro Igor */ public class RolePolicyProviderFactory implements PolicyProviderFactory { + public static final String ID = "role"; private RolePolicyProvider provider = new RolePolicyProvider(this::toRepresentation); @Override @@ -77,18 +76,14 @@ public PolicyProvider create(KeycloakSession session) { @Override public RolePolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) { RolePolicyRepresentation representation = new RolePolicyRepresentation(); + String roles = policy.getConfig().get("roles"); - try { - String roles = policy.getConfig().get("roles"); + representation.setRoles(getRoles(roles, authorization.getRealm())); - if (roles == null) { - representation.setRoles(Collections.emptySet()); - } else { - representation.setRoles(new HashSet<>( - Arrays.asList(JsonSerialization.readValue(roles, RolePolicyRepresentation.RoleDefinition[].class)))); - } - } catch (IOException cause) { - throw new RuntimeException("Failed to deserialize roles", cause); + String fetchRoles = policy.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + representation.setFetchRoles(Boolean.parseBoolean(fetchRoles)); } return representation; @@ -111,10 +106,11 @@ public void onUpdate(Policy policy, RolePolicyRepresentation representation, Aut @Override public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { - try { - updateRoles(policy, authorization, new HashSet<>(Arrays.asList(JsonSerialization.readValue(representation.getConfig().get("roles"), RolePolicyRepresentation.RoleDefinition[].class)))); - } catch (IOException cause) { - throw new RuntimeException("Failed to deserialize roles during import", cause); + updateRoles(policy, authorization, getRoles(representation.getConfig().get("roles"), authorization.getRealm())); + String fetchRoles = representation.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + policy.putConfig("fetchRoles", fetchRoles); } } @@ -139,52 +135,37 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori throw new RuntimeException("Failed to export role policy [" + policy.getName() + "]", cause); } + String fetchRoles = policy.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + config.put("fetchRoles", fetchRoles); + } + representation.setConfig(config); } private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) { + if (representation.isFetchRoles() != null) { + policy.putConfig("fetchRoles", String.valueOf(representation.isFetchRoles())); + } updateRoles(policy, authorization, representation.getRoles()); } private void updateRoles(Policy policy, AuthorizationProvider authorization, Set roles) { Set updatedRoles = new HashSet<>(); - + Set processedRoles = new HashSet<>(); if (roles != null) { RealmModel realm = authorization.getRealm(); for (RolePolicyRepresentation.RoleDefinition definition : roles) { - String roleName = definition.getId(); - String clientId = null; - int clientIdSeparator = roleName.indexOf("/"); - - if (clientIdSeparator != -1) { - clientId = roleName.substring(0, clientIdSeparator); - roleName = roleName.substring(clientIdSeparator + 1); - } - - RoleModel role; - - if (clientId == null) { - role = realm.getRole(roleName); - - if (role == null) { - role = realm.getRoleById(roleName); - } - } else { - ClientModel client = realm.getClientByClientId(clientId); - - if (client == null) { - throw new RuntimeException("Client with id [" + clientId + "] not found."); - } - - role = client.getRole(roleName); - } - + RoleModel role = getRole(definition, realm); if (role == null) { - throw new RuntimeException("Error while updating policy [" + policy.getName() + "]. Role [" + roleName + "] could not be found."); + continue; } + if (!processedRoles.add(role.getId())) { + throw new PolicyValidationException("Role can't be specified multiple times - " + role.getName()); + } definition.setId(role.getId()); - updatedRoles.add(definition); } } @@ -203,58 +184,7 @@ public void init(Config.Scope config) { @Override public void postInit(KeycloakSessionFactory factory) { - factory.register(event -> { - if (event instanceof RoleRemovedEvent) { - KeycloakSession keycloakSession = ((RoleRemovedEvent) event).getKeycloakSession(); - AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class); - StoreFactory storeFactory = provider.getStoreFactory(); - PolicyStore policyStore = storeFactory.getPolicyStore(); - RoleModel removedRole = ((RoleRemovedEvent) event).getRole(); - RoleContainerModel container = removedRole.getContainer(); - ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore(); - - if (container instanceof RealmModel) { - RealmModel realm = (RealmModel) container; - realm.getClientsStream() - .forEach(clientModel -> updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore)); - } else { - ClientModel clientModel = (ClientModel) container; - updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore); - } - } - }); - } - private void updateResourceServer(ClientModel clientModel, RoleModel removedRole, ResourceServerStore resourceServerStore, PolicyStore policyStore) { - ResourceServer resourceServer = resourceServerStore.findByClient(clientModel); - - if (resourceServer != null) { - policyStore.findByType(resourceServer, getId()).forEach(policy -> { - List roles = new ArrayList<>(); - - for (Map role : getRoles(policy)) { - if (!role.get("id").equals(removedRole.getId())) { - Map updated = new HashMap(); - updated.put("id", role.get("id")); - Object required = role.get("required"); - if (required != null) { - updated.put("required", required); - } - roles.add(updated); - } - } - - try { - if (roles.isEmpty()) { - policyStore.delete(clientModel.getRealm(), policy.getId()); - } else { - policy.putConfig("roles", JsonSerialization.writeValueAsString(roles)); - } - } catch (IOException e) { - throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e); - } - }); - } } @Override @@ -264,20 +194,57 @@ public void close() { @Override public String getId() { - return "role"; + return ID; } - private Map[] getRoles(Policy policy) { - String roles = policy.getConfig().get("roles"); - - if (roles != null) { + private Set getRoles(String rawRoles, RealmModel realm) { + if (rawRoles != null) { try { - return JsonSerialization.readValue(roles.getBytes(), Map[].class); + return Arrays.stream(JsonSerialization.readValue(rawRoles, RoleDefinition[].class)) + .filter(definition -> getRole(definition, realm) != null) + .sorted() + .collect(Collectors.toCollection(LinkedHashSet::new)); } catch (IOException e) { - throw new RuntimeException("Could not parse roles [" + roles + "] from policy config [" + policy.getName() + ".", e); + throw new RuntimeException("Could not parse roles from config: [" + rawRoles + "]", e); + } + } + + return Collections.emptySet(); + } + + public static final Pattern UUID_PATTERN = Pattern.compile("[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}"); + + private RoleModel getRole(RolePolicyRepresentation.RoleDefinition definition, RealmModel realm) { + String roleName = definition.getId(); + String clientId = null; + int clientIdSeparator = roleName.indexOf("/"); + + if (clientIdSeparator != -1) { + clientId = roleName.substring(0, clientIdSeparator); + roleName = roleName.substring(clientIdSeparator + 1); + } + + RoleModel role; + + if (clientId == null) { + // if the role name looks like a UUID, it is likely that it is a role ID. Then do this look-up first to avoid hitting the database twice + // TODO: In a future version of the auth feature, make this more strict to avoid the double lookup and any ambiguity + boolean looksLikeAUuid = UUID_PATTERN.matcher(roleName).matches(); + role = looksLikeAUuid ? realm.getRoleById(roleName) : realm.getRole(roleName); + + if (role == null) { + role = !looksLikeAUuid ? realm.getRoleById(roleName) : realm.getRole(roleName);; } + } else { + ClientModel client = realm.getClientByClientId(clientId); + + if (client == null) { + throw new RuntimeException("Client with id [" + clientId + "] not found."); + } + + role = client.getRole(roleName); } - return new Map[] {}; + return role; } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java index b99caaccef31..4725c04d814e 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java @@ -21,6 +21,7 @@ import java.util.Calendar; import java.util.Date; +import org.jboss.logging.Logger; import org.keycloak.authorization.attribute.Attributes; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; @@ -32,6 +33,8 @@ */ public class TimePolicyProvider implements PolicyProvider { + private static final Logger logger = Logger.getLogger(TimePolicyProvider.class); + static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss"; static String CONTEXT_TIME_ENTRY = "kc.time.date_time"; @@ -40,6 +43,7 @@ public class TimePolicyProvider implements PolicyProvider { public void evaluate(Evaluation evaluation) { Policy policy = evaluation.getPolicy(); SimpleDateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN); + logger.debugf("Time policy %s evaluating", policy.getName()); try { String contextTime = null; EvaluationContext context = evaluation.getContext(); @@ -54,6 +58,7 @@ public void evaluate(Evaluation evaluation) { String notBefore = policy.getConfig().get("nbf"); if (notBefore != null && !"".equals(notBefore)) { if (actualDate.before(dateFormat.parse(format(notBefore)))) { + logger.debugv("Provided date is before the accepted date: (nbf) ", notBefore); evaluation.deny(); return; } @@ -62,6 +67,7 @@ public void evaluate(Evaluation evaluation) { String notOnOrAfter = policy.getConfig().get("noa"); if (notOnOrAfter != null && !"".equals(notOnOrAfter)) { if (actualDate.after(dateFormat.parse(format(notOnOrAfter)))) { + logger.debugf("Provided date is after the accepted date: (noa) %s", notOnOrAfter); evaluation.deny(); return; } @@ -72,6 +78,7 @@ public void evaluate(Evaluation evaluation) { || isInvalid(actualDate, Calendar.YEAR, "year", policy) || isInvalid(actualDate, Calendar.HOUR_OF_DAY, "hour", policy) || isInvalid(actualDate, Calendar.MINUTE, "minute", policy)) { + logger.debugf("Invalid date provided to time policy %s", policy.getName()); evaluation.deny(); return; } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java index c1e3f4116a14..e83a0fe269dd 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java @@ -1,6 +1,23 @@ +/* + * 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.authorization.policy.provider.time; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -9,6 +26,7 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.authorization.policy.provider.util.PolicyValidationException; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.representations.idm.authorization.PolicyRepresentation; @@ -38,7 +56,7 @@ public PolicyProvider create(AuthorizationProvider authorization) { @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override @@ -116,8 +134,7 @@ private void updatePolicy(Policy policy, TimePolicyRepresentation representation String noa = representation.getNotOnOrAfter(); if (nbf != null && noa != null) { - validateFormat(nbf); - validateFormat(noa); + validateFormat(nbf, noa); } Map config = new HashMap(policy.getConfig()); @@ -143,11 +160,20 @@ private void updatePolicy(Policy policy, TimePolicyRepresentation representation policy.setConfig(config); } - private void validateFormat(String date) { + private void validateFormat(String notBefore, String notOnOrAfter) { + Date nbf, noa; + try { + nbf = new SimpleDateFormat(TimePolicyProvider.DEFAULT_DATE_PATTERN).parse(TimePolicyProvider.format(notBefore)); + } catch (Exception e) { + throw new PolicyValidationException("Unable not parse a date using format [" + notBefore + "]"); + } try { - new SimpleDateFormat(TimePolicyProvider.DEFAULT_DATE_PATTERN).parse(TimePolicyProvider.format(date)); + noa = new SimpleDateFormat(TimePolicyProvider.DEFAULT_DATE_PATTERN).parse(TimePolicyProvider.format(notOnOrAfter)); } catch (Exception e) { - throw new RuntimeException("Could not parse a date using format [" + date + "]"); + throw new PolicyValidationException("Unable not parse a date using format [" + notOnOrAfter + "]"); + } + if (noa.before(nbf)) { + throw new PolicyValidationException("Expire time can't be set to a date before start time"); } } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java index 3e1f9b738409..5f75c26afa3b 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java @@ -18,19 +18,30 @@ package org.keycloak.authorization.policy.provider.user; import java.util.function.BiFunction; -import java.util.function.Function; +import java.util.stream.Stream; +import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.policy.evaluation.Evaluation; -import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.fgap.evaluation.partial.PartialEvaluationPolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.authorization.ResourceType; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; /** * @author Pedro Igor */ -public class UserPolicyProvider implements PolicyProvider { +public class UserPolicyProvider implements PolicyProvider, PartialEvaluationPolicyProvider { + + private static final Logger logger = Logger.getLogger(UserPolicyProvider.class); private final BiFunction representationFunction; @@ -40,17 +51,39 @@ public UserPolicyProvider(BiFunction getPermissions(KeycloakSession session, ResourceType resourceType, UserModel subject) { + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + RealmModel realm = session.getContext().getRealm(); + ClientModel adminPermissionsClient = realm.getAdminPermissionsClient(); + StoreFactory storeFactory = provider.getStoreFactory(); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(adminPermissionsClient); + PolicyStore policyStore = storeFactory.getPolicyStore(); + + return policyStore.findDependentPolicies(resourceServer, resourceType.getType(), UserPolicyProviderFactory.ID, "users", subject.getId()); + } + + @Override + public boolean evaluate(KeycloakSession session, Policy policy, UserModel adminUser) { + return policy.getConfig().getOrDefault("users", "").contains(adminUser.getId()); + } + + @Override + public boolean supports(Policy policy) { + return UserPolicyProviderFactory.ID.equals(policy.getType()); + } + @Override public void close() { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java index 7a516040f65b..42b807d33608 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -45,6 +46,8 @@ */ public class UserPolicyProviderFactory implements PolicyProviderFactory { + public static final String ID = "user"; + private UserPolicyProvider provider = new UserPolicyProvider(this::toRepresentation); @Override @@ -64,7 +67,7 @@ public PolicyProvider create(AuthorizationProvider authorization) { @Override public PolicyProvider create(KeycloakSession session) { - return null; + return provider; } @Override @@ -77,7 +80,9 @@ public UserPolicyRepresentation toRepresentation(Policy policy, AuthorizationPro if (users == null) { representation.setUsers(Collections.emptySet()); } else { - representation.setUsers(JsonSerialization.readValue(users, Set.class)); + representation.setUsers((Set) JsonSerialization.readValue(users, Set.class).stream() + .filter(id -> getUser((String) id, authorization) != null) + .collect(Collectors.toSet())); } } catch (IOException cause) { throw new RuntimeException("Failed to deserialize roles", cause); @@ -93,12 +98,12 @@ public Class getRepresentationType() { @Override public void onCreate(Policy policy, UserPolicyRepresentation representation, AuthorizationProvider authorization) { - updateUsers(policy, representation, authorization); + updateUsers(policy, authorization, representation.getUsers()); } @Override public void onUpdate(Policy policy, UserPolicyRepresentation representation, AuthorizationProvider authorization) { - updateUsers(policy, representation, authorization); + updateUsers(policy, authorization, representation.getUsers()); } @Override @@ -119,7 +124,12 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori UserProvider userProvider = authorizationProvider.getKeycloakSession().users(); RealmModel realm = authorizationProvider.getRealm(); - config.put("users", JsonSerialization.writeValueAsString(userRep.getUsers().stream().map(id -> userProvider.getUserById(realm, id).getUsername()).collect(Collectors.toList()))); + config.put("users", JsonSerialization.writeValueAsString(userRep.getUsers().stream() + .map(id -> { + UserModel user = userProvider.getUserById(realm, id); + return user != null ? user.getUsername() : null; + }) + .filter(Objects::nonNull).collect(Collectors.toList()))); } catch (IOException cause) { throw new RuntimeException("Failed to export user policy [" + policy.getName() + "]", cause); } @@ -127,28 +137,12 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori representation.setConfig(config); } - private void updateUsers(Policy policy, UserPolicyRepresentation representation, AuthorizationProvider authorization) { - updateUsers(policy, authorization, representation.getUsers()); - } - private void updateUsers(Policy policy, AuthorizationProvider authorization, Set users) { - KeycloakSession session = authorization.getKeycloakSession(); - RealmModel realm = authorization.getRealm(); - UserProvider userProvider = session.users(); Set updatedUsers = new HashSet<>(); if (users != null) { for (String userId : users) { - UserModel user = null; - - try { - user = userProvider.getUserByUsername(realm, userId); - } catch (Exception ignore) { - } - - if (user == null) { - user = userProvider.getUserById(realm, userId); - } + UserModel user = getUser(userId, authorization); if (user == null) { throw new RuntimeException("Error while updating policy [" + policy.getName() + "]. User [" + userId + "] could not be found."); @@ -166,6 +160,24 @@ private void updateUsers(Policy policy, AuthorizationProvider authorization, Set } } + private static UserModel getUser(String userId, AuthorizationProvider authorization) { + if (userId == null) { + return null; + } + + KeycloakSession session = authorization.getKeycloakSession(); + RealmModel realm = authorization.getRealm(); + UserProvider userProvider = session.users(); + UserModel user = userProvider.getUserById(realm, userId); + + if (user == null) { + // fallback - userId is possibly a username + user = userProvider.getUserByUsername(realm, userId); + } + + return user; + } + @Override public void init(Config.Scope config) { @@ -183,20 +195,6 @@ public void close() { @Override public String getId() { - return "user"; - } - - static String[] getUsers(Policy policy) { - String users = policy.getConfig().get("users"); - - if (users != null) { - try { - return JsonSerialization.readValue(users.getBytes(), String[].class); - } catch (IOException e) { - throw new RuntimeException("Could not parse users [" + users + "] from policy config [" + policy.getName() + ".", e); - } - } - - return new String[0]; + return ID; } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/util/PolicyValidationException.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/util/PolicyValidationException.java new file mode 100644 index 000000000000..b37ce91119d9 --- /dev/null +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/util/PolicyValidationException.java @@ -0,0 +1,29 @@ +/* + * 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.authorization.policy.provider.util; + +import jakarta.ws.rs.BadRequestException; + +/** + * Exception that is thrown when validation errors are found when creating/updating policies. + */ +public class PolicyValidationException extends BadRequestException { + + public PolicyValidationException(String message) { + super(message); + } +} diff --git a/authz/pom.xml b/authz/pom.xml index 31224da50309..711d68b47108 100644 --- a/authz/pom.xml +++ b/authz/pom.xml @@ -20,6 +20,5 @@ policy client - policy-enforcer \ No newline at end of file diff --git a/boms/adapter/pom.xml b/boms/adapter/pom.xml deleted file mode 100644 index 417c7107fde5..000000000000 --- a/boms/adapter/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - 4.0.0 - - - org.keycloak.bom - keycloak-bom-parent - 999.0.0-SNAPSHOT - - - org.keycloak.bom - keycloak-adapter-bom - pom - - Keycloak BOM for adapters - Keycloak BOM for adapters - - - - - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-adapter-core - ${project.version} - - - org.keycloak - keycloak-crypto-default - ${project.version} - - - org.keycloak - keycloak-adapter-spi - ${project.version} - - - org.keycloak - keycloak-saml-adapter-core - ${project.version} - - - org.keycloak - keycloak-saml-adapter-api-public - ${project.version} - - - org.keycloak - keycloak-tomcat-adapter - ${project.version} - - - org.keycloak - keycloak-undertow-adapter - ${project.version} - - - org.keycloak - keycloak-spring-boot-2-adapter - ${project.version} - - - org.keycloak - spring-boot-container-bundle - ${project.version} - - - org.keycloak - keycloak-spring-security-adapter - ${project.version} - - - org.keycloak - keycloak-spring-boot-starter - ${project.version} - - - org.keycloak - keycloak-authz-client - ${project.version} - - - - - diff --git a/boms/misc/pom.xml b/boms/misc/pom.xml deleted file mode 100644 index 7305aa3bb610..000000000000 --- a/boms/misc/pom.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - 4.0.0 - - - org.keycloak.bom - keycloak-bom-parent - 999.0.0-SNAPSHOT - - - org.keycloak.bom - keycloak-misc-bom - pom - - Keycloak BOM utilities for the quickstarts - Keycloak BOM utilities for the quickstarts - - - - - org.keycloak - keycloak-test-helper - ${project.version} - - - - diff --git a/boms/pom.xml b/boms/pom.xml index c67416d7afa0..5a54a03310e9 100644 --- a/boms/pom.xml +++ b/boms/pom.xml @@ -45,17 +45,23 @@ - https://s01.oss.sonatype.org/ - jboss-releases-repository - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - jboss-snapshots-repository - https://s01.oss.sonatype.org/content/repositories/snapshots/ + 0.7.0 + 1.0.7 + + + keycloak-publish + https://central.sonatype.com/ + + + keycloak-publish + https://central.sonatype.com/repository/maven-snapshots/ + + + - adapter spi - misc @@ -67,7 +73,7 @@ 1.6.13 true - jboss-releases-repository + ${jboss.releases.repo.id} ${jboss.repo.nexusUrl} @@ -106,13 +112,42 @@ + - nexus-staging + central-staging + + + + org.sonatype.central + central-publishing-maven-plugin + ${central.publishing.plugin.version} + true + + true + all + keycloak-${project.version} + keycloak-publish + published + + + + + + + + nexus3-staging org.sonatype.plugins - nexus-staging-maven-plugin + nxrm3-maven-plugin + ${nexus3.staging.plugin.version} + true + + ${jboss.releases.repo.id} + ${jboss.repo.nexusUrl} + ${jboss.releases.repo.name} + diff --git a/common/pom.xml b/common/pom.xml index 5f644cbc0af2..1fea851011f7 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -32,14 +32,11 @@ Common library and dependencies shared with server and all adapters + 8 + 8 + 8 ${maven.build.timestamp} yyyy-MM-dd HH:mm - - org.keycloak.common.* - - - *;resolution:=optional - @@ -70,40 +67,6 @@ true - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - - diff --git a/common/src/main/java/org/keycloak/common/ClientConnection.java b/common/src/main/java/org/keycloak/common/ClientConnection.java index 5afbed6741de..909f14b86ee8 100755 --- a/common/src/main/java/org/keycloak/common/ClientConnection.java +++ b/common/src/main/java/org/keycloak/common/ClientConnection.java @@ -25,7 +25,13 @@ */ public interface ClientConnection { + /** + * @return the IP address as a string if it is available, otherwise null + */ String getRemoteAddr(); + /** + * @return the remote host, which will be an IP address or whatever is provided via proxy headers + */ String getRemoteHost(); int getRemotePort(); diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 4f52aa927b59..1280e469e4e6 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -18,19 +18,23 @@ package org.keycloak.common; import org.jboss.logging.Logger; +import org.keycloak.common.Profile.Feature.Type; import org.keycloak.common.profile.ProfileConfigResolver; +import org.keycloak.common.profile.ProfileConfigResolver.FeatureConfig; import org.keycloak.common.profile.ProfileException; -import org.keycloak.common.profile.PropertiesFileProfileConfigResolver; -import org.keycloak.common.profile.PropertiesProfileConfigResolver; import org.keycloak.common.util.KerberosJdkProvider; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; +import java.util.function.BooleanSupplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -40,18 +44,28 @@ */ public class Profile { + private static volatile Map> FEATURES; + public enum Feature { AUTHORIZATION("Authorization Service", Type.DEFAULT), ACCOUNT_API("Account Management REST API", Type.DEFAULT), - ACCOUNT2("Account Management Console version 2", Type.DEFAULT, Feature.ACCOUNT_API), - ACCOUNT3("Account Management Console version 3", Type.PREVIEW, Feature.ACCOUNT_API), - ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW), + ACCOUNT_V3("Account Console version 3", Type.DEFAULT, 3, Feature.ACCOUNT_API), + + ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW, 1), + + ADMIN_FINE_GRAINED_AUTHZ_V2("Fine-Grained Admin Permissions version 2", Type.DEFAULT, 2, Feature.AUTHORIZATION), ADMIN_API("Admin API", Type.DEFAULT), - ADMIN2("New Admin Console", Type.DEFAULT, Feature.ADMIN_API), + ADMIN_V2("New Admin Console", Type.DEFAULT, 2, Feature.ADMIN_API), + + LOGIN_V2("New Login Theme", Type.DEFAULT, 2, FeatureUpdatePolicy.ROLLING_NO_UPGRADE), + + LOGIN_V1("Legacy Login Theme", Type.DEPRECATED, 1, FeatureUpdatePolicy.ROLLING_NO_UPGRADE), + + QUICK_THEME("WYSIWYG theme configuration tool", Type.EXPERIMENTAL, 1), DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT), @@ -59,7 +73,9 @@ public enum Feature { SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW), - TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW), + TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW, 1), + TOKEN_EXCHANGE_STANDARD_V2("Standard Token Exchange version 2", Type.DEFAULT, 2), + TOKEN_EXCHANGE_EXTERNAL_INTERNAL_V2("External to Internal Token Exchange version 2", Type.EXPERIMENTAL, 2), WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT), @@ -67,12 +83,8 @@ public enum Feature { CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT), - MAP_STORAGE("New store", Type.EXPERIMENTAL), - PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT), - DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW), - DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL), CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW), @@ -80,42 +92,126 @@ public enum Feature { STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT), // Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that - KERBEROS("Kerberos", KerberosJdkProvider.getProvider().isKerberosAvailable() ? Type.DEFAULT : Type.DISABLED_BY_DEFAULT), - - RECOVERY_CODES("Recovery codes", Type.PREVIEW), + KERBEROS("Kerberos", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()), - UPDATE_EMAIL("Update Email Action", Type.PREVIEW), + RECOVERY_CODES("Recovery codes", Type.DEFAULT), - JS_ADAPTER("Host keycloak.js and keycloak-authz.js through the Keycloak sever", Type.DEFAULT), + UPDATE_EMAIL("Update Email Action", Type.DEFAULT), FIPS("FIPS 140-2 mode", Type.DISABLED_BY_DEFAULT), DPOP("OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer", Type.PREVIEW), - LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED), - DEVICE_FLOW("OAuth 2.0 Device Authorization Grant", Type.DEFAULT), TRANSIENT_USERS("Transient users for brokering", Type.EXPERIMENTAL), - ; + + MULTI_SITE("Multi-site support", Type.DISABLED_BY_DEFAULT, FeatureUpdatePolicy.SHUTDOWN), + + CLUSTERLESS("Store all session data, work cache and login failure data in an external Infinispan cluster.", Type.EXPERIMENTAL, FeatureUpdatePolicy.SHUTDOWN), + + CLIENT_TYPES("Client Types", Type.EXPERIMENTAL), + + HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2), + + PERSISTENT_USER_SESSIONS("Persistent online user sessions across restarts and upgrades", Type.DEFAULT, FeatureUpdatePolicy.SHUTDOWN), + + OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL), + + OPENTELEMETRY("OpenTelemetry Tracing", Type.DEFAULT), + + DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL), + + ORGANIZATION("Organization support within realms", Type.DEFAULT), + + PASSKEYS("Passkeys", Type.PREVIEW, Feature.WEB_AUTHN), + + USER_EVENT_METRICS("Collect metrics based on user events", Type.DEFAULT), + + IPA_TUURA_FEDERATION("IPA-Tuura user federation provider", Type.EXPERIMENTAL), + + LOGOUT_ALL_SESSIONS_V1("Logout all sessions logs out only regular sessions", Type.DEPRECATED, 1), + + ROLLING_UPDATES_V1("Rolling Updates", Type.DEFAULT, 1), + ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.PREVIEW, 2), + + LOG_MDC("Mapped Diagnostic Context (MDC) information in logs", Type.PREVIEW), + + /** + * @see Deprecate for removal the Instagram social broker. + */ + @Deprecated + INSTAGRAM_BROKER("Instagram Identity Broker", Type.DEPRECATED, 1); private final Type type; private final String label; + private final String unversionedKey; + private final String key; + private final BooleanSupplier isAvailable; + private final FeatureUpdatePolicy updatePolicy; + private final Set dependencies; + private final int version; - private Set dependencies; - Feature(String label, Type type) { - this.label = label; - this.type = type; + Feature(String label, Type type, Feature... dependencies) { + this(label, type, 1, null, null, dependencies); } - Feature(String label, Type type, Feature... dependencies) { + Feature(String label, Type type, FeatureUpdatePolicy updatePolicy, Feature... dependencies) { + this(label, type, 1, null, updatePolicy, dependencies); + } + + Feature(String label, Type type, int version, FeatureUpdatePolicy updatePolicy, Feature... dependencies) { + this(label, type, version, null, updatePolicy, dependencies); + } + + Feature(String label, Type type, int version, Feature... dependencies) { + this(label, type, version, null, null, dependencies); + } + + Feature(String label, Type type, int version, BooleanSupplier isAvailable, Feature... dependencies) { + this(label, type, version, isAvailable, null, dependencies); + } + + Feature(String label, Type type, int version, BooleanSupplier isAvailable, FeatureUpdatePolicy updatePolicy, Feature... dependencies) { this.label = label; this.type = type; + this.version = version; + this.isAvailable = isAvailable; + this.updatePolicy = updatePolicy == null ? FeatureUpdatePolicy.ROLLING : updatePolicy; + this.key = name().toLowerCase().replaceAll("_", "-"); + if (this.name().endsWith("_V" + version)) { + unversionedKey = key.substring(0, key.length() - (String.valueOf(version).length() + 2)); + } else { + this.unversionedKey = key; + if (this.version > 1) { + throw new IllegalStateException("It is expected that the enum name ends with the version"); + } + } this.dependencies = Arrays.stream(dependencies).collect(Collectors.toSet()); } + /** + * Get the key that uniquely identifies this feature + *

+ * {@link #getVersionedKey()} should instead be shown to users where possible. + */ public String getKey() { - return name().toLowerCase().replaceAll("_", "-"); + return key; + } + + /** + * Return the key without any versioning. All features of the same type + * will share this key. + */ + public String getUnversionedKey() { + return unversionedKey; + } + + /** + * Return the key in the form key:v{version} + */ + public String getVersionedKey() { + return getUnversionedKey() + ":v" + version; } public String getLabel() { @@ -130,13 +226,26 @@ public Set getDependencies() { return dependencies; } + public int getVersion() { + return version; + } + + public boolean isAvailable() { + return isAvailable == null || isAvailable.getAsBoolean(); + } + + public FeatureUpdatePolicy getUpdatePolicy() { + return updatePolicy; + } + public enum Type { + // in priority order DEFAULT("Default"), DISABLED_BY_DEFAULT("Disabled by default"), + DEPRECATED("Deprecated"), PREVIEW("Preview"), PREVIEW_DISABLED_BY_DEFAULT("Preview disabled by default"), // Preview features, which are not automatically enabled even with enabled preview profile (Needs to be enabled explicitly) - EXPERIMENTAL("Experimental"), - DEPRECATED("Deprecated"); + EXPERIMENTAL("Experimental"); private final String label; @@ -150,15 +259,11 @@ public String getLabel() { } } - private static final Logger logger = Logger.getLogger(Profile.class); + private static final Set ESSENTIAL_FEATURES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Feature.HOSTNAME_V2.getUnversionedKey()))); - private static final List DEFAULT_RESOLVERS = new LinkedList<>(); - static { - DEFAULT_RESOLVERS.add(new PropertiesProfileConfigResolver(System.getProperties())); - DEFAULT_RESOLVERS.add(new PropertiesFileProfileConfigResolver()); - }; + private static final Logger logger = Logger.getLogger(Profile.class); - private static Profile CURRENT; + private static volatile Profile CURRENT; private final ProfileName profileName; @@ -170,13 +275,140 @@ public static Profile defaults() { public static Profile configure(ProfileConfigResolver... resolvers) { ProfileName profile = Arrays.stream(resolvers).map(ProfileConfigResolver::getProfileName).filter(Objects::nonNull).findFirst().orElse(ProfileName.DEFAULT); - Map features = Arrays.stream(Feature.values()).collect(Collectors.toMap(f -> f, f -> isFeatureEnabled(profile, f, resolvers))); + + Map features = new LinkedHashMap<>(); + + for (Map.Entry> entry : getOrderedFeatures().entrySet()) { + + // first check by unversioned key - if enabled, choose the highest priority feature + String unversionedFeature = entry.getKey(); + ProfileConfigResolver.FeatureConfig unversionedConfig = getFeatureConfig(unversionedFeature, resolvers); + Feature enabledFeature = null; + if (unversionedConfig == FeatureConfig.ENABLED) { + enabledFeature = entry.getValue().iterator().next(); + if (!enabledFeature.isAvailable()) { + throw new ProfileException(String.format("Feature %s cannot be enabled as it is not available.", unversionedFeature)); + } + } else if (unversionedConfig == FeatureConfig.DISABLED && ESSENTIAL_FEATURES.contains(unversionedFeature)) { + throw new ProfileException(String.format("Feature %s cannot be disabled.", unversionedFeature)); + } + + // now check each feature version to ensure consistency and select any features enabled by default + boolean isExplicitlyEnabledFeature = false; + for (Feature f : entry.getValue()) { + ProfileConfigResolver.FeatureConfig configuration = getFeatureConfig(f.getVersionedKey(), resolvers); + + if (configuration != FeatureConfig.UNCONFIGURED && unversionedConfig != FeatureConfig.UNCONFIGURED) { + throw new ProfileException("Versioned feature " + f.getVersionedKey() + " is not expected as " + unversionedFeature + " is already " + unversionedConfig.name().toLowerCase()); + } + + switch (configuration) { + case ENABLED: + if (isExplicitlyEnabledFeature) { + throw new ProfileException( + String.format("Multiple versions of the same feature %s, %s should not be enabled.", + enabledFeature.getVersionedKey(), f.getVersionedKey())); + } + // even if something else was enabled by default, explicitly enabling a lower priority feature takes precedence + if (!f.isAvailable()) { + throw new ProfileException(String.format("Feature %s cannot be enabled as it is not available.", f.getVersionedKey())); + } + enabledFeature = f; + isExplicitlyEnabledFeature = true; + break; + case DISABLED: + throw new ProfileException("Feature " + f.getVersionedKey() + " should not be disabled using a versioned key."); + default: + if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null + && isEnabledByDefault(profile, f) && f.isAvailable()) { + enabledFeature = f; + } + break; + } + } + for (Feature f : entry.getValue()) { + features.put(f, f == enabledFeature); + } + } + verifyConfig(features); CURRENT = new Profile(profile, features); return CURRENT; } + private static boolean isEnabledByDefault(ProfileName profile, Feature f) { + switch (f.getType()) { + case DEFAULT: + return true; + case PREVIEW: + return profile.equals(ProfileName.PREVIEW); + default: + return false; + } + } + + private static ProfileConfigResolver.FeatureConfig getFeatureConfig(String feature, + ProfileConfigResolver... resolvers) { + ProfileConfigResolver.FeatureConfig configuration = Arrays.stream(resolvers).map(r -> r.getFeatureConfig(feature)) + .filter(r -> !r.equals(ProfileConfigResolver.FeatureConfig.UNCONFIGURED)) + .findFirst() + .orElse(ProfileConfigResolver.FeatureConfig.UNCONFIGURED); + return configuration; + } + + /** + * Compute a map of unversioned feature keys to ordered sets (highest first) of features. The priority order for features is: + *

+ *

    + *
  • The highest default supported version + *
  • The highest non-default supported version + *
  • The highest deprecated version + *
  • The highest preview version + *
  • The highest experimental version + *
      + *

      + * Note the {@link Type} enum is ordered based upon priority. + */ + private static Map> getOrderedFeatures() { + if (FEATURES == null) { + // "natural" ordering low to high between two features (type has precedence and then reversed version is used) + Comparator comparator = Comparator.comparing(Feature::getType).thenComparing(Comparator.comparingInt(Feature::getVersion).reversed()); + // aggregate the features by unversioned key + HashMap> features = new HashMap<>(); + Stream.of(Feature.values()).forEach(f -> features.compute(f.getUnversionedKey(), (k, v) -> { + if (v == null) { + v = new TreeSet<>(comparator); + } + v.add(f); + return v; + })); + FEATURES = features; + } + return FEATURES; + } + + public static Set getAllUnversionedFeatureNames() { + return Collections.unmodifiableSet(getOrderedFeatures().keySet()); + } + + public static Set getDisableableUnversionedFeatureNames() { + return getOrderedFeatures().keySet().stream().filter(f -> !ESSENTIAL_FEATURES.contains(f)).collect(Collectors.toSet()); + } + + /** + * Get all of the feature versions for the given feature. They will be ordered by priority. + *

      + * If the feature does not exist an empty collection will be returned. + */ + public static Set getFeatureVersions(String feature) { + TreeSet versions = getOrderedFeatures().get(feature); + if (versions == null) { + return Collections.emptySet(); + } + return Collections.unmodifiableSet(versions); + } + public static Profile init(ProfileName profileName, Map features) { CURRENT = new Profile(profileName, features); return CURRENT; @@ -193,10 +425,21 @@ public static Profile getInstance() { return CURRENT; } + public static void reset() { + CURRENT = null; + } + public static boolean isFeatureEnabled(Feature feature) { return getInstance().features.get(feature); } + public static boolean isAnyVersionOfFeatureEnabled(Feature feature) { + return isFeatureEnabled(feature) || + getInstance().getEnabledFeatures() + .stream() + .anyMatch(f -> Objects.equals(f.getUnversionedKey(), feature.getUnversionedKey())); + } + public ProfileName getName() { return profileName; } @@ -209,6 +452,10 @@ public Set getDisabledFeatures() { return features.entrySet().stream().filter(e -> !e.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet()); } + public Set getEnabledFeatures() { + return features.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toSet()); + } + /** * @return all features of type "preview" or "preview_disabled_by_default" */ @@ -238,28 +485,6 @@ public enum ProfileName { PREVIEW } - private static Boolean isFeatureEnabled(ProfileName profile, Feature feature, ProfileConfigResolver... resolvers) { - ProfileConfigResolver.FeatureConfig configuration = Arrays.stream(resolvers).map(r -> r.getFeatureConfig(feature)) - .filter(r -> !r.equals(ProfileConfigResolver.FeatureConfig.UNCONFIGURED)) - .findFirst() - .orElse(ProfileConfigResolver.FeatureConfig.UNCONFIGURED); - switch (configuration) { - case ENABLED: - return true; - case DISABLED: - return false; - default: - switch (feature.getType()) { - case DEFAULT: - return true; - case PREVIEW: - return profile.equals(ProfileName.PREVIEW); - default: - return false; - } - } - } - private static void verifyConfig(Map features) { for (Feature f : features.keySet()) { if (features.get(f) && f.getDependencies() != null) { @@ -285,11 +510,19 @@ private void logUnsupportedFeatures(Feature.Type type, Set checkedFeatu String enabledFeaturesOfType = features.entrySet().stream() .filter(e -> e.getValue() && checkedFeatureTypes.contains(e.getKey().getType())) - .map(e -> e.getKey().getKey()).sorted().collect(Collectors.joining(", ")); + .map(e -> e.getKey().getVersionedKey()).sorted().collect(Collectors.joining(", ")); if (!enabledFeaturesOfType.isEmpty()) { logger.logv(level, "{0} features enabled: {1}", type.getLabel(), enabledFeaturesOfType); } } + public enum FeatureUpdatePolicy { + // Always allow a rolling update when the Feature is enabled/disabled + ROLLING, + // Allow rolling update, but not when going from V1 to V2 or V2 to V1 + ROLLING_NO_UPGRADE, + // Always require a cluster shutdown when the Feature is enabled/disabled + SHUTDOWN; + } } diff --git a/common/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java b/common/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java index 4dc0dd1cf76b..a1ea929f3adb 100644 --- a/common/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java +++ b/common/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java @@ -35,4 +35,6 @@ public interface ServiceAccountConstants { String CLIENT_HOST = "clientHost"; String CLIENT_ADDRESS = "clientAddress"; + String SERVICE_ACCOUNT_SCOPE = "service_account"; + } diff --git a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java index 5000d64f61a1..6a15c9b98fed 100755 --- a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java @@ -63,6 +63,8 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber); + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate); + public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException; public List getCRLDistributionPoints(X509Certificate cert) throws IOException; diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java index 90a06e2e661c..1c53e6186cde 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -10,6 +10,10 @@ public class CryptoConstants { public static final String RSA1_5 = "RSA1_5"; public static final String RSA_OAEP = "RSA-OAEP"; public static final String RSA_OAEP_256 = "RSA-OAEP-256"; + public static final String ECDH_ES = "ECDH-ES"; + public static final String ECDH_ES_A128KW = "ECDH-ES+A128KW"; + public static final String ECDH_ES_A192KW = "ECDH-ES+A192KW"; + public static final String ECDH_ES_A256KW = "ECDH-ES+A256KW"; // Constant for the OCSP provider // public static final String OCSP = "OCSP"; diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java b/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java index 404cf01c6d6d..7f9c35bd6e44 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java @@ -3,6 +3,7 @@ import java.security.KeyStore; import java.security.Provider; import java.security.Security; +import java.util.Comparator; import java.util.List; import java.util.ServiceLoader; import java.util.stream.Collectors; @@ -53,15 +54,20 @@ public static CryptoProvider getProvider() { // Try to auto-detect provider private static CryptoProvider detectProvider(ClassLoader classLoader) { List foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false) + .sorted(Comparator.comparingInt(CryptoProvider::order).reversed()) .collect(Collectors.toList()); if (foundProviders.isEmpty()) { throw new IllegalStateException("Not able to load any cryptoProvider with the classLoader: " + classLoader); - } else if (foundProviders.size() > 1) { - throw new IllegalStateException("Multiple crypto providers loaded with the classLoader: " + classLoader + - ". Make sure only one cryptoProvider available on the classpath. Available providers: " +foundProviders); } else { logger.debugf("Detected crypto provider: %s", foundProviders.get(0).getClass().getName()); + if (foundProviders.size() > 1) { + StringBuilder builder = new StringBuilder("Ignored crypto providers: "); + for (int i = 1 ; i < foundProviders.size() ; i++) { + builder.append(foundProviders.get(i).getClass().getName() + ", "); + } + logger.debugf(builder.toString()); + } return foundProviders.get(0); } } @@ -88,7 +94,7 @@ public static String dumpSecurityProperties() { } public static void setProvider(CryptoProvider provider) { - logger.debugf("Using the crypto provider: %s", provider.getClass().getName()); + logger.debugf("Using the crypto provider: %s", provider != null ? provider.getClass().getName() : "null"); cryptoProvider = provider; } } diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java index 0b2448fed94f..283836c595da 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java @@ -36,6 +36,13 @@ public interface CryptoProvider { */ Provider getBouncyCastleProvider(); + /** + * Order of this provider. This allows to specify which CryptoProvider will have preference in case that more of them are on the classpath. + * + * The higher number has preference over the lower number + */ + int order(); + /** * Get some algorithm provider implementation. Returned implementation can be dependent according to if we have * non-fips bouncycastle or fips bouncycastle on the classpath. @@ -84,7 +91,7 @@ public interface CryptoProvider { KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException; Cipher getAesCbcCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException; - + Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException; SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException, NoSuchProviderException; diff --git a/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java index 4a63c0b3858d..1294d6dc643e 100644 --- a/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java @@ -1,13 +1,14 @@ package org.keycloak.common.crypto; import java.io.IOException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; public interface ECDSACryptoProvider { - - + public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException; public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException; - + public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey); } diff --git a/common/src/main/java/org/keycloak/common/enums/HostnameVerificationPolicy.java b/common/src/main/java/org/keycloak/common/enums/HostnameVerificationPolicy.java new file mode 100755 index 000000000000..bc21763e0a33 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/enums/HostnameVerificationPolicy.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.common.enums; + +public enum HostnameVerificationPolicy { + + /** + * Hostname verification is not done on the server's certificate + */ + ANY, + + /** + * Allows wildcards in subdomain names (e.g. *.foo.com) to match anything, including multiple levels (e.g. a.b.foo.com) + */ + @Deprecated + WILDCARD, + + /** + * CN must match hostname connecting to + */ + @Deprecated + STRICT, + + /** + * Similar to STRICT, but checks against a more complete public suffix matcher + */ + DEFAULT +} \ No newline at end of file diff --git a/common/src/main/java/org/keycloak/common/enums/SslRequired.java b/common/src/main/java/org/keycloak/common/enums/SslRequired.java index 19d30ccf6a57..c928438ec20a 100644 --- a/common/src/main/java/org/keycloak/common/enums/SslRequired.java +++ b/common/src/main/java/org/keycloak/common/enums/SslRequired.java @@ -35,26 +35,45 @@ public boolean isRequired(ClientConnection connection) { return isRequired(connection.getRemoteAddr()); } - public boolean isRequired(String address) { + public boolean isRequired(String host) { switch (this) { case ALL: return true; case NONE: return false; case EXTERNAL: - return !isLocal(address); + // NOTE: this is sometimes using hostnames here, which require DNS resolution + // It assumes that the resolution will be the same on the client side + // - this will go away once EXTERNAL is no longer supported + return !isLocal(host); default: return true; } } - private boolean isLocal(String remoteAddress) { + private boolean isLocal(String host) { + if (host == null || host.isEmpty()) { + return false; // InetAddress.getByName returns localhost for these + } try { - InetAddress inetAddress = InetAddress.getByName(remoteAddress); - return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress(); + InetAddress inetAddress = InetAddress.getByName(host); + return inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress() || inetAddress.isLinkLocalAddress() || isUniqueLocal(inetAddress); } catch (UnknownHostException e) { return false; } } + /** + * Check if the address is within IPv6 unique local address (ULA) range RFC4193. + */ + private boolean isUniqueLocal(InetAddress address) { + if (address instanceof java.net.Inet6Address) { + byte[] addr = address.getAddress(); + // Check if address is in unique local range fc00::/7 + return ((byte) (addr[0] & 0b11111110)) == (byte) 0xFC; + } + + return false; + } + } diff --git a/common/src/main/java/org/keycloak/common/profile/CommaSeparatedListProfileConfigResolver.java b/common/src/main/java/org/keycloak/common/profile/CommaSeparatedListProfileConfigResolver.java index 212aedf48a83..9a113d7f4955 100644 --- a/common/src/main/java/org/keycloak/common/profile/CommaSeparatedListProfileConfigResolver.java +++ b/common/src/main/java/org/keycloak/common/profile/CommaSeparatedListProfileConfigResolver.java @@ -3,8 +3,8 @@ import org.keycloak.common.Profile; import java.util.Arrays; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; public class CommaSeparatedListProfileConfigResolver implements ProfileConfigResolver { @@ -13,10 +13,10 @@ public class CommaSeparatedListProfileConfigResolver implements ProfileConfigRes public CommaSeparatedListProfileConfigResolver(String enabledFeatures, String disabledFeatures) { if (enabledFeatures != null) { - this.enabledFeatures = Arrays.stream(enabledFeatures.split(",")).collect(Collectors.toSet()); + this.enabledFeatures = new HashSet<>(Arrays.asList(enabledFeatures.split(","))); } if (disabledFeatures != null) { - this.disabledFeatures = Arrays.stream(disabledFeatures.split(",")).collect(Collectors.toSet()); + this.disabledFeatures = new HashSet<>(Arrays.asList(disabledFeatures.split(","))); } } @@ -29,11 +29,14 @@ public Profile.ProfileName getProfileName() { } @Override - public FeatureConfig getFeatureConfig(Profile.Feature feature) { - String key = feature.getKey(); - if (enabledFeatures != null && enabledFeatures.contains(key)) { + public FeatureConfig getFeatureConfig(String feature) { + if (enabledFeatures != null && enabledFeatures.contains(feature)) { + if (disabledFeatures != null && disabledFeatures.contains(feature)) { + throw new ProfileException(feature + " is in both the enabled and disabled feature lists."); + } return FeatureConfig.ENABLED; - } else if (disabledFeatures != null && disabledFeatures.contains(key)) { + } + if (disabledFeatures != null && disabledFeatures.contains(feature)) { return FeatureConfig.DISABLED; } return FeatureConfig.UNCONFIGURED; diff --git a/common/src/main/java/org/keycloak/common/profile/ProfileConfigResolver.java b/common/src/main/java/org/keycloak/common/profile/ProfileConfigResolver.java index b6a2be965ab6..f302667c8af8 100644 --- a/common/src/main/java/org/keycloak/common/profile/ProfileConfigResolver.java +++ b/common/src/main/java/org/keycloak/common/profile/ProfileConfigResolver.java @@ -6,7 +6,7 @@ public interface ProfileConfigResolver { Profile.ProfileName getProfileName(); - FeatureConfig getFeatureConfig(Profile.Feature feature); + FeatureConfig getFeatureConfig(String feature); public enum FeatureConfig { ENABLED, diff --git a/common/src/main/java/org/keycloak/common/profile/PropertiesFileProfileConfigResolver.java b/common/src/main/java/org/keycloak/common/profile/PropertiesFileProfileConfigResolver.java deleted file mode 100644 index c3d72d38c17d..000000000000 --- a/common/src/main/java/org/keycloak/common/profile/PropertiesFileProfileConfigResolver.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.keycloak.common.profile; - -import org.keycloak.common.Profile; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; - -public class PropertiesFileProfileConfigResolver implements ProfileConfigResolver { - - private Properties properties; - - public PropertiesFileProfileConfigResolver() { - try { - String jbossServerConfigDir = System.getProperty("jboss.server.config.dir"); - if (jbossServerConfigDir != null) { - File file = new File(jbossServerConfigDir, "profile.properties"); - if (file.isFile()) { - try (FileInputStream is = new FileInputStream(file)) { - properties = new Properties(); - properties.load(is); - } - } - } - } catch (IOException e) { - throw new ProfileException("Failed to load profile propeties file", e); - } - } - - @Override - public Profile.ProfileName getProfileName() { - if (properties != null) { - String profile = properties.getProperty("profile"); - if (profile != null) { - return Profile.ProfileName.valueOf(profile.toUpperCase()); - } - } - return null; - } - - @Override - public FeatureConfig getFeatureConfig(Profile.Feature feature) { - if (properties != null) { - String config = properties.getProperty("feature." + feature.name().toLowerCase()); - if (config != null) { - switch (config) { - case "enabled": - return FeatureConfig.ENABLED; - case "disabled": - return FeatureConfig.DISABLED; - default: - throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey()); - } - } - } - return FeatureConfig.UNCONFIGURED; - } -} diff --git a/common/src/main/java/org/keycloak/common/profile/PropertiesProfileConfigResolver.java b/common/src/main/java/org/keycloak/common/profile/PropertiesProfileConfigResolver.java index d6fe5d82ca9c..804582805a91 100644 --- a/common/src/main/java/org/keycloak/common/profile/PropertiesProfileConfigResolver.java +++ b/common/src/main/java/org/keycloak/common/profile/PropertiesProfileConfigResolver.java @@ -1,26 +1,41 @@ package org.keycloak.common.profile; import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; import java.util.Properties; +import java.util.function.UnaryOperator; public class PropertiesProfileConfigResolver implements ProfileConfigResolver { - private Properties properties; + private UnaryOperator getter; public PropertiesProfileConfigResolver(Properties properties) { - this.properties = properties; + this(properties::getProperty); + } + + public PropertiesProfileConfigResolver(UnaryOperator getter) { + this.getter = getter; } @Override public Profile.ProfileName getProfileName() { - String profile = properties.getProperty("keycloak.profile"); - return profile != null ? Profile.ProfileName.valueOf(profile.toUpperCase()) : null; + String profile = getter.apply("keycloak.profile"); + + if (profile != null) { + try { + return Profile.ProfileName.valueOf(profile.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ProfileException(String.format("Invalid profile '%s' specified via 'keycloak.profile' property", profile)); + } + } + return null; } @Override - public FeatureConfig getFeatureConfig(Profile.Feature feature) { - String config = properties.getProperty("keycloak.profile.feature." + feature.name().toLowerCase()); + public FeatureConfig getFeatureConfig(String feature) { + String key = getPropertyKey(feature); + String config = getter.apply(key); if (config != null) { switch (config) { case "enabled": @@ -28,9 +43,17 @@ public FeatureConfig getFeatureConfig(Profile.Feature feature) { case "disabled": return FeatureConfig.DISABLED; default: - throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey()); + throw new ProfileException("Invalid config value '" + config + "' for feature key " + key); } } return FeatureConfig.UNCONFIGURED; } + + public static String getPropertyKey(Feature feature) { + return getPropertyKey(feature.getKey()); + } + + public static String getPropertyKey(String feature) { + return "keycloak.profile.feature." + feature.replaceAll("[-:]", "_"); + } } diff --git a/common/src/main/java/org/keycloak/common/util/Base64.java b/common/src/main/java/org/keycloak/common/util/Base64.java index 3840d688fc85..ddd7cc986d21 100644 --- a/common/src/main/java/org/keycloak/common/util/Base64.java +++ b/common/src/main/java/org/keycloak/common/util/Base64.java @@ -1012,7 +1012,7 @@ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int op * anywhere along their length by specifying * srcOffset and destOffset. * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for + * are large enough to accommodate srcOffset + 4 for * the source array or destOffset + 3 for * the destination array. * This method returns the actual number of bytes that diff --git a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java index 1a4f3585e189..30964729ea84 100755 --- a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java +++ b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java @@ -21,6 +21,7 @@ import java.security.KeyPair; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.util.Date; import org.keycloak.common.crypto.CryptoIntegration; @@ -66,5 +67,9 @@ public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) { return CryptoIntegration.getProvider().getCertificateUtils().generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber); } + + public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) { + return CryptoIntegration.getProvider().getCertificateUtils().generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, validityEndDate); + } } diff --git a/common/src/main/java/org/keycloak/common/util/CollectionUtil.java b/common/src/main/java/org/keycloak/common/util/CollectionUtil.java index 9adc13f4b394..c546b0521070 100644 --- a/common/src/main/java/org/keycloak/common/util/CollectionUtil.java +++ b/common/src/main/java/org/keycloak/common/util/CollectionUtil.java @@ -18,9 +18,8 @@ package org.keycloak.common.util; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -35,15 +34,7 @@ public static String join(Collection strings) { } public static String join(Collection strings, String separator) { - Iterator iter = strings.iterator(); - StringBuilder sb = new StringBuilder(); - if(iter.hasNext()){ - sb.append(iter.next()); - while(iter.hasNext()){ - sb.append(separator).append(iter.next()); - } - } - return sb.toString(); + return strings.stream().collect(Collectors.joining(String.valueOf(separator))); } // Return true if all items from col1 are in col2 and viceversa. Order is not taken into account @@ -53,22 +44,20 @@ public static boolean collectionEquals(Collection col1, Collection col } Map countMap = new HashMap<>(); for(T o : col1) { - Integer v = countMap.get(o); - countMap.put(o, v==null ? 1 : v+1); + countMap.merge(o, 1, (v1, v2) -> v1 + v2); } for(T o : col2) { Integer v = countMap.get(o); if (v==null) { return false; } - countMap.put(o, v-1); - } - for(Integer count : countMap.values()) { - if (count!=0) { - return false; + if (v == 1) { + countMap.remove(o); + } else { + countMap.put(o, v-1); } } - return true; + return countMap.isEmpty(); } public static boolean isEmpty(Collection collection) { @@ -79,14 +68,7 @@ public static boolean isNotEmpty(Collection collection) { return !isEmpty(collection); } - public static Set intersection(Collection col1, Collection col2) { - if (isEmpty(col1) || isEmpty(col2)) return Collections.emptySet(); - - final Collection iteratorCollection = col1.size() <= col2.size() ? col1 : col2; - final Collection searchCollection = iteratorCollection.equals(col1) ? col2 : col1; - - return iteratorCollection.stream() - .filter(searchCollection::contains) - .collect(Collectors.toSet()); + public static Set collectionToSet(Collection collection) { + return collection == null ? null : new HashSet<>(collection); } } diff --git a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java index 258f0765467b..703ef9146f57 100755 --- a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java +++ b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java @@ -17,8 +17,8 @@ package org.keycloak.common.util; -import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -27,84 +27,20 @@ * @version $Revision: 1 $ */ @SuppressWarnings("serial") -public class ConcurrentMultivaluedHashMap extends ConcurrentHashMap> +public class ConcurrentMultivaluedHashMap extends ConcurrentHashMap> implements MultivaluedMap { - public void putSingle(K key, V value) - { - List list = createListInstance(); - list.add(value); - put(key, list); // Just override with new List instance - } - - public void addAll(K key, V... newValues) - { - for (V value : newValues) - { - add(key, value); - } - } - - public void addAll(K key, List valueList) - { - for (V value : valueList) - { - add(key, value); - } - } - - public void addFirst(K key, V value) - { - List list = get(key); - if (list == null) - { - add(key, value); - } - else - { - list.add(0, value); - } - } - public final void add(K key, V value) - { - getList(key).add(value); - } - - - public final void addMultiple(K key, Collection values) - { - getList(key).addAll(values); - } - - public V getFirst(K key) - { - List list = get(key); - return list == null ? null : list.get(0); - } - - public final List getList(K key) - { - List list = get(key); - - if (list == null) { - list = createListInstance(); - List existing = putIfAbsent(key, list); - if (existing != null) { - list = existing; - } - } - - return list; - } - - public void addAll(ConcurrentMultivaluedHashMap other) - { - for (Entry> entry : other.entrySet()) - { - getList(entry.getKey()).addAll(entry.getValue()); - } - } - - protected List createListInstance() { + public ConcurrentMultivaluedHashMap() { + } + + public ConcurrentMultivaluedHashMap(Map> map) { + if (map == null) { + throw new IllegalArgumentException("Map can not be null"); + } + putAll(map); + } + + @Override + public List createListInstance() { return new CopyOnWriteArrayList<>(); } diff --git a/common/src/main/java/org/keycloak/common/util/DerUtils.java b/common/src/main/java/org/keycloak/common/util/DerUtils.java index f00e547d5240..5652d5475534 100755 --- a/common/src/main/java/org/keycloak/common/util/DerUtils.java +++ b/common/src/main/java/org/keycloak/common/util/DerUtils.java @@ -52,10 +52,7 @@ public static PrivateKey decodePrivateKey(InputStream is) dis.readFully(keyBytes); dis.close(); - PKCS8EncodedKeySpec spec = - new PKCS8EncodedKeySpec(keyBytes); - KeyFactory kf =CryptoIntegration.getProvider().getKeyFactory("RSA"); - return kf.generatePrivate(spec); + return decodePrivateKey(keyBytes); } public static PublicKey decodePublicKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { @@ -79,7 +76,14 @@ public static X509Certificate decodeCertificate(InputStream is) throws Exception public static PrivateKey decodePrivateKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der); - KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("RSA"); - return kf.generatePrivate(spec); - } + String[] algorithms = { "RSA", "EC" }; + for (String algorithm : algorithms) { + try { + return CryptoIntegration.getProvider().getKeyFactory(algorithm).generatePrivate(spec); + } catch (InvalidKeySpecException e) { + // Ignore and try the next algorithm. + } + } + throw new InvalidKeySpecException("Unable to decode the private key with supported algorithms: " + String.join(", ", algorithms)); + } } 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..488508bfb18c 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 { @@ -85,7 +86,11 @@ public class Encode case '@': continue; } - pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); + try { + pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i), UTF_8); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } pathEncoding[' '] = "%20"; System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length); @@ -118,7 +123,11 @@ public class Encode queryNameValueEncoding[i] = "+"; continue; } - queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); + try { + queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i), UTF_8); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } /* @@ -156,7 +165,49 @@ public class Encode queryStringEncoding[i] = "%20"; continue; } - queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); + try { + queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i), UTF_8); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /* + * 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 +228,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. */ @@ -348,7 +417,7 @@ public static String encodeValue(String segment, String[] encoding) } /** - * Encode via RFC 3986. PCHAR is allowed allong with '/' + * Encode via RFC 3986. PCHAR is allowed along with '/' *

      * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" @@ -374,7 +443,7 @@ public static String encodePathSaveEncodings(String segment) } /** - * Encode via RFC 3986. PCHAR is allowed allong with '/' + * Encode via RFC 3986. PCHAR is allowed along with '/' *

      * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" @@ -424,6 +493,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/Environment.java b/common/src/main/java/org/keycloak/common/util/Environment.java index 02aa6da79d00..f491eed5d305 100644 --- a/common/src/main/java/org/keycloak/common/util/Environment.java +++ b/common/src/main/java/org/keycloak/common/util/Environment.java @@ -29,6 +29,10 @@ public class Environment { public static final int DEFAULT_JBOSS_AS_STARTUP_TIMEOUT = 300; + public static final String PROFILE = "kc.profile"; + public static final String ENV_PROFILE = "KC_PROFILE"; + public static final String DEV_PROFILE_VALUE = "dev"; + public static int getServerStartupTimeout() { String timeout = System.getProperty("jboss.as.management.blocking.timeout"); if (timeout != null) { @@ -57,4 +61,17 @@ public static boolean isJavaInFipsMode() { return false; } + public static boolean isDevMode() { + return DEV_PROFILE_VALUE.equalsIgnoreCase(getProfile()); + } + + public static String getProfile() { + String profile = System.getProperty(PROFILE); + + if (profile != null) { + return profile; + } + + return System.getenv(ENV_PROFILE); + } } diff --git a/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java b/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java index c931ac469de7..134fd3c57ffe 100644 --- a/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java +++ b/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java @@ -72,9 +72,12 @@ public String buildHtml(String title, String actionUrl, Map para builder.append("") .append("") - .append("

      "); + .append(""); for (Map.Entry param : params.entrySet()) { - builder.append(""); + builder.append(""); } diff --git a/common/src/main/java/org/keycloak/common/util/IoUtils.java b/common/src/main/java/org/keycloak/common/util/IoUtils.java new file mode 100644 index 000000000000..ed16843603eb --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/IoUtils.java @@ -0,0 +1,52 @@ +/* + * 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.common.util; + +import java.io.Console; + +public class IoUtils { + + public static String readFromConsole(String kind, String defaultValue, boolean password) { + Console cons = System.console(); + if (cons == null) { + if (defaultValue != null) { + return defaultValue; + } + throw new RuntimeException(String.format("Console is not active, but %s is required", kind)); + } + String prompt = String.format("Enter %s", kind) + (defaultValue != null ? String.format(" [%s]:", defaultValue) : ":"); + if (password) { + char[] passwd; + if ((passwd = cons.readPassword(prompt)) != null) { + return new String(passwd); + } + } else { + return cons.readLine(prompt); + } + throw new RuntimeException(String.format("No %s provided", kind)); + } + + public static String readPasswordFromConsole(String kind) { + return readFromConsole(kind, null, true); + } + + public static String readLineFromConsole(String kind, String defaultValue) { + return readFromConsole(kind, defaultValue, false); + } + +} 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..2591d6879c74 100755 --- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java +++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java @@ -80,7 +80,6 @@ public KeycloakUriBuilder clone() { } private static final Pattern opaqueUri = Pattern.compile("^([^:/?#]+):([^/].*)"); - private static final Pattern hierarchicalUri = Pattern.compile("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"); private static final Pattern hostPortPattern = Pattern.compile("([^/:]+):(\\d+)"); public static boolean compare(String s1, String s2) { @@ -139,18 +138,46 @@ public KeycloakUriBuilder uriTemplate(String uriTemplate) { return uri(uriTemplate, true); } - protected KeycloakUriBuilder parseHierarchicalUri(String uri, Matcher match, boolean template) { - boolean scheme = match.group(2) != null; - if (scheme) this.scheme = match.group(2); - String authority = match.group(4); + private String matchesHierarchicalUriPart(Map map, String s, String regex, String part) { + if (!s.isEmpty()) { + Matcher m = Pattern.compile(regex).matcher(s); + if (m.find()) { + map.put(part, m.group(1)); + return s.substring(m.end()); + } + } + return s; + } + + private Map matchesHierarchicalUri(final String uri) { + // hierarchicalUri regex: ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))? + Map result = new HashMap<>(); + // scheme + String s = matchesHierarchicalUriPart(result, uri, "^([^:/?#]+):", "scheme"); + // authority + s = matchesHierarchicalUriPart(result, s, "^//([^/?#]*)", "authority"); + // path + s = matchesHierarchicalUriPart(result, s, "^([^?#]*)", "path"); + // query + s = matchesHierarchicalUriPart(result, s, "^\\?([^#]*)", "query"); + // fragment + s = matchesHierarchicalUriPart(result, s, "^#(.*)", "fragment"); + // if the uri is parsed completely it is a valid uri + return s.isEmpty() ? result : null; + } + + protected KeycloakUriBuilder parseHierarchicalUri(String uri, Map match, boolean template) { + boolean scheme = match.get("scheme") != null; + if (scheme) this.scheme = match.get("scheme"); + String authority = match.get("authority"); if (authority != null) { this.authority = null; - String host = match.group(4); + String host = authority; int at = host.indexOf('@'); 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()) { @@ -164,14 +191,14 @@ protected KeycloakUriBuilder parseHierarchicalUri(String uri, Matcher match, boo this.host = host; } } - if (match.group(5) != null) { - String group = match.group(5); + if (match.get("path") != null) { + String group = match.get("path"); if (!scheme && !"".equals(group) && !group.startsWith("/") && group.indexOf(':') > -1) throw new IllegalArgumentException("Illegal uri template: " + uri); if (!"".equals(group)) replacePath(group, template); } - if (match.group(7) != null) replaceQuery(match.group(7), template); - if (match.group(9) != null) fragment(match.group(9), template); + if (match.get("query") != null) replaceQuery(match.get("query"), template); + if (match.get("fragment") != null) fragment(match.get("fragment"), template); return this; } @@ -193,8 +220,8 @@ public KeycloakUriBuilder uri(String uri, boolean template) throws IllegalArgume this.ssp = opaque.group(2); return this; } else { - Matcher match = hierarchicalUri.matcher(uri); - if (match.matches()) { + Map match = matchesHierarchicalUri(uri); + if (match != null) { ssp = null; return parseHierarchicalUri(uri, match, template); } @@ -458,13 +485,15 @@ private String buildString(Map paramMap, boolean fromEncodedMap, bool buffer.append(ssp); } else if (userInfo != null || host != null || port != -1) { buffer.append("//"); - if (userInfo != null) - replaceParameter(paramMap, fromEncodedMap, isTemplate, userInfo, buffer, encodeSlash).append("@"); + if (userInfo != null) { + if (host == null || host.isEmpty()) throw new RuntimeException("empty host name, but userInfo supplied"); + 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); } if (port != -1 && (preserveDefaultPort || !(("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443)))) { + if (host == null || host.isEmpty()) throw new RuntimeException("empty host name, but port supplied"); buffer.append(":").append(Integer.toString(port)); } } else if (authority != null) { @@ -571,6 +600,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 +798,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/KeystoreUtil.java b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java index a05ff43c1c68..167d621ff5d4 100755 --- a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java +++ b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java @@ -28,6 +28,7 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.util.Arrays; +import java.util.List; import java.util.Optional; /** @@ -38,22 +39,30 @@ public class KeystoreUtil { public enum KeystoreFormat { JKS("jks"), - PKCS12("p12"), + PKCS12("p12", "pfx", "pkcs12"), BCFKS("bcfks"); // Typical file extension for this keystore format - private final String fileExtension; - KeystoreFormat(String extension) { - this.fileExtension = extension; + private final List fileExtensions; + KeystoreFormat(String... extensions) { + this.fileExtensions = Arrays.asList(extensions); } - public String getFileExtension() { - return fileExtension; + public List getFileExtensions() { + return fileExtensions; + } + + public String getPrimaryExtension() { + return fileExtensions.get(0); } } public static KeyStore loadKeyStore(String filename, String password) throws Exception { - String keystoreType = getKeystoreType(null, filename, KeyStore.getDefaultType()); + return loadKeyStore(filename, password, null); + } + + public static KeyStore loadKeyStore(String filename, String password, String preferedType) throws Exception { + String keystoreType = getKeystoreType(preferedType, filename, KeyStore.getDefaultType()); KeyStore trustStore = KeyStore.getInstance(keystoreType); InputStream trustStream = null; if (filename.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { @@ -71,7 +80,7 @@ public static KeyStore loadKeyStore(String filename, String password) throws Exc trustStream = new FileInputStream(new File(filename)); } try (InputStream is = trustStream) { - trustStore.load(is, password.toCharArray()); + trustStore.load(is, password == null ? null : password.toCharArray()); } return trustStore; } @@ -88,11 +97,25 @@ public static KeyPair loadKeyPairFromKeystore(String keystoreFile, String storeP throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore"); } PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey(); + if (publicKey == null) { + throw new RuntimeException("Couldn't load public key with alias '" + keyAlias + "' from keystore"); + } return new KeyPair(publicKey, privateKey); } catch (Exception e) { throw new RuntimeException("Failed to load private key: " + e.getMessage(), e); } } + + public static Optional getKeystoreFormat(String path) { + int lastDotIndex = path.lastIndexOf('.'); + if (lastDotIndex > -1) { + String ext = path.substring(lastDotIndex + 1).toLowerCase(); + return Arrays.stream(KeystoreUtil.KeystoreFormat.values()) + .filter(ksFormat -> ksFormat.getFileExtensions().contains(ext)) + .findFirst(); + } + return Optional.empty(); + } /** @@ -108,13 +131,9 @@ public static String getKeystoreType(String preferredType, String path, String d if (preferredType != null) return preferredType; // Fallback to path - int lastDotIndex = path.lastIndexOf('.'); - if (lastDotIndex > -1) { - String ext = path.substring(lastDotIndex + 1).toLowerCase(); - Optional detectedType = Arrays.stream(KeystoreUtil.KeystoreFormat.values()) - .filter(ksFormat -> ksFormat.getFileExtension().equals(ext)) - .findFirst(); - if (detectedType.isPresent()) return detectedType.get().toString(); + Optional format = getKeystoreFormat(path); + if (format.isPresent()) { + return format.get().toString(); } // Fallback to default diff --git a/common/src/main/java/org/keycloak/common/util/MimeTypeUtil.java b/common/src/main/java/org/keycloak/common/util/MimeTypeUtil.java index bb602e196fc6..80646186aae6 100644 --- a/common/src/main/java/org/keycloak/common/util/MimeTypeUtil.java +++ b/common/src/main/java/org/keycloak/common/util/MimeTypeUtil.java @@ -29,7 +29,7 @@ public class MimeTypeUtil { static { map.addMimeTypes("text/css css CSS"); map.addMimeTypes("text/javascript js JS"); - map.addMimeTypes("text/javascript js JS"); + map.addMimeTypes("application/json json JSON"); map.addMimeTypes("image/png png PNG"); map.addMimeTypes("image/svg+xml svg SVG"); map.addMimeTypes("text/html html htm HTML HTM"); diff --git a/common/src/main/java/org/keycloak/common/util/MultiSiteUtils.java b/common/src/main/java/org/keycloak/common/util/MultiSiteUtils.java new file mode 100644 index 000000000000..b0770ec85144 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/MultiSiteUtils.java @@ -0,0 +1,34 @@ +/* + * 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.common.util; + +import org.keycloak.common.Profile; + +public class MultiSiteUtils { + + public static boolean isMultiSiteEnabled() { + return Profile.isFeatureEnabled(Profile.Feature.MULTI_SITE); + } + + /** + * @return true when user sessions are stored in the database. In multi-site setup this is false when REMOTE_CACHE feature is enabled + */ + public static boolean isPersistentSessionsEnabled() { + return Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) || (isMultiSiteEnabled() && !Profile.isFeatureEnabled(Profile.Feature.CLUSTERLESS)); + } +} diff --git a/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java index 8bc5dfd13e0d..e2e3bb479144 100755 --- a/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java +++ b/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java @@ -17,8 +17,6 @@ package org.keycloak.common.util; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,7 +26,7 @@ * @version $Revision: 1 $ */ @SuppressWarnings("serial") -public class MultivaluedHashMap extends HashMap> +public class MultivaluedHashMap extends HashMap> implements MultivaluedMap { public MultivaluedHashMap() { } @@ -44,89 +42,4 @@ public MultivaluedHashMap(Map> map) { public MultivaluedHashMap(MultivaluedHashMap config) { addAll(config); } - - public void putSingle(K key, V value) - { - List list = new ArrayList<>(); - list.add(value); - put(key, list); - } - - public void addAll(K key, V... newValues) - { - for (V value : newValues) - { - add(key, value); - } - } - - public void addAll(K key, List valueList) - { - for (V value : valueList) - { - add(key, value); - } - } - - public void addFirst(K key, V value) - { - List list = get(key); - if (list == null) - { - add(key, value); - } - else - { - list.add(0, value); - } - } - public final void add(K key, V value) - { - getList(key).add(value); - } - - - public final void addMultiple(K key, Collection values) - { - getList(key).addAll(values); - } - - public V getFirst(K key) - { - List list = get(key); - return list == null ? null : list.get(0); - } - - public final List getList(K key) - { - List list = get(key); - if (list == null) - put(key, list = new ArrayList<>()); - return list; - } - - public final void addAll(MultivaluedHashMap other) - { - for (Map.Entry> entry : other.entrySet()) - { - getList(entry.getKey()).addAll(entry.getValue()); - } - } - - public boolean equalsIgnoreValueOrder(MultivaluedHashMap omap) { - if (this == omap) { - return true; - } - if (!keySet().equals(omap.keySet())) { - return false; - } - for (Map.Entry> e : entrySet()) { - List list = e.getValue(); - List olist = omap.get(e.getKey()); - if (!CollectionUtil.collectionEquals(list, olist)) { - return false; - } - } - return true; - } } diff --git a/common/src/main/java/org/keycloak/common/util/MultivaluedMap.java b/common/src/main/java/org/keycloak/common/util/MultivaluedMap.java new file mode 100644 index 000000000000..5fa8b471bb5e --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/MultivaluedMap.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.common.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface MultivaluedMap extends Map> { + + public default void putSingle(K key, V value) { + List list = createListInstance(); + list.add(value); + put(key, list); // Just override with new List instance + } + + public default void addAll(K key, V... newValues) { + for (V value : newValues) { + add(key, value); + } + } + + public default void addAll(K key, List valueList) { + for (V value : valueList) { + add(key, value); + } + } + + public default void addFirst(K key, V value) { + getList(key).add(0, value); + } + + public default void add(K key, V value) { + getList(key).add(value); + } + + public default void addMultiple(K key, Collection values) { + getList(key).addAll(values); + } + + public default V getFirst(K key) { + return Optional.ofNullable(get(key)).filter(l -> !l.isEmpty()).map(l -> l.get(0)).orElse(null); + } + + default V getFirstOrDefault(K key, V defaultValue) { + return Optional.ofNullable(getFirst(key)).orElse(defaultValue); + } + + public default List getList(K key) { + return compute(key, (k, v) -> v != null ? v : createListInstance()); + } + + public default void addAll(MultivaluedMap other) { + for (Entry> entry : other.entrySet()) { + getList(entry.getKey()).addAll(entry.getValue()); + } + } + + public default boolean equalsIgnoreValueOrder(MultivaluedMap omap) { + if (this == omap) { + return true; + } + if (!keySet().equals(omap.keySet())) { + return false; + } + for (Map.Entry> e : entrySet()) { + List list = e.getValue(); + List olist = omap.get(e.getKey()); + if (!CollectionUtil.collectionEquals(list, olist)) { + return false; + } + } + return true; + } + + public default List createListInstance() { + return new ArrayList<>(); + } + +} diff --git a/common/src/main/java/org/keycloak/common/util/NetworkUtils.java b/common/src/main/java/org/keycloak/common/util/NetworkUtils.java index 989bc5944257..5aaed8cba7d6 100644 --- a/common/src/main/java/org/keycloak/common/util/NetworkUtils.java +++ b/common/src/main/java/org/keycloak/common/util/NetworkUtils.java @@ -57,11 +57,11 @@ public static String formatPossibleIpv6Address(String address) { } /** - *

      Convert IPv6 adress into RFC 5952 form. + *

      Convert IPv6 address into RFC 5952 form. * E.g. 2001:db8:0:1:0:0:0:1 -> 2001:db8:0:1::1

      * *

      Method is null safe, and if IPv4 address or host name is passed to the - * method it is returned wihout any processing.

      + * method it is returned without any processing.

      * *

      Method also supports IPv4 in IPv6 (e.g. 0:0:0:0:0:ffff:192.0.2.1 -> * ::ffff:192.0.2.1), and zone ID (e.g. fe80:0:0:0:f0f0:c0c0:1919:1234%4 @@ -417,7 +417,7 @@ private static boolean checkForSolaris() { return checkForPresence("os.name", "sun"); } - private static boolean checkForWindows() { + public static boolean checkForWindows() { return checkForPresence("os.name", "win"); } diff --git a/common/src/main/java/org/keycloak/common/util/PathHelper.java b/common/src/main/java/org/keycloak/common/util/PathHelper.java index 4e5949d75a0e..1bcffbd9d951 100755 --- a/common/src/main/java/org/keycloak/common/util/PathHelper.java +++ b/common/src/main/java/org/keycloak/common/util/PathHelper.java @@ -40,7 +40,7 @@ public class PathHelper /** * A regex pattern that searches for a URI template parameter in the form of {*} */ - public static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("(\\{([^}]+)\\})"); + public static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("(\\{([^{}]+)\\})"); public static final char openCurlyReplacement = 6; public static final char closeCurlyReplacement = 7; diff --git a/common/src/main/java/org/keycloak/common/util/PemUtils.java b/common/src/main/java/org/keycloak/common/util/PemUtils.java index 955f509bdf00..fca4be634f09 100755 --- a/common/src/main/java/org/keycloak/common/util/PemUtils.java +++ b/common/src/main/java/org/keycloak/common/util/PemUtils.java @@ -23,6 +23,9 @@ import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import org.keycloak.common.crypto.CryptoIntegration; @@ -53,6 +56,20 @@ public static X509Certificate decodeCertificate(String cert) { return CryptoIntegration.getProvider().getPemUtils().decodeCertificate(cert); } + /** + * Decode one or more X509 Certificates from a PEM string (certificate bundle) + * + * @param certs + * @return + * @throws Exception + */ + public static X509Certificate[] decodeCertificates(String certs) { + return Arrays.stream(certs.split(END_CERT)) + .map(String::trim) + .filter(pemBlock -> !pemBlock.isEmpty()) + .map(pemBlock -> PemUtils.decodeCertificate(pemBlock + END_CERT)) + .toArray(X509Certificate[]::new); + } /** * Decode a Public Key from a PEM string @@ -124,6 +141,13 @@ public static String addPrivateKeyBeginEnd(String privateKeyPem) { .toString(); } + public static String addCertificateBeginEnd(String certificate) { + return new StringBuilder(BEGIN_CERT + "\n") + .append(certificate) + .append("\n" + END_CERT) + .toString(); + } + public static String addRsaPrivateKeyBeginEnd(String privateKeyPem) { return new StringBuilder(PemUtils.BEGIN_RSA_PRIVATE_KEY + "\n") .append(privateKeyPem) diff --git a/common/src/main/java/org/keycloak/common/util/Resteasy.java b/common/src/main/java/org/keycloak/common/util/Resteasy.java new file mode 100644 index 000000000000..c1eb0969865b --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/Resteasy.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 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.common.util; + +import java.util.HashMap; +import java.util.Map; + +/** + *

      Provides a way for obtaining the KeycloakSession + * + * @author Pedro Igor + * + * @deprecated use org.keycloak.util.KeycloakSessionUtil instead + */ +@Deprecated +public final class Resteasy { + + private static final ThreadLocal, Object>> contextualData = new ThreadLocal, Object>>() { + @Override + protected Map, Object> initialValue() { + return new HashMap<>(1); + }; + }; + + /** + * Push the given {@code instance} with type/key {@code type} to the context associated with the current thread. + *
      Should not be called directly + * + * @param type the type/key to associate the {@code instance} with + * @param instance the instance + */ + public static R pushContext(Class type, R instance) { + return (R) contextualData.get().put(type, instance); + } + + /** + * Clear the context associated with the current thread. + *
      Should not be called directly + */ + public static void clearContextData() { + contextualData.remove(); + } + + /** + * Lookup the instance associated with the given type/key {@code type} from the context associated with the current thread. + *
      Should only be used to obtain the KeycloakSession + * + * @param type the type/key to lookup + * @return the instance associated with the given {@code type} or null if non-existent. + */ + public static R getContextData(Class type) { + return (R) contextualData.get().get(type); + } + + /** + * Push the given {@code instance} with type/key {@code type} to the Resteasy global context. + * + * @param type the type/key to associate the {@code instance} with + * @param instance the instance + */ + @Deprecated + public static void pushDefaultContextObject(Class type, Object instance) { + pushContext(type, instance); + } + +} 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/main/java/org/keycloak/common/util/SecretGenerator.java b/common/src/main/java/org/keycloak/common/util/SecretGenerator.java index ff73e855eeec..20d8ad82cc93 100644 --- a/common/src/main/java/org/keycloak/common/util/SecretGenerator.java +++ b/common/src/main/java/org/keycloak/common/util/SecretGenerator.java @@ -30,6 +30,15 @@ private SecretGenerator() { public static SecretGenerator getInstance() { return instance; } + + public String generateSecureID() { + StringBuilder builder = new StringBuilder(instance.randomBytesHex(16)); + builder.insert(8, '-'); + builder.insert(13, '-'); + builder.insert(18, '-'); + builder.insert(23, '-'); + return builder.toString(); + } public String randomString() { return randomString(SECRET_LENGTH_256_BITS, ALPHANUM); @@ -56,6 +65,7 @@ public String randomString(int length, char[] symbols) { return new String(buf); } + public byte[] randomBytes() { return randomBytes(SECRET_LENGTH_256_BITS); } @@ -70,4 +80,37 @@ public byte[] randomBytes(int length) { return buf; } + public String randomBytesHex(int length) { + final StringBuilder sb = new StringBuilder(); + for (byte b : randomBytes(length)) { + sb.append(Character.forDigit((b >> 4) & 0xF, 16)); + sb.append(Character.forDigit((b & 0xF), 16)); + } + return sb.toString(); + } + + /** + * Returns the equivalent length for a destination alphabet to have the same + * entropy bits than a byte array random generated. + * + * @param byteLengthEntropy The desired entropy in bytes + * @param dstAlphabetLeng The length of the destination alphabet + * @return The equivalent length in destination alphabet to have the same entropy bits + */ + public static int equivalentEntropySize(int byteLengthEntropy, int dstAlphabetLeng) { + return equivalentEntropySize(byteLengthEntropy, 256, dstAlphabetLeng); + } + + /** + * Returns the equivalent length for a destination alphabet to have the same + * entropy bits than another source alphabet. + * + * @param length The length of the string encoded in source alphabet + * @param srcAlphabetLength The length of the source alphabet + * @param dstAlphabetLeng The length of the destination alphabet + * @return The equivalent length (same entropy) in destination alphabet for a string of length in source alphabet + */ + public static int equivalentEntropySize(int length, int srcAlphabetLength, int dstAlphabetLeng) { + return (int) Math.ceil(length * ((Math.log(srcAlphabetLength)) / (Math.log(dstAlphabetLeng)))); + } } diff --git a/common/src/main/java/org/keycloak/common/util/ServerCookie.java b/common/src/main/java/org/keycloak/common/util/ServerCookie.java deleted file mode 100755 index 1ce9901d5b71..000000000000 --- a/common/src/main/java/org/keycloak/common/util/ServerCookie.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright 2016 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.common.util; - -import java.io.Serializable; -import java.text.DateFormat; -import java.text.FieldPosition; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Server-side cookie representation. borrowed from Tomcat. - */ -public class ServerCookie implements Serializable { - private static final String tspecials = ",; "; - private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; - - public enum SameSiteAttributeValue { - NONE("None"); // we currently support only SameSite=None; this might change in the future - - private final String specValue; - SameSiteAttributeValue(String specValue) { - this.specValue = specValue; - } - - @Override - public java.lang.String toString() { - return specValue; - } - } - - /* - * Tests a string and returns true if the string counts as a - * reserved token in the Java language. - * - * @param value the String to be tested - * - * @return true if the String is a reserved - * token; false if it is not - */ - public static boolean isToken(String value) { - if (value == null) return true; - int len = value.length(); - - for (int i = 0; i < len; i++) { - char c = value.charAt(i); - - if (tspecials.indexOf(c) != -1) - return false; - } - return true; - } - - public static boolean containsCTL(String value, int version) { - if (value == null) return false; - int len = value.length(); - for (int i = 0; i < len; i++) { - char c = value.charAt(i); - if (c < 0x20 || c >= 0x7f) { - if (c == 0x09) - continue; //allow horizontal tabs - return true; - } - } - return false; - } - - - public static boolean isToken2(String value) { - if (value == null) return true; - int len = value.length(); - - for (int i = 0; i < len; i++) { - char c = value.charAt(i); - if (tspecials2.indexOf(c) != -1) - return false; - } - return true; - } - - /** - * @deprecated - Not used - */ - public static boolean checkName(String name) { - if (!isToken(name) - || name.equalsIgnoreCase("Comment") // rfc2019 - || name.equalsIgnoreCase("Discard") // rfc2965 - || name.equalsIgnoreCase("Domain") // rfc2019 - || name.equalsIgnoreCase("Expires") // Netscape - || name.equalsIgnoreCase("Max-Age") // rfc2019 - || name.equalsIgnoreCase("Path") // rfc2019 - || name.equalsIgnoreCase("Secure") // rfc2019 - || name.equalsIgnoreCase("Version") // rfc2019 - // TODO remaining RFC2965 attributes - ) { - return false; - } - return true; - } - - // -------------------- Cookie parsing tools - - - /** - * Return the header name to set the cookie, based on cookie version. - */ - public static String getCookieHeaderName(int version) { - // TODO Re-enable logging when RFC2965 is implemented - // log( (version==1) ? "Set-Cookie2" : "Set-Cookie"); - if (version == 1) { - // XXX RFC2965 not referenced in Servlet Spec - // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5 - // Set-Cookie2 is supported by Lynx and Opera - // Need to check on later IE and FF releases but for now... - // RFC2109 - return "Set-Cookie"; - // return "Set-Cookie2"; - } else { - // Old Netscape - return "Set-Cookie"; - } - } - - /** - * US locale - all HTTP dates are in english - */ - private final static Locale LOCALE_US = Locale.US; - - /** - * GMT timezone - all HTTP dates are on GMT - */ - public final static TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); - /** - * Pattern used for old cookies - */ - private final static String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; - - - private final static DateFormat OLD_COOKIE_FORMAT = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); - static{ - OLD_COOKIE_FORMAT.setTimeZone(GMT_ZONE); - } - - public static String formatOldCookie(Date d) { - String ocf = null; - synchronized (OLD_COOKIE_FORMAT) { - ocf = OLD_COOKIE_FORMAT.format(d); - } - return ocf; - } - - public static void formatOldCookie(Date d, StringBuffer sb, - FieldPosition fp) { - synchronized (OLD_COOKIE_FORMAT) { - OLD_COOKIE_FORMAT.format(d, sb, fp); - } - } - - - private static final String ancientDate = formatOldCookie(new Date(10000)); - - - // TODO RFC2965 fields also need to be passed - public static void appendCookieValue(StringBuilder headerBuf, - int version, - String name, - String value, - String path, - String domain, - String comment, - int maxAge, - boolean isSecure, - boolean httpOnly, - SameSiteAttributeValue sameSite) { - StringBuffer buf = new StringBuffer(); - // Servlet implementation checks name - buf.append(name); - buf.append("="); - // Servlet implementation does not check anything else - - // NOTE!!! BROWSERS REALLY DON'T LIKE QUOTING - //maybeQuote2(version, buf, value); - buf.append(value); - - // Add version 1 specific information - if (version == 1) { - // Version=1 ... required - buf.append("; Version=1"); - - // Comment=comment - if (comment != null) { - buf.append("; Comment="); - //maybeQuote2(version, buf, comment); - buf.append(comment); - } - } - - // Add domain information, if present - if (domain != null) { - buf.append("; Domain="); - //maybeQuote2(version, buf, domain); - buf.append(domain); - } - - // Max-Age=secs ... or use old "Expires" format - // TODO RFC2965 Discard - if (maxAge >= 0) { - // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) - buf.append("; Expires="); - // To expire immediately we need to set the time in past - if (maxAge == 0) - buf.append(ancientDate); - else - formatOldCookie - (new Date(System.currentTimeMillis() + - maxAge * 1000L), buf, - new FieldPosition(0)); - - buf.append("; Max-Age="); - buf.append(maxAge); - } - - // Path=path - if (path != null) { - buf.append("; Path="); - buf.append(path); - } - - // SameSite - if (sameSite != null) { - buf.append("; SameSite="); - buf.append(sameSite.toString()); - } - - // Secure - if (isSecure) { - buf.append("; Secure"); - } - - // HttpOnly - if (httpOnly) { - buf.append("; HttpOnly"); - } - - headerBuf.append(buf); - } - - /** - * @deprecated - Not used - */ - @Deprecated - public static void maybeQuote(int version, StringBuffer buf, String value) { - // special case - a \n or \r shouldn't happen in any case - if (isToken(value)) { - buf.append(value); - } else { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } - } - - public static boolean alreadyQuoted(String value) { - if (value == null || value.length() == 0) return false; - return (value.charAt(0) == '\"' && value.charAt(value.length() - 1) == '\"'); - } - - /** - * Quotes values using rules that vary depending on Cookie version. - * - * @param version - * @param buf - * @param value - */ - public static void maybeQuote2(int version, StringBuffer buf, String value) { - if (value == null || value.length() == 0) { - buf.append("\"\""); - } else if (containsCTL(value, version)) - throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value"); - else if (alreadyQuoted(value)) { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 1, value.length() - 1)); - buf.append('"'); - } else if (version == 0 && !isToken(value)) { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } else if (version == 1 && !isToken2(value)) { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } else { - buf.append(value); - } - } - - - /** - * Escapes any double quotes in the given string. - * - * @param s the input string - * @param beginIndex start index inclusive - * @param endIndex exclusive - * @return The (possibly) escaped string - */ - private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) { - - if (s == null || s.length() == 0 || s.indexOf('"') == -1) { - return s; - } - - StringBuffer b = new StringBuffer(); - for (int i = beginIndex; i < endIndex; i++) { - char c = s.charAt(i); - if (c == '\\') { - b.append(c); - //ignore the character after an escape, just append it - if (++i >= endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value."); - b.append(s.charAt(i)); - } else if (c == '"') - b.append('\\').append('"'); - else - b.append(c); - } - - return b.toString(); - } - -} diff --git a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java index a85e9255dd10..4d70f15da635 100755 --- a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java +++ b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java @@ -17,22 +17,20 @@ package org.keycloak.common.util; import java.io.File; -import java.util.Properties; +import java.util.Optional; /** - * A utility class for replacing properties in strings. + * A utility class for replacing properties in strings. * * @author Jason Dillon * @author Scott Stark * @author Claudio Vesco * @author Adrian Brock * @author Dimitris Andreadis - * @version $Revision: 2898 $ + * @version $Revision: 2898 $ */ public final class StringPropertyReplacer { - /** New line string constant */ - public static final String NEWLINE = System.getProperty("line.separator", "\n"); /** File separator value */ private static final String FILE_SEPARATOR = File.separator; @@ -51,10 +49,15 @@ public final class StringPropertyReplacer private static final int SEEN_DOLLAR = 1; private static final int IN_BRACKET = 2; - private static final Properties systemEnvProperties = new SystemEnvProperties(); + private static final PropertyResolver NULL_RESOLVER = property -> null; + private static PropertyResolver DEFAULT_PROPERTY_RESOLVER; + + public static void setDefaultPropertyResolver(PropertyResolver systemVariables) { + DEFAULT_PROPERTY_RESOLVER = systemVariables; + } /** - * Go through the input string and replace any occurance of ${p} with + * Go through the input string and replace any occurrence of ${p} with * the System.getProperty(p) value. If there is no such property p defined, * then the ${p} reference will remain unchanged. * @@ -72,14 +75,13 @@ public final class StringPropertyReplacer * @return the input string with all property references replaced if any. * If there are no valid references the input string will be returned. */ - public static String replaceProperties(final String string) - { - return replaceProperties(string, (Properties) null); + public static String replaceProperties(final String string) { + return replaceProperties(string, getDefaultPropertyResolver()); } /** - * Go through the input string and replace any occurance of ${p} with - * the props.getProperty(p) value. If there is no such property p defined, + * Go through the input string and replace any occurrence of ${p} with + * the value resolves from {@code resolver}. If there is no such property p defined, * then the ${p} reference will remain unchanged. * * If the property reference is of the form ${p:v} and there is no such property p, @@ -93,22 +95,10 @@ public static String replaceProperties(final String string) * value and the property ${:} is replaced with System.getProperty("path.separator"). * * @param string - the string with possible ${} references - * @param props - the source for ${x} property ref values, null means use System.getProperty() + * @param resolver - the property resolver * @return the input string with all property references replaced if any. * If there are no valid references the input string will be returned. */ - public static String replaceProperties(final String string, final Properties props) { - if (props == null) { - return replaceProperties(string, (PropertyResolver) null); - } - return replaceProperties(string, new PropertyResolver() { - @Override - public String resolve(String property) { - return props.getProperty(property); - } - }); - } - public static String replaceProperties(final String string, PropertyResolver resolver) { if(string == null) { @@ -176,10 +166,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) else { // check from the properties - if (resolver != null) - value = resolver.resolve(key); - else - value = systemEnvProperties.getProperty(key); + value = resolveValue(resolver, key); if (value == null) { @@ -188,10 +175,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) if (colon > 0) { String realKey = key.substring(0, colon); - if (resolver != null) - value = resolver.resolve(realKey); - else - value = systemEnvProperties.getProperty(realKey); + value = resolveValue(resolver, realKey); if (value == null) { @@ -238,36 +222,17 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) buffer.append(string.substring(start, chars.length)); if (buffer.indexOf("${") != -1) { - return replaceProperties(buffer.toString(), resolver); + try { + return replaceProperties(buffer.toString(), resolver); + } catch (StackOverflowError ex) { + throw new IllegalStateException("Infinite recursion happening when replacing properties on '" + buffer + "'"); + } } - + // Done return buffer.toString(); } - /** - * Try to resolve a "key" from the provided properties by - * checking if it is actually a "key1,key2", in which case - * try first "key1", then "key2". If all fails, return null. - * - * It also accepts "key1," and ",key2". - * - * @param key the key to resolve - * @param props the properties to use - * @return the resolved key or null - */ - private static String resolveCompositeKey(String key, final Properties props) { - if (props == null) { - return resolveCompositeKey(key, (PropertyResolver) null); - } - return resolveCompositeKey(key, new PropertyResolver() { - @Override - public String resolve(String property) { - return props.getProperty(property); - } - }); - } - private static String resolveCompositeKey(String key, PropertyResolver resolver) { String value = null; @@ -281,26 +246,32 @@ private static String resolveCompositeKey(String key, PropertyResolver resolver) { // Check the first part String key1 = key.substring(0, comma); - if (resolver != null) - value = resolver.resolve(key1); - else - value = systemEnvProperties.getProperty(key1); + value = resolveValue(resolver, key1); } // Check the second part, if there is one and first lookup failed if (value == null && comma < key.length() - 1) { String key2 = key.substring(comma + 1); - if (resolver != null) - value = resolver.resolve(key2); - else - value = systemEnvProperties.getProperty(key2); + value = resolveValue(resolver, key2); } } // Return whatever we've found or null return value; } - + public interface PropertyResolver { String resolve(String property); } + + private static String resolveValue(PropertyResolver resolver, String key) { + if (resolver == null) { + return getDefaultPropertyResolver().resolve(key); + } + + return resolver.resolve(key); + } + + private static PropertyResolver getDefaultPropertyResolver() { + return Optional.ofNullable(DEFAULT_PROPERTY_RESOLVER).orElse(NULL_RESOLVER); + } } diff --git a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java index 78fc4c7f98f0..55f277e07971 100644 --- a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java +++ b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java @@ -18,32 +18,52 @@ package org.keycloak.common.util; import java.util.Collections; -import java.util.Map; +import java.util.Optional; import java.util.Properties; +import java.util.Set; /** + *

      An utility class to resolve the value of a key based on the environment variables + * and system properties available at runtime. In most cases, you do not want to resolve whatever system variable is available at runtime but specify which ones + * can be used when resolving placeholders. + * + *

      To resolve to an environment variable, the key must have a format like {@code env.} where {@code key} is the name of an environment variable. + * For system properties, there is no specific format and the value is resolved from a system property that matches the key. + * * @author Stian Thorgersen */ public class SystemEnvProperties extends Properties { - private final Map overrides; + /** + *

      An variation of {@link SystemEnvProperties} that gives unrestricted access to any system variable available at runtime. + * Most of the time you don't want to use this class but favor creating a {@link SystemEnvProperties} instance that + * filters which system variables should be available at runtime. + */ + public static final SystemEnvProperties UNFILTERED = new SystemEnvProperties(Collections.emptySet()) { + @Override + protected boolean isAllowed(String key) { + return true; + } + }; - public SystemEnvProperties(Map overrides) { - this.overrides = overrides; - } + private final Set allowedSystemVariables; - public SystemEnvProperties() { - this.overrides = Collections.EMPTY_MAP; + /** + * Creates a new instance where system variables where only specific keys can be resolved from system variables. + * + * @param allowedSystemVariables the keys of system variables that should be available at runtime + */ + public SystemEnvProperties(Set allowedSystemVariables) { + this.allowedSystemVariables = Optional.ofNullable(allowedSystemVariables).orElse(Collections.emptySet()); } @Override public String getProperty(String key) { - if (overrides.containsKey(key)) { - return overrides.get(key); - } else if (key.startsWith("env.")) { - return System.getenv().get(key.substring(4)); + if (key.startsWith("env.")) { + String envKey = key.substring(4); + return isAllowed(envKey) ? System.getenv().get(envKey) : null; } else { - return System.getProperty(key); + return isAllowed(key) ? System.getProperty(key) : null; } } @@ -53,4 +73,7 @@ public String getProperty(String key, String defaultValue) { return value != null ? value : defaultValue; } + protected boolean isAllowed(String key) { + return allowedSystemVariables.contains(key); + } } diff --git a/common/src/main/java/org/keycloak/common/util/TriFunction.java b/common/src/main/java/org/keycloak/common/util/TriFunction.java new file mode 100644 index 000000000000..2b9391e70e12 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/TriFunction.java @@ -0,0 +1,33 @@ +/* + * 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.common.util; + +@FunctionalInterface +public interface TriFunction { + + /** + * Applies this function to the given arguments. + * + * @param t the first function argument + * @param u the second function argument + * @param v the third function argument + * @return the function result + */ + R apply(T t, U u, V v); + +} diff --git a/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java b/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java index c2131e83d2e6..2dbffd55c33f 100644 --- a/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java +++ b/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java @@ -31,7 +31,6 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.security.AccessController; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -398,7 +397,7 @@ public static T invokeMethod(boolean setAccessible, Method method, /** * Set the accessibility flag on the {@link AccessibleObject} as described in {@link - * AccessibleObject#setAccessible(boolean)} within the context of a {link PrivilegedAction}. + * AccessibleObject#setAccessible(boolean)}. * * @param member the accessible object type * @param member the accessible object @@ -406,13 +405,13 @@ public static T invokeMethod(boolean setAccessible, Method method, * @return the accessible object after the accessible flag has been altered */ public static A setAccessible(A member) { - AccessController.doPrivileged(new SetAccessiblePrivilegedAction(member)); + member.setAccessible(true); return member; } /** * Set the accessibility flag on the {@link AccessibleObject} to false as described in {@link - * AccessibleObject#setAccessible(boolean)} within the context of a {link PrivilegedAction}. + * AccessibleObject#setAccessible(boolean)}. * * @param member the accessible object type * @param member the accessible object @@ -420,7 +419,7 @@ public static A setAccessible(A member) { * @return the accessible object after the accessible flag has been altered */ public static A unsetAccessible(A member) { - AccessController.doPrivileged(new UnSetAccessiblePrivilegedAction(member)); + member.setAccessible(false); return member; } @@ -705,12 +704,12 @@ public static boolean containsWildcards(Type[] types) { } /** - * Check the assignability of one type to another, taking into account the actual type arguements + * Check the assignability of one type to another, taking into account the actual type arguments * * @param rawType1 the raw type of the class to check - * @param actualTypeArguments1 the actual type arguements to check, or an empty array if not a parameterized type + * @param actualTypeArguments1 the actual type arguments to check, or an empty array if not a parameterized type * @param rawType2 the raw type of the class to check - * @param actualTypeArguments2 the actual type arguements to check, or an empty array if not a parameterized type + * @param actualTypeArguments2 the actual type arguments to check, or an empty array if not a parameterized type * * @return */ @@ -987,7 +986,9 @@ public static boolean isPrimitive(Type type) { * @throws ClassNotFoundException * @throws IllegalAccessException * @throws InstantiationException + * @deprecated for removal in Keycloak 27 */ + @Deprecated public static T newInstance(final Class fromClass) throws ClassNotFoundException, IllegalAccessException, InstantiationException { return newInstance(fromClass, fromClass.getName()); } @@ -1005,7 +1006,9 @@ public static T newInstance(final Class fromClass) throws ClassNotFoundEx * @throws ClassNotFoundException * @throws IllegalAccessException * @throws InstantiationException + * @deprecated for removal in Keycloak 27 */ + @Deprecated public static T newInstance(final Class type, final String fullQualifiedName) throws ClassNotFoundException, IllegalAccessException, InstantiationException { return (T) classForName(fullQualifiedName, type.getClassLoader()).newInstance(); } @@ -1048,4 +1051,4 @@ public static Class resolveListType(Field field, Object instance) throws Ille return Object.class; } -} \ No newline at end of file +} diff --git a/common/src/main/java/org/keycloak/common/util/reflections/SetAccessiblePrivilegedAction.java b/common/src/main/java/org/keycloak/common/util/reflections/SetAccessiblePrivilegedAction.java index f4c8d34fa08d..db7379cf6e6d 100644 --- a/common/src/main/java/org/keycloak/common/util/reflections/SetAccessiblePrivilegedAction.java +++ b/common/src/main/java/org/keycloak/common/util/reflections/SetAccessiblePrivilegedAction.java @@ -22,7 +22,9 @@ /** * A {@link java.security.PrivilegedAction} that calls {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} + * @deprecated for removal in Keycloak 27 */ +@Deprecated public class SetAccessiblePrivilegedAction implements PrivilegedAction { private final AccessibleObject member; diff --git a/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java b/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java index cfdb1302e43d..f697eb690a65 100644 --- a/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java +++ b/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java @@ -22,7 +22,9 @@ /** * A {@link PrivilegedAction} that calls {@link AccessibleObject#setAccessible(boolean)} + * @deprecated for removal in Keycloak 27 */ +@Deprecated public class UnSetAccessiblePrivilegedAction implements PrivilegedAction { private final AccessibleObject member; diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index 003d6a46f982..d68b4a2d7f0c 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -1,5 +1,7 @@ package org.keycloak.common; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.BeforeClass; @@ -8,27 +10,28 @@ import org.junit.rules.TemporaryFolder; import org.keycloak.common.profile.CommaSeparatedListProfileConfigResolver; import org.keycloak.common.profile.ProfileException; -import org.keycloak.common.profile.PropertiesFileProfileConfigResolver; import org.keycloak.common.profile.PropertiesProfileConfigResolver; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; +import java.security.Provider; +import java.security.Security; +import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; +import java.util.Map; import java.util.Properties; import java.util.Set; +import static org.junit.Assert.assertThrows; + public class ProfileTest { - private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.AUTHORIZATION; + private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.CLIENT_POLICIES; private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER; - private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ; + private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.TOKEN_EXCHANGE; private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES; - private static Profile.Feature DEPRECATED_FEATURE = null; + private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.LOGIN_V1; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -67,33 +70,14 @@ public void checkDefaults() { Assert.assertFalse(Profile.isFeatureEnabled(EXPERIMENTAL_FEATURE)); if (DEPRECATED_FEATURE != null) { Assert.assertFalse(Profile.isFeatureEnabled(DEPRECATED_FEATURE)); + } else { + MatcherAssert.assertThat(profile.getDeprecatedFeatures(), Matchers.empty()); } Assert.assertEquals(Profile.ProfileName.DEFAULT, profile.getName()); - Set disabledFeatures = new HashSet<>(Arrays.asList( - Profile.Feature.TRANSIENT_USERS, - Profile.Feature.DPOP, - Profile.Feature.FIPS, - Profile.Feature.ACCOUNT3, - Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, - Profile.Feature.DYNAMIC_SCOPES, - Profile.Feature.DOCKER, - Profile.Feature.RECOVERY_CODES, - Profile.Feature.SCRIPTS, - Profile.Feature.TOKEN_EXCHANGE, - Profile.Feature.MAP_STORAGE, - Profile.Feature.DECLARATIVE_USER_PROFILE, - Profile.Feature.CLIENT_SECRET_ROTATION, - Profile.Feature.UPDATE_EMAIL, - Profile.Feature.LINKEDIN_OAUTH - )); - - // KERBEROS can be disabled (i.e. FIPS mode disables SunJGSS provider) - if (Profile.Feature.KERBEROS.getType() == Profile.Feature.Type.DISABLED_BY_DEFAULT) { - disabledFeatures.add(Profile.Feature.KERBEROS); - } - assertEquals(profile.getDisabledFeatures(), disabledFeatures); - assertEquals(profile.getPreviewFeatures(), Profile.Feature.ACCOUNT3, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Profile.Feature.CLIENT_SECRET_ROTATION, Profile.Feature.UPDATE_EMAIL, Profile.Feature.DPOP); + + MatcherAssert.assertThat(profile.getDisabledFeatures(), Matchers.hasItem(DISABLED_BY_DEFAULT_FEATURE)); + MatcherAssert.assertThat(profile.getPreviewFeatures(), Matchers.hasItem(PREVIEW_FEATURE)); } @Test @@ -101,20 +85,18 @@ public void checkFailureIfDependencyDisabled() { Properties properties = new Properties(); properties.setProperty("keycloak.profile.feature.account_api", "disabled"); - try { - Profile.configure(new PropertiesProfileConfigResolver(properties)); - } catch (ProfileException e) { - Assert.assertEquals("Feature account2 depends on disabled feature account-api", e.getMessage()); - } + Assert.assertEquals("Feature account-v3 depends on disabled feature account-api", + assertThrows(ProfileException.class, + () -> Profile.configure(new PropertiesProfileConfigResolver(properties))).getMessage()); } @Test public void checkSuccessIfFeatureDisabledWithDisabledDependencies() { Properties properties = new Properties(); - properties.setProperty("keycloak.profile.feature.account2", "disabled"); + properties.setProperty("keycloak.profile.feature.account", "disabled"); properties.setProperty("keycloak.profile.feature.account_api", "disabled"); Profile.configure(new PropertiesProfileConfigResolver(properties)); - Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)); + Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_V3)); Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_API)); } @@ -123,11 +105,19 @@ public void checkErrorOnBadConfig() { Properties properties = new Properties(); properties.setProperty("keycloak.profile.feature.account_api", "invalid"); - try { - Profile.configure(new PropertiesProfileConfigResolver(properties)); - } catch (ProfileException e) { - Assert.assertEquals("Invalid config value 'invalid' for feature account-api", e.getMessage()); - } + Assert.assertEquals("Invalid config value 'invalid' for feature key keycloak.profile.feature.account_api", + assertThrows(ProfileException.class, + () -> Profile.configure(new PropertiesProfileConfigResolver(properties))).getMessage()); + } + + @Test + public void wrongProfileInProperties() { + Properties properties = new Properties(); + properties.setProperty("keycloak.profile", "experimental"); + + Assert.assertEquals("Invalid profile 'experimental' specified via 'keycloak.profile' property", + assertThrows(ProfileException.class, + () -> Profile.configure(new PropertiesProfileConfigResolver(properties))).getMessage()); } @Test @@ -148,35 +138,11 @@ public void enablePreviewWithCommaSeparatedList() { Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE)); } - @Test - public void enablePreviewWithPropertiesFile() throws IOException { - Properties properties = new Properties(); - properties.setProperty("profile", "preview"); - - Path tempDirectory = Files.createTempDirectory("jboss-config"); - System.setProperty("jboss.server.config.dir", tempDirectory.toString()); - - Path profileProperties = tempDirectory.resolve("profile.properties"); - - try(OutputStream out = Files.newOutputStream(profileProperties.toFile().toPath())) { - properties.store(out, ""); - } - - Profile.configure(new PropertiesFileProfileConfigResolver()); - - Assert.assertEquals(Profile.ProfileName.PREVIEW, Profile.getInstance().getName()); - Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE)); - - Files.delete(profileProperties); - Files.delete(tempDirectory); - System.getProperties().remove("jboss.server.config.dir"); - } - @Test public void configWithCommaSeparatedList() { String enabledFeatures = DISABLED_BY_DEFAULT_FEATURE.getKey() + "," + PREVIEW_FEATURE.getKey() + "," + EXPERIMENTAL_FEATURE.getKey(); if (DEPRECATED_FEATURE != null) { - enabledFeatures += "," + DEPRECATED_FEATURE.getKey(); + enabledFeatures += "," + DEPRECATED_FEATURE.getVersionedKey(); } String disabledFeatures = DEFAULT_FEATURE.getKey(); @@ -192,17 +158,21 @@ public void configWithCommaSeparatedList() { } @Test - public void configWithProperties() { - Properties properties = new Properties(); - properties.setProperty("keycloak.profile.feature." + DEFAULT_FEATURE.name().toLowerCase(), "disabled"); - properties.setProperty("keycloak.profile.feature." + DISABLED_BY_DEFAULT_FEATURE.name().toLowerCase(), "enabled"); - properties.setProperty("keycloak.profile.feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled"); - properties.setProperty("keycloak.profile.feature." + EXPERIMENTAL_FEATURE.name().toLowerCase(), "enabled"); + public void testKeys() { + Assert.assertEquals("account-v3", Profile.Feature.ACCOUNT_V3.getKey()); + Assert.assertEquals("account", Profile.Feature.ACCOUNT_V3.getUnversionedKey()); + Assert.assertEquals("account:v3", Profile.Feature.ACCOUNT_V3.getVersionedKey()); + } + + @Test + public void configWithCommaSeparatedVersionedList() { + String enabledFeatures = DISABLED_BY_DEFAULT_FEATURE.getVersionedKey() + "," + PREVIEW_FEATURE.getVersionedKey() + "," + EXPERIMENTAL_FEATURE.getVersionedKey(); if (DEPRECATED_FEATURE != null) { - properties.setProperty("keycloak.profile.feature." + DEPRECATED_FEATURE.name().toLowerCase(), "enabled"); + enabledFeatures += "," + DEPRECATED_FEATURE.getVersionedKey(); } - Profile.configure(new PropertiesProfileConfigResolver(properties)); + String disabledFeatures = DEFAULT_FEATURE.getUnversionedKey(); + Profile.configure(new CommaSeparatedListProfileConfigResolver(enabledFeatures, disabledFeatures)); Assert.assertFalse(Profile.isFeatureEnabled(DEFAULT_FEATURE)); Assert.assertTrue(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE)); @@ -214,26 +184,39 @@ public void configWithProperties() { } @Test - public void configWithPropertiesFile() throws IOException { - Properties properties = new Properties(); - properties.setProperty("feature." + DEFAULT_FEATURE.name().toLowerCase(), "disabled"); - properties.setProperty("feature." + DISABLED_BY_DEFAULT_FEATURE.name().toLowerCase(), "enabled"); - properties.setProperty("feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled"); - properties.setProperty("feature." + EXPERIMENTAL_FEATURE.name().toLowerCase(), "enabled"); - if (DEPRECATED_FEATURE != null) { - properties.setProperty("feature." + DEPRECATED_FEATURE.name().toLowerCase(), "enabled"); - } + public void configWithCommaSeparatedInvalidDisabled() { + String disabledFeatures = DEFAULT_FEATURE.getVersionedKey(); + CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(null, disabledFeatures); + assertThrows(ProfileException.class, () -> Profile.configure(resolver)); + } - Path tempDirectory = Files.createTempDirectory("jboss-config"); - System.setProperty("jboss.server.config.dir", tempDirectory.toString()); + @Test + public void commaSeparatedVersionedConflict() { + String enabledFeatures = DEFAULT_FEATURE.getVersionedKey(); + String disabledFeatures = DEFAULT_FEATURE.getVersionedKey(); + CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(enabledFeatures, disabledFeatures); + assertThrows(ProfileException.class, () -> Profile.configure(resolver)); + } - Path profileProperties = tempDirectory.resolve("profile.properties"); + @Test + public void commaSeparatedDuplicateEnabled() { + String enabledFeatures = DEFAULT_FEATURE.getVersionedKey() + "," + DEFAULT_FEATURE.getUnversionedKey(); + CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(enabledFeatures, null); + assertThrows(ProfileException.class, () -> Profile.configure(resolver)); + } - try(OutputStream out = Files.newOutputStream(profileProperties.toFile().toPath())) { - properties.store(out, ""); + @Test + public void configWithProperties() { + Properties properties = new Properties(); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DEFAULT_FEATURE), "disabled"); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DISABLED_BY_DEFAULT_FEATURE), "enabled"); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(PREVIEW_FEATURE), "enabled"); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(EXPERIMENTAL_FEATURE), "enabled"); + if (DEPRECATED_FEATURE != null) { + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DEPRECATED_FEATURE), "enabled"); } - Profile.configure(new PropertiesFileProfileConfigResolver()); + Profile.configure(new PropertiesProfileConfigResolver(properties)); Assert.assertFalse(Profile.isFeatureEnabled(DEFAULT_FEATURE)); Assert.assertTrue(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE)); @@ -242,16 +225,12 @@ public void configWithPropertiesFile() throws IOException { if (DEPRECATED_FEATURE != null) { Assert.assertTrue(Profile.isFeatureEnabled(DEPRECATED_FEATURE)); } - - Files.delete(profileProperties); - Files.delete(tempDirectory); - System.getProperties().remove("jboss.server.config.dir"); } @Test public void configWithMultipleResolvers() { Properties properties = new Properties(); - properties.setProperty("keycloak.profile.feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled"); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(PREVIEW_FEATURE), "enabled"); Profile.configure(new CommaSeparatedListProfileConfigResolver(DISABLED_BY_DEFAULT_FEATURE.getKey(), ""), new PropertiesProfileConfigResolver(properties)); @@ -259,15 +238,38 @@ public void configWithMultipleResolvers() { Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE)); } + @Test + public void kerberosConfigAvailability() { + // remove SunJGSS to remove kerberos availability + Map.Entry removed = removeSecurityProvider("SunJGSS"); + try { + Properties properties = new Properties(); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.KERBEROS), "enabled"); + ProfileException e = Assert.assertThrows(ProfileException.class, () -> Profile.configure(new PropertiesProfileConfigResolver(properties))); + Assert.assertEquals("Feature kerberos cannot be enabled as it is not available.", e.getMessage()); + + Profile.defaults(); + properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.KERBEROS), "disabled"); + Profile.configure(new PropertiesProfileConfigResolver(properties)); + Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.KERBEROS)); + + Profile.defaults(); + properties.clear(); + Profile.configure(new PropertiesProfileConfigResolver(properties)); + Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.KERBEROS)); + } finally { + if (removed != null) { + Security.insertProviderAt(removed.getValue(), removed.getKey()); + } + } + } + public static void assertEquals(Set actual, Collection expected) { - assertEquals(actual, expected.toArray(new Profile.Feature[0])); + MatcherAssert.assertThat(actual, Matchers.equalTo(expected)); } public static void assertEquals(Set actual, Profile.Feature... expected) { - Profile.Feature[] a = actual.toArray(new Profile.Feature[0]); - Arrays.sort(a, new FeatureComparator()); - Arrays.sort(expected, new FeatureComparator()); - Assert.assertArrayEquals(expected, a); + assertEquals(actual, new HashSet<>(Arrays.asList(expected))); } private static class FeatureComparator implements Comparator { @@ -277,4 +279,15 @@ public int compare(Profile.Feature o1, Profile.Feature o2) { } } + private Map.Entry removeSecurityProvider(String name) { + int position = 1; + for (Provider p : Security.getProviders()) { + if (name.equals(p.getName())) { + Security.removeProvider(name); + return new AbstractMap.SimpleEntry<>(position, p); + } + position++; + } + return null; + } } diff --git a/common/src/test/java/org/keycloak/common/crypto/CryptoIntegrationTest.java b/common/src/test/java/org/keycloak/common/crypto/CryptoIntegrationTest.java new file mode 100644 index 000000000000..170ae1ce3294 --- /dev/null +++ b/common/src/test/java/org/keycloak/common/crypto/CryptoIntegrationTest.java @@ -0,0 +1,36 @@ +package org.keycloak.common.crypto; + +import static org.junit.Assert.assertNull; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CryptoIntegrationTest { + private static CryptoProvider originalProvider; + + @BeforeClass + public static void keepOriginalProvider() { + CryptoIntegrationTest.originalProvider = getSelectedProvider(); + } + + // doing our best to avoid any side effects on other tests by restoring the initial state of CryptoIntegration + @AfterClass + public static void restoreOriginalProvider() { + CryptoIntegration.setProvider(originalProvider); + } + + @Test + public void canSetNullProvider() { + CryptoIntegration.setProvider(null); + assertNull(getSelectedProvider()); + } + + private static CryptoProvider getSelectedProvider() { + try { + return CryptoIntegration.getProvider(); + } catch (IllegalStateException e) { + return null; + } + } +} diff --git a/common/src/test/java/org/keycloak/common/enums/SslRequiredTest.java b/common/src/test/java/org/keycloak/common/enums/SslRequiredTest.java new file mode 100644 index 000000000000..346a4eaade5b --- /dev/null +++ b/common/src/test/java/org/keycloak/common/enums/SslRequiredTest.java @@ -0,0 +1,20 @@ +package org.keycloak.common.enums; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.junit.Test; + +public class SslRequiredTest { + + @Test + public void sslRequiredExternalTest() throws IOException { + assertFalse(SslRequired.EXTERNAL.isRequired("127.0.0.1")); + assertTrue(SslRequired.EXTERNAL.isRequired((String)null)); + assertTrue(SslRequired.EXTERNAL.isRequired("")); + assertTrue(SslRequired.EXTERNAL.isRequired("0.0.0.0")); + } + +} 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..e902fa7df781 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,22 @@ 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()); + } + + @Test + public void testEmptyHostname() { + Assert.assertEquals("app.immich:///oauth-callback", KeycloakUriBuilder.fromUri( + "app.immich:///oauth-callback").buildAsString()); + } } diff --git a/common/src/test/java/org/keycloak/common/util/KeystoreUtilTest.java b/common/src/test/java/org/keycloak/common/util/KeystoreUtilTest.java new file mode 100644 index 000000000000..2a35fd5e3d58 --- /dev/null +++ b/common/src/test/java/org/keycloak/common/util/KeystoreUtilTest.java @@ -0,0 +1,26 @@ +package org.keycloak.common.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +public class KeystoreUtilTest { + + @Test + public void testGetType() { + assertEquals("x", KeystoreUtil.getKeystoreType("x", "y", "z")); + assertEquals("z", KeystoreUtil.getKeystoreType(null, "y", "z")); + assertEquals(KeystoreUtil.KeystoreFormat.PKCS12.name(), KeystoreUtil.getKeystoreType(null, "y.pfx", "z")); + assertEquals(KeystoreUtil.KeystoreFormat.PKCS12.name(), KeystoreUtil.getKeystoreType(null, "y.pkcs12", "z")); + } + + @Test + public void testGetFormat() { + assertFalse(KeystoreUtil.getKeystoreFormat("some.file").isPresent()); + assertFalse(KeystoreUtil.getKeystoreFormat("somepfx").isPresent()); + assertEquals(KeystoreUtil.KeystoreFormat.PKCS12, KeystoreUtil.getKeystoreFormat("file.pfx").get()); + assertEquals(KeystoreUtil.KeystoreFormat.JKS, KeystoreUtil.getKeystoreFormat("file.jks").get()); + } + +} \ No newline at end of file diff --git a/common/src/test/java/org/keycloak/common/util/StringPropertyReplacerTest.java b/common/src/test/java/org/keycloak/common/util/StringPropertyReplacerTest.java index e019e1e37daf..f2854766e872 100644 --- a/common/src/test/java/org/keycloak/common/util/StringPropertyReplacerTest.java +++ b/common/src/test/java/org/keycloak/common/util/StringPropertyReplacerTest.java @@ -21,7 +21,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Map; -import java.util.Set; import org.junit.Assert; import org.junit.Test; @@ -34,29 +33,36 @@ public class StringPropertyReplacerTest { @Test public void testSystemProperties() throws NoSuchAlgorithmException { System.setProperty("prop1", "val1"); - Assert.assertEquals("foo-val1", StringPropertyReplacer.replaceProperties("foo-${prop1}")); + Assert.assertEquals("foo-val1", replaceProperties("foo-${prop1}")); - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop2:def}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop2:def}")); System.setProperty("prop2", "val2"); - Assert.assertEquals("foo-val2", StringPropertyReplacer.replaceProperties("foo-${prop2:def}")); + Assert.assertEquals("foo-val2", replaceProperties("foo-${prop2:def}")); // It looks for the property "prop3", then fallback to "prop4", then fallback to "prop5" and finally default value. // This syntax is supported by Quarkus (and underlying Microprofile) - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop5", "val5"); - Assert.assertEquals("foo-val5", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val5", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop4", "val4"); - Assert.assertEquals("foo-val4", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val4", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop3", "val3"); - Assert.assertEquals("foo-val3", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val3", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); // It looks for the property "prop6", then fallback to "prop7" then fallback to value "def" . // This syntax is not supported by Quarkus (microprofile), however Wildfly probably supports this - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop6,prop7:def}")); System.setProperty("prop7", "val7"); - Assert.assertEquals("foo-val7", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-val7", replaceProperties("foo-${prop6,prop7:def}")); System.setProperty("prop6", "val6"); - Assert.assertEquals("foo-val6", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-val6", replaceProperties("foo-${prop6,prop7:def}")); + } + + @Test + public void testStackOverflow() { + System.setProperty("prop", "${prop}"); + IllegalStateException ise = Assert.assertThrows(IllegalStateException.class, () -> replaceProperties("${prop}")); + Assert.assertEquals("Infinite recursion happening when replacing properties on '${prop}'", ise.getMessage()); } @Test @@ -66,9 +72,13 @@ public void testEnvironmentVariables() throws NoSuchAlgorithmException { for (String key : env.keySet()) { String value = env.get(key); if ( !(value == null || "".equals(value)) ) { - Assert.assertEquals("foo-" + value, StringPropertyReplacer.replaceProperties("foo-${env." + key + "}")); + Assert.assertEquals("foo-" + value, replaceProperties("foo-${env." + key + "}")); break; } } } + + private String replaceProperties(String key) { + return StringPropertyReplacer.replaceProperties(key, SystemEnvProperties.UNFILTERED::getProperty); + } } diff --git a/core/pom.xml b/core/pom.xml index 2a0840235033..8363871a6732 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,14 +32,11 @@ + 8 + 8 + 8 ${maven.build.timestamp} yyyy-MM-dd HH:mm - - org.keycloak.* - - - *;resolution:=optional - @@ -54,6 +51,14 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + org.jboss.logging jboss-logging @@ -69,6 +74,38 @@ test + + + + jdk-15 + + [15,) + + + + + maven-compiler-plugin + + + compile-java15 + compile + + compile + + + 15 + + ${project.basedir}/src/main/java15 + + + + + + + + + + @@ -77,14 +114,8 @@ - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - @@ -93,29 +124,6 @@ - - org.apache.felix - maven-bundle-plugin - true - - - bundle-manifest - process-classes - - manifest - - - - - - . - ${project.name} - ${project.groupId}.${project.artifactId} - ${keycloak.osgi.import} - ${keycloak.osgi.export} - - - diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java index 40ada8a3ee6c..ced1f3552c2a 100644 --- a/core/src/main/java/org/keycloak/AbstractOAuthClient.java +++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java @@ -19,9 +19,9 @@ import org.keycloak.common.enums.RelativeUrlsUsed; import org.keycloak.common.util.KeycloakUriBuilder; +import org.keycloak.common.util.SecretGenerator; import java.util.Map; -import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; /** @@ -43,7 +43,7 @@ public class AbstractOAuthClient { protected boolean isSecure; protected boolean publicClient; protected String getStateCode() { - return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); + return counter.getAndIncrement() + "/" + SecretGenerator.getInstance().generateSecureID(); } public String getClientId() { diff --git a/core/src/main/java/org/keycloak/Config.java b/core/src/main/java/org/keycloak/Config.java index 3eb1a760e2f6..538f571a85cb 100755 --- a/core/src/main/java/org/keycloak/Config.java +++ b/core/src/main/java/org/keycloak/Config.java @@ -17,8 +17,15 @@ package org.keycloak; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.StringPropertyReplacer.PropertyResolver; +import org.keycloak.common.util.SystemEnvProperties; + /** * @author Stian Thorgersen */ @@ -28,6 +35,14 @@ public class Config { public static void init(ConfigProvider configProvider) { Config.configProvider = configProvider; + StringPropertyReplacer.setDefaultPropertyResolver(new PropertyResolver() { + SystemEnvProperties systemVariables = new SystemEnvProperties(Config.getAllowedSystemVariables()); + + @Override + public String resolve(String property) { + return systemVariables.getProperty(property); + } + }); } public static String getAdminRealm() { @@ -43,14 +58,41 @@ public static String getProvider(String spi) { } } + public static String getDefaultProvider(String spi) { + String provider = configProvider.getDefaultProvider(spi); + if (provider == null || provider.trim().equals("")) { + return null; + } else { + return provider; + } + } + public static Scope scope(String... scope) { return configProvider.scope(scope); } + private static Set getAllowedSystemVariables() { + Scope adminScope = configProvider.scope("admin"); + + if (adminScope == null) { + return Collections.emptySet(); + } + + String[] allowedSystemVariables = adminScope.getArray("allowed-system-variables"); + + if (allowedSystemVariables == null) { + return Collections.emptySet(); + } + + return new HashSet<>(Arrays.asList(allowedSystemVariables)); + } + public static interface ConfigProvider { String getProvider(String spi); + String getDefaultProvider(String spi); + Scope scope(String... scope); } @@ -62,6 +104,11 @@ public String getProvider(String spi) { return System.getProperties().getProperty("keycloak." + spi + ".provider"); } + @Override + public String getDefaultProvider(String spi) { + return System.getProperties().getProperty("keycloak." + spi + ".provider.default"); + } + @Override public Scope scope(String... scope) { StringBuilder sb = new StringBuilder(); @@ -188,6 +235,13 @@ public static interface Scope { Scope scope(String... scope); + /** + * @deprecated since 26.3.0, to be removed + * + *
      Was introduced for testing purposes and was not fully / correctly implements + * across Scope implementations + */ + @Deprecated Set getPropertyNames(); } } diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 93c4a53703e8..389aab1aae95 100755 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -92,6 +92,9 @@ public interface OAuth2Constants { String SCOPE_ADDRESS = "address"; String SCOPE_PHONE = "phone"; + String ORGANIZATION = "organization"; + String ORGANIZATION_ID = "id"; + String UI_LOCALES_PARAM = "ui_locales"; String PROMPT = "prompt"; @@ -148,6 +151,10 @@ public interface OAuth2Constants { // https://www.rfc-editor.org/rfc/rfc9207.html String ISSUER = "iss"; + + String AUTHENTICATOR_METHOD_REFERENCE = "amr"; + + String CNF = "cnf"; } diff --git a/core/src/main/java/org/keycloak/TokenIdGenerator.java b/core/src/main/java/org/keycloak/TokenIdGenerator.java index fa085363a966..4320a7f69756 100755 --- a/core/src/main/java/org/keycloak/TokenIdGenerator.java +++ b/core/src/main/java/org/keycloak/TokenIdGenerator.java @@ -17,7 +17,8 @@ package org.keycloak; -import java.util.UUID; +import org.keycloak.common.util.SecretGenerator; + import java.util.concurrent.atomic.AtomicLong; /** @@ -28,6 +29,6 @@ public class TokenIdGenerator { private static final AtomicLong counter = new AtomicLong(); public static String generateId() { - return UUID.randomUUID().toString() + "-" + System.currentTimeMillis(); + return SecretGenerator.getInstance().generateSecureID() + "-" + System.currentTimeMillis(); } } diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index dc2321a9b2be..ac402d19ee08 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -32,6 +32,7 @@ import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; import java.security.PublicKey; import java.util.Arrays; import java.util.Iterator; @@ -115,11 +116,11 @@ public boolean test(JsonWebToken t) throws VerificationException { return true; } - }; + } public static class TokenTypeCheck implements Predicate { - private static final TokenTypeCheck INSTANCE_DEFAULT_TOKEN_TYPE = new TokenTypeCheck(Arrays.asList(TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_DPOP)); + private static final TokenTypeCheck INSTANCE_DEFAULT_TOKEN_TYPE = new TokenTypeCheck(Arrays.asList(TokenUtil.TOKEN_TYPE_BEARER)); private final List tokenTypes; @@ -134,7 +135,7 @@ public boolean test(JsonWebToken t) throws VerificationException { } throw new VerificationException("Token type is incorrect. Expected '" + tokenTypes.toString() + "' but was '" + t.getType() + "'"); } - }; + } public static class AudienceCheck implements Predicate { @@ -162,7 +163,7 @@ public boolean test(JsonWebToken t) throws VerificationException { throw new VerificationException("Expected audience not available in the token"); } - }; + } public static class IssuedForCheck implements Predicate { @@ -256,7 +257,6 @@ public static TokenVerifier createWithoutSignature(T public TokenVerifier withDefaultChecks() { return withChecks( RealmUrlCheck.NULL_INSTANCE, - SUBJECT_EXISTS_CHECK, TokenTypeCheck.INSTANCE_DEFAULT_TOKEN_TYPE, IS_ACTIVE ); @@ -318,7 +318,7 @@ public TokenVerifier publicKey(PublicKey publicKey) { /** * Sets the key for verification of HMAC-based signature. * @param secretKey - * @return + * @return */ public TokenVerifier secretKey(SecretKey secretKey) { this.secretKey = secretKey; @@ -433,7 +433,7 @@ public JWSHeader getHeader() throws VerificationException { public void verifySignature() throws VerificationException { if (this.verifier != null) { try { - if (!verifier.verify(jws.getEncodedSignatureInput().getBytes("UTF-8"), jws.getSignature())) { + if (!verifier.verify(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jws.getSignature())) { throw new TokenSignatureInvalidException(token, "Invalid token signature"); } } catch (Exception e) { diff --git a/core/src/main/java/org/keycloak/crypto/Algorithm.java b/core/src/main/java/org/keycloak/crypto/Algorithm.java index dbba7942d97b..ab6efb6e2a05 100755 --- a/core/src/main/java/org/keycloak/crypto/Algorithm.java +++ b/core/src/main/java/org/keycloak/crypto/Algorithm.java @@ -27,13 +27,21 @@ public interface Algorithm { String RS256 = "RS256"; String RS384 = "RS384"; String RS512 = "RS512"; - String ES256 = "ES256"; - String ES384 = "ES384"; - String ES512 = "ES512"; String PS256 = "PS256"; String PS384 = "PS384"; String PS512 = "PS512"; + /* ECDSA signing algorithms */ + String ES256 = "ES256"; + String ES384 = "ES384"; + String ES512 = "ES512"; + + /* EdDSA signing algorithms */ + String EdDSA = "EdDSA"; + /* EdDSA Curve */ + String Ed25519 = "Ed25519"; + String Ed448 = "Ed448"; + /* RSA Encryption Algorithms */ String RSA1_5 = CryptoConstants.RSA1_5; String RSA_OAEP = CryptoConstants.RSA_OAEP; @@ -41,4 +49,9 @@ public interface Algorithm { /* AES */ String AES = "AES"; + + String ECDH_ES = CryptoConstants.ECDH_ES; + String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; } diff --git a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java index 30455c04be2a..649f91df9ba5 100644 --- a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java +++ b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java @@ -39,13 +39,13 @@ public String getAlgorithm() { @Override public String getHashAlgorithm() { - return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault()); + return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault(), key.getCurve()); } @Override public byte[] sign(byte[] data) throws SignatureException { try { - Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault())); + Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve())); signature.initSign((PrivateKey) key.getPrivateKey()); signature.update(data); return signature.sign(); diff --git a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java index c77eae65cb8e..202d19d9914b 100644 --- a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java +++ b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java @@ -17,7 +17,10 @@ package org.keycloak.crypto; import org.keycloak.common.VerificationException; +import org.keycloak.common.crypto.CryptoIntegration; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.PublicKey; import java.security.Signature; @@ -42,7 +45,7 @@ public String getAlgorithm() { @Override public boolean verify(byte[] data, byte[] signature) throws VerificationException { try { - Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault())); + Signature verifier = getSignature(); verifier.initVerify((PublicKey) key.getPublicKey()); verifier.update(data); return verifier.verify(signature); @@ -51,4 +54,13 @@ public boolean verify(byte[] data, byte[] signature) throws VerificationExceptio } } + private Signature getSignature() + throws NoSuchAlgorithmException, NoSuchProviderException { + try { + return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve())); + } catch (NoSuchAlgorithmException e) { + // Retry using the current crypto provider's override implementation + return CryptoIntegration.getProvider().getSignature(key.getAlgorithmOrDefault()); + } + } } diff --git a/core/src/main/java/org/keycloak/crypto/ECCurve.java b/core/src/main/java/org/keycloak/crypto/ECCurve.java new file mode 100644 index 000000000000..c17819f8be14 --- /dev/null +++ b/core/src/main/java/org/keycloak/crypto/ECCurve.java @@ -0,0 +1,43 @@ +/* + * 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.crypto; + +public enum ECCurve { + P256, + P384, + P521; + + /** + * Convert standard EC curve names (and aliases) into this enum. + */ + public static ECCurve fromStdCrv(String crv) { + switch (crv) { + case "P-256": + case "secp256r1": + return P256; + case "P-384": + case "secp384r1": + return P384; + case "P-521": + case "secp521r1": + return P521; + default: + throw new IllegalArgumentException("Unexpected EC curve: " + crv); + } + } +} diff --git a/core/src/main/java/org/keycloak/crypto/ECDSASignatureVerifierContext.java b/core/src/main/java/org/keycloak/crypto/ECDSASignatureVerifierContext.java new file mode 100644 index 000000000000..177aaeb2d9a8 --- /dev/null +++ b/core/src/main/java/org/keycloak/crypto/ECDSASignatureVerifierContext.java @@ -0,0 +1,36 @@ +/* + * 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.crypto; + +import org.keycloak.common.VerificationException; + +public class ECDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext{ + public ECDSASignatureVerifierContext(KeyWrapper key) { + super(key); + } + + @Override + public boolean verify(byte[] data, byte[] signature) throws VerificationException { + try { + int expectedSize = ECDSAAlgorithm.getSignatureLength(getAlgorithm()); + byte[] derSignature = ECDSAAlgorithm.concatenatedRSToASN1DER(signature, expectedSize); + return super.verify(data, derSignature); + } catch (Exception e) { + throw new VerificationException("Verification failed", e); + } + } +} diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index d194727945c8..682ac6153edd 100644 --- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -30,13 +30,20 @@ public class JavaAlgorithm { public static final String PS256 = "SHA256withRSAandMGF1"; public static final String PS384 = "SHA384withRSAandMGF1"; public static final String PS512 = "SHA512withRSAandMGF1"; + public static final String Ed25519 = "Ed25519"; + public static final String Ed448 = "Ed448"; public static final String AES = "AES"; public static final String SHA256 = "SHA-256"; public static final String SHA384 = "SHA-384"; public static final String SHA512 = "SHA-512"; + public static final String SHAKE256 = "SHAKE256"; public static String getJavaAlgorithm(String algorithm) { + return getJavaAlgorithm(algorithm, null); + } + + public static String getJavaAlgorithm(String algorithm, String curve) { switch (algorithm) { case Algorithm.RS256: return RS256; @@ -62,6 +69,11 @@ public static String getJavaAlgorithm(String algorithm) { return PS384; case Algorithm.PS512: return PS512; + case Algorithm.EdDSA: + if (curve != null) { + return curve; + } + return Ed25519; case Algorithm.AES: return AES; default: @@ -69,8 +81,11 @@ public static String getJavaAlgorithm(String algorithm) { } } - public static String getJavaAlgorithmForHash(String algorithm) { + return getJavaAlgorithmForHash(algorithm, null); + } + + public static String getJavaAlgorithmForHash(String algorithm, String curve) { switch (algorithm) { case Algorithm.RS256: return SHA256; @@ -96,6 +111,18 @@ public static String getJavaAlgorithmForHash(String algorithm) { return SHA384; case Algorithm.PS512: return SHA512; + case Algorithm.EdDSA: + if (curve != null) { + switch (curve) { + case Algorithm.Ed25519: + return SHA512; + case Algorithm.Ed448: + return SHAKE256; + default: + throw new IllegalArgumentException("Unknown curve for EdDSA " + curve); + } + } + return SHA512; case Algorithm.AES: return AES; default: @@ -111,6 +138,10 @@ public static boolean isECJavaAlgorithm(String algorithm) { return getJavaAlgorithm(algorithm).contains("ECDSA"); } + public static boolean isEddsaJavaAlgorithm(String algorithm) { + return getJavaAlgorithm(algorithm).contains("Ed"); + } + public static boolean isHMACJavaAlgorithm(String algorithm) { return getJavaAlgorithm(algorithm).contains("HMAC"); } diff --git a/core/src/main/java/org/keycloak/crypto/KeyType.java b/core/src/main/java/org/keycloak/crypto/KeyType.java index 2fcc999065c7..0de5e376b264 100644 --- a/core/src/main/java/org/keycloak/crypto/KeyType.java +++ b/core/src/main/java/org/keycloak/crypto/KeyType.java @@ -21,5 +21,6 @@ public interface KeyType { String EC = "EC"; String RSA = "RSA"; String OCT = "OCT"; + String OKP = "OKP"; } diff --git a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java index f99b3d2946e9..fb15c3c5904b 100644 --- a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java +++ b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java @@ -49,6 +49,7 @@ public class KeyWrapper { private X509Certificate certificate; private List certificateChain; private boolean isDefaultClientCertificate; + private String curve; public String getProviderId() { return providerId; @@ -176,6 +177,14 @@ public void setIsDefaultClientCertificate(boolean isDefaultClientCertificate) { this.isDefaultClientCertificate = isDefaultClientCertificate; } + public void setCurve(String curve) { + this.curve = curve; + } + + public String getCurve() { + return curve; + } + public KeyWrapper cloneKey() { KeyWrapper key = new KeyWrapper(); key.providerId = this.providerId; @@ -189,6 +198,7 @@ public KeyWrapper cloneKey() { key.publicKey = this.publicKey; key.privateKey = this.privateKey; key.certificate = this.certificate; + key.curve = this.curve; if (this.certificateChain != null) { key.certificateChain = new ArrayList<>(this.certificateChain); } diff --git a/core/src/main/java/org/keycloak/crypto/PublicKeysWrapper.java b/core/src/main/java/org/keycloak/crypto/PublicKeysWrapper.java index 71966071428d..283c00ab6d53 100644 --- a/core/src/main/java/org/keycloak/crypto/PublicKeysWrapper.java +++ b/core/src/main/java/org/keycloak/crypto/PublicKeysWrapper.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -72,4 +73,13 @@ public KeyWrapper getKeyByKidAndAlg(String kid, String alg) { return potentialMatches.findFirst().orElse(null); } + + /** + * Returns the first key that matches the predicate. + * @param predicate The predicate + * @return The first key that matches the predicate or null + */ + public KeyWrapper getKeyByPredicate(Predicate predicate) { + return keys.stream().filter(predicate).findFirst().orElse(null); + } } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java index e3f7eba873ac..99d3d109fe35 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -20,6 +20,7 @@ import org.keycloak.common.util.Base64Url; import org.keycloak.jose.JOSE; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.util.JsonSerialization; @@ -143,8 +144,10 @@ public String encodeJwe(JWEAlgorithmProvider algorithmProvider, JWEEncryptionPro keyStorage.setEncryptionProvider(encryptionProvider); keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, true); // Will generate CEK if it's not already present - byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey()); + JWEHeaderBuilder headerBuilder = header.toBuilder(); + byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey(), headerBuilder); base64Cek = Base64Url.encode(encodedCEK); + header = headerBuilder.build(); encryptionProvider.encodeJwe(this); @@ -191,7 +194,7 @@ private JWE getProcessedJWE(JWEAlgorithmProvider algorithmProvider, JWEEncryptio keyStorage.setEncryptionProvider(encryptionProvider); - byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey()); + byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey(), this.header, encryptionProvider); keyStorage.setCEKBytes(decodedCek); encryptionProvider.verifyAndDecodeJwe(this); diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java index ad33e73f7bf1..89406e44290b 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java @@ -29,6 +29,10 @@ public class JWEConstants { public static final String RSA1_5 = CryptoConstants.RSA1_5; public static final String RSA_OAEP = CryptoConstants.RSA_OAEP; public static final String RSA_OAEP_256 = CryptoConstants.RSA_OAEP_256; + public static final String ECDH_ES = CryptoConstants.ECDH_ES; + public static final String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + public static final String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + public static final String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; public static final String A128CBC_HS256 = "A128CBC-HS256"; public static final String A192CBC_HS384 = "A192CBC-HS384"; diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java index 55cb782080fc..c3b84011b869 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -18,6 +18,7 @@ package org.keycloak.jose.jwe; import java.io.IOException; +import java.io.UncheckedIOException; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -25,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwk.ECPublicJWK; /** * @author Marek Posolda @@ -41,7 +43,6 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("zip") private String compressionAlgorithm; - @JsonProperty("typ") private String type; @@ -51,6 +52,15 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("kid") private String keyId; + @JsonProperty("epk") + private ECPublicJWK ephemeralPublicKey; + + @JsonProperty("apu") + private String agreementPartyUInfo; + + @JsonProperty("apv") + private String agreementPartyVInfo; + public JWEHeader() { } @@ -75,6 +85,19 @@ public JWEHeader(String algorithm, String encryptionAlgorithm, String compressio this.contentType = contentType; } + public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm, String keyId, String contentType, + String type, ECPublicJWK ephemeralPublicKey, String agreementPartyUInfo, String agreementPartyVInfo) { + this.algorithm = algorithm; + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + this.keyId = keyId; + this.type = type; + this.contentType = contentType; + this.ephemeralPublicKey = ephemeralPublicKey; + this.agreementPartyUInfo = agreementPartyUInfo; + this.agreementPartyVInfo = agreementPartyVInfo; + } + public String getAlgorithm() { return algorithm; } @@ -105,21 +128,102 @@ public String getKeyId() { return keyId; } + public ECPublicJWK getEphemeralPublicKey() { + return ephemeralPublicKey; + } + + public String getAgreementPartyUInfo() { + return agreementPartyUInfo; + } + + public String getAgreementPartyVInfo() { + return agreementPartyVInfo; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } public String toString() { try { return mapper.writeValueAsString(this); } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } + } + public JWEHeaderBuilder toBuilder() { + return builder().algorithm(algorithm).encryptionAlgorithm(encryptionAlgorithm) + .compressionAlgorithm(compressionAlgorithm).type(type).contentType(contentType) + .keyId(keyId).ephemeralPublicKey(ephemeralPublicKey).agreementPartyUInfo(agreementPartyUInfo) + .agreementPartyVInfo(agreementPartyVInfo); + } + public static JWEHeaderBuilder builder() { + return new JWEHeaderBuilder(); } + public static class JWEHeaderBuilder { + private String algorithm = null; + private String encryptionAlgorithm = null; + private String compressionAlgorithm = null; + private String type = null; + private String contentType = null; + private String keyId = null; + private ECPublicJWK ephemeralPublicKey = null; + private String agreementPartyUInfo = null; + private String agreementPartyVInfo = null; + + public JWEHeaderBuilder algorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public JWEHeaderBuilder encryptionAlgorithm(String encryptionAlgorithm) { + this.encryptionAlgorithm = encryptionAlgorithm; + return this; + } + + public JWEHeaderBuilder compressionAlgorithm(String compressionAlgorithm) { + this.compressionAlgorithm = compressionAlgorithm; + return this; + } + + public JWEHeaderBuilder type(String type) { + this.type = type; + return this; + } + + public JWEHeaderBuilder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public JWEHeaderBuilder keyId(String keyId) { + this.keyId = keyId; + return this; + } + + public JWEHeaderBuilder ephemeralPublicKey(ECPublicJWK ephemeralPublicKey) { + this.ephemeralPublicKey = ephemeralPublicKey; + return this; + } + + public JWEHeaderBuilder agreementPartyUInfo(String agreementPartyUInfo) { + this.agreementPartyUInfo = agreementPartyUInfo; + return this; + } + + public JWEHeaderBuilder agreementPartyVInfo(String agreementPartyVInfo) { + this.agreementPartyVInfo = agreementPartyVInfo; + return this; + } + + public JWEHeader build() { + return new JWEHeader(algorithm, encryptionAlgorithm, compressionAlgorithm, keyId, contentType, + type, ephemeralPublicKey, agreementPartyUInfo, agreementPartyVInfo); + } + } } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java index dc9aa1f80ea2..2785f33910f7 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -38,6 +38,8 @@ class JWERegistry { private static final Map ENC_PROVIDERS = new HashMap<>(); static { + ENC_PROVIDERS.put(JWEConstants.A128GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A128GCM)); + ENC_PROVIDERS.put(JWEConstants.A192GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A192GCM)); ENC_PROVIDERS.put(JWEConstants.A256GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A256GCM)); ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider()); ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider()); diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java index ef3ca0242ab0..ed949bbb02a9 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -19,6 +19,8 @@ import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -28,12 +30,12 @@ public class DirectAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) { return new byte[0]; } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) { return new byte[0]; } } diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java index 80347f8e6864..4ad5653764b2 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -19,7 +19,9 @@ import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; /** @@ -27,8 +29,8 @@ */ public interface JWEAlgorithmProvider { - byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception; + byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception; - byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception; + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception; } diff --git a/core/src/main/java/org/keycloak/jose/jwk/ECPublicJWK.java b/core/src/main/java/org/keycloak/jose/jwk/ECPublicJWK.java index d97c7e8cfa5f..cc0d90aeefaf 100644 --- a/core/src/main/java/org/keycloak/jose/jwk/ECPublicJWK.java +++ b/core/src/main/java/org/keycloak/jose/jwk/ECPublicJWK.java @@ -18,6 +18,9 @@ package org.keycloak.jose.jwk; import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.common.util.PemUtils; + +import java.security.NoSuchAlgorithmException; /** * @author Stian Thorgersen diff --git a/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java b/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java new file mode 100644 index 000000000000..52cdc7209823 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java @@ -0,0 +1,35 @@ +/* + * 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.jose.jwk; + +import java.security.Key; +import java.security.PublicKey; +import org.keycloak.crypto.KeyUse; + +/** + *

      Interface for the EdECUtils that will be implemented only for JDK 15+.

      + * + * @author rmartinc + */ +interface EdECUtils { + + boolean isEdECSupported(); + + JWK okp(String kid, String algorithm, Key key, KeyUse keyUse); + + PublicKey createOKPPublicKey(JWK jwk); +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java b/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java new file mode 100644 index 000000000000..35121e4f800d --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java @@ -0,0 +1,44 @@ +/* + * 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.jose.jwk; + +import java.security.Key; +import java.security.PublicKey; +import org.keycloak.crypto.KeyUse; + +/** + *

      Unsupported implementation for old jdk versions.

      + * + * @author rmartinc + */ +class EdECUtilsUnsupportedImpl implements EdECUtils { + + @Override + public boolean isEdECSupported() { + return false; + } + + @Override + public JWK okp(String kid, String algorithm, Key key, KeyUse keyUse) { + throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + } + + @Override + public PublicKey createOKPPublicKey(JWK jwk) { + throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWK.java b/core/src/main/java/org/keycloak/jose/jwk/JWK.java index e75963aa9f85..eee4401fc49e 100755 --- a/core/src/main/java/org/keycloak/jose/jwk/JWK.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWK.java @@ -20,7 +20,9 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.common.util.PemUtils; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; @@ -37,6 +39,12 @@ public class JWK { public static final String PUBLIC_KEY_USE = "use"; + public static final String X5C = "x5c"; + + public static final String SHA1_509_THUMBPRINT = "x5t"; + + public static final String SHA256_509_THUMBPRINT = "x5t#S256"; + public enum Use { SIG("sig"), ENCRYPTION("enc"); @@ -64,6 +72,15 @@ public String asString() { @JsonProperty(PUBLIC_KEY_USE) private String publicKeyUse; + @JsonProperty(X5C) + private String[] x509CertificateChain; + + @JsonProperty(SHA1_509_THUMBPRINT) + private String sha1x509Thumbprint; + + @JsonProperty(SHA256_509_THUMBPRINT) + private String sha256x509Thumbprint; + protected Map otherClaims = new HashMap(); @@ -99,6 +116,44 @@ public void setPublicKeyUse(String publicKeyUse) { this.publicKeyUse = publicKeyUse; } + public String[] getX509CertificateChain() { + return x509CertificateChain; + } + + public void setX509CertificateChain(String[] x509CertificateChain) { + this.x509CertificateChain = x509CertificateChain; + } + + public String getSha1x509Thumbprint() { + if (sha1x509Thumbprint == null && x509CertificateChain != null && x509CertificateChain.length > 0) { + try { + sha1x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + return sha1x509Thumbprint; + } + + public void setSha1x509Thumbprint(String sha1x509Thumbprint) { + this.sha1x509Thumbprint = sha1x509Thumbprint; + } + + public String getSha256x509Thumbprint() { + if (sha256x509Thumbprint == null && x509CertificateChain != null && x509CertificateChain.length > 0) { + try { + sha256x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + return sha256x509Thumbprint; + } + + public void setSha256x509Thumbprint(String sha256x509Thumbprint) { + this.sha256x509Thumbprint = sha256x509Thumbprint; + } + @JsonAnyGetter public Map getOtherClaims() { return otherClaims; diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java index d799c73809a0..daeca3df0879 100644 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java @@ -17,8 +17,16 @@ package org.keycloak.jose.jwk; +import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes; + +import java.security.Key; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; import java.util.Collections; import java.util.List; + import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.PemUtils; @@ -26,24 +34,32 @@ import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; -import java.security.Key; -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; - -import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes; - /** * @author Stian Thorgersen */ public class JWKBuilder { + // internal util class only loaded for jdk versions with EdEC support + protected static final EdECUtils EdEC_UTILS; + + static { + EdECUtils tmp; + try { + // check if the impl class for EdEC can be loaded in the runtime + tmp = (EdECUtils) Class.forName("org.keycloak.jose.jwk.EdECUtilsImpl") + .getDeclaredConstructor().newInstance(); + } catch(Throwable e) { + // not supported implementation + tmp = new EdECUtilsUnsupportedImpl(); + } + EdEC_UTILS = tmp; + } + public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG; - private String kid; + protected String kid; - private String algorithm; + protected String algorithm; private JWKBuilder() { } @@ -63,14 +79,14 @@ public JWKBuilder algorithm(String algorithm) { } public JWK rs256(PublicKey key) { - algorithm(Algorithm.RS256); + this.algorithm = Algorithm.RS256; return rsa(key); } public JWK rsa(Key key) { return rsa(key, null, KeyUse.SIG); } - + public JWK rsa(Key key, X509Certificate certificate) { return rsa(key, Collections.singletonList(certificate), KeyUse.SIG); } @@ -116,6 +132,10 @@ public JWK ec(Key key) { } public JWK ec(Key key, KeyUse keyUse) { + return this.ec(key, null, keyUse); + } + + public JWK ec(Key key, List certificates, KeyUse keyUse) { ECPublicKey ecKey = (ECPublicKey) key; ECPublicJWK k = new ECPublicJWK(); @@ -130,7 +150,23 @@ public JWK ec(Key key, KeyUse keyUse) { k.setCrv("P-" + fieldSize); k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); - + + if (certificates != null && !certificates.isEmpty()) { + String[] certificateChain = new String[certificates.size()]; + for (int i = 0; i < certificates.size(); i++) { + certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i)); + } + k.setX509CertificateChain(certificateChain); + } + return k; } + + public JWK okp(Key key) { + return okp(key, DEFAULT_PUBLIC_KEY_USE); + } + + public JWK okp(Key key, KeyUse keyUse) { + return EdEC_UTILS.okp(kid, algorithm, key, keyUse); + } } diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java old mode 100755 new mode 100644 index 67bb3dc34a72..e1c9bd651279 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java @@ -17,12 +17,6 @@ package org.keycloak.jose.jwk; - -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.util.Base64Url; -import org.keycloak.crypto.KeyType; -import org.keycloak.util.JsonSerialization; - import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; @@ -31,24 +25,30 @@ import java.security.spec.ECPublicKeySpec; import java.security.spec.RSAPublicKeySpec; +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.KeyType; +import org.keycloak.util.JsonSerialization; + /** * @author Stian Thorgersen */ public class JWKParser { - private JWK jwk; + protected JWK jwk; private JWKParser() { } - public JWKParser(JWK jwk) { - this.jwk = jwk; - } - public static JWKParser create() { return new JWKParser(); } + public JWKParser(JWK jwk) { + this.jwk = jwk; + } + public static JWKParser create(JWK jwk) { return new JWKParser(jwk); } @@ -67,27 +67,32 @@ public JWK getJwk() { } public PublicKey toPublicKey() { + if (jwk == null) { + throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set"); + } String keyType = jwk.getKeyType(); - if (keyType.equals(KeyType.RSA)) { - return createRSAPublicKey(); - } else if (keyType.equals(KeyType.EC)) { - return createECPublicKey(); + // subtypes may store properties differently while representing the same JWK, serializing it to nodes + // makes sure there is no difference when creating the keys + JsonNode normalizedJwkNode = JsonSerialization.writeValueAsNode(jwk); + if (KeyType.RSA.equals(keyType)) { + return createRSAPublicKey(normalizedJwkNode); + } else if (KeyType.EC.equals(keyType)) { + return createECPublicKey(normalizedJwkNode); + } else if (KeyType.OKP.equals(keyType)) { + return JWKBuilder.EdEC_UTILS.createOKPPublicKey(jwk); } else { throw new RuntimeException("Unsupported keyType " + keyType); } } - private PublicKey createECPublicKey() { - /* Check if jwk.getOtherClaims return an empty map */ - if (jwk.getOtherClaims().size() == 0) { - throw new RuntimeException("JWK Otherclaims map is empty."); - } + private static PublicKey createECPublicKey(JsonNode jwk) { + /* Try retrieving the necessary fields */ - String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV); - String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X); - String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y); + String crv = jwk.path(ECPublicJWK.CRV).asText(null); + String xStr = jwk.get(ECPublicJWK.X).asText(null); + String yStr = jwk.get(ECPublicJWK.Y).asText(null); /* Check if the retrieving of necessary fields success */ if (crv == null || xStr == null || yStr == null) { @@ -113,7 +118,7 @@ private PublicKey createECPublicKey() { } try { - + ECPoint point = new ECPoint(x, y); ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name); ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); @@ -125,9 +130,9 @@ private PublicKey createECPublicKey() { } } - private PublicKey createRSAPublicKey() { - BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString())); - BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString())); + private static PublicKey createRSAPublicKey(JsonNode jwk) { + BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.path(RSAPublicJWK.MODULUS).asText(null))); + BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.path(RSAPublicJWK.PUBLIC_EXPONENT).asText(null))); try { KeyFactory kf = KeyFactory.getInstance("RSA"); @@ -138,7 +143,7 @@ private PublicKey createRSAPublicKey() { } public boolean isKeyTypeSupported(String keyType) { - return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType)); + return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) + || (JWKBuilder.EdEC_UTILS.isEdECSupported() && OKPPublicJWK.OKP.equals(keyType))); } - } diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKUtil.java b/core/src/main/java/org/keycloak/jose/jwk/JWKUtil.java index 2b0eaf008186..09ac4b394d85 100644 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKUtil.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKUtil.java @@ -23,7 +23,7 @@ public class JWKUtil { /** - * Coverts {@code BigInteger} to 64-byte array removing the sign byte if + * Converts {@code BigInteger} to 64-byte array removing the sign byte if * necessary. * * @param bigInt {@code BigInteger} to be converted @@ -34,7 +34,7 @@ public static byte[] toIntegerBytes(final BigInteger bigInt) { } /** - * Coverts {@code BigInteger} to 64-byte array but maintaining the length + * Converts {@code BigInteger} to 64-byte array but maintaining the length * to bitlen as specified in rfc7518 for certain fields (X and Y parameter * for EC keys). * diff --git a/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java b/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java new file mode 100644 index 000000000000..7050d6281294 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java @@ -0,0 +1,56 @@ +/* + * 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.jose.jwk; + +import org.keycloak.crypto.KeyType; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Takashi Norimatsu + */ +public class OKPPublicJWK extends JWK { + + public static final String OKP = KeyType.OKP; + + public static final String CRV = "crv"; + public static final String X = "x"; + + @JsonProperty(CRV) + private String crv; + + @JsonProperty(X) + private String x; + + public String getCrv() { + return crv; + } + + public void setCrv(String crv) { + this.crv = crv; + } + + public String getX() { + return x; + } + + public void setX(String x) { + this.x = x; + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java index 979d88137a04..e223fb76424c 100755 --- a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java +++ b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java @@ -39,7 +39,10 @@ public enum Algorithm { PS512(AlgorithmType.RSA, null), ES256(AlgorithmType.ECDSA, null), ES384(AlgorithmType.ECDSA, null), - ES512(AlgorithmType.ECDSA, null) + ES512(AlgorithmType.ECDSA, null), + EdDSA(AlgorithmType.EDDSA, null), + Ed25519(AlgorithmType.EDDSA, null), + Ed448(AlgorithmType.EDDSA, null) ; private AlgorithmType type; diff --git a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java index 236f84c1df88..1d3a800c520d 100755 --- a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java +++ b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java @@ -26,6 +26,7 @@ public enum AlgorithmType { RSA, HMAC, AES, - ECDSA + ECDSA, + EDDSA } diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java index 0a92461d0b18..bf13bc761623 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java @@ -17,8 +17,11 @@ package org.keycloak.jose.jws; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64Url; import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.util.JsonSerialization; @@ -27,16 +30,22 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; /** * @author Bill Burke * @version $Revision: 1 $ */ public class JWSBuilder { - String type; - String kid; - String contentType; - byte[] contentBytes; + protected String type; + protected String kid; + protected String x5t; + protected JWK jwk; + protected List x5c; + protected String contentType; + protected byte[] contentBytes; public JWSBuilder type(String type) { this.type = type; @@ -48,6 +57,21 @@ public JWSBuilder kid(String kid) { return this; } + public JWSBuilder x5t(String x5t) { + this.x5t = x5t; + return this; + } + + public JWSBuilder jwk(JWK jwk) { + this.jwk = jwk; + return this; + } + + public JWSBuilder x5c(List x5c) { + this.x5c = x5c; + return this; + } + public JWSBuilder contentType(String type) { this.contentType = type; return this; @@ -70,10 +94,39 @@ public EncodingBuilder jsonContent(Object object) { protected String encodeHeader(String sigAlgName) { StringBuilder builder = new StringBuilder("{"); - builder.append("\"alg\":\"").append(sigAlgName).append("\""); + + if (org.keycloak.crypto.Algorithm.Ed25519.equals(sigAlgName) || org.keycloak.crypto.Algorithm.Ed448.equals(sigAlgName)) { + builder.append("\"alg\":\"").append(org.keycloak.crypto.Algorithm.EdDSA).append("\""); + builder.append(",\"crv\":\"").append(sigAlgName).append("\""); + } else { + builder.append("\"alg\":\"").append(sigAlgName).append("\""); + } if (type != null) builder.append(",\"typ\" : \"").append(type).append("\""); if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\""); + if (x5t != null) builder.append(",\"x5t\" : \"").append(x5t).append("\""); + if (x5c != null && !x5c.isEmpty()) { + builder.append(",\"x5c\" : ["); + for (int i = 0; i < x5c.size(); i++) { + X509Certificate certificate = x5c.get(i); + if (i > 0) { + builder.append(","); + } + try { + builder.append("\"").append(Base64.encodeBytes(certificate.getEncoded())).append("\""); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + } + builder.append("]"); + } + if (jwk != null) { + try { + builder.append(",\"jwk\" : ").append(JsonSerialization.mapper.writeValueAsString(jwk)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } if (contentType != null) builder.append(",\"cty\":\"").append(contentType).append("\""); builder.append("}"); return Base64Url.encode(builder.toString().getBytes(StandardCharsets.UTF_8)); diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java index 30a32a5d4dc4..16a1aa2e634e 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java @@ -22,10 +22,12 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; + import org.keycloak.jose.JOSEHeader; import org.keycloak.jose.jwk.JWK; import java.io.IOException; +import java.util.List; /** * @author Bill Burke @@ -48,6 +50,9 @@ public class JWSHeader implements JOSEHeader { @JsonProperty("jwk") private JWK key; + @JsonProperty("x5c") + private List x5c; + public JWSHeader() { } @@ -90,6 +95,10 @@ public JWK getKey() { return key; } + public List getX5c() { + return x5c; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java index 47084ae21b92..55f2c1aeee09 100644 --- a/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java +++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java @@ -21,6 +21,7 @@ import org.keycloak.crypto.HashException; import org.keycloak.crypto.JavaAlgorithm; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Arrays; @@ -34,15 +35,23 @@ public class HashUtils { // - "at_hash" and "c_hash" in OIDC specification (full = false) // - "ath" in DPoP specification (full = true) public static String accessTokenHash(String jwtAlgorithmName, String input, boolean full) { + return accessTokenHash(jwtAlgorithmName, null, input, full); + } + + public static String accessTokenHash(String jwtAlgorithmName, String curve, String input, boolean full) { byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); - String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName); + String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName, curve); byte[] hash = hash(javaAlgName, inputBytes); return encodeHashToOIDC(hash, full); } public static String accessTokenHash(String jwtAlgorithmName, String input) { - return HashUtils.accessTokenHash(jwtAlgorithmName, input, false); + return HashUtils.accessTokenHash(jwtAlgorithmName, null, input, false); + } + + public static String accessTokenHash(String jwtAlgorithmName, String curve, String input) { + return HashUtils.accessTokenHash(jwtAlgorithmName, curve, input, false); } public static byte[] hash(String javaAlgorithmName, byte[] inputBytes) { @@ -66,4 +75,10 @@ public static String encodeHashToOIDC(byte[] hash, boolean full) { return Base64Url.encode(hashInput); } + public static String sha256UrlEncodedHash(String input, Charset charset) { + byte[] inputBytes = input.getBytes(charset); + byte[] hashedOutput = hash(JavaAlgorithm.SHA256, inputBytes); + return Base64Url.encode(hashedOutput); + } + } diff --git a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java index d18c639c0cd7..759842284b23 100644 --- a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java @@ -20,11 +20,11 @@ import java.security.KeyPair; import java.security.PublicKey; import java.util.Map; -import java.util.UUID; import org.keycloak.OAuth2Constants; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.AsymmetricSignatureSignerContext; @@ -177,15 +177,15 @@ public String createSignedRequestToken(String clientId, String realmInfoUrl) { protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { JsonWebToken reqToken = new JsonWebToken(); - reqToken.id(UUID.randomUUID().toString()); + reqToken.id(SecretGenerator.getInstance().generateSecureID()); reqToken.issuer(clientId); reqToken.subject(clientId); reqToken.audience(realmInfoUrl); - int now = Time.currentTime(); - reqToken.issuedAt(now); - reqToken.expiration(now + this.tokenTimeout); - reqToken.notBefore(now); + long now = Time.currentTime(); + reqToken.iat(now); + reqToken.exp(now + this.tokenTimeout); + reqToken.nbf(now); return reqToken; } diff --git a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java index 2764cb3761e0..210cb4be5ae1 100644 --- a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java @@ -18,13 +18,13 @@ import java.nio.charset.StandardCharsets; import java.util.Map; -import java.util.UUID; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; +import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.JavaAlgorithm; @@ -126,16 +126,16 @@ protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) // JWT claims is the same as one by private_key_jwt JsonWebToken reqToken = new JsonWebToken(); - reqToken.id(UUID.randomUUID().toString()); + reqToken.id(SecretGenerator.getInstance().generateSecureID()); reqToken.issuer(clientId); reqToken.subject(clientId); reqToken.audience(realmInfoUrl); - int now = Time.currentTime(); - reqToken.issuedAt(now); + long now = Time.currentTime(); + reqToken.iat(now); // the same as in KEYCLOAK-2986, JWTClientCredentialsProvider's timeout field - reqToken.expiration(now + 10); - reqToken.notBefore(now); + reqToken.exp(now + 10); + reqToken.nbf(now); return reqToken; } diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index e1196728a2b2..ea4e4bad2b5d 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -73,6 +73,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("subject_types_supported") private List subjectTypesSupported; + @JsonProperty("prompt_values_supported") + private List promptValuesSupported; + @JsonProperty("id_token_signing_alg_values_supported") private List idTokenSigningAlgValuesSupported; @@ -635,10 +638,18 @@ public Boolean getFrontChannelLogoutSessionSupported() { return frontChannelLogoutSessionSupported; } + public void setFrontChannelLogoutSessionSupported(Boolean frontChannelLogoutSessionSupported) { + this.frontChannelLogoutSessionSupported = frontChannelLogoutSessionSupported; + } + public Boolean getFrontChannelLogoutSupported() { return frontChannelLogoutSupported; } + public void setFrontChannelLogoutSupported(Boolean frontChannelLogoutSupported) { + this.frontChannelLogoutSupported = frontChannelLogoutSupported; + } + public Boolean getAuthorizationResponseIssParameterSupported() { return authorizationResponseIssParameterSupported; } @@ -647,4 +658,11 @@ public void setAuthorizationResponseIssParameterSupported(Boolean authorizationR this.authorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported; } + public List getPromptValuesSupported() { + return promptValuesSupported; + } + + public void setPromptValuesSupported(List promptValuesSupported) { + this.promptValuesSupported = promptValuesSupported; + } } diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 0983b2ae1bde..d16b72443ed3 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -205,22 +205,6 @@ public AccessToken id(String id) { return (AccessToken) super.id(id); } - @Override - public AccessToken expiration(int expiration) { - return (AccessToken) super.expiration(expiration); - } - - @Override - public AccessToken notBefore(int notBefore) { - return (AccessToken) super.notBefore(notBefore); - } - - - @Override - public AccessToken issuedAt(int issuedAt) { - return (AccessToken) super.issuedAt(issuedAt); - } - @Override public AccessToken issuer(String issuer) { return (AccessToken) super.issuer(issuer); diff --git a/core/src/main/java/org/keycloak/representations/IDToken.java b/core/src/main/java/org/keycloak/representations/IDToken.java index 68e5290d8940..8f7f25958fb7 100755 --- a/core/src/main/java/org/keycloak/representations/IDToken.java +++ b/core/src/main/java/org/keycloak/representations/IDToken.java @@ -17,7 +17,6 @@ package org.keycloak.representations; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import org.keycloak.TokenCategory; @@ -66,10 +65,8 @@ public class IDToken extends JsonWebToken { protected Long auth_time; - // session_state is deprecated, sid should be used instead - @JsonProperty(SESSION_STATE) - @JsonAlias(SESSION_ID) - protected String sessionState; + @JsonProperty(SESSION_ID) + protected String sessionId; @JsonProperty(AT_HASH) protected String accessTokenHash; @@ -143,7 +140,7 @@ public class IDToken extends JsonWebToken { // Financial API - Part 2: Read and Write API Security Profile // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server @JsonProperty(S_HASH) - protected String stateHash; + protected String stateHash; public String getNonce() { return nonce; @@ -157,37 +154,25 @@ public Long getAuth_time() { return auth_time; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #getAuth_time()} instead. - */ - @Deprecated - @JsonIgnore - public int getAuthTime() { - return auth_time != null ? auth_time.intValue() : 0; - } - public void setAuth_time(Long auth_time) { this.auth_time = auth_time; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #setAuth_time(Long)} ()} instead. - */ - public void setAuthTime(int authTime) { - this.auth_time = Long.valueOf(authTime); - } - - @JsonProperty(SESSION_ID) public String getSessionId() { - return sessionState; + return sessionId; } - public String getSessionState() { - return sessionState; + public void setSessionId(String sessionId) { + this.sessionId = sessionId; } - public void setSessionState(String sessionState) { - this.sessionState = sessionState; + /** + * @deprecated Use {@link #getSessionId()} instead. + */ + @Deprecated + @JsonIgnore + public String getSessionState() { + return getSessionId(); } public String getAccessTokenHash() { diff --git a/core/src/main/java/org/keycloak/representations/JsonWebToken.java b/core/src/main/java/org/keycloak/representations/JsonWebToken.java index 0ed565171fdf..ce1a98fb1e76 100755 --- a/core/src/main/java/org/keycloak/representations/JsonWebToken.java +++ b/core/src/main/java/org/keycloak/representations/JsonWebToken.java @@ -41,6 +41,7 @@ */ public class JsonWebToken implements Serializable, Token { public static final String AZP = "azp"; + public static final String AUD = "aud"; public static final String SUBJECT = "sub"; @JsonProperty("jti") @@ -52,7 +53,7 @@ public class JsonWebToken implements Serializable, Token { @JsonProperty("iss") protected String issuer; - @JsonProperty("aud") + @JsonProperty(AUD) @JsonSerialize(using = StringOrArraySerializer.class) @JsonDeserialize(using = StringOrArrayDeserializer.class) protected String[] audience; @@ -77,64 +78,28 @@ public Long getExp() { return exp; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #getExp()} instead. - */ - @Deprecated - @JsonIgnore - public int getExpiration() { - return exp != null ? exp.intValue() : 0; - } - public JsonWebToken exp(Long exp) { this.exp = exp; return this; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #exp(Long)} instead. - */ - public JsonWebToken expiration(int expiration) { - this.exp = Long.valueOf(expiration); - return this; - } - @JsonIgnore public boolean isExpired() { - return exp != null && exp != 0 ? Time.currentTime() > exp : false; + return exp != null && exp != 0 && Time.currentTime() > exp; } public Long getNbf() { return nbf; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #getNbf()} instead. - */ - @Deprecated - @JsonIgnore - public int getNotBefore() { - return nbf != null ? nbf.intValue() : 0; - } - public JsonWebToken nbf(Long nbf) { this.nbf = nbf; return this; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #nbf(Long)} instead. - */ - @Deprecated @JsonIgnore - public JsonWebToken notBefore(int notBefore) { - this.nbf = Long.valueOf(notBefore); - return this; - } - - @JsonIgnore - public boolean isNotBefore(int allowedTimeSkew) { - return nbf != null ? Time.currentTime() + allowedTimeSkew >= nbf : true; + public boolean isNotBefore(long allowedTimeSkew) { + return nbf == null || Time.currentTime() + allowedTimeSkew >= nbf; } /** @@ -165,21 +130,12 @@ public Long getIat() { return iat; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #getIat()} instead. - */ - @Deprecated - @JsonIgnore - public int getIssuedAt() { - return iat != null ? iat.intValue() : 0; - } - /** * Set issuedAt to the current time */ @JsonIgnore public JsonWebToken issuedNow() { - iat = Long.valueOf(Time.currentTime()); + iat = (long) Time.currentTime(); return this; } @@ -188,17 +144,6 @@ public JsonWebToken iat(Long iat) { return this; } - /** - * @deprecated int will overflow with values after 2038. Use {@link #iat(Long)} ()} instead. - */ - @Deprecated - @JsonIgnore - public JsonWebToken issuedAt(int issuedAt) { - this.iat = Long.valueOf(issuedAt); - return this; - } - - public String getIssuer() { return issuer; } diff --git a/core/src/main/java/org/keycloak/representations/KeyStoreConfig.java b/core/src/main/java/org/keycloak/representations/KeyStoreConfig.java index 782669472c33..0364417eb5b8 100644 --- a/core/src/main/java/org/keycloak/representations/KeyStoreConfig.java +++ b/core/src/main/java/org/keycloak/representations/KeyStoreConfig.java @@ -29,6 +29,8 @@ public class KeyStoreConfig { protected String keyAlias; protected String realmAlias; protected String format; + protected Integer keySize; + protected Integer validity; public Boolean isRealmCertificate() { return realmCertificate; @@ -77,4 +79,20 @@ public String getFormat() { public void setFormat(String format) { this.format = format; } + + public Integer getKeySize() { + return keySize; + } + + public void setKeySize(Integer keySize) { + this.keySize = keySize; + } + + public Integer getValidity() { + return validity; + } + + public void setValidity(Integer validity) { + this.validity = validity; + } } diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java index 497e806ffc22..d449450807be 100755 --- a/core/src/main/java/org/keycloak/representations/RefreshToken.java +++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java @@ -36,21 +36,46 @@ private RefreshToken() { /** * Deep copies issuer, subject, issuedFor, sessionState from AccessToken. * - * @param token */ public RefreshToken(AccessToken token) { this(); this.issuer = token.issuer; this.subject = token.subject; this.issuedFor = token.issuedFor; - this.sessionState = token.sessionState; + this.sessionId = token.sessionId; + this.nonce = token.nonce; + this.audience = new String[] { token.issuer }; + this.scope = token.scope; + } + + /** + * Deep copies issuer, subject, issuedFor, sessionState from AccessToken. + * + * @param token + * @param confirmation optional confirmation parameter that might be processed during authentication but should not + * always be included in the response + */ + public RefreshToken(AccessToken token, Confirmation confirmation) { + this(); + this.issuer = token.issuer; + this.subject = token.subject; + this.issuedFor = token.issuedFor; + this.sessionId = token.sessionId; this.nonce = token.nonce; this.audience = new String[] { token.issuer }; this.scope = token.scope; + this.confirmation = confirmation; } @Override public TokenCategory getCategory() { return TokenCategory.INTERNAL; } + + @Override + public String getSessionId() { + String sessionId = super.getSessionId(); + // Fallback as offline tokens created in Keycloak 14 or earlier have only the "session_state" claim, but not "sid" + return sessionId != null ? sessionId : (String) getOtherClaims().get(IDToken.SESSION_STATE); + } } diff --git a/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java index a4b6400a1641..31eb0eb60b93 100644 --- a/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java @@ -23,15 +23,15 @@ public class ConsentScopeRepresentation { private String name; - private String displayTest; + private String displayText; public ConsentScopeRepresentation() { } - public ConsentScopeRepresentation(String id, String name, String displayTest) { + public ConsentScopeRepresentation(String id, String name, String displayText) { this.id = id; this.name = name; - this.displayTest = displayTest; + this.displayText = displayText; } public String getId() { @@ -50,11 +50,27 @@ public void setName(String name) { this.name = name; } + public String getDisplayText() { + return displayText; + } + + public void setDisplayText(String displayText) { + this.displayText = displayText; + } + + /** + * @deprecated Use {@link #getDisplayText()} instead. This method will be removed in KC 27.0. + */ + @Deprecated public String getDisplayTest() { - return displayTest; + return displayText; } + /** + * @deprecated Use {@link #setDisplayText(String)} instead. This method will be removed in KC 27.0. + */ + @Deprecated public void setDisplayTest(String displayTest) { - this.displayTest = displayTest; + this.displayText = displayTest; } } diff --git a/core/src/main/java/org/keycloak/representations/account/CredentialMetadataRepresentation.java b/core/src/main/java/org/keycloak/representations/account/CredentialMetadataRepresentation.java index da42b416bc02..594ab0d64a0b 100644 --- a/core/src/main/java/org/keycloak/representations/account/CredentialMetadataRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/CredentialMetadataRepresentation.java @@ -4,9 +4,9 @@ public class CredentialMetadataRepresentation { - String infoMessage; - String warningMessageTitle; - String warningMessageDescription; + LocalizedMessage infoMessage; + LocalizedMessage warningMessageTitle; + LocalizedMessage warningMessageDescription; private CredentialRepresentation credential; @@ -19,27 +19,27 @@ public void setCredential(CredentialRepresentation credential) { this.credential = credential; } - public String getInfoMessage() { + public LocalizedMessage getInfoMessage() { return infoMessage; } - public void setInfoMessage(String infoMessage) { + public void setInfoMessage(LocalizedMessage infoMessage) { this.infoMessage = infoMessage; } - public String getWarningMessageTitle() { + public LocalizedMessage getWarningMessageTitle() { return warningMessageTitle; } - public void setWarningMessageTitle(String warningMessageTitle) { + public void setWarningMessageTitle(LocalizedMessage warningMessageTitle) { this.warningMessageTitle = warningMessageTitle; } - public String getWarningMessageDescription() { + public LocalizedMessage getWarningMessageDescription() { return warningMessageDescription; } - public void setWarningMessageDescription(String warningMessageDescription) { + public void setWarningMessageDescription(LocalizedMessage warningMessageDescription) { this.warningMessageDescription = warningMessageDescription; } } diff --git a/core/src/main/java/org/keycloak/representations/account/DeviceRepresentation.java b/core/src/main/java/org/keycloak/representations/account/DeviceRepresentation.java index 9ec8b370d496..903cdba7205e 100644 --- a/core/src/main/java/org/keycloak/representations/account/DeviceRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/DeviceRepresentation.java @@ -57,6 +57,11 @@ public void setId(String id) { this.id = id; } + /** + * Note: will not be an address when a proxy does not provide a valid one + * + * @return the ip address + */ public String getIpAddress() { return ipAddress; } diff --git a/core/src/main/java/org/keycloak/representations/account/LinkedAccountRepresentation.java b/core/src/main/java/org/keycloak/representations/account/LinkedAccountRepresentation.java index 4970dc355afe..b9911510ad5e 100644 --- a/core/src/main/java/org/keycloak/representations/account/LinkedAccountRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/LinkedAccountRepresentation.java @@ -29,7 +29,7 @@ public class LinkedAccountRepresentation implements Comparable domains; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getDomains() { + return domains; + } + + public void setDomains(Set domains) { + this.domains = domains; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationRepresentation)) return false; + + OrganizationRepresentation that = (OrganizationRepresentation) o; + + return id != null && id.equals(that.getId()); + } + + @Override + public int hashCode() { + if (id == null) { + return super.hashCode(); + } + return id.hashCode(); + } +} diff --git a/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java index 996c85226c74..4b86319d70f6 100644 --- a/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java @@ -24,6 +24,11 @@ public void setId(String id) { this.id = id; } + /** + * Note: will not be an address when a proxy does not provide a valid one + * + * @return the ip address + */ public String getIpAddress() { return ipAddress; } diff --git a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java index 6276e36396be..6040cf8d376f 100755 --- a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java @@ -17,128 +17,11 @@ package org.keycloak.representations.account; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.keycloak.json.StringListMapDeserializer; -import org.keycloak.representations.idm.UserProfileMetadata; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import org.keycloak.representations.idm.AbstractUserRepresentation; /** * @author Stian Thorgersen */ -public class UserRepresentation { - - private String id; - private String username; - private String firstName; - private String lastName; - private String email; - private boolean emailVerified; - private UserProfileMetadata userProfileMetadata; - - @JsonDeserialize(using = StringListMapDeserializer.class) - private Map> attributes; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public boolean isEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(boolean emailVerified) { - this.emailVerified = emailVerified; - } - - public Map> getAttributes() { - return attributes; - } - - public void setAttributes(Map> attributes) { - this.attributes = attributes; - } - - public void singleAttribute(String name, String value) { - if (this.attributes == null) this.attributes=new HashMap<>(); - attributes.put(name, (value == null ? new ArrayList() : Arrays.asList(value))); - } - - public String firstAttribute(String key) { - return this.attributes == null ? null : this.attributes.containsKey(key) ? this.attributes.get(key).get(0) : null; - } - - public Map> toAttributes() { - Map> attrs = new HashMap<>(); - - if (getAttributes() != null) attrs.putAll(getAttributes()); - - if (getUsername() != null) - attrs.put("username", Collections.singletonList(getUsername())); - else - attrs.remove("username"); - - if (getEmail() != null) - attrs.put("email", Collections.singletonList(getEmail())); - else - attrs.remove("email"); - - if (getLastName() != null) - attrs.put("lastName", Collections.singletonList(getLastName())); - - if (getFirstName() != null) - attrs.put("firstName", Collections.singletonList(getFirstName())); - - - return attrs; - } - - public UserProfileMetadata getUserProfileMetadata() { - return userProfileMetadata; - } +public class UserRepresentation extends AbstractUserRepresentation { - public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { - this.userProfileMetadata = userProfileMetadata; - } } diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java index faee452c5b93..fe02a6459ce1 100644 --- a/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java +++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java @@ -42,14 +42,14 @@ public DockerResponseToken id(final String id) { } @Override - public DockerResponseToken expiration(final int expiration) { - super.expiration(expiration); + public DockerResponseToken exp(final Long expiration) { + super.exp(expiration); return this; } @Override - public DockerResponseToken notBefore(final int notBefore) { - super.notBefore(notBefore); + public DockerResponseToken nbf(final Long notBefore) { + super.nbf(notBefore); return this; } @@ -60,8 +60,8 @@ public DockerResponseToken issuedNow() { } @Override - public DockerResponseToken issuedAt(final int issuedAt) { - super.issuedAt(issuedAt); + public DockerResponseToken iat(final Long issuedAt) { + super.iat(issuedAt); return this; } diff --git a/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java index 85f27352f88c..9bc0c990ac2c 100644 --- a/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java @@ -28,7 +28,7 @@ public class AbstractAuthenticationExecutionRepresentation implements Serializab private String authenticator; private boolean authenticatorFlow; private String requirement; - private int priority; + private Integer priority; public String getAuthenticatorConfig() { return authenticatorConfig; @@ -54,11 +54,11 @@ public void setRequirement(String requirement) { this.requirement = requirement; } - public int getPriority() { + public Integer getPriority() { return priority; } - public void setPriority(int priority) { + public void setPriority(Integer priority) { this.priority = priority; } diff --git a/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java new file mode 100644 index 000000000000..b17c72f360b4 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java @@ -0,0 +1,166 @@ +/* + * 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.representations.idm; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.keycloak.json.StringListMapDeserializer; + +public abstract class AbstractUserRepresentation { + + public static String USERNAME = "username"; + public static String FIRST_NAME = "firstName"; + public static String LAST_NAME = "lastName"; + public static String EMAIL = "email"; + public static String LOCALE = "locale"; + + protected String id; + protected String username; + protected String firstName; + protected String lastName; + protected String email; + protected Boolean emailVerified; + @JsonDeserialize(using = StringListMapDeserializer.class) + protected Map> attributes; + private UserProfileMetadata userProfileMetadata; + protected Boolean enabled; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean isEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(Boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns all the attributes set to this user except the root attributes. + * + * @return the user attributes. + */ + public Map> getAttributes() { + return attributes; + } + + /** + * Returns all the user attributes including the root attributes. + * + * @return all the user attributes. + */ + @JsonIgnore + public Map> getRawAttributes() { + Map> attrs = new HashMap<>(Optional.ofNullable(attributes).orElse(new HashMap<>())); + + if (username != null) + attrs.put(USERNAME, Collections.singletonList(getUsername())); + else + attrs.remove(USERNAME); + + if (email != null) + attrs.put(EMAIL, Collections.singletonList(getEmail())); + else + attrs.remove(EMAIL); + + if (lastName != null) + attrs.put(LAST_NAME, Collections.singletonList(getLastName())); + + if (firstName != null) + attrs.put(FIRST_NAME, Collections.singletonList(getFirstName())); + + return attrs; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + + @SuppressWarnings("unchecked") + public R singleAttribute(String name, String value) { + if (this.attributes == null) this.attributes=new HashMap<>(); + attributes.put(name, (value == null ? Collections.emptyList() : Arrays.asList(value))); + return (R) this; + } + + public String firstAttribute(String key) { + return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0); + } + + public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { + this.userProfileMetadata = userProfileMetadata; + } + + public UserProfileMetadata getUserProfileMetadata() { + return userProfileMetadata; + } + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java index 9aa1e16f8fd0..f015762288b4 100644 --- a/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java @@ -17,11 +17,14 @@ package org.keycloak.representations.idm; +import java.util.Map; + /** * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class AdminEventRepresentation { + private String id; private long time; private String realmId; private AuthDetailsRepresentation authDetails; @@ -30,6 +33,15 @@ public class AdminEventRepresentation { private String resourcePath; private String representation; private String error; + private Map details; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } public long getTime() { return time; @@ -94,4 +106,12 @@ public String getError() { public void setError(String error) { this.error = error; } + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java index 746aa718de8d..b65f5aa05a3e 100644 --- a/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AuthDetailsRepresentation.java @@ -52,6 +52,11 @@ public void setUserId(String userId) { this.userId = userId; } + /** + * Note: will not be an address when a proxy does not provide a valid one + * + * @return the ip address + */ public String getIpAddress() { return ipAddress; } diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java index 49c9f53abe68..9ed381ca16b0 100755 --- a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java @@ -39,6 +39,7 @@ public class AuthenticationExecutionInfoRepresentation implements Serializable { protected String flowId; protected int level; protected int index; + protected int priority; public String getId() { return id; @@ -143,4 +144,12 @@ public String getFlowId() { public void setFlowId(String flowId) { this.flowId = flowId; } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPoliciesRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPoliciesRepresentation.java index fdb5fa493d79..f2a076c707ab 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPoliciesRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPoliciesRepresentation.java @@ -30,6 +30,7 @@ */ public class ClientPoliciesRepresentation { protected List policies = new ArrayList<>(); + private List globalPolicies; public List getPolicies() { return policies; @@ -39,6 +40,14 @@ public void setPolicies(List policies) { this.policies = policies; } + public List getGlobalPolicies() { + return globalPolicies; + } + + public void setGlobalPolicies(List globalPolicies) { + this.globalPolicies = globalPolicies; + } + @Override public int hashCode() { return JsonSerialization.mapper.convertValue(this, JsonNode.class).hashCode(); diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java index a06178592cbd..bc86bd022aa3 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java @@ -18,8 +18,12 @@ package org.keycloak.representations.idm; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * @author Marek Posolda @@ -30,6 +34,9 @@ public class ClientPolicyConditionRepresentation { private String conditionProviderId; @JsonProperty("configuration") + @Schema(type= SchemaType.OBJECT, + description = "Configuration settings as a JSON object", + additionalProperties = Schema.True.class) private JsonNode configuration; public String getConditionProviderId() { @@ -47,4 +54,17 @@ public JsonNode getConfiguration() { public void setConfiguration(JsonNode configuration) { this.configuration = configuration; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientPolicyConditionRepresentation that = (ClientPolicyConditionRepresentation) o; + return Objects.equals(conditionProviderId, that.conditionProviderId) && Objects.equals(configuration, that.configuration); + } + + @Override + public int hashCode() { + return Objects.hash(conditionProviderId, configuration); + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java index c0215bf49864..81dbe4887afc 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java @@ -18,8 +18,12 @@ package org.keycloak.representations.idm; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * @author Marek Posolda @@ -30,6 +34,9 @@ public class ClientPolicyExecutorRepresentation { private String executorProviderId; @JsonProperty("configuration") + @Schema(type=SchemaType.OBJECT, + description = "Configuration settings as a JSON object", + additionalProperties = Schema.True.class) private JsonNode configuration; public String getExecutorProviderId() { @@ -47,4 +54,17 @@ public JsonNode getConfiguration() { public void setConfiguration(JsonNode configuration) { this.configuration = configuration; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientPolicyExecutorRepresentation that = (ClientPolicyExecutorRepresentation) o; + return Objects.equals(executorProviderId, that.executorProviderId) && Objects.equals(configuration, that.configuration); + } + + @Override + public int hashCode() { + return Objects.hash(executorProviderId, configuration); + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyRepresentation.java index 3674d8c5b7ee..c34e795920f6 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyRepresentation.java @@ -18,6 +18,7 @@ package org.keycloak.representations.idm; import java.util.List; +import java.util.Objects; /** * Client Policy's external representation class @@ -72,4 +73,16 @@ public void setProfiles(List profiles) { this.profiles = profiles; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientPolicyRepresentation that = (ClientPolicyRepresentation) o; + return Objects.equals(name, that.name) && Objects.equals(description, that.description) && Objects.equals(enabled, that.enabled) && Objects.equals(conditions, that.conditions) && Objects.equals(profiles, that.profiles); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, enabled, conditions, profiles); + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientProfileRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientProfileRepresentation.java index 12b05c3af421..ac95e01c48bd 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientProfileRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientProfileRepresentation.java @@ -18,6 +18,7 @@ package org.keycloak.representations.idm; import java.util.List; +import java.util.Objects; /** * Client Profile's external representation class @@ -53,4 +54,17 @@ public List getExecutors() { public void setExecutors(List executors) { this.executors = executors; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientProfileRepresentation that = (ClientProfileRepresentation) o; + return Objects.equals(name, that.name) && Objects.equals(description, that.description) && Objects.equals(executors, that.executors); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, executors); + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index 62c0a19a3ccf..5aca87603572 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -31,6 +31,7 @@ public class ClientRepresentation { protected String clientId; protected String name; protected String description; + protected String type; protected String rootUrl; protected String adminUrl; protected String baseUrl; @@ -51,7 +52,6 @@ public class ClientRepresentation { protected Boolean implicitFlowEnabled; protected Boolean directAccessGrantsEnabled; protected Boolean serviceAccountsEnabled; - protected Boolean oauth2DeviceAuthorizationGrantEnabled; protected Boolean authorizationServicesEnabled; @Deprecated protected Boolean directGrantsOnly; @@ -106,6 +106,14 @@ public void setDescription(String description) { this.description = description; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public String getClientId() { return clientId; } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java new file mode 100644 index 000000000000..8170118edbb9 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 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.representations.idm; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Marek Posolda + */ +public class ClientTypeRepresentation { + + @JsonProperty("name") + private String name; + + @JsonProperty("provider") + private String provider; + + @JsonProperty("parent") + private String parent; + + @JsonProperty("config") + private Map config; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public static class PropertyConfig { + + @JsonProperty("applicable") + private Boolean applicable; + + @JsonProperty("value") + private Object value; + + public Boolean getApplicable() { + return applicable; + } + + public void setApplicable(Boolean applicable) { + this.applicable = applicable; + } + + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTypesRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTypesRepresentation.java new file mode 100644 index 000000000000..9484d188b372 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTypesRepresentation.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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.representations.idm; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Marek Posolda + */ +public class ClientTypesRepresentation { + + @JsonProperty("client-types") + private List realmClientTypes; + + @JsonProperty("global-client-types") + private List globalClientTypes; + + public ClientTypesRepresentation() { + } + + public ClientTypesRepresentation(List realmClientTypes, List globalClientTypes) { + this.realmClientTypes = realmClientTypes; + this.globalClientTypes = globalClientTypes; + } + + public List getRealmClientTypes() { + return realmClientTypes; + } + + public void setRealmClientTypes(List realmClientTypes) { + this.realmClientTypes = realmClientTypes; + } + + public List getGlobalClientTypes() { + return globalClientTypes; + } + + public void setGlobalClientTypes(List globalClientTypes) { + this.globalClientTypes = globalClientTypes; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java index 1662fa252223..b141d3d1f707 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java @@ -57,8 +57,9 @@ public void setProperties(List properties) { } /** - * Extra information about the component that might come from annotations or interfaces that the component implements - * For example, if UserStorageProvider implements ImportSynchronization + * Extra information about the component + * that might come from annotations or interfaces that the component implements. + * For example, if UserStorageProviderFactory implements ImportSynchronization * * @return */ diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java index 0917196da897..d75b44808295 100755 --- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java @@ -62,6 +62,7 @@ public class CredentialRepresentation { private Integer period; @Deprecated private MultivaluedHashMap config; + private String federationLink; public String getId() { return id; @@ -246,5 +247,11 @@ public boolean equals(Object obj) { return true; } + public void setFederationLink(String federationLink) { + this.federationLink = federationLink; + } + public String getFederationLink() { + return federationLink; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java index 0621b9b040fd..b47337ba03e0 100644 --- a/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java @@ -24,6 +24,7 @@ */ public class EventRepresentation { + private String id; private long time; private String type; private String realmId; @@ -34,6 +35,14 @@ public class EventRepresentation { private String error; private Map details; + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public long getTime() { return time; } @@ -82,6 +91,11 @@ public void setSessionId(String sessionId) { this.sessionId = sessionId; } + /** + * Note: will not be an address when a proxy does not provide a valid one + * + * @return the ip address + */ public String getIpAddress() { return ipAddress; } @@ -128,6 +142,7 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = (int) (time ^ (time >>> 32)); + result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + (realmId != null ? realmId.hashCode() : 0); result = 31 * result + (clientId != null ? clientId.hashCode() : 0); diff --git a/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java index e83fab0ecc7a..f811a0b575c1 100755 --- a/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java @@ -17,7 +17,6 @@ package org.keycloak.representations.idm; -import com.fasterxml.jackson.annotation.JsonInclude; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -30,12 +29,12 @@ * @author Bill Burke * @version $Revision: 1 $ */ -@JsonInclude(JsonInclude.Include.NON_EMPTY) public class GroupRepresentation { // For an individual group these are the sufficient minimum fields // to identify a group and operate on it in a basic way protected String id; protected String name; + protected String description; protected String path; protected String parentId; protected Long subGroupCount; @@ -64,6 +63,14 @@ public void setName(String name) { this.name = name; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public String getPath() { return path; } diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java index 9ca5a0d23b5e..5d6c65861439 100755 --- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -41,7 +41,7 @@ public class IdentityProviderRepresentation { *
    • missing - update profile page is presented for users with missing some of mandatory user profile fields *
    • off - update profile page is newer shown after first login *
    - * + * * @see #UPFLM_ON * @see #UPFLM_MISSING * @see #UPFLM_OFF @@ -54,8 +54,10 @@ public class IdentityProviderRepresentation { protected boolean addReadTokenRoleOnCreate; protected boolean authenticateByDefault; protected boolean linkOnly; + protected boolean hideOnLogin; protected String firstBrokerLoginFlowAlias; protected String postBrokerLoginFlowAlias; + protected String organizationId; protected Map config = new HashMap<>(); public String getInternalId() { @@ -106,10 +108,18 @@ public void setLinkOnly(boolean linkOnly) { this.linkOnly = linkOnly; } + public boolean isHideOnLogin() { + return this.hideOnLogin; + } + + public void setHideOnLogin(boolean hideOnLogin) { + this.hideOnLogin = hideOnLogin; + } + /** - * + * * Deprecated because replaced by {@link #updateProfileFirstLoginMode}. Kept here to allow import of old realms. - * + * * @deprecated {@link #setUpdateProfileFirstLoginMode(String)} */ @Deprecated @@ -194,4 +204,12 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getOrganizationId() { + return this.organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + } diff --git a/core/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java new file mode 100644 index 000000000000..17b41e7f7b2a --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/MemberRepresentation.java @@ -0,0 +1,39 @@ +/* + * 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.representations.idm; + +public class MemberRepresentation extends UserRepresentation { + + private MembershipType membershipType; + + public MemberRepresentation() { + super(); + } + + public MemberRepresentation(UserRepresentation user) { + super(user); + } + + public MembershipType getMembershipType() { + return membershipType; + } + + public void setMembershipType(MembershipType membershipType) { + this.membershipType = membershipType; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/MembershipType.java b/core/src/main/java/org/keycloak/representations/idm/MembershipType.java new file mode 100644 index 000000000000..69ac2e3d220c --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/MembershipType.java @@ -0,0 +1,32 @@ +/* + * 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.representations.idm; + +public enum MembershipType { + + /** + * Indicates that member can exist without group/organization. + */ + UNMANAGED, + + /** + * Indicates that member cannot exist without group/organization. + */ + MANAGED; + + public static final String NAME = "membershipType"; +} diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java new file mode 100644 index 000000000000..02d99452f599 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java @@ -0,0 +1,71 @@ +/* + * 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.representations.idm; + +/** + * Representation implementation of an organization internet domain. + * + * @author Stefan Guilhen + */ +public class OrganizationDomainRepresentation { + + private String name; + private boolean verified; + + public OrganizationDomainRepresentation() { + // for reflection + } + + public OrganizationDomainRepresentation(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isVerified() { + return this.verified; + } + + public void setVerified(boolean verified) { + this.verified = verified; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationDomainRepresentation)) return false; + + OrganizationDomainRepresentation that = (OrganizationDomainRepresentation) o; + return name != null && name.equals(that.getName()); + } + + @Override + public int hashCode() { + if (name == null) { + return super.hashCode(); + } + return name.hashCode(); + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java new file mode 100644 index 000000000000..0c22cd502901 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -0,0 +1,179 @@ +/* + * 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.representations.idm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.HashSet; +import java.util.Set; + +public class OrganizationRepresentation { + + private String id; + private String name; + private String alias; + private boolean enabled = true; + private String description; + private String redirectUrl; + private Map> attributes; + private Set domains; + private List members; + private List identityProviders; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getRedirectUrl() { + return redirectUrl; + } + + public void setRedirectUrl(String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + public Map> getAttributes() { + return attributes; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + + public OrganizationRepresentation singleAttribute(String name, String value) { + if (this.attributes == null) attributes = new HashMap<>(); + attributes.put(name, Arrays.asList(value)); + return this; + } + + public Set getDomains() { + return domains; + } + + public OrganizationDomainRepresentation getDomain(String name) { + if (domains == null) { + return null; + } + return domains.stream() + .filter(organizationDomainRepresentation -> name.equals(organizationDomainRepresentation.getName())) + .findAny() + .orElse(null); + } + + public void addDomain(OrganizationDomainRepresentation domain) { + if (domains == null) { + domains = new HashSet<>(); + } + domains.add(domain); + } + + public void removeDomain(OrganizationDomainRepresentation domain) { + if (domains == null) { + return; + } + getDomains().remove(domain); + } + + public List getMembers() { + return members; + } + + public void setMembers(List members) { + this.members = members; + } + + public void addMember(MemberRepresentation member) { + if (members == null) { + members = new ArrayList<>(); + } + members.add(member); + } + + public List getIdentityProviders() { + return identityProviders; + } + + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; + } + + public void addIdentityProvider(IdentityProviderRepresentation idp) { + if (identityProviders == null) { + identityProviders = new ArrayList<>(); + } + identityProviders.add(idp); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationRepresentation)) return false; + + OrganizationRepresentation that = (OrganizationRepresentation) o; + + return id != null && id.equals(that.getId()); + } + + @Override + public int hashCode() { + if (id == null) { + return super.hashCode(); + } + return id.hashCode(); + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java index ea11f4f14ddd..b3546eb0a741 100755 --- a/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java @@ -17,11 +17,7 @@ package org.keycloak.representations.idm; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.keycloak.common.util.PemUtils; - -import java.security.PublicKey; /** * @author Bill Burke @@ -42,9 +38,6 @@ public class PublishedRealmRepresentation { @JsonProperty("tokens-not-before") protected int notBefore; - @JsonIgnore - protected volatile transient PublicKey publicKey; - public String getRealm() { return realm; } @@ -59,27 +52,6 @@ public String getPublicKeyPem() { public void setPublicKeyPem(String publicKeyPem) { this.publicKeyPem = publicKeyPem; - this.publicKey = null; - } - - - @JsonIgnore - public PublicKey getPublicKey() { - if (publicKey != null) return publicKey; - if (publicKeyPem != null) { - try { - publicKey = PemUtils.decodePublicKey(publicKeyPem); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - return publicKey; - } - - @JsonIgnore - public void setPublicKey(PublicKey publicKey) { - this.publicKey = publicKey; - this.publicKeyPem = PemUtils.encodeKey(publicKey); } public String getTokenServiceUrl() { diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 47d62c7e1b42..33df4f495b3b 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -91,6 +91,8 @@ public class RealmRepresentation { //--- brute force settings protected Boolean bruteForceProtected; protected Boolean permanentLockout; + protected Integer maxTemporaryLockouts; + protected BruteForceStrategy bruteForceStrategy; protected Integer maxFailureWaitSeconds; protected Integer minimumQuickLoginWaitSeconds; protected Integer waitIncrementSeconds; @@ -112,6 +114,7 @@ public class RealmRepresentation { @Deprecated protected List defaultRoles; protected RoleRepresentation defaultRole; + protected ClientRepresentation adminPermissionsClient; protected List defaultGroups; @Deprecated protected Set requiredCredentials; @@ -153,6 +156,7 @@ public class RealmRepresentation { protected Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister; protected List webAuthnPolicyPasswordlessAcceptableAaguids; protected List webAuthnPolicyPasswordlessExtraOrigins; + protected Boolean webAuthnPolicyPasswordlessPasskeysEnabled; // Client Policies/Profiles @@ -180,15 +184,15 @@ public class RealmRepresentation { protected String accountTheme; protected String adminTheme; protected String emailTheme; - + protected Boolean eventsEnabled; protected Long eventsExpiration; protected List eventsListeners; protected List enabledEventTypes; - + protected Boolean adminEventsEnabled; protected Boolean adminEventsDetailsEnabled; - + private List identityProviders; private List identityProviderMappers; private List protocolMappers; @@ -205,6 +209,7 @@ public class RealmRepresentation { protected String resetCredentialsFlow; protected String clientAuthenticationFlow; protected String dockerAuthenticationFlow; + protected String firstBrokerLoginFlow; protected Map attributes; @@ -212,6 +217,13 @@ public class RealmRepresentation { protected Boolean userManagedAccessAllowed; + protected Boolean organizationsEnabled; + private List organizations; + + protected Boolean verifiableCredentialsEnabled; + + protected Boolean adminPermissionsEnabled; + @Deprecated protected Boolean social; @Deprecated @@ -543,6 +555,14 @@ public void setDefaultRole(RoleRepresentation defaultRole) { this.defaultRole = defaultRole; } + public ClientRepresentation getAdminPermissionsClient() { + return adminPermissionsClient; + } + + public void setAdminPermissionsClient(ClientRepresentation adminPermissionsClient) { + this.adminPermissionsClient = adminPermissionsClient; + } + public List getDefaultGroups() { return defaultGroups; } @@ -618,7 +638,7 @@ public Boolean isVerifyEmail() { public void setVerifyEmail(Boolean verifyEmail) { this.verifyEmail = verifyEmail; } - + public Boolean isLoginWithEmailAllowed() { return loginWithEmailAllowed; } @@ -626,7 +646,7 @@ public Boolean isLoginWithEmailAllowed() { public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) { this.loginWithEmailAllowed = loginWithEmailAllowed; } - + public Boolean isDuplicateEmailsAllowed() { return duplicateEmailsAllowed; } @@ -764,6 +784,22 @@ public void setPermanentLockout(Boolean permanentLockout) { this.permanentLockout = permanentLockout; } + public Integer getMaxTemporaryLockouts() { + return maxTemporaryLockouts; + } + + public void setMaxTemporaryLockouts(Integer maxTemporaryLockouts) { + this.maxTemporaryLockouts = maxTemporaryLockouts; + } + + public BruteForceStrategy getBruteForceStrategy() { + return this.bruteForceStrategy; + } + + public void setBruteForceStrategy(BruteForceStrategy bruteForceStrategy) { + this.bruteForceStrategy = bruteForceStrategy; + } + public Integer getMaxFailureWaitSeconds() { return maxFailureWaitSeconds; } @@ -835,7 +871,7 @@ public List getEventsListeners() { public void setEventsListeners(List eventsListeners) { this.eventsListeners = eventsListeners; } - + public List getEnabledEventTypes() { return enabledEventTypes; } @@ -1228,6 +1264,14 @@ public void setWebAuthnPolicyPasswordlessExtraOrigins(List extraOrigins) this.webAuthnPolicyPasswordlessExtraOrigins = extraOrigins; } + public Boolean getWebAuthnPolicyPasswordlessPasskeysEnabled(){ + return webAuthnPolicyPasswordlessPasskeysEnabled; + } + + public void setWebAuthnPolicyPasswordlessPasskeysEnabled(Boolean webAuthnPolicyPasswordlessPasskeysEnabled) { + this.webAuthnPolicyPasswordlessPasskeysEnabled = webAuthnPolicyPasswordlessPasskeysEnabled; + } + // Client Policies/Profiles @JsonIgnore @@ -1319,6 +1363,15 @@ public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthen return this; } + public String getFirstBrokerLoginFlow() { + return firstBrokerLoginFlow; + } + + public RealmRepresentation setFirstBrokerLoginFlow(String firstBrokerLoginFlow) { + this.firstBrokerLoginFlow = firstBrokerLoginFlow; + return this; + } + public String getKeycloakVersion() { return keycloakVersion; } @@ -1401,8 +1454,51 @@ public Boolean isUserManagedAccessAllowed() { return userManagedAccessAllowed; } + public Boolean isOrganizationsEnabled() { + return organizationsEnabled; + } + + public void setOrganizationsEnabled(Boolean organizationsEnabled) { + this.organizationsEnabled = organizationsEnabled; + } + + public Boolean isAdminPermissionsEnabled() { + return adminPermissionsEnabled; + } + + public void setAdminPermissionsEnabled(Boolean adminPermissionsEnabled) { + this.adminPermissionsEnabled = adminPermissionsEnabled; + } + + public Boolean isVerifiableCredentialsEnabled() { + return verifiableCredentialsEnabled; + } + + public void setVerifiableCredentialsEnabled(Boolean verifiableCredentialsEnabled) { + this.verifiableCredentialsEnabled = verifiableCredentialsEnabled; + } + @JsonIgnore public Map getAttributesOrEmpty() { return (Map) (attributes == null ? Collections.emptyMap() : attributes); } + + public List getOrganizations() { + return organizations; + } + + public void setOrganizations(List organizations) { + this.organizations = organizations; + } + + public void addOrganization(OrganizationRepresentation org) { + if (organizations == null) { + organizations = new ArrayList<>(); + } + organizations.add(org); + } + + public enum BruteForceStrategy { + LINEAR, MULTIPLE; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigInfoRepresentation.java new file mode 100644 index 000000000000..856ff92f9340 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigInfoRepresentation.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.representations.idm; + +import java.util.List; + +/** + * Represents the configurable properties of a RequiredAction. + */ +public class RequiredActionConfigInfoRepresentation { + + private List properties; + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } +} + diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigRepresentation.java new file mode 100755 index 000000000000..eb7a31d9fd02 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/RequiredActionConfigRepresentation.java @@ -0,0 +1,38 @@ +/* + * 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.representations.idm; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the configuration of a RequiredAction. + */ +public class RequiredActionConfigRepresentation implements Serializable { + + private Map config = new HashMap<>(); + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java index 6f6085e9e65e..74d77683675e 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java @@ -17,7 +17,6 @@ package org.keycloak.representations.idm; -import java.util.HashMap; import java.util.Map; /** @@ -32,8 +31,7 @@ public class RequiredActionProviderRepresentation { private boolean enabled; private boolean defaultAction; private int priority; - private Map config = new HashMap<>(); - + private Map config; public String getAlias() { return alias; diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderSimpleRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderSimpleRepresentation.java index 492204dd289d..88092efe488d 100644 --- a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderSimpleRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderSimpleRepresentation.java @@ -52,4 +52,5 @@ public String getProviderId() { public void setProviderId(String providerId) { this.providerId = providerId; } + } diff --git a/core/src/main/java/org/keycloak/representations/idm/SecurityProfileConfiguration.java b/core/src/main/java/org/keycloak/representations/idm/SecurityProfileConfiguration.java new file mode 100644 index 000000000000..0da5f06ab733 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/SecurityProfileConfiguration.java @@ -0,0 +1,80 @@ +/* + * 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.representations.idm; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** + * Default configuration for security profile. For the moment just a name and pointers + * to default global client profiles and policies. + * + * @author rmartinc + */ +public class SecurityProfileConfiguration { + + private String name; + @JsonProperty("client-profiles") + private String clientProfiles; + @JsonProperty("client-policies") + private String clientPolicies; + @JsonIgnore + private List defaultClientProfiles; + @JsonIgnore + private List defaultClientPolicies; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getClientProfiles() { + return clientProfiles; + } + + public void setClientProfiles(String clientProfiles) { + this.clientProfiles = clientProfiles; + } + + public String getClientPolicies() { + return clientPolicies; + } + + public void setClientPolicies(String clientPolicies) { + this.clientPolicies = clientPolicies; + } + + public List getDefaultClientProfiles() { + return defaultClientProfiles; + } + + public void setDefaultClientProfiles(List defaultClientProfiles) { + this.defaultClientProfiles = defaultClientProfiles; + } + + public List getDefaultClientPolicies() { + return defaultClientPolicies; + } + + public void setDefaultClientPolicies(List defaultClientPolicies) { + this.defaultClientPolicies = defaultClientPolicies; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/TestLdapConnectionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/TestLdapConnectionRepresentation.java index fbf75feedb07..715ac6f65907 100644 --- a/core/src/main/java/org/keycloak/representations/idm/TestLdapConnectionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/TestLdapConnectionRepresentation.java @@ -16,10 +16,15 @@ public TestLdapConnectionRepresentation() { } public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout) { - this(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, null, null); + this(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, null, null, null); } public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout, String startTls, String authType) { + this(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls, authType, null); + } + + public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, + String useTruststoreSpi, String connectionTimeout, String startTls, String authType, String componentId) { this.action = action; this.connectionUrl = connectionUrl; this.bindDn = bindDn; @@ -28,6 +33,7 @@ public TestLdapConnectionRepresentation(String action, String connectionUrl, Str this.connectionTimeout = connectionTimeout; this.startTls = startTls; this.authType = authType; + this.componentId = componentId; } public String getAction() { diff --git a/core/src/main/java/org/keycloak/representations/idm/UserProfileAttributeMetadata.java b/core/src/main/java/org/keycloak/representations/idm/UserProfileAttributeMetadata.java index 2c46aca7b0ca..2911ab70a200 100644 --- a/core/src/main/java/org/keycloak/representations/idm/UserProfileAttributeMetadata.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserProfileAttributeMetadata.java @@ -30,13 +30,14 @@ public class UserProfileAttributeMetadata { private Map annotations; private Map> validators; private String group; + private boolean multivalued; public UserProfileAttributeMetadata() { } public UserProfileAttributeMetadata(String name, String displayName, boolean required, boolean readOnly, String group, Map annotations, - Map> validators) { + Map> validators, boolean multivalued) { this.name = name; this.displayName = displayName; this.required = required; @@ -44,6 +45,7 @@ public UserProfileAttributeMetadata(String name, String displayName, boolean req this.annotations = annotations; this.validators = validators; this.group = group; + this.multivalued = multivalued; } public String getName() { @@ -85,4 +87,11 @@ public Map> getValidators() { return validators; } + public void setMultivalued(boolean multivalued) { + this.multivalued = multivalued; + } + + public boolean isMultivalued() { + return multivalued; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 6871796e35a1..889301b77fc7 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -17,14 +17,7 @@ package org.keycloak.representations.idm; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.keycloak.json.StringListMapDeserializer; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -32,24 +25,15 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public class UserRepresentation { +public class UserRepresentation extends AbstractUserRepresentation{ protected String self; // link - protected String id; protected String origin; protected Long createdTimestamp; - protected String username; - protected Boolean enabled; protected Boolean totp; - protected Boolean emailVerified; - protected String firstName; - protected String lastName; - protected String email; protected String federationLink; protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID) - @JsonDeserialize(using = StringListMapDeserializer.class) - protected Map> attributes; protected List credentials; protected Set disableableCredentialTypes; protected List requiredActions; @@ -66,7 +50,42 @@ public class UserRepresentation { protected List groups; private Map access; - private UserProfileMetadata userProfileMetadata; + + public UserRepresentation() { + } + + public UserRepresentation(UserRepresentation rep) { + // AbstractUserRepresentation + this.id = rep.getId(); + this.username = rep.getUsername(); + this.firstName = rep.getFirstName(); + this.lastName = rep.getLastName(); + this.email = rep.getEmail(); + this.emailVerified = rep.isEmailVerified(); + this.attributes = rep.getAttributes(); + this.setUserProfileMetadata(rep.getUserProfileMetadata()); + + this.self = rep.getSelf(); + this.createdTimestamp = rep.getCreatedTimestamp(); + this.enabled = rep.isEnabled(); + this.totp = rep.isTotp(); + this.federationLink = rep.getFederationLink(); + this.serviceAccountClientId = rep.getServiceAccountClientId(); + this.credentials = rep.getCredentials(); + this.disableableCredentialTypes = rep.getDisableableCredentialTypes(); + this.requiredActions = rep.getRequiredActions(); + this.federatedIdentities = rep.getFederatedIdentities(); + this.realmRoles = rep.getRealmRoles(); + this.clientRoles = rep.getClientRoles(); + this.clientConsents = rep.getClientConsents(); + this.notBefore = rep.getNotBefore(); + + this.applicationRoles = rep.getApplicationRoles(); + this.socialLinks = rep.getSocialLinks(); + + this.groups = rep.getGroups(); + this.access = rep.getAccess(); + } public String getSelf() { return self; @@ -76,14 +95,6 @@ public void setSelf(String self) { this.self = self; } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - public Long getCreatedTimestamp() { return createdTimestamp; } @@ -92,46 +103,6 @@ public void setCreatedTimestamp(Long createdTimestamp) { this.createdTimestamp = createdTimestamp; } - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public Boolean isEnabled() { - return enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - @Deprecated public Boolean isTotp() { return totp; @@ -142,32 +113,6 @@ public void setTotp(Boolean totp) { this.totp = totp; } - public Boolean isEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(Boolean emailVerified) { - this.emailVerified = emailVerified; - } - - public Map> getAttributes() { - return attributes; - } - - public void setAttributes(Map> attributes) { - this.attributes = attributes; - } - - public UserRepresentation singleAttribute(String name, String value) { - if (this.attributes == null) this.attributes=new HashMap<>(); - attributes.put(name, (value == null ? new ArrayList() : Arrays.asList(value))); - return this; - } - - public String firstAttribute(String key) { - return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0); - } - public List getCredentials() { return credentials; } @@ -265,13 +210,21 @@ public void setGroups(List groups) { * Returns id of UserStorageProvider that loaded this user * * @return NULL if user stored locally + * @deprecated Use {@link #getFederationLink()} instead */ + @Deprecated public String getOrigin() { - return origin; + return federationLink; } + /** + * + * @param origin the origin + * @deprecated Use {@link #setFederationLink(String)} instead + */ + @Deprecated public void setOrigin(String origin) { - this.origin = origin; + // deprecated } public Set getDisableableCredentialTypes() { @@ -289,36 +242,4 @@ public Map getAccess() { public void setAccess(Map access) { this.access = access; } - - public Map> toAttributes() { - Map> attrs = new HashMap<>(); - - if (getAttributes() != null) attrs.putAll(getAttributes()); - - if (getUsername() != null) - attrs.put("username", Collections.singletonList(getUsername())); - else - attrs.remove("username"); - - if (getEmail() != null) - attrs.put("email", Collections.singletonList(getEmail())); - else - attrs.remove("email"); - - if (getLastName() != null) - attrs.put("lastName", Collections.singletonList(getLastName())); - - if (getFirstName() != null) - attrs.put("firstName", Collections.singletonList(getFirstName())); - - return attrs; - } - - public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { - this.userProfileMetadata = userProfileMetadata; - } - - public UserProfileMetadata getUserProfileMetadata() { - return userProfileMetadata; - } } diff --git a/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java index 2e9294fa526e..a9f8679bd9fc 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java @@ -33,6 +33,7 @@ public class UserSessionRepresentation { private long lastAccess; private boolean rememberMe; private Map clients = new HashMap<>(); + private boolean transientUser; public String getId() { return id; @@ -58,6 +59,11 @@ public void setUserId(String userId) { this.userId = userId; } + /** + * Note: will not be an address when a proxy does not provide a valid one + * + * @return the ip address + */ public String getIpAddress() { return ipAddress; } @@ -97,4 +103,12 @@ public Map getClients() { public void setClients(Map clients) { this.clients = clients; } + + public boolean isTransientUser() { + return transientUser; + } + + public void setTransientUser(boolean transientUser) { + this.transientUser = transientUser; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java index ed9bc9c30e13..079e3c7bb6e8 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java @@ -38,6 +38,7 @@ public class AbstractPolicyRepresentation { private Logic logic = Logic.POSITIVE; private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; private String owner; + private String resourceType; @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set resourcesData; @@ -186,4 +187,13 @@ public void setScopesData(Set scopesData) { public Set getScopesData() { return scopesData; } -} \ No newline at end of file + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceType() { + return resourceType; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationSchema.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationSchema.java new file mode 100644 index 000000000000..2d888d5cb4e5 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationSchema.java @@ -0,0 +1,69 @@ +/* + * 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.representations.idm.authorization; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class AuthorizationSchema { + + @JsonDeserialize(using = ResourceTypeDeserializer.class) + private final Map resourceTypes; + + @JsonCreator + public AuthorizationSchema(@JsonProperty("resourceTypes") Map resourceTypes) { + this.resourceTypes = resourceTypes; + } + + public Map getResourceTypes() { + return Collections.unmodifiableMap(resourceTypes); + } + + // Custom deserializer to handle both arrays and maps + public static class ResourceTypeDeserializer extends JsonDeserializer> { + @Override + public Map deserialize(JsonParser parser, DeserializationContext context) throws IOException { + // Check if the input is an array or an object + if (parser.isExpectedStartArrayToken()) { + // Deserialize array of ResourceType and convert to Map + List resourceTypeList = parser.readValueAs(new TypeReference>() {}); + return resourceTypeList.stream() + .collect(Collectors.toMap(ResourceType::getType, Function.identity())); + } else if (parser.isExpectedStartObjectToken()) { + // Deserialize directly as a Map + return parser.readValueAs(new TypeReference>() {}); + } else { + // Throw JsonMappingException for unexpected formats + throw JsonMappingException.from(parser, "Expected an array or object for resourceTypes"); + } + } + } +} + diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java index d134aa20046e..80a9f38d5ecf 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java @@ -89,7 +89,7 @@ public void removeGroup(String... ids) { } } - public static class GroupDefinition { + public static class GroupDefinition implements Comparable { private String id; private String path; @@ -136,5 +136,13 @@ public boolean isExtendChildren() { public void setExtendChildren(boolean extendChildren) { this.extendChildren = extendChildren; } + + @Override + public int compareTo(GroupDefinition o) { + if (o.id == null || id == null) { + return 1; + } + return id.compareTo(o.id); + } } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java index b1cdafa1df32..9f2fa5a81af4 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java @@ -44,9 +44,9 @@ public PermissionTicketToken(List permissions, String audience, Acce if (accessToken != null) { id(TokenIdGenerator.generateId()); subject(accessToken.getSubject()); - expiration(accessToken.getExpiration()); - notBefore(accessToken.getNotBefore()); - issuedAt(accessToken.getIssuedAt()); + this.exp(accessToken.getExp()); + this.nbf(accessToken.getNbf()); + iat(accessToken.getIat()); issuedFor(accessToken.getIssuedFor()); } if (audience != null) { diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationRequest.java index d5663940a765..ee08df650cdb 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationRequest.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationRequest.java @@ -30,6 +30,7 @@ public class PolicyEvaluationRequest { private Map> context = new HashMap<>(); private List resources = new LinkedList<>(); + private String resourceType; private String clientId; private String userId; private List roleIds = new LinkedList<>(); @@ -51,6 +52,14 @@ public void setResources(List resources) { this.resources = resources; } + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + public String getClientId() { return this.clientId; } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationResponse.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationResponse.java index bfa4c30d115b..b7bbd24c368c 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationResponse.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEvaluationResponse.java @@ -71,9 +71,10 @@ public static class EvaluationResultRepresentation { private ResourceRepresentation resource; private List scopes; - private List policies; + private Set policies; private DecisionEffect status; - private List allowedScopes = new ArrayList<>(); + private Set allowedScopes = new HashSet<>(); + private Set deniedScopes = new HashSet<>(); public void setResource(final ResourceRepresentation resource) { this.resource = resource; @@ -91,11 +92,11 @@ public List getScopes() { return scopes; } - public void setPolicies(final List policies) { + public void setPolicies(final Set policies) { this.policies = policies; } - public List getPolicies() { + public Set getPolicies() { return policies; } @@ -107,13 +108,21 @@ public DecisionEffect getStatus() { return status; } - public void setAllowedScopes(List allowedScopes) { + public void setAllowedScopes(Set allowedScopes) { this.allowedScopes = allowedScopes; } - public List getAllowedScopes() { + public Set getAllowedScopes() { return allowedScopes; } + + public void setDeniedScopes(Set deniedScopes) { + this.deniedScopes = deniedScopes; + } + + public Set getDeniedScopes() { + return deniedScopes; + } } public static class PolicyResultRepresentation { @@ -122,6 +131,7 @@ public static class PolicyResultRepresentation { private DecisionEffect status; private List associatedPolicies; private Set scopes = new HashSet<>(); + private String resourceType; public PolicyRepresentation getPolicy() { return policy; @@ -149,7 +159,7 @@ public void setAssociatedPolicies(final List associa @Override public int hashCode() { - return this.policy.hashCode(); + return this.policy.getName().hashCode(); } @Override @@ -157,7 +167,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final PolicyResultRepresentation policy = (PolicyResultRepresentation) o; - return this.policy.equals(policy.getPolicy()); + return this.policy.getName().equals(policy.getPolicy().getName()); } public void setScopes(Set scopes) { @@ -167,5 +177,13 @@ public void setScopes(Set scopes) { public Set getScopes() { return scopes; } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceType() { + return resourceType; + } } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/RegexPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/RegexPolicyRepresentation.java index 4dc39401b170..90adebe17d8f 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/RegexPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/RegexPolicyRepresentation.java @@ -23,6 +23,7 @@ public class RegexPolicyRepresentation extends AbstractPolicyRepresentation { private String targetClaim; private String pattern; + private boolean targetContextAttributes; @Override public String getType() { @@ -45,4 +46,12 @@ public void setPattern(String pattern) { this.pattern = pattern; } + public boolean isTargetContextAttributes() { + return targetContextAttributes; + } + + public void setTargetContextAttributes(boolean targetContextAttributes) { + this.targetContextAttributes = targetContextAttributes; + } + } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java index f5ce88dedf50..b3f5f9014fe3 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java @@ -35,6 +35,7 @@ public class ResourceServerRepresentation { private List policies = emptyList(); private List scopes = emptyList(); private DecisionStrategy decisionStrategy; + private AuthorizationSchema authorizationSchema; public void setId(String id) { this.id = id; @@ -107,4 +108,12 @@ public void setDecisionStrategy(DecisionStrategy decisionStrategy) { public DecisionStrategy getDecisionStrategy() { return decisionStrategy; } + + public void setAuthorizationSchema(AuthorizationSchema authorizationSchema) { + this.authorizationSchema = authorizationSchema; + } + + public AuthorizationSchema getAuthorizationSchema() { + return authorizationSchema; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceType.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceType.java new file mode 100644 index 000000000000..ce48472fb22d --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceType.java @@ -0,0 +1,63 @@ +/* + * 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.representations.idm.authorization; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class ResourceType { + + private final String type; + private final Set scopes; + private final Map> scopeAliases; + private final String groupType; + + @JsonCreator + public ResourceType(@JsonProperty("type") String type, @JsonProperty("scopes") Set scopes) { + this(type, scopes, Collections.emptyMap()); + } + + public ResourceType(String type, Set scopes, Map> scopeAliases) { + this(type, scopes, scopeAliases, null); + } + + public ResourceType(String type, Set scopes, Map> scopeAliases, String groupType) { + this.type = type; + this.scopes = Collections.unmodifiableSet(scopes); + this.scopeAliases = scopeAliases; + this.groupType = groupType; + } + + public String getType() { + return type; + } + + public Set getScopes() { + return Collections.unmodifiableSet(scopes); + } + + public Map> getScopeAliases() { + return Collections.unmodifiableMap(scopeAliases); + } + + public String getGroupType() { + return groupType; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java index a4f29738f40a..7dfda004e35d 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java @@ -25,6 +25,7 @@ public class RolePolicyRepresentation extends AbstractPolicyRepresentation { private Set roles; + private Boolean fetchRoles; @Override public String getType() { @@ -39,7 +40,7 @@ public void setRoles(Set roles) { this.roles = roles; } - public void addRole(String name, boolean required) { + public void addRole(String name, Boolean required) { if (roles == null) { roles = new HashSet<>(); } @@ -58,16 +59,24 @@ public void addClientRole(String clientId, String name, boolean required) { addRole(clientId + "/" + name, required); } - public static class RoleDefinition { + public Boolean isFetchRoles() { + return fetchRoles; + } + + public void setFetchRoles(Boolean fetchRoles) { + this.fetchRoles = fetchRoles; + } + + public static class RoleDefinition implements Comparable { private String id; - private boolean required; + private Boolean required; public RoleDefinition() { this(null, false); } - public RoleDefinition(String id, boolean required) { + public RoleDefinition(String id, Boolean required) { this.id = id; this.required = required; } @@ -80,12 +89,20 @@ public void setId(String id) { this.id = id; } - public boolean isRequired() { + public Boolean isRequired() { return required; } - public void setRequired(boolean required) { + public void setRequired(Boolean required) { this.required = required; } + + @Override + public int compareTo(RoleDefinition o) { + if (id == null || o.id == null) { + return 1; + } + return id.compareTo(o.id); + } } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java index 1f08553f3294..b6a02b414d7c 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java @@ -21,18 +21,8 @@ */ public class ScopePermissionRepresentation extends AbstractPolicyRepresentation { - private String resourceType; - @Override public String getType() { return "scope"; } - - public void setResourceType(String resourceType) { - this.resourceType = resourceType; - } - - public String getResourceType() { - return resourceType; - } } diff --git a/core/src/main/java/org/keycloak/representations/info/CpuInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/CpuInfoRepresentation.java new file mode 100644 index 000000000000..7cd43fdd288d --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/info/CpuInfoRepresentation.java @@ -0,0 +1,21 @@ +package org.keycloak.representations.info; + +public class CpuInfoRepresentation { + + protected long processorCount; + + public static CpuInfoRepresentation create() { + Runtime runtime = Runtime.getRuntime(); + CpuInfoRepresentation rep = new CpuInfoRepresentation(); + rep.setProcessorCount(runtime.availableProcessors()); + return rep; + } + + public long getProcessorCount() { + return processorCount; + } + + public void setProcessorCount(long processorCount) { + this.processorCount = processorCount; + } +} diff --git a/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java index 8c582ddd2f1e..0f3d1f874827 100644 --- a/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java @@ -20,11 +20,6 @@ package org.keycloak.representations.info; import java.util.List; -import java.util.stream.Collectors; - -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.crypto.CryptoProvider; -import org.keycloak.common.util.KeystoreUtil; /** * @author Marek Posolda @@ -38,20 +33,6 @@ public class CryptoInfoRepresentation { private List clientSignatureAsymmetricAlgorithms; - public static CryptoInfoRepresentation create(List clientSignatureSymmetricAlgorithms, List clientSignatureAsymmetricAlgorithms) { - CryptoInfoRepresentation info = new CryptoInfoRepresentation(); - - CryptoProvider cryptoProvider = CryptoIntegration.getProvider(); - info.cryptoProvider = cryptoProvider.getClass().getSimpleName(); - info.supportedKeystoreTypes = CryptoIntegration.getProvider().getSupportedKeyStoreTypes() - .map(KeystoreUtil.KeystoreFormat::toString) - .collect(Collectors.toList()); - info.clientSignatureSymmetricAlgorithms = clientSignatureSymmetricAlgorithms; - info.clientSignatureAsymmetricAlgorithms = clientSignatureAsymmetricAlgorithms; - - return info; - } - public String getCryptoProvider() { return cryptoProvider; } diff --git a/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java b/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java index 2cafa4bc3143..4667844eed53 100644 --- a/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java @@ -1,41 +1,17 @@ package org.keycloak.representations.info; -import org.keycloak.common.Profile; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; public class FeatureRepresentation { private String name; private String label; - private Type type; + private FeatureType type; private boolean isEnabled; private Set dependencies; public FeatureRepresentation() { } - public FeatureRepresentation(Profile.Feature feature, boolean isEnabled) { - this.name = feature.name(); - this.label = feature.getLabel(); - this.type = Type.valueOf(feature.getType().name()); - this.isEnabled = isEnabled; - this.dependencies = feature.getDependencies() != null ? - feature.getDependencies().stream().map(Enum::name).collect(Collectors.toSet()) : Collections.emptySet(); - } - - public static List create() { - List featureRepresentationList = new ArrayList<>(); - Profile profile = Profile.getInstance(); - final Map features = profile.getFeatures(); - features.forEach((f, enabled) -> featureRepresentationList.add(new FeatureRepresentation(f, enabled))); - return featureRepresentationList; - } - public String getName() { return name; } @@ -52,11 +28,11 @@ public void setLabel(String label) { this.label = label; } - public Type getType() { + public FeatureType getType() { return type; } - public void setType(Type type) { + public void setType(FeatureType type) { this.type = type; } @@ -76,12 +52,3 @@ public void setDependencies(Set dependencies) { this.dependencies = dependencies; } } - -enum Type { - DEFAULT, - DISABLED_BY_DEFAULT, - PREVIEW, - PREVIEW_DISABLED_BY_DEFAULT, - EXPERIMENTAL, - DEPRECATED; -} \ No newline at end of file diff --git a/core/src/main/java/org/keycloak/representations/info/FeatureType.java b/core/src/main/java/org/keycloak/representations/info/FeatureType.java new file mode 100644 index 000000000000..c9564cbfbac9 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/info/FeatureType.java @@ -0,0 +1,32 @@ +/* + * 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.representations.info; + +/** + * @author Marek Posolda + */ +public enum FeatureType { + DEFAULT, + DISABLED_BY_DEFAULT, + PREVIEW, + PREVIEW_DISABLED_BY_DEFAULT, + EXPERIMENTAL, + DEPRECATED; +} diff --git a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java index 34e9e54d6cae..a25440af09c1 100644 --- a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java @@ -17,12 +17,8 @@ package org.keycloak.representations.info; -import org.keycloak.common.Profile; -import java.util.LinkedList; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; /** * @author Stian Thorgersen @@ -34,41 +30,36 @@ public class ProfileInfoRepresentation { private List previewFeatures; private List experimentalFeatures; - public static ProfileInfoRepresentation create() { - ProfileInfoRepresentation info = new ProfileInfoRepresentation(); - - Profile profile = Profile.getInstance(); - - info.name = profile.getName().name().toLowerCase(); - info.disabledFeatures = names(profile.getDisabledFeatures()); - info.previewFeatures = names(profile.getPreviewFeatures()); - info.experimentalFeatures = names(profile.getExperimentalFeatures()); - - return info; - } - public String getName() { return name; } + public void setName(String name) { + this.name = name; + } + public List getDisabledFeatures() { return disabledFeatures; } + public void setDisabledFeatures(List disabledFeatures) { + this.disabledFeatures = disabledFeatures; + } + public List getPreviewFeatures() { return previewFeatures; } + public void setPreviewFeatures(List previewFeatures) { + this.previewFeatures = previewFeatures; + } + public List getExperimentalFeatures() { return experimentalFeatures; } - private static List names(Set featureSet) { - List l = new LinkedList(); - for (Profile.Feature f : featureSet) { - l.add(f.name()); - } - return l; + public void setExperimentalFeatures(List experimentalFeatures) { + this.experimentalFeatures = experimentalFeatures; } } diff --git a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java index adb152098dc8..a20bed5d73df 100755 --- a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java @@ -31,6 +31,7 @@ public class ServerInfoRepresentation { private SystemInfoRepresentation systemInfo; + private CpuInfoRepresentation cpuInfo; private MemoryInfoRepresentation memoryInfo; private ProfileInfoRepresentation profileInfo; @@ -71,6 +72,14 @@ public void setMemoryInfo(MemoryInfoRepresentation memoryInfo) { this.memoryInfo = memoryInfo; } + public CpuInfoRepresentation getCpuInfo() { + return cpuInfo; + } + + public void setCpuInfo(CpuInfoRepresentation cpuInfo) { + this.cpuInfo = cpuInfo; + } + public ProfileInfoRepresentation getProfileInfo() { return profileInfo; } diff --git a/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java index a0f45f2092de..094834389161 100755 --- a/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java @@ -17,7 +17,6 @@ package org.keycloak.representations.info; -import org.keycloak.common.Version; import java.util.Date; import java.util.Locale; @@ -43,9 +42,9 @@ public class SystemInfoRepresentation { private String userTimezone; private String userLocale; - public static SystemInfoRepresentation create(long serverStartupTime) { + public static SystemInfoRepresentation create(long serverStartupTime, String serverVersion) { SystemInfoRepresentation rep = new SystemInfoRepresentation(); - rep.version = Version.VERSION; + rep.version = serverVersion; rep.serverTime = new Date().toString(); rep.uptimeMillis = System.currentTimeMillis() - serverStartupTime; rep.uptime = formatUptime(rep.uptimeMillis); @@ -63,7 +62,7 @@ public static SystemInfoRepresentation create(long serverStartupTime) { rep.userDir = System.getProperty("user.dir"); rep.userTimezone = System.getProperty("user.timezone"); if (System.getProperty("user.country") != null && System.getProperty("user.language") != null) { - rep.userLocale = (new Locale(System.getProperty("user.country"), System.getProperty("user.language")).toString()); + rep.userLocale = (new Locale(System.getProperty("user.language"), System.getProperty("user.country")).toString()); } return rep; } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttribute.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttribute.java index 3e7ba12bfaa8..be01faf8ae95 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttribute.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttribute.java @@ -19,7 +19,9 @@ package org.keycloak.representations.userprofile.config; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; /** * Configuration of the Attribute. @@ -27,7 +29,7 @@ * @author Vlastimil Elias * */ -public class UPAttribute { +public class UPAttribute implements Cloneable { private String name; private String displayName; @@ -41,6 +43,7 @@ public class UPAttribute { /** null means it is always selected */ private UPAttributeSelector selector; private String group; + private boolean multivalued; public UPAttribute() { } @@ -49,6 +52,31 @@ public UPAttribute(String name) { this.name = name != null ? name.trim() : null; } + public UPAttribute(String name, UPGroup group) { + this(name); + this.group = group.getName(); + } + + public UPAttribute(String name, UPAttributePermissions permissions, UPAttributeRequired required, UPAttributeSelector selector) { + this(name); + this.permissions = permissions; + this.required = required; + this.selector = selector; + } + + public UPAttribute(String name, UPAttributePermissions permissions, UPAttributeRequired required) { + this(name, permissions, required, null); + } + + public UPAttribute(String name, UPAttributePermissions permissions) { + this(name, permissions, null); + } + + public UPAttribute(String name, boolean multivalued, UPAttributePermissions permissions) { + this(name, permissions, null); + setMultivalued(multivalued); + } + public String getName() { return name; } @@ -120,8 +148,67 @@ public void setGroup(String group) { this.group = group != null ? group.trim() : null; } + public void setMultivalued(boolean multivalued) { + this.multivalued = multivalued; + } + + public boolean isMultivalued() { + return multivalued; + } + @Override public String toString() { - return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + "]"; + return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + ", multivalued=" + multivalued + "]"; + } + + @Override + protected UPAttribute clone() { + UPAttribute attr = new UPAttribute(this.name); + attr.setDisplayName(this.displayName); + + Map> validations; + if (this.validations == null) { + validations = null; + } else { + validations = new LinkedHashMap<>(); + for (Map.Entry> entry : this.validations.entrySet()) { + Map newVal = entry.getValue() == null ? null : new LinkedHashMap<>(entry.getValue()); + validations.put(entry.getKey(), newVal); + } + } + attr.setValidations(validations); + + attr.setAnnotations(this.annotations == null ? null : new HashMap<>(this.annotations)); + attr.setRequired(this.required == null ? null : this.required.clone()); + attr.setPermissions(this.permissions == null ? null : this.permissions.clone()); + attr.setSelector(this.selector == null ? null : this.selector.clone()); + attr.setGroup(this.group); + attr.setMultivalued(this.multivalued); + return attr; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPAttribute other = (UPAttribute) obj; + return Objects.equals(this.name, other.name) + && Objects.equals(this.displayName, other.displayName) + && Objects.equals(this.group, other.group) + && Objects.equals(this.validations, other.validations) + && Objects.equals(this.annotations, other.annotations) + && Objects.equals(this.required, other.required) + && Objects.equals(this.permissions, other.permissions) + && Objects.equals(this.selector, other.selector) + && Objects.equals(this.multivalued, other.multivalued); } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributePermissions.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributePermissions.java index c04d3c54758f..69841d692f5f 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributePermissions.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributePermissions.java @@ -19,6 +19,8 @@ package org.keycloak.representations.userprofile.config; import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -28,11 +30,20 @@ * @author Vlastimil Elias * */ -public class UPAttributePermissions { +public class UPAttributePermissions implements Cloneable { private Set view = Collections.emptySet(); private Set edit = Collections.emptySet(); + public UPAttributePermissions() { + // for reflection + } + + public UPAttributePermissions(Set view, Set edit) { + this.view = view; + this.edit = edit; + } + public Set getView() { return view; } @@ -58,4 +69,29 @@ public String toString() { public boolean isEmpty() { return getEdit().isEmpty() && getView().isEmpty(); } + + @Override + protected UPAttributePermissions clone() { + Set view = this.view == null ? null : new HashSet<>(this.view); + Set edit = this.edit == null ? null : new HashSet<>(this.edit); + return new UPAttributePermissions(view, edit); + } + + @Override + public int hashCode() { + return Objects.hash(view, edit); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPAttributePermissions other = (UPAttributePermissions) obj; + return Objects.equals(this.view, other.view) + && Objects.equals(this.edit, other.edit); + } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeRequired.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeRequired.java index 51e7eea9f5e2..a3027b03516f 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeRequired.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeRequired.java @@ -18,6 +18,8 @@ */ package org.keycloak.representations.userprofile.config; +import java.util.HashSet; +import java.util.Objects; import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -28,11 +30,20 @@ * @author Vlastimil Elias * */ -public class UPAttributeRequired { +public class UPAttributeRequired implements Cloneable { private Set roles; private Set scopes; + public UPAttributeRequired() { + // for reflection + } + + public UPAttributeRequired(Set roles, Set scopes) { + this.roles = roles; + this.scopes = scopes; + } + /** * Check if this config means that the attribute is ALWAYS required. * @@ -65,4 +76,28 @@ public String toString() { return "UPAttributeRequired [isAlways=" + isAlways() + ", roles=" + roles + ", scopes=" + scopes + "]"; } + @Override + protected UPAttributeRequired clone() { + Set scopes = this.scopes == null ? null : new HashSet<>(this.scopes); + Set roles = this.roles == null ? null : new HashSet<>(this.roles); + return new UPAttributeRequired(roles, scopes); + } + + @Override + public int hashCode() { + return Objects.hash(roles, scopes); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPAttributeRequired other = (UPAttributeRequired) obj; + return Objects.equals(this.roles, other.roles) + && Objects.equals(this.scopes, other.scopes); + } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeSelector.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeSelector.java index 925ff0618da7..fac2dbd2f7a8 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeSelector.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPAttributeSelector.java @@ -18,6 +18,8 @@ */ package org.keycloak.representations.userprofile.config; +import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -26,10 +28,18 @@ * @author Vlastimil Elias * */ -public class UPAttributeSelector { +public class UPAttributeSelector implements Cloneable { private Set scopes; + public UPAttributeSelector() { + // for reflection + } + + public UPAttributeSelector(Set scopes) { + this.scopes = scopes; + } + public Set getScopes() { return scopes; } @@ -43,4 +53,25 @@ public String toString() { return "UPAttributeSelector [scopes=" + scopes + "]"; } + @Override + protected UPAttributeSelector clone() { + return new UPAttributeSelector(scopes == null ? null : new HashSet<>(scopes)); + } + + @Override + public int hashCode() { + return Objects.hash(scopes); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPAttributeSelector other = (UPAttributeSelector) obj; + return Objects.equals(this.scopes, other.scopes); + } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java index ee97faa9f5a9..f417bf23de88 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java @@ -21,19 +21,42 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + import com.fasterxml.jackson.annotation.JsonIgnore; /** * Configuration of the User Profile for one realm. - * + * * @author Vlastimil Elias * */ -public class UPConfig { +public class UPConfig implements Cloneable { + + public enum UnmanagedAttributePolicy { + + /** + * Unmanaged attributes are enabled and available from any context. + */ + ENABLED, + + /** + * Unmanaged attributes are only available as read-only and only through the management interfaces. + */ + ADMIN_VIEW, + + /** + * Unmanaged attributes are only available as read-write and only through the management interfaces. + */ + ADMIN_EDIT + } private List attributes; private List groups; + private UnmanagedAttributePolicy unmanagedAttributePolicy; + public List getAttributes() { return attributes; } @@ -42,16 +65,21 @@ public void setAttributes(List attributes) { this.attributes = attributes; } - public UPConfig addAttribute(UPAttribute attribute) { + public UPConfig addOrReplaceAttribute(UPAttribute attribute) { if (attributes == null) { attributes = new ArrayList<>(); } + removeAttribute(attribute.getName()); attributes.add(attribute); return this; } + public boolean removeAttribute(String name) { + return attributes != null && attributes.removeIf(attribute -> attribute.getName().equals(name)); + } + public List getGroups() { if (groups == null) { return Collections.emptyList(); @@ -83,8 +111,50 @@ public UPAttribute getAttribute(String name) { return null; } + public UnmanagedAttributePolicy getUnmanagedAttributePolicy() { + return unmanagedAttributePolicy; + } + + public void setUnmanagedAttributePolicy(UnmanagedAttributePolicy unmanagedAttributePolicy) { + this.unmanagedAttributePolicy = unmanagedAttributePolicy; + } + @Override public String toString() { return "UPConfig [attributes=" + attributes + ", groups=" + groups + "]"; } + + @Override + public UPConfig clone() { + UPConfig cfg = new UPConfig(); + + cfg.setUnmanagedAttributePolicy(this.unmanagedAttributePolicy); + if (attributes != null) { + cfg.setAttributes(attributes.stream().map(UPAttribute::clone).collect(Collectors.toList())); + } + if (groups != null) { + cfg.setGroups(groups.stream().map(UPGroup::clone).collect(Collectors.toList())); + } + + return cfg; + } + + @Override + public int hashCode() { + return Objects.hash(attributes, groups, unmanagedAttributePolicy); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPConfig other = (UPConfig) obj; + return Objects.equals(this.attributes, other.attributes) + && Objects.equals(this.groups, other.groups) + && this.unmanagedAttributePolicy == other.unmanagedAttributePolicy; + } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPGroup.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPGroup.java index 124427309aaa..fc768b036f13 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPGroup.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPGroup.java @@ -19,20 +19,30 @@ package org.keycloak.representations.userprofile.config; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Configuration of the attribute group. * * @author Jörg Matysiak */ -public class UPGroup { +public class UPGroup implements Cloneable { private String name; private String displayHeader; private String displayDescription; private Map annotations; + public UPGroup() { + // for reflection + } + + public UPGroup(String name) { + this.name = name; + } + public String getName() { return name; } @@ -64,4 +74,33 @@ public Map getAnnotations() { public void setAnnotations(Map annotations) { this.annotations = annotations; } + + @Override + protected UPGroup clone() { + UPGroup group = new UPGroup(this.name); + group.setDisplayHeader(displayHeader); + group.setDisplayDescription(displayDescription); + group.setAnnotations(this.annotations == null ? null : new HashMap<>(this.annotations)); + return group; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UPGroup other = (UPGroup) obj; + return Objects.equals(this.name, other.name) + && Objects.equals(this.displayHeader, other.displayHeader) + && Objects.equals(this.displayDescription, other.displayDescription) + && Objects.equals(this.annotations, other.annotations); + } } diff --git a/core/src/main/java/org/keycloak/sdjwt/AbstractSdJwtClaim.java b/core/src/main/java/org/keycloak/sdjwt/AbstractSdJwtClaim.java new file mode 100644 index 000000000000..48ec1d8704ff --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/AbstractSdJwtClaim.java @@ -0,0 +1,39 @@ +/* + * 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.sdjwt; + +/** + * @author Francis Pouatcha + * + */ +public abstract class AbstractSdJwtClaim implements SdJwtClaim { + private final SdJwtClaimName claimName; + + public AbstractSdJwtClaim(SdJwtClaimName claimName) { + this.claimName = claimName; + } + + @Override + public SdJwtClaimName getClaimName() { + return claimName; + } + + @Override + public String getClaimNameAsString() { + return claimName.toString(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/ArrayDisclosure.java b/core/src/main/java/org/keycloak/sdjwt/ArrayDisclosure.java new file mode 100644 index 000000000000..35720db81752 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/ArrayDisclosure.java @@ -0,0 +1,126 @@ +/* + * 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.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * Handles selective disclosure of elements within a top-level array claim, + * supporting both visible and undisclosed elements. + * + * @author Francis Pouatcha + * + */ +public class ArrayDisclosure extends AbstractSdJwtClaim { + private final List elements; + private JsonNode visibleClaimValue = null; + private final List decoyElements; + + private ArrayDisclosure(SdJwtClaimName claimName, List elements, + List decoyElements) { + super(claimName); + this.elements = elements; + this.decoyElements = decoyElements; + } + + /** + * Print the array with visible and invisible elements. + */ + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + if (visibleClaimValue != null) + return visibleClaimValue; + + List visibleElts = new ArrayList<>(); + elements.stream() + .filter(Objects::nonNull) + .forEach(e -> visibleElts.add(e.getVisibleValue(hashAlgo))); + + decoyElements.stream() + .filter(Objects::nonNull) + .forEach(e -> { + if (e.getIndex() < visibleElts.size()) + visibleElts.add(e.getIndex(), e.getVisibleValue(hashAlgo)); + else + visibleElts.add(e.getVisibleValue(hashAlgo)); + }); + + final ArrayNode n = SdJwtUtils.mapper.createArrayNode(); + visibleElts.forEach(n::add); + visibleClaimValue = n; + return visibleClaimValue; + } + + @Override + public List getDisclosureStrings() { + final List disclosureStrings = new ArrayList<>(); + elements.stream() + .filter(Objects::nonNull) + .forEach(e -> { + String disclosureString = e.getDisclosureString(); + if (disclosureString != null) + disclosureStrings.add(disclosureString); + }); + return disclosureStrings; + } + + public static class Builder { + private SdJwtClaimName claimName; + private final List elements = new ArrayList<>(); + private final List decoyElements = new ArrayList<>(); + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withVisibleElement(JsonNode elementValue) { + this.elements.add(new VisibleArrayElement(elementValue)); + return this; + } + + public Builder withUndisclosedElement(SdJwtSalt salt, JsonNode elementValue) { + SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + this.elements.add(UndisclosedArrayElement.builder() + .withSalt(sdJwtSalt) + .withArrayElement(elementValue) + .build()); + return this; + } + + public void withDecoyElt(Integer position, SdJwtSalt salt) { + SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + DecoyArrayElement decoyElement = DecoyArrayElement.builder().withSalt(sdJwtSalt).atIndex(position).build(); + this.decoyElements.add(decoyElement); + } + + public ArrayDisclosure build() { + return new ArrayDisclosure(claimName, Collections.unmodifiableList(elements), + Collections.unmodifiableList(decoyElements)); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/DecoyArrayElement.java b/core/src/main/java/org/keycloak/sdjwt/DecoyArrayElement.java new file mode 100644 index 000000000000..dfdaea2a6b3c --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/DecoyArrayElement.java @@ -0,0 +1,65 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class DecoyArrayElement extends DecoyEntry { + + private final Integer index; + + private DecoyArrayElement(SdJwtSalt salt, Integer index) { + super(salt); + this.index = index; + } + + public JsonNode getVisibleValue(String hashAlg) { + return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg)); + } + + public Integer getIndex() { + return index; + } + + public static class Builder { + private SdJwtSalt salt; + private Integer index; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder atIndex(Integer index) { + this.index = index; + return this; + } + + public DecoyArrayElement build() { + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new DecoyArrayElement(salt, index); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/DecoyClaim.java b/core/src/main/java/org/keycloak/sdjwt/DecoyClaim.java new file mode 100644 index 000000000000..234abf8f7a94 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/DecoyClaim.java @@ -0,0 +1,46 @@ +/* + * 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.sdjwt; + +/** + * + * @author Francis Pouatcha + */ +public class DecoyClaim extends DecoyEntry { + + private DecoyClaim(SdJwtSalt salt) { + super(salt); + } + + public static class Builder { + private SdJwtSalt salt; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public DecoyClaim build() { + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new DecoyClaim(salt); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/DecoyEntry.java b/core/src/main/java/org/keycloak/sdjwt/DecoyEntry.java new file mode 100644 index 000000000000..e0c2fc59b97d --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/DecoyEntry.java @@ -0,0 +1,43 @@ +/* + * 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.sdjwt; + +import java.util.Objects; + +import org.keycloak.jose.jws.crypto.HashUtils; + +/** + * Handles hash production for a decoy entry from the given salt. + * + * @author Francis Pouatcha + * + */ +public abstract class DecoyEntry { + private final SdJwtSalt salt; + + protected DecoyEntry(SdJwtSalt salt) { + this.salt = Objects.requireNonNull(salt, "DecoyEntry always requires a non null salt"); + } + + public SdJwtSalt getSalt() { + return salt; + } + + public String getDisclosureDigest(String hashAlg) { + return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, salt.toString().getBytes())); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/Disclosable.java b/core/src/main/java/org/keycloak/sdjwt/Disclosable.java new file mode 100644 index 000000000000..a2c68d386c0c --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/Disclosable.java @@ -0,0 +1,77 @@ +/* + * 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.sdjwt; + +import java.util.Objects; + +import org.keycloak.jose.jws.crypto.HashUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Handles undisclosed claims and array elements, providing functionality + * to generate disclosure digests from Base64Url encoded strings. + * + * Hiding claims and array elements occurs by including their digests + * instead of plaintext in the signed verifiable credential. + * + * @author Francis Pouatcha + * + */ +public abstract class Disclosable { + private final SdJwtSalt salt; + + /** + * Returns the array of undisclosed value, for + * encoding (disclosure string) and hashing (_sd digest array in the VC). + */ + abstract Object[] toArray(); + + protected Disclosable(SdJwtSalt salt) { + this.salt = Objects.requireNonNull(salt, "Disclosure always requires a salt must not be null"); + } + + public SdJwtSalt getSalt() { + return salt; + } + + public String getSaltAsString() { + return salt.toString(); + } + + public String toJson() { + try { + return SdJwtUtils.printJsonArray(toArray()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public String getDisclosureString() { + String json = toJson(); + return SdJwtUtils.encodeNoPad(json.getBytes()); + } + + public String getDisclosureDigest(String hashAlg) { + return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, getDisclosureString().getBytes())); + } + + @Override + public String toString() { + return getDisclosureString(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java b/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java new file mode 100644 index 000000000000..ca30c47f3026 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java @@ -0,0 +1,53 @@ +/* + * 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.sdjwt; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class DisclosureRedList { + public static final List redList = Collections + .unmodifiableList(Arrays.asList("iss", "iat", "nbf", "exp", "cnf", "vct", "status")); + + private final Set redListClaimNames; + public static final DisclosureRedList defaultList = defaultList(); + + public static DisclosureRedList of(Set redListClaimNames) { + return new DisclosureRedList(redListClaimNames); + } + + private static DisclosureRedList defaultList() { + return new DisclosureRedList(redList.stream().map(SdJwtClaimName::of).collect(Collectors.toSet())); + } + + private DisclosureRedList(Set redListClaimNames) { + this.redListClaimNames = Collections.unmodifiableSet(redListClaimNames); + } + + public boolean isRedListedClaimName(SdJwtClaimName claimName) { + return redListClaimNames.contains(claimName); + } + + public boolean containsRedListedClaimNames(Collection claimNames) { + return !redListClaimNames.isEmpty() && !claimNames.isEmpty() + && !Collections.disjoint(redListClaimNames, claimNames); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/DisclosureSpec.java b/core/src/main/java/org/keycloak/sdjwt/DisclosureSpec.java new file mode 100644 index 000000000000..58c2caf86344 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/DisclosureSpec.java @@ -0,0 +1,191 @@ +/* + * 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.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages the specification of undisclosed claims and array elements. + * + * @author Francis Pouatcha + * + */ +public class DisclosureSpec { + + // Map of undisclosed claims and corresponding salt. + // salt can be null; + private final Map undisclosedClaims; + + // List of decoy claim. Digest will be produced from disclosure data (salt) + private final List decoyClaims; + + // Key is the claim name, value is the list of undisclosed elements + private final Map> undisclosedArrayElts; + + // Key is the claim name, value is the list of decoy elements + // Digest will be produced from disclosure data (salt) + private final Map> decoyArrayElts; + + private DisclosureSpec(Map undisclosedClaims, + List decoyClaims, + Map> undisclosedArrayElts, + Map> decoyArrayElts) { + this.undisclosedClaims = undisclosedClaims; + this.decoyClaims = decoyClaims; + this.undisclosedArrayElts = undisclosedArrayElts; + this.decoyArrayElts = decoyArrayElts; + } + + public Map getUndisclosedArrayElts(SdJwtClaimName arrayClaimName) { + return undisclosedArrayElts.get(arrayClaimName); + } + + public Map getDecoyArrayElts(SdJwtClaimName arrayClaimName) { + return decoyArrayElts.get(arrayClaimName); + } + + public Map getUndisclosedClaims() { + return undisclosedClaims; + } + + public List getDecoyClaims() { + return decoyClaims; + } + + // check if a claim is undisclosed + public DisclosureData getUndisclosedClaim(SdJwtClaimName claimName) { + return undisclosedClaims.get(claimName); + } + + // test is claim has undisclosed array elements + public boolean hasUndisclosedArrayElts(SdJwtClaimName claimName) { + return undisclosedArrayElts.containsKey(claimName); + } + + public static class Builder { + private final Map undisclosedClaims = new HashMap<>(); + private final List decoyClaims = new ArrayList<>(); + private final Map> undisclosedArrayElts = new HashMap<>(); + private final Map> decoyArrayElts = new HashMap<>(); + private DisclosureRedList redListedClaimNames; + + public Builder withUndisclosedClaim(String claimName, String salt) { + this.undisclosedClaims.put(SdJwtClaimName.of(claimName), DisclosureData.of(salt)); + return this; + } + + public Builder withUndisclosedClaim(String claimName) { + return withUndisclosedClaim(claimName, null); + } + + public Builder withDecoyClaim(String salt) { + this.decoyClaims.add(DisclosureData.of(salt)); + return this; + } + + public Builder withUndisclosedArrayElt(String claimName, Integer undisclosedEltIndex, String salt) { + Map indexes = this.undisclosedArrayElts.computeIfAbsent( + SdJwtClaimName.of(claimName), + k -> new HashMap<>()); + indexes.put(undisclosedEltIndex, DisclosureData.of(salt)); + return this; + } + + public Builder withDecoyArrayElt(String claimName, Integer decoyEltIndex, String salt) { + Map indexes = this.decoyArrayElts.computeIfAbsent(SdJwtClaimName.of(claimName), + k -> new HashMap<>()); + + indexes.put(decoyEltIndex, DisclosureData.of(salt)); + return this; + } + + public Builder withRedListedClaimNames(DisclosureRedList redListedClaimNames) { + this.redListedClaimNames = redListedClaimNames; + return this; + } + + public DisclosureSpec build() { + // Validate redlist + validateRedList(); + + Map> undisclosedArrayEltMap = new HashMap<>(); + undisclosedArrayElts.forEach((k, v) -> { + undisclosedArrayEltMap.put(k, Collections.unmodifiableMap((v))); + }); + + Map> decoyArrayEltMap = new HashMap<>(); + decoyArrayElts.forEach((k, v) -> { + decoyArrayEltMap.put(k, Collections.unmodifiableMap((v))); + }); + + return new DisclosureSpec(Collections.unmodifiableMap(undisclosedClaims), + Collections.unmodifiableList(decoyClaims), + Collections.unmodifiableMap(undisclosedArrayEltMap), + Collections.unmodifiableMap(decoyArrayEltMap)); + } + + private void validateRedList() { + // Work with default if none set. + if (redListedClaimNames == null) { + redListedClaimNames = DisclosureRedList.defaultList; + } + + // Validate undisclosed claims + if (redListedClaimNames.containsRedListedClaimNames(undisclosedClaims.keySet())) { + throw new IllegalArgumentException("UndisclosedClaims contains red listed claim names"); + } + + // Validate undisclosed array claims + if (redListedClaimNames.containsRedListedClaimNames(undisclosedArrayElts.keySet())) { + throw new IllegalArgumentException("UndisclosedArrays with red listed claim names"); + } + + // Validate undisclosed claims + if (redListedClaimNames.containsRedListedClaimNames(decoyArrayElts.keySet())) { + throw new IllegalArgumentException("decoyArrayElts contains red listed claim names"); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class DisclosureData { + private final SdJwtSalt salt; + + private DisclosureData() { + this.salt = null; + } + + private DisclosureData(String salt) { + this.salt = salt == null ? null : SdJwtSalt.of(salt); + } + + public static DisclosureData of(String salt) { + return salt == null ? new DisclosureData() : new DisclosureData(salt); + } + + public SdJwtSalt getSalt() { + return salt; + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java new file mode 100644 index 000000000000..250dafad58a8 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java @@ -0,0 +1,240 @@ +/* + * 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.sdjwt; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jws.JWSInput; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Handle verifiable credentials (SD-JWT VC), enabling the parsing + * of existing VCs as well as the creation and signing of new ones. + * It integrates with Keycloak's SignatureSignerContext to facilitate + * the generation of issuer signature. + * + * @author Francis Pouatcha + */ +public class IssuerSignedJWT extends SdJws { + + public IssuerSignedJWT(JsonNode payload, SignatureSignerContext signer, String jwsType) { + super(payload, signer, jwsType); + } + + public static IssuerSignedJWT fromJws(String jwsString) { + return new IssuerSignedJWT(jwsString); + } + + private IssuerSignedJWT(String jwsString) { + super(jwsString); + } + + private IssuerSignedJWT(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures) { + super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures)); + } + + private IssuerSignedJWT(JsonNode payload, JWSInput jwsInput) { + super(payload, jwsInput); + } + + private IssuerSignedJWT(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) { + super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType); + } + + /* + * Generates the payload of the issuer signed jwt from the list + * of claims. + */ + private static JsonNode generatePayloadString(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures) { + + SdJwtUtils.requireNonEmpty(hashAlg, "hashAlg must not be null or empty"); + final List claimsInternal = claims == null ? Collections.emptyList() + : Collections.unmodifiableList(claims); + final List decoyClaimsInternal = decoyClaims == null ? Collections.emptyList() + : Collections.unmodifiableList(decoyClaims); + + try { + // Check no dupplicate claim names + claimsInternal.stream() + .filter(Objects::nonNull) + // is any duplicate, toMap will throw IllegalStateException + .collect(Collectors.toMap(SdJwtClaim::getClaimName, claim -> claim)); + } catch (IllegalStateException e) { + throw new IllegalArgumentException("claims must not contain duplicate claim names", e); + } + + ArrayNode sdArray = SdJwtUtils.mapper.createArrayNode(); + // first filter all UndisclosedClaim + // then sort by salt + // then push digest into the sdArray + List digests = claimsInternal.stream() + .filter(claim -> claim instanceof UndisclosedClaim) + .map(claim -> (UndisclosedClaim) claim) + .collect(Collectors.toMap(UndisclosedClaim::getSalt, claim -> claim)) + .entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .filter(Objects::nonNull) + .map(od -> od.getDisclosureDigest(hashAlg)) + .collect(Collectors.toList()); + + // add decoy claims + decoyClaimsInternal.stream().map(claim -> claim.getDisclosureDigest(hashAlg)).forEach(digests::add); + + digests.stream().sorted().forEach(sdArray::add); + + ObjectNode payload = SdJwtUtils.mapper.createObjectNode(); + + if (sdArray.size() > 0) { + // drop _sd claim if empty + payload.set(CLAIM_NAME_SELECTIVE_DISCLOSURE, sdArray); + } + if (sdArray.size() > 0 || nestedDisclosures) { + // add sd alg only if ay disclosure. + payload.put(CLAIM_NAME_SD_HASH_ALGORITHM, hashAlg); + } + + // then put all other claims in the paypload + // Disclosure of array of elements is handled + // by the corresponding claim object. + claimsInternal.stream() + .filter(Objects::nonNull) + .filter(claim -> !(claim instanceof UndisclosedClaim)) + .forEach(nullableClaim -> { + SdJwtClaim claim = Objects.requireNonNull(nullableClaim); + payload.set(claim.getClaimNameAsString(), claim.getVisibleClaimValue(hashAlg)); + }); + + return payload; + } + + /** + * Returns `cnf` claim (establishing key binding) + */ + public Optional getCnfClaim() { + JsonNode cnf = getPayload().get("cnf"); + return Optional.ofNullable(cnf); + } + + /** + * Returns declared hash algorithm from SD hash claim. + */ + public String getSdHashAlg() { + JsonNode hashAlgNode = getPayload().get(CLAIM_NAME_SD_HASH_ALGORITHM); + return hashAlgNode == null ? "sha-256" : hashAlgNode.asText(); + } + + /** + * Verifies that the SD hash algorithm is understood and deemed secure. + * + * @throws VerificationException if not + */ + public void verifySdHashAlgorithm() throws VerificationException { + // Known secure algorithms + final Set secureAlgorithms = new HashSet<>(Arrays.asList( + "sha-256", "sha-384", "sha-512", + "sha3-256", "sha3-384", "sha3-512" + )); + + // Read SD hash claim + String hashAlg = getSdHashAlg(); + + // Safeguard algorithm + if (!secureAlgorithms.contains(hashAlg)) { + throw new VerificationException("Unexpected or insecure hash algorithm: " + hashAlg); + } + } + + // SD-JWT Claims + public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd"; + public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg"; + + // Builder + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private List claims; + private String hashAlg; + private SignatureSignerContext signer; + private List decoyClaims; + private boolean nestedDisclosures; + private String jwsType; + + public Builder withClaims(List claims) { + this.claims = claims; + return this; + } + + public Builder withDecoyClaims(List decoyClaims) { + this.decoyClaims = decoyClaims; + return this; + } + + public Builder withHashAlg(String hashAlg) { + this.hashAlg = hashAlg; + return this; + } + + public Builder withSigner(SignatureSignerContext signer) { + this.signer = signer; + return this; + } + + public Builder withNestedDisclosures(boolean nestedDisclosures) { + this.nestedDisclosures = nestedDisclosures; + return this; + } + + public Builder withJwsType(String jwsType) { + this.jwsType = jwsType; + return this; + } + + public IssuerSignedJWT build() { + // Preinitialize hashAlg to sha-256 if not provided + hashAlg = hashAlg == null ? "sha-256" : hashAlg; + jwsType = jwsType == null ? "vc+sd-jwt" : jwsType; + // send an empty lise if claims not set. + claims = claims == null ? Collections.emptyList() : claims; + decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims; + if (signer != null) { + return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures, signer, jwsType); + } else { + return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures); + } + } + } + +} diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java new file mode 100644 index 000000000000..45a82310d444 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java @@ -0,0 +1,83 @@ +/* + * 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.sdjwt; + +/** + * Options for Issuer-signed JWT verification. + * + * @author Ingrid Kamga + */ +public class IssuerSignedJwtVerificationOpts { + private final boolean validateIssuedAtClaim; + private final boolean validateExpirationClaim; + private final boolean validateNotBeforeClaim; + + public IssuerSignedJwtVerificationOpts( + boolean validateIssuedAtClaim, + boolean validateExpirationClaim, + boolean validateNotBeforeClaim) { + this.validateIssuedAtClaim = validateIssuedAtClaim; + this.validateExpirationClaim = validateExpirationClaim; + this.validateNotBeforeClaim = validateNotBeforeClaim; + } + + public boolean mustValidateIssuedAtClaim() { + return validateIssuedAtClaim; + } + + public boolean mustValidateExpirationClaim() { + return validateExpirationClaim; + } + + public boolean mustValidateNotBeforeClaim() { + return validateNotBeforeClaim; + } + + public static IssuerSignedJwtVerificationOpts.Builder builder() { + return new IssuerSignedJwtVerificationOpts.Builder(); + } + + public static class Builder { + private boolean validateIssuedAtClaim; + private boolean validateExpirationClaim = true; + private boolean validateNotBeforeClaim = true; + + public Builder withValidateIssuedAtClaim(boolean validateIssuedAtClaim) { + this.validateIssuedAtClaim = validateIssuedAtClaim; + return this; + } + + public Builder withValidateExpirationClaim(boolean validateExpirationClaim) { + this.validateExpirationClaim = validateExpirationClaim; + return this; + } + + public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) { + this.validateNotBeforeClaim = validateNotBeforeClaim; + return this; + } + + public IssuerSignedJwtVerificationOpts build() { + return new IssuerSignedJwtVerificationOpts( + validateIssuedAtClaim, + validateExpirationClaim, + validateNotBeforeClaim + ); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java b/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java new file mode 100644 index 000000000000..e6025fb64622 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java @@ -0,0 +1,98 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.AsymmetricSignatureVerifierContext; +import org.keycloak.crypto.ECCurve; +import org.keycloak.crypto.ECDSASignatureVerifierContext; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.util.JWKSUtils; + +import java.util.Objects; + +/** + * @author Ingrid Kamga + */ +public class JwkParsingUtils { + + public static SignatureVerifierContext convertJwkNodeToVerifierContext(JsonNode jwkNode) { + JWK jwk; + + try { + jwk = SdJwtUtils.mapper.convertValue(jwkNode, JWK.class); + } catch (Exception e) { + throw new IllegalArgumentException("Malformed JWK"); + } + + return convertJwkToVerifierContext(jwk); + } + + public static SignatureVerifierContext convertJwkToVerifierContext(JWK jwk) { + // Wrap JWK + + KeyWrapper keyWrapper; + + try { + keyWrapper = JWKSUtils.getKeyWrapper(jwk); + Objects.requireNonNull(keyWrapper); + } catch (Exception e) { + throw new IllegalArgumentException("Unsupported or invalid JWK"); + } + + // Build verifier + + // KeyType.EC + if (keyWrapper.getType().equals(KeyType.EC)) { + if (keyWrapper.getAlgorithm() == null) { + Objects.requireNonNull(keyWrapper.getCurve()); + + String alg = null; + switch (ECCurve.fromStdCrv(keyWrapper.getCurve())) { + case P256: + alg = Algorithm.ES256; + break; + case P384: + alg = Algorithm.ES384; + break; + case P521: + alg = Algorithm.ES512; + break; + } + + keyWrapper.setAlgorithm(alg); + } + + return new ECDSASignatureVerifierContext(keyWrapper); + } + + // KeyType.RSA + if (keyWrapper.getType().equals(KeyType.RSA)) { + return new AsymmetricSignatureVerifierContext(keyWrapper); + } + + // KeyType is not supported + // This is unreachable as of now given that `JWKSUtils.getKeyWrapper` will fail + // on JWKs with key type not equal to EC or RSA. + throw new IllegalArgumentException("Unexpected key type: " + keyWrapper.getType()); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJws.java b/core/src/main/java/org/keycloak/sdjwt/SdJws.java new file mode 100644 index 000000000000..d7a6ea1b7598 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJws.java @@ -0,0 +1,184 @@ +/* + * 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.sdjwt; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Handle jws, either the issuer jwt or the holder key binding jwt. + * + * @author Francis Pouatcha + * + */ +public abstract class SdJws { + + public static final String CLAIM_NAME_ISSUER = "iss"; + + private final JWSInput jwsInput; + private final JsonNode payload; + + public String toJws() { + if (jwsInput == null) { + throw new IllegalStateException("JWS not yet signed"); + } + return jwsInput.getWireString(); + } + + public JsonNode getPayload() { + return payload; + } + + // Constructor for unsigned JWS + protected SdJws(JsonNode payload) { + this.payload = payload; + this.jwsInput = null; + } + + // Constructor from jws string with all parts + protected SdJws(String jwsString) { + this.jwsInput = parse(jwsString); + this.payload = readPayload(jwsInput); + } + + // Constructor for signed JWS + protected SdJws(JsonNode payload, JWSInput jwsInput) { + this.payload = payload; + this.jwsInput = jwsInput; + } + + protected SdJws(JsonNode payload, SignatureSignerContext signer, String jwsType) { + this.payload = payload; + this.jwsInput = sign(payload, signer, jwsType); + } + + protected static JWSInput sign(JsonNode payload, SignatureSignerContext signer, String jwsType) { + String jwsString = new JWSBuilder().type(jwsType).jsonContent(payload).sign(signer); + return parse(jwsString); + } + + public void verifySignature(SignatureVerifierContext verifier) throws VerificationException { + Objects.requireNonNull(verifier, "verifier must not be null"); + try { + if (!verifier.verify(jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jwsInput.getSignature())) { + throw new VerificationException("Invalid jws signature"); + } + } catch (Exception e) { + throw new VerificationException(e); + } + } + + private static final JWSInput parse(String jwsString) { + try { + return new JWSInput(Objects.requireNonNull(jwsString, "jwsString must not be null")); + } catch (JWSInputException e) { + throw new RuntimeException(e); + } + } + + private static final JsonNode readPayload(JWSInput jwsInput) { + try { + return SdJwtUtils.mapper.readTree(jwsInput.getContent()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public JWSHeader getHeader() { + return this.jwsInput.getHeader(); + } + + public void verifyIssuedAtClaim() throws VerificationException { + long now = Instant.now().getEpochSecond(); + long iat = SdJwtUtils.readTimeClaim(payload, "iat"); + + if (now < iat) { + throw new VerificationException("jwt issued in the future"); + } + } + + public void verifyExpClaim() throws VerificationException { + long now = Instant.now().getEpochSecond(); + long exp = SdJwtUtils.readTimeClaim(payload, "exp"); + + if (now >= exp) { + throw new VerificationException("jwt has expired"); + } + } + + public void verifyNotBeforeClaim() throws VerificationException { + long now = Instant.now().getEpochSecond(); + long nbf = SdJwtUtils.readTimeClaim(payload, "nbf"); + + if (now < nbf) { + throw new VerificationException("jwt not valid yet"); + } + } + + /** + * Verifies that the JWS is not too old. + * + * @param maxAge Maximum age in seconds + * @throws VerificationException if too old + */ + public void verifyAge(int maxAge) throws VerificationException { + long now = Instant.now().getEpochSecond(); + long iat = SdJwtUtils.readTimeClaim(getPayload(), "iat"); + + if (now - iat > maxAge) { + throw new VerificationException("jwt is too old"); + } + } + + /** + * Verifies that SD-JWT was issued by one of the provided issuers. + * @param issuers List of trusted issuers + */ + public void verifyIssClaim(List issuers) throws VerificationException { + verifyClaimAgainstTrustedValues(issuers, CLAIM_NAME_ISSUER); + } + + /** + * Verifies that SD-JWT vct claim matches the expected one. + * @param vcts list of supported verifiable credential types + */ + public void verifyVctClaim(List vcts) throws VerificationException { + verifyClaimAgainstTrustedValues(vcts, "vct"); + } + + private void verifyClaimAgainstTrustedValues(List trustedValues, String claimName) + throws VerificationException { + String claimValue = SdJwtUtils.readClaim(payload, claimName); + + if (!trustedValues.contains(claimValue)) { + throw new VerificationException(String.format("Unknown '%s' claim value: %s", claimName, claimValue)); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwt.java b/core/src/main/java/org/keycloak/sdjwt/SdJwt.java new file mode 100644 index 000000000000..275f7a18b501 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwt.java @@ -0,0 +1,279 @@ +/* + * 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.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.vp.KeyBindingJWT; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Main entry class for selective disclosure jwt (SD-JWT). + * + * @author Francis Pouatcha + */ +public class SdJwt { + public static final String DELIMITER = "~"; + + private final IssuerSignedJWT issuerSignedJWT; + private final List claims; + private final List disclosures = new ArrayList<>(); + private final SdJwtVerificationContext sdJwtVerificationContext; + + private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List nesteSdJwts, + Optional keyBindingJWT, + SignatureSignerContext signer, + String hashAlgorithm, + String jwsType) { + claims = new ArrayList<>(); + claimSet.fields() + .forEachRemaining(entry -> claims.add(createClaim(entry.getKey(), entry.getValue(), disclosureSpec))); + + this.issuerSignedJWT = IssuerSignedJWT.builder() + .withClaims(claims) + .withDecoyClaims(createdDecoyClaims(disclosureSpec)) + .withNestedDisclosures(!nesteSdJwts.isEmpty()) + .withSigner(signer) + .withHashAlg(hashAlgorithm) + .withJwsType(jwsType) + .build(); + + nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures())); + this.disclosures.addAll(getDisclosureStrings(claims)); + + // Instantiate context for verification + this.sdJwtVerificationContext = new SdJwtVerificationContext( + this.issuerSignedJWT, + this.disclosures + ); + } + + private Optional sdJwtString = Optional.empty(); + + private List createdDecoyClaims(DisclosureSpec disclosureSpec) { + return disclosureSpec.getDecoyClaims().stream() + .map(disclosureData -> DecoyClaim.builder().withSalt(disclosureData.getSalt()).build()) + .collect(Collectors.toList()); + } + + /** + * Prepare to a nested payload to this SD-JWT. + *

    + * dropping the algo claim. + * + * @param nestedSdJwt + * @return + */ + public JsonNode asNestedPayload() { + JsonNode nestedPayload = issuerSignedJWT.getPayload(); + ((ObjectNode) nestedPayload).remove(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM); + return nestedPayload; + } + + public String toSdJwtString() { + List parts = new ArrayList<>(); + + parts.add(issuerSignedJWT.toJws()); + parts.addAll(disclosures); + parts.add(""); + + return String.join(DELIMITER, parts); + } + + private static List getDisclosureStrings(List claims) { + List disclosureStrings = new ArrayList<>(); + claims.stream() + .map(SdJwtClaim::getDisclosureStrings) + .forEach(disclosureStrings::addAll); + return Collections.unmodifiableList(disclosureStrings); + } + + @Override + public String toString() { + return sdJwtString.orElseGet(() -> { + String sdString = toSdJwtString(); + sdJwtString = Optional.of(sdString); + return sdString; + }); + } + + private SdJwtClaim createClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) { + DisclosureSpec.DisclosureData disclosureData = disclosureSpec.getUndisclosedClaim(SdJwtClaimName.of(claimName)); + + if (disclosureData != null) { + return createUndisclosedClaim(claimName, claimValue, disclosureData.getSalt()); + } else { + return createArrayOrVisibleClaim(claimName, claimValue, disclosureSpec); + } + } + + private SdJwtClaim createUndisclosedClaim(String claimName, JsonNode claimValue, SdJwtSalt salt) { + return UndisclosedClaim.builder() + .withClaimName(claimName) + .withClaimValue(claimValue) + .withSalt(salt) + .build(); + } + + private SdJwtClaim createArrayOrVisibleClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) { + SdJwtClaimName sdJwtClaimName = SdJwtClaimName.of(claimName); + Map undisclosedArrayElts = disclosureSpec + .getUndisclosedArrayElts(sdJwtClaimName); + Map decoyArrayElts = disclosureSpec.getDecoyArrayElts(sdJwtClaimName); + + if (undisclosedArrayElts != null || decoyArrayElts != null) { + return createArrayDisclosure(claimName, claimValue, undisclosedArrayElts, decoyArrayElts); + } else { + return VisibleSdJwtClaim.builder() + .withClaimName(claimName) + .withClaimValue(claimValue) + .build(); + } + } + + private SdJwtClaim createArrayDisclosure(String claimName, JsonNode claimValue, + Map undisclosedArrayElts, + Map decoyArrayElts) { + ArrayNode arrayNode = validateArrayNode(claimName, claimValue); + ArrayDisclosure.Builder arrayDisclosureBuilder = ArrayDisclosure.builder().withClaimName(claimName); + + if (undisclosedArrayElts != null) { + IntStream.range(0, arrayNode.size()) + .forEach(i -> processArrayElement(arrayDisclosureBuilder, arrayNode.get(i), + undisclosedArrayElts.get(i))); + } + + if (decoyArrayElts != null) { + decoyArrayElts.entrySet().stream() + .forEach(e -> arrayDisclosureBuilder.withDecoyElt(e.getKey(), e.getValue().getSalt())); + } + + return arrayDisclosureBuilder.build(); + } + + private ArrayNode validateArrayNode(String claimName, JsonNode claimValue) { + return Optional.of(claimValue) + .filter(v -> v.getNodeType() == JsonNodeType.ARRAY) + .map(v -> (ArrayNode) v) + .orElseThrow( + () -> new IllegalArgumentException("Expected array for claim with name: " + claimName)); + } + + private void processArrayElement(ArrayDisclosure.Builder builder, JsonNode elementValue, + DisclosureSpec.DisclosureData disclosureData) { + if (disclosureData != null) { + builder.withUndisclosedElement(disclosureData.getSalt(), elementValue); + } else { + builder.withVisibleElement(elementValue); + } + } + + public IssuerSignedJWT getIssuerSignedJWT() { + return issuerSignedJWT; + } + + public List getDisclosures() { + return disclosures; + } + + /** + * Verifies SD-JWT as to whether the Issuer-signed JWT's signature and disclosures are valid. + * + * @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller + * is responsible for establishing trust in that the keys belong + * to the intended issuer. + * @param verificationOpts Options to parameterize the Issuer-Signed JWT verification. + * @throws VerificationException if verification failed + */ + public void verify( + List issuerVerifyingKeys, + IssuerSignedJwtVerificationOpts verificationOpts + ) throws VerificationException { + sdJwtVerificationContext.verifyIssuance( + issuerVerifyingKeys, + verificationOpts, + null + ); + } + + // builder for SdJwt + public static class Builder { + private DisclosureSpec disclosureSpec; + private JsonNode claimSet; + private Optional keyBindingJWT = Optional.empty(); + private SignatureSignerContext signer; + private final List nestedSdJwts = new ArrayList<>(); + private String hashAlgorithm; + private String jwsType; + + public Builder withDisclosureSpec(DisclosureSpec disclosureSpec) { + this.disclosureSpec = disclosureSpec; + return this; + } + + public Builder withClaimSet(JsonNode claimSet) { + this.claimSet = claimSet; + return this; + } + + public Builder withKeyBindingJWT(KeyBindingJWT keyBindingJWT) { + this.keyBindingJWT = Optional.of(keyBindingJWT); + return this; + } + + public Builder withSigner(SignatureSignerContext signer) { + this.signer = signer; + return this; + } + + public Builder withNestedSdJwt(SdJwt nestedSdJwt) { + nestedSdJwts.add(nestedSdJwt); + return this; + } + + public Builder withHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + public Builder withJwsType(String jwsType) { + this.jwsType = jwsType; + return this; + } + + public SdJwt build() { + return new SdJwt(disclosureSpec, claimSet, nestedSdJwts, keyBindingJWT, signer, hashAlgorithm, jwsType); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtArrayElement.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtArrayElement.java new file mode 100644 index 000000000000..0a3a579e9dbc --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtArrayElement.java @@ -0,0 +1,34 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + * + */ +public interface SdJwtArrayElement { + /** + * Returns the value visibly printed as array element + * in the issuer signed jwt. + */ + public JsonNode getVisibleValue(String hashAlg); + + public String getDisclosureString(); +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtClaim.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtClaim.java new file mode 100644 index 000000000000..bd908fff64bc --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtClaim.java @@ -0,0 +1,38 @@ +/* + * 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.sdjwt; + +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Represents a top level claim in the payload of a JWT. + * + * @author Francis Pouatcha + */ +public interface SdJwtClaim { + + public SdJwtClaimName getClaimName(); + + public String getClaimNameAsString(); + + public JsonNode getVisibleClaimValue(String hashAlgo); + + public List getDisclosureStrings(); + +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtClaimName.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtClaimName.java new file mode 100644 index 000000000000..94ec3654f5e1 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtClaimName.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.sdjwt; + +/** + * Strong typing claim name to avoid parameter mismatch. + * + * Used as map key. Beware of the hashcode and equals implementation. + * + * @author Francis Pouatcha + */ +public class SdJwtClaimName { + private final String claimName; + + public SdJwtClaimName(String claimName) { + this.claimName = SdJwtUtils.requireNonEmpty(claimName, "claimName must not be empty"); + } + + public static SdJwtClaimName of(String claimName) { + return new SdJwtClaimName(claimName); + } + + @Override + public String toString() { + return claimName; + } + + @Override + public int hashCode() { + return claimName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SdJwtClaimName) { + return claimName.equals(((SdJwtClaimName) obj).claimName); + } + return false; + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtFacade.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtFacade.java new file mode 100644 index 000000000000..4c9e719cf8ab --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtFacade.java @@ -0,0 +1,87 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; + +import java.util.List; + +/** + * Simplified service for creating and managing SD-JWTs with easy-to-use methods. + * + * @author Rodrick Awambeng + */ +public class SdJwtFacade { + + private final SignatureSignerContext signer; + private final String hashAlgorithm; + private final String jwsType; + + public SdJwtFacade(SignatureSignerContext signer, String hashAlgorithm, String jwsType) { + this.signer = signer; + this.hashAlgorithm = hashAlgorithm; + this.jwsType = jwsType; + } + + /** + * Create a new SD-JWT with the provided claim set and disclosure specification. + * + * @param claimSet The claim set in JSON format. + * @param disclosureSpec The disclosure specification. + * @return A new SD-JWT. + */ + public SdJwt createSdJwt(JsonNode claimSet, DisclosureSpec disclosureSpec) { + return SdJwt.builder() + .withClaimSet(claimSet) + .withDisclosureSpec(disclosureSpec) + .withSigner(signer) + .withHashAlgorithm(hashAlgorithm) + .withJwsType(jwsType) + .build(); + } + + /** + * Verify the SD-JWT using the provided signature verification keys. + * + * @param sdJwt The SD-JWT to verify. + * @param issuerVerifyingKeys List of issuer verifying keys. + * @param verificationOpts Options for verification. + * @throws VerificationException if verification fails. + */ + public void verifySdJwt(SdJwt sdJwt, List issuerVerifyingKeys, + IssuerSignedJwtVerificationOpts verificationOpts + ) throws VerificationException { + try { + sdJwt.verify(issuerVerifyingKeys, verificationOpts); + } catch (VerificationException e) { + throw new VerificationException("SD-JWT verification failed: " + e.getMessage(), e); + } + } + + /** + * Retrieve the SD-JWT as a string representation. + * + * @param sdJwt The SD-JWT to convert. + * @return The string representation of the SD-JWT. + */ + public String getSdJwtString(SdJwt sdJwt) { + return sdJwt.toString(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtSalt.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtSalt.java new file mode 100644 index 000000000000..ea67a6d2ded1 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtSalt.java @@ -0,0 +1,47 @@ +/* + * 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.sdjwt; + +/** + * Strong typing salt to avoid parameter mismatch. + * + * Comparable to allow sorting in SD-JWT VC. + * + * @author Francis Pouatcha + */ +public class SdJwtSalt implements Comparable { + private final String salt; + + public SdJwtSalt(String salt) { + this.salt = SdJwtUtils.requireNonEmpty(salt, "salt must not be empty"); + } + + // Handy factory method + public static SdJwtSalt of(String salt) { + return new SdJwtSalt(salt); + } + + @Override + public String toString() { + return salt; + } + + @Override + public int compareTo(SdJwtSalt o) { + return salt.compareTo(o.salt); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java new file mode 100644 index 000000000000..add83f7c52cc --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java @@ -0,0 +1,162 @@ +/* + * 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.sdjwt; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Optional; + +import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Base64Url; +import org.keycloak.jose.jws.crypto.HashUtils; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * + * @author Francis Pouatcha + */ +public class SdJwtUtils { + + public static final ObjectMapper mapper = new ObjectMapper(); + private static final SecureRandom RANDOM = new SecureRandom(); + + public static String encodeNoPad(byte[] bytes) { + return Base64Url.encode(bytes); + } + + public static byte[] decodeNoPad(String encoded) { + return Base64Url.decode(encoded); + } + + public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) { + return encodeNoPad(HashUtils.hash(hashAlg, disclosureBytes)); + } + + public static String requireNonEmpty(String str, String message) { + return Optional.ofNullable(str) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException(message)); + } + + public static String randomSalt() { + // 16 bytes for 128-bit entropy. + // Base64url-encoded + return encodeNoPad(randomBytes(16)); + } + + public static byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + RANDOM.nextBytes(bytes); + return bytes; + } + + public static String printJsonArray(Object[] array) throws JsonProcessingException { + if (arrayEltSpaced) { + return arraySpacedPrettyPrinter.writer.writeValueAsString(array); + } else { + return mapper.writeValueAsString(array); + } + } + + public static ArrayNode decodeDisclosureString(String disclosure) throws VerificationException { + JsonNode jsonNode; + + // Decode Base64URL-encoded disclosure + String decoded = new String(decodeNoPad(disclosure)); + + // Parse the disclosure string into a JSON array + try { + jsonNode = mapper.readTree(decoded); + } catch (JsonProcessingException e) { + throw new VerificationException("Disclosure is not a valid JSON", e); + } + + // Check if the parsed JSON is an array + if (!jsonNode.isArray()) { + throw new VerificationException("Disclosure is not a JSON array"); + } + + return (ArrayNode) jsonNode; + } + + public static long readTimeClaim(JsonNode payload, String claimName) throws VerificationException { + JsonNode claim = payload.get(claimName); + if (claim == null || !claim.isNumber()) { + throw new VerificationException("Missing or invalid '" + claimName + "' claim"); + } + + return claim.asLong(); + } + + public static String readClaim(JsonNode payload, String claimName) throws VerificationException { + JsonNode claim = payload.get(claimName); + if (claim == null) { + throw new VerificationException("Missing '" + claimName + "' claim"); + } + + return claim.textValue(); + } + + public static JsonNode deepClone(JsonNode node) { + try { + byte[] serializedNode = mapper.writeValueAsBytes(node); + return mapper.readTree(serializedNode); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static ArraySpacedPrettyPrinter arraySpacedPrettyPrinter = new ArraySpacedPrettyPrinter(); + + static class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter { + final ObjectMapper prettyPrinObjectMapper; + final ObjectWriter writer; + + public ArraySpacedPrettyPrinter() { + prettyPrinObjectMapper = new ObjectMapper(); + prettyPrinObjectMapper.setDefaultPrettyPrinter(this); + writer = prettyPrinObjectMapper.writer(this); + } + + @Override + public void writeArrayValueSeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(','); + jg.writeRaw(' '); + } + + @Override + public void writeObjectEntrySeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(','); + jg.writeRaw(' '); // Add a space after comma + } + + @Override + public void writeObjectFieldValueSeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(':'); + jg.writeRaw(' '); // Add a space after comma + } + } + + public static boolean arrayEltSpaced = true; +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java new file mode 100644 index 000000000000..857a23feacd7 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java @@ -0,0 +1,720 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.jboss.logging.Logger; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.consumer.PresentationRequirements; +import org.keycloak.sdjwt.vp.KeyBindingJWT; +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; + +import java.time.Instant; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Runs SD-JWT verification in isolation with only essential properties. + * + * @author Ingrid Kamga + */ +public class SdJwtVerificationContext { + + private static final Logger logger = Logger.getLogger(SdJwtVerificationContext.class.getName()); + + private String sdJwtVpString; + + private final IssuerSignedJWT issuerSignedJwt; + private final Map disclosures; + private KeyBindingJWT keyBindingJwt; + + public SdJwtVerificationContext( + String sdJwtVpString, + IssuerSignedJWT issuerSignedJwt, + Map disclosures, + KeyBindingJWT keyBindingJwt) { + this(issuerSignedJwt, disclosures); + this.keyBindingJwt = keyBindingJwt; + this.sdJwtVpString = sdJwtVpString; + } + + public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, Map disclosures) { + this.issuerSignedJwt = issuerSignedJwt; + this.disclosures = disclosures; + } + + public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, List disclosureStrings) { + this.issuerSignedJwt = issuerSignedJwt; + this.disclosures = computeDigestDisclosureMap(disclosureStrings); + } + + private Map computeDigestDisclosureMap(List disclosureStrings) { + return disclosureStrings.stream() + .map(disclosureString -> { + String digest = SdJwtUtils.hashAndBase64EncodeNoPad( + disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg()); + return new AbstractMap.SimpleEntry<>(digest, disclosureString); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Verifies SD-JWT as to whether the Issuer-signed JWT's signature and disclosures are valid. + * + *

    Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that:

    + * - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid, and + * - all Disclosures are valid and correspond to a respective digest value in the Issuer-signed JWT + * (directly in the payload or recursively included in the contents of other Disclosures). + * + * @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller + * is responsible for establishing trust in that the keys belong + * to the intended issuer. + * @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. + * @param presentationRequirements If set, the presentation requirements will be enforced upon fully + * disclosing the Issuer-signed JWT during the verification. + * @throws VerificationException if verification failed + */ + public void verifyIssuance( + List issuerVerifyingKeys, + IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts, + PresentationRequirements presentationRequirements + ) throws VerificationException { + // Validate the Issuer-signed JWT. + validateIssuerSignedJwt(issuerVerifyingKeys); + + // Validate disclosures. + JsonNode disclosedPayload = validateDisclosuresDigests(); + + // Validate time claims. + // Issuers will typically include claims controlling the validity of the SD-JWT in plaintext in the + // SD-JWT payload, but there is no guarantee they would do so. Therefore, Verifiers cannot reliably + // depend on that and need to operate as though security-critical claims might be selectively disclosable. + validateIssuerSignedJwtTimeClaims(disclosedPayload, issuerSignedJwtVerificationOpts); + + // Enforce presentation requirements. + if (presentationRequirements != null) { + presentationRequirements.checkIfSatisfiedBy(disclosedPayload); + } + } + + /** + * Verifies SD-JWT presentation. + * + *

    + * Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}, Verifiers need + * to ensure that if Key Binding is required, the Key Binding JWT is signed by the Holder and valid. + *

    + * + * @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller + * is responsible for establishing trust in that the keys belong + * to the intended issuer. + * @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. + * @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification. + * Must, among others, specify the Verifier's policy whether + * to check Key Binding. + * @param presentationRequirements If set, the presentation requirements will be enforced upon fully + * disclosing the Issuer-signed JWT during the verification. + * @throws VerificationException if verification failed + */ + public void verifyPresentation( + List issuerVerifyingKeys, + IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts, + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts, + PresentationRequirements presentationRequirements + ) throws VerificationException { + // If Key Binding is required and a Key Binding JWT is not provided, + // the Verifier MUST reject the Presentation. + if (keyBindingJwtVerificationOpts.isKeyBindingRequired() && keyBindingJwt == null) { + throw new VerificationException("Missing Key Binding JWT"); + } + + // Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}... + verifyIssuance(issuerVerifyingKeys, issuerSignedJwtVerificationOpts, presentationRequirements); + + // Validate Key Binding JWT if required + if (keyBindingJwtVerificationOpts.isKeyBindingRequired()) { + validateKeyBindingJwt(keyBindingJwtVerificationOpts); + } + } + + /** + * Validate Issuer-signed JWT + * + *

    + * Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that: + * - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid + *

    + * + * @param verifiers Verifying keys for validating the Issuer-signed JWT. + * @throws VerificationException if verification failed + */ + private void validateIssuerSignedJwt( + List verifiers + ) throws VerificationException { + // Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure + issuerSignedJwt.verifySdHashAlgorithm(); + + // Validate the signature over the Issuer-signed JWT + Iterator iterator = verifiers.iterator(); + while (iterator.hasNext()) { + try { + SignatureVerifierContext verifier = iterator.next(); + issuerSignedJwt.verifySignature(verifier); + return; + } catch (VerificationException e) { + logger.debugf(e, "Issuer-signed JWT's signature verification failed against one potential verifying key"); + if (iterator.hasNext()) { + logger.debugf("Retrying Issuer-signed JWT's signature verification with next potential verifying key"); + } + } + } + + // No potential verifier could verify the JWT's signature + throw new VerificationException("Invalid Issuer-Signed JWT: Signature could not be verified"); + } + + /** + * Validate Key Binding JWT + * + * @throws VerificationException if verification failed + */ + private void validateKeyBindingJwt( + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts + ) throws VerificationException { + // Check that the typ of the Key Binding JWT is kb+jwt + validateKeyBindingJwtTyp(); + + // Determine the public key for the Holder from the SD-JWT + JsonNode cnf = issuerSignedJwt.getCnfClaim().orElseThrow( + () -> new VerificationException("No cnf claim in Issuer-signed JWT for key binding") + ); + + // Ensure that a signing algorithm was used that was deemed secure for the application. + // The none algorithm MUST NOT be accepted. + SignatureVerifierContext holderVerifier = buildHolderVerifier(cnf); + + // Validate the signature over the Key Binding JWT + try { + keyBindingJwt.verifySignature(holderVerifier); + } catch (VerificationException e) { + throw new VerificationException("Key binding JWT invalid", e); + } + + // Check that the creation time of the Key Binding JWT is within an acceptable window. + validateKeyBindingJwtTimeClaims(keyBindingJwtVerificationOpts); + + // Determine that the Key Binding JWT is bound to the current transaction and was created + // for this Verifier (replay protection) by validating nonce and aud claims. + preventKeyBindingJwtReplay(keyBindingJwtVerificationOpts); + + // The same hash algorithm as for the Disclosures MUST be used (defined by the _sd_alg element + // in the Issuer-signed JWT or the default value, as defined in Section 5.1.1). + validateKeyBindingJwtSdHashIntegrity(); + + // Check that the Key Binding JWT is a valid JWT in all other respects + // -> Covered in part by `keyBindingJwt` being an instance of SdJws? + // -> Time claims are checked above + } + + /** + * Validate Key Binding JWT's typ header attribute + * + * @throws VerificationException if verification failed + */ + private void validateKeyBindingJwtTyp() throws VerificationException { + String typ = keyBindingJwt.getHeader().getType(); + if (!typ.equals(KeyBindingJWT.TYP)) { + throw new VerificationException("Key Binding JWT is not of declared typ " + KeyBindingJWT.TYP); + } + } + + /** + * Build holder verifier from JWK node. + * + * @throws VerificationException if unable + */ + private SignatureVerifierContext buildHolderVerifier(JsonNode cnf) throws VerificationException { + Objects.requireNonNull(cnf); + + // Read JWK + JsonNode cnfJwk = cnf.get("jwk"); + if (cnfJwk == null) { + throw new UnsupportedOperationException("Only cnf/jwk claim supported"); + } + + // Convert JWK + try { + return JwkParsingUtils.convertJwkNodeToVerifierContext(cnfJwk); + } catch (Exception e) { + throw new VerificationException("Could not process cnf/jwk", e); + } + } + + /** + * Validate Issuer-Signed JWT time claims. + * + *

    + * Check that the SD-JWT is valid using claims such as nbf, iat, and exp in the processed payload. + * If a required validity-controlling claim is missing, the SD-JWT MUST be rejected. + *

    + * + * @throws VerificationException if verification failed + */ + private void validateIssuerSignedJwtTimeClaims( + JsonNode payload, + IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts + ) throws VerificationException { + long now = Instant.now().getEpochSecond(); + + try { + if (issuerSignedJwtVerificationOpts.mustValidateIssuedAtClaim() + && now < SdJwtUtils.readTimeClaim(payload, "iat")) { + throw new VerificationException("JWT issued in the future"); + } + } catch (VerificationException e) { + throw new VerificationException("Issuer-Signed JWT: Invalid `iat` claim", e); + } + + try { + if (issuerSignedJwtVerificationOpts.mustValidateExpirationClaim() + && now >= SdJwtUtils.readTimeClaim(payload, "exp")) { + throw new VerificationException("JWT has expired"); + } + } catch (VerificationException e) { + throw new VerificationException("Issuer-Signed JWT: Invalid `exp` claim", e); + } + + try { + if (issuerSignedJwtVerificationOpts.mustValidateNotBeforeClaim() + && now < SdJwtUtils.readTimeClaim(payload, "nbf")) { + throw new VerificationException("JWT is not yet valid"); + } + } catch (VerificationException e) { + throw new VerificationException("Issuer-Signed JWT: Invalid `nbf` claim", e); + } + } + + /** + * Validate key binding JWT time claims. + * + * @throws VerificationException if verification failed + */ + private void validateKeyBindingJwtTimeClaims( + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts + ) throws VerificationException { + // Check that the creation time of the Key Binding JWT, as determined by the iat claim, + // is within an acceptable window + + try { + keyBindingJwt.verifyIssuedAtClaim(); + } catch (VerificationException e) { + throw new VerificationException("Key binding JWT: Invalid `iat` claim", e); + } + + try { + keyBindingJwt.verifyAge(keyBindingJwtVerificationOpts.getAllowedMaxAge()); + } catch (VerificationException e) { + throw new VerificationException("Key binding JWT is too old"); + } + + // Check other time claims + + try { + if (keyBindingJwtVerificationOpts.mustValidateExpirationClaim()) { + keyBindingJwt.verifyExpClaim(); + } + } catch (VerificationException e) { + throw new VerificationException("Key binding JWT: Invalid `exp` claim", e); + } + + try { + if (keyBindingJwtVerificationOpts.mustValidateNotBeforeClaim()) { + keyBindingJwt.verifyNotBeforeClaim(); + } + } catch (VerificationException e) { + throw new VerificationException("Key binding JWT: Invalid `nbf` claim", e); + } + } + + /** + * Validate disclosures' digests + * + *

    + * Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that: + * - all Disclosures are valid and correspond to a respective digest value in the Issuer-signed JWT + * (directly in the payload or recursively included in the contents of other Disclosures) + *

    + * + *

    + * We additionally check that salt values are not reused: + * The salt value MUST be unique for each claim that is to be selectively disclosed. + *

    + * + * @return the fully disclosed SdJwt payload + * @throws VerificationException if verification failed + */ + private JsonNode validateDisclosuresDigests() throws VerificationException { + // Validate SdJwt digests by attempting full recursive disclosing. + Set visitedSalts = new HashSet<>(); + Set visitedDigests = new HashSet<>(); + Set visitedDisclosureStrings = new HashSet<>(); + JsonNode disclosedPayload = validateViaRecursiveDisclosing( + SdJwtUtils.deepClone(issuerSignedJwt.getPayload()), + visitedSalts, visitedDigests, visitedDisclosureStrings); + + // Validate all disclosures where visited + validateDisclosuresVisits(visitedDisclosureStrings); + + return disclosedPayload; + } + + /** + * Validate SdJwt digests by attempting full recursive disclosing. + * + *

    + * By recursively disclosing all disclosable fields in the SdJwt payload, validation rules are + * enforced regarding the conformance of linked disclosures. Additional rules should be enforced + * after calling this method based on the visited data arguments. + *

    + * + * @return the fully disclosed SdJwt payload + */ + private JsonNode validateViaRecursiveDisclosing( + JsonNode currentNode, + Set visitedSalts, + Set visitedDigests, + Set visitedDisclosureStrings + ) throws VerificationException { + if (!currentNode.isObject() && !currentNode.isArray()) { + return currentNode; + } + + // Find all objects having an _sd key that refers to an array of strings. + if (currentNode.isObject()) { + ObjectNode currentObjectNode = ((ObjectNode) currentNode); + + JsonNode sdArray = currentObjectNode.get(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE); + if (sdArray != null && sdArray.isArray()) { + for (JsonNode el : sdArray) { + if (!el.isTextual()) { + throw new VerificationException( + "Unexpected non-string element inside _sd array: " + el + ); + } + + // Compare the value with the digests calculated previously and find the matching Disclosure. + // If no such Disclosure can be found, the digest MUST be ignored. + + String digest = el.asText(); + markDigestAsVisited(digest, visitedDigests); + String disclosure = disclosures.get(digest); + + if (disclosure != null) { + // Mark disclosure as visited + visitedDisclosureStrings.add(disclosure); + + // Validate disclosure format + DisclosureFields decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure); + + // Mark salt as visited + markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts); + + // Insert, at the level of the _sd key, a new claim using the claim name + // and claim value from the Disclosure + currentObjectNode.set( + decodedDisclosure.getClaimName(), + decodedDisclosure.getClaimValue() + ); + } + } + } + + // Remove all _sd keys and their contents from the Issuer-signed JWT payload. + // If this results in an object with no properties, it should be represented as an empty object {} + currentObjectNode.remove(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE); + + // Remove the claim _sd_alg from the SD-JWT payload. + currentObjectNode.remove(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM); + } + + // Find all array elements that are objects with one key, that key being ... and referring to a string + if (currentNode.isArray()) { + ArrayNode currentArrayNode = ((ArrayNode) currentNode); + ArrayList indexesToRemove = new ArrayList<>(); + + for (int i = 0; i < currentArrayNode.size(); ++i) { + JsonNode itemNode = currentArrayNode.get(i); + if (itemNode.isObject() && itemNode.size() == 1) { + // Check single "..." field + Map.Entry field = itemNode.fields().next(); + if (field.getKey().equals(UndisclosedArrayElement.SD_CLAIM_NAME) + && field.getValue().isTextual()) { + // Compare the value with the digests calculated previously and find the matching Disclosure. + // If no such Disclosure can be found, the digest MUST be ignored. + + String digest = field.getValue().asText(); + markDigestAsVisited(digest, visitedDigests); + String disclosure = disclosures.get(digest); + + if (disclosure != null) { + // Mark disclosure as visited + visitedDisclosureStrings.add(disclosure); + + // Validate disclosure format + DisclosureFields decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure); + + // Mark salt as visited + markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts); + + // Replace the array element with the value from the Disclosure. + // Removal is done below. + currentArrayNode.set(i, decodedDisclosure.getClaimValue()); + } else { + // Remove all array elements for which the digest was not found in the previous step. + indexesToRemove.add(i); + } + } + } + } + + // Remove all array elements for which the digest was not found in the previous step. + indexesToRemove.forEach(currentArrayNode::remove); + } + + for (JsonNode childNode : currentNode) { + validateViaRecursiveDisclosing(childNode, visitedSalts, visitedDigests, visitedDisclosureStrings); + } + + return currentNode; + } + + /** + * Mark digest as visited. + * + *

    + * If any digest value is encountered more than once in the Issuer-signed JWT payload + * (directly or recursively via other Disclosures), the SD-JWT MUST be rejected. + *

    + * + * @throws VerificationException if not first visit + */ + private void markDigestAsVisited(String digest, Set visitedDigests) + throws VerificationException { + if (!visitedDigests.add(digest)) { + // If add returns false, then it is a duplicate + throw new VerificationException("A digest was encountered more than once: " + digest); + } + } + + /** + * Mark salt as visited. + * + *

    + * The salt value MUST be unique for each claim that is to be selectively disclosed. + *

    + * + * @throws VerificationException if not first visit + */ + private void markSaltAsVisited(String salt, Set visitedSalts) + throws VerificationException { + if (!visitedSalts.add(salt)) { + // If add returns false, then it is a duplicate + throw new VerificationException("A salt value was reused: " + salt); + } + } + + /** + * Validate disclosure assuming digest was found in an object's _sd key. + * + *

    + * If the contents of the respective Disclosure is not a JSON-encoded array of three elements + * (salt, claim name, claim value), the SD-JWT MUST be rejected. + *

    + * + *

    + * If the claim name is _sd or ..., the SD-JWT MUST be rejected. + *

    + * + * @return decoded disclosure (salt, claim name, claim value) + */ + private DisclosureFields validateSdArrayDigestDisclosureFormat(String disclosure) + throws VerificationException { + ArrayNode arrayNode = SdJwtUtils.decodeDisclosureString(disclosure); + + // Check if the array has exactly three elements + if (arrayNode.size() != 3) { + throw new VerificationException("A field disclosure must contain exactly three elements"); + } + + // If the claim name is _sd or ..., the SD-JWT MUST be rejected. + + List denylist = Arrays.asList( + IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE, + UndisclosedArrayElement.SD_CLAIM_NAME + ); + + String claimName = arrayNode.get(1).asText(); + if (denylist.contains(claimName)) { + throw new VerificationException("Disclosure claim name must not be '_sd' or '...'"); + } + + // Return decoded disclosure + return new DisclosureFields( + arrayNode.get(0).asText(), + claimName, + arrayNode.get(2) + ); + } + + /** + * Validate disclosure assuming digest was found as an undisclosed array element. + * + *

    + * If the contents of the respective Disclosure is not a JSON-encoded array of + * two elements (salt, value), the SD-JWT MUST be rejected. + *

    + * + * @return decoded disclosure (salt, value) + */ + private DisclosureFields validateArrayElementDigestDisclosureFormat(String disclosure) + throws VerificationException { + ArrayNode arrayNode = SdJwtUtils.decodeDisclosureString(disclosure); + + // Check if the array has exactly two elements + if (arrayNode.size() != 2) { + throw new VerificationException("An array element disclosure must contain exactly two elements"); + } + + // Return decoded disclosure + return new DisclosureFields( + arrayNode.get(0).asText(), + null, + arrayNode.get(1) + ); + } + + /** + * Validate all disclosures where visited + * + *

    + * If any Disclosure was not referenced by digest value in the Issuer-signed JWT (directly or recursively via + * other Disclosures), the SD-JWT MUST be rejected. + *

    + * + * @throws VerificationException if not the case + */ + private void validateDisclosuresVisits(Set visitedDisclosureStrings) + throws VerificationException { + if (visitedDisclosureStrings.size() < disclosures.size()) { + throw new VerificationException("At least one disclosure is not protected by digest"); + } + } + + /** + * Run checks for replay protection. + * + *

    + * Determine that the Key Binding JWT is bound to the current transaction and was created for this + * Verifier (replay protection) by validating nonce and aud claims. + *

    + * + * @throws VerificationException if verification failed + */ + private void preventKeyBindingJwtReplay( + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts + ) throws VerificationException { + JsonNode nonce = keyBindingJwt.getPayload().get("nonce"); + if (nonce == null || !nonce.isTextual() + || !nonce.asText().equals(keyBindingJwtVerificationOpts.getNonce())) { + throw new VerificationException("Key binding JWT: Unexpected `nonce` value"); + } + + JsonNode aud = keyBindingJwt.getPayload().get("aud"); + if (aud == null || !aud.isTextual() + || !aud.asText().equals(keyBindingJwtVerificationOpts.getAud())) { + throw new VerificationException("Key binding JWT: Unexpected `aud` value"); + } + } + + /** + * Validate integrity of Key Binding JWT's sd_hash. + * + *

    + * Calculate the digest over the Issuer-signed JWT and Disclosures and verify that it matches + * the value of the sd_hash claim in the Key Binding JWT. + *

    + * + * @throws VerificationException if verification failed + */ + private void validateKeyBindingJwtSdHashIntegrity() throws VerificationException { + Objects.requireNonNull(sdJwtVpString); + + JsonNode sdHash = keyBindingJwt.getPayload().get("sd_hash"); + if (sdHash == null || !sdHash.isTextual()) { + throw new VerificationException("Key binding JWT: Claim `sd_hash` missing or not a string"); + } + + int lastDelimiterIndex = sdJwtVpString.lastIndexOf(SdJwt.DELIMITER); + String toHash = sdJwtVpString.substring(0, lastDelimiterIndex + 1); + + String digest = SdJwtUtils.hashAndBase64EncodeNoPad( + toHash.getBytes(), issuerSignedJwt.getSdHashAlg()); + + if (!digest.equals(sdHash.asText())) { + throw new VerificationException("Key binding JWT: Invalid `sd_hash` digest"); + } + } + + /** + * Plain record for disclosure fields. + */ + private static class DisclosureFields { + String saltValue; + String claimName; + JsonNode claimValue; + + public DisclosureFields(String saltValue, String claimName, JsonNode claimValue) { + this.saltValue = saltValue; + this.claimName = claimName; + this.claimValue = claimValue; + } + + public String getSaltValue() { + return saltValue; + } + + public String getClaimName() { + return claimName; + } + + public JsonNode getClaimValue() { + return claimValue; + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java b/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java new file mode 100644 index 000000000000..da6aaf131470 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java @@ -0,0 +1,70 @@ +/* + * 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.sdjwt; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement { + public static final String SD_CLAIM_NAME = "..."; + private final JsonNode arrayElement; + + private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) { + super(salt); + this.arrayElement = arrayElement; + } + + @Override + public JsonNode getVisibleValue(String hashAlg) { + return SdJwtUtils.mapper.createObjectNode().put(SD_CLAIM_NAME, getDisclosureDigest(hashAlg)); + } + + @Override + Object[] toArray() { + return new Object[] { getSaltAsString(), arrayElement }; + } + + public static class Builder { + private SdJwtSalt salt; + private JsonNode arrayElement; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder withArrayElement(JsonNode arrayElement) { + this.arrayElement = arrayElement; + return this; + } + + public UndisclosedArrayElement build() { + arrayElement = Objects.requireNonNull(arrayElement, "arrayElement must not be null"); + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new UndisclosedArrayElement(salt, arrayElement); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/UndisclosedClaim.java b/core/src/main/java/org/keycloak/sdjwt/UndisclosedClaim.java new file mode 100644 index 000000000000..cc8744b38a81 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/UndisclosedClaim.java @@ -0,0 +1,98 @@ +/* + * 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.sdjwt; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class UndisclosedClaim extends Disclosable implements SdJwtClaim { + private final SdJwtClaimName claimName; + private final JsonNode claimValue; + + private UndisclosedClaim(SdJwtClaimName claimName, SdJwtSalt salt, JsonNode claimValue) { + super(salt); + this.claimName = claimName; + this.claimValue = claimValue; + } + + @Override + Object[] toArray() { + return new Object[] { getSaltAsString(), getClaimNameAsString(), claimValue }; + } + + @Override + public SdJwtClaimName getClaimName() { + return claimName; + } + + @Override + public String getClaimNameAsString() { + return claimName.toString(); + } + + /** + * Recall no info is visible on these claims in the JWT. + */ + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + throw new UnsupportedOperationException("Unimplemented method 'getVisibleClaimValue'"); + } + + public static class Builder { + private SdJwtClaimName claimName; + private SdJwtSalt salt; + private JsonNode claimValue; + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder withClaimValue(JsonNode claimValue) { + this.claimValue = claimValue; + return this; + } + + public UndisclosedClaim build() { + claimName = Objects.requireNonNull(claimName, "claimName must not be null"); + claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null"); + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new UndisclosedClaim(claimName, salt, claimValue); + } + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public List getDisclosureStrings() { + return Collections.singletonList(getDisclosureString()); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/VisibleArrayElement.java b/core/src/main/java/org/keycloak/sdjwt/VisibleArrayElement.java new file mode 100644 index 000000000000..5e7a03ad0217 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/VisibleArrayElement.java @@ -0,0 +1,41 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class VisibleArrayElement implements SdJwtArrayElement { + private final JsonNode arrayElement; + + public VisibleArrayElement(JsonNode arrayElement) { + this.arrayElement = arrayElement; + } + + @Override + public JsonNode getVisibleValue(String hashAlg) { + return arrayElement; + } + + @Override + public String getDisclosureString() { + return null; + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/VisibleSdJwtClaim.java b/core/src/main/java/org/keycloak/sdjwt/VisibleSdJwtClaim.java new file mode 100644 index 000000000000..f36ca80e6569 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/VisibleSdJwtClaim.java @@ -0,0 +1,73 @@ +/* + * 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.sdjwt; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class VisibleSdJwtClaim extends AbstractSdJwtClaim { + private final JsonNode claimValue; + + public VisibleSdJwtClaim(SdJwtClaimName claimName, JsonNode claimValue) { + super(claimName); + this.claimValue = claimValue; + } + + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + return claimValue; + } + + // Static method to create a builder instance + public static Builder builder() { + return new Builder(); + } + + // Static inner Builder class + public static class Builder { + private SdJwtClaimName claimName; + private JsonNode claimValue; + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withClaimValue(JsonNode claimValue) { + this.claimValue = claimValue; + return this; + } + + public VisibleSdJwtClaim build() { + claimName = Objects.requireNonNull(claimName, "claimName must not be null"); + claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null"); + return new VisibleSdJwtClaim(claimName, claimValue); + } + } + + @Override + public List getDisclosureStrings() { + return Collections.emptyList(); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/HttpDataFetcher.java b/core/src/main/java/org/keycloak/sdjwt/consumer/HttpDataFetcher.java new file mode 100644 index 000000000000..12cc8b4932c7 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/HttpDataFetcher.java @@ -0,0 +1,34 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +/** + * @author Ingrid Kamga + */ +public interface HttpDataFetcher { + + /** + * Performs an HTTP GET at the URI and parses the response as JSON + * @throws IOException if I/O error or HTTP status not OK (200) + */ + JsonNode fetchJsonData(String uri) throws IOException; +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadata.java b/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadata.java new file mode 100644 index 000000000000..51ce2f1e0c3e --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadata.java @@ -0,0 +1,65 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.jose.jwk.JSONWebKeySet; + +/** + * POJO for JWT VC Metadata + * + * @author Ingrid Kamga + * @see + * JWT VC Issuer Metadata Response + * + */ +public class JwtVcMetadata { + + @JsonProperty("issuer") + private String issuer; + + @JsonProperty("jwks_uri") + private String jwksUri; + + @JsonProperty("jwks") + private JSONWebKeySet jwks; + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + public JSONWebKeySet getJwks() { + return jwks; + } + + public void setJwks(JSONWebKeySet jwks) { + this.jwks = jwks; + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuer.java b/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuer.java new file mode 100644 index 000000000000..e7ed03831e1c --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuer.java @@ -0,0 +1,228 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.JwkParsingUtils; +import org.keycloak.sdjwt.SdJws; +import org.keycloak.sdjwt.SdJwtUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * A trusted Issuer for running SD-JWT VP verification. + * + *

    + * This implementation targets issuers exposing verifying keys on a normalized JWT VC Issuer metadata endpoint. + *

    + * + * @author Ingrid Kamga + * @see + * JWT VC Issuer Metadata + * + */ +public class JwtVcMetadataTrustedSdJwtIssuer implements TrustedSdJwtIssuer { + + private static final String JWT_VC_ISSUER_END_POINT = "/.well-known/jwt-vc-issuer"; + + private final Pattern issuerUriPattern; + private final HttpDataFetcher httpDataFetcher; + + /** + * @param issuerUri a trusted issuer URI + */ + public JwtVcMetadataTrustedSdJwtIssuer(String issuerUri, HttpDataFetcher httpDataFetcher) { + try { + validateHttpsIssuerUri(issuerUri); + } catch (VerificationException e) { + throw new IllegalArgumentException(e); + } + + // Build a Regex pattern to only match the argument URI + this.issuerUriPattern = Pattern.compile(Pattern.quote(issuerUri)); + + // Assign HttpDataFetcher implementation + this.httpDataFetcher = httpDataFetcher; + } + + /** + * @param issuerUriPattern a regex pattern for trusted issuer URIs + */ + public JwtVcMetadataTrustedSdJwtIssuer(Pattern issuerUriPattern, HttpDataFetcher httpDataFetcher) { + this.issuerUriPattern = issuerUriPattern; + this.httpDataFetcher = httpDataFetcher; + } + + @Override + public List resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT) + throws VerificationException { + // Read iss (claim) and kid (header) + String iss = Optional.ofNullable(issuerSignedJWT.getPayload().get(SdJws.CLAIM_NAME_ISSUER)) + .map(JsonNode::asText) + .orElse(""); + String kid = issuerSignedJWT.getHeader().getKeyId(); + + // Match the read iss claim against the trusted pattern + Matcher matcher = issuerUriPattern.matcher(iss); + if (!matcher.matches()) { + throw new VerificationException(String.format( + "Unexpected Issuer URI claim. Expected=/%s/, Got=%s", + issuerUriPattern.pattern(), iss + )); + } + + // As per specs, only HTTPS URIs are supported + validateHttpsIssuerUri(iss); + + // Fetch exposed JWKs + List jwks = fetchIssuerMetadataJwks(iss); + if (jwks.isEmpty()) { + throw new VerificationException( + String.format("Issuer JWKs were unexpectedly resolved to an empty list. Issuer URI: %s", iss) + ); + } + + // If kid specified, only consider the (single) matching key + if (kid != null) { + List matchingJwks = jwks.stream() + .filter(jwk -> { + String jwkKid = jwk.getKeyId(); + return jwkKid != null && jwkKid.equals(kid); + }) + .collect(Collectors.toList()); + + if (matchingJwks.isEmpty()) { + throw new VerificationException( + String.format("No published JWK was found to match kid: %s", kid) + ); + } + + if (matchingJwks.size() > 1) { + throw new VerificationException( + String.format("Cannot choose between multiple exposed JWKs with same kid: %s", kid) + ); + } + + jwks = Collections.singletonList(matchingJwks.get(0)); + } + + // Build SignatureVerifierContext's + List verifiers = new ArrayList<>(); + for (JWK jwk : jwks) { + try { + verifiers.add(JwkParsingUtils.convertJwkToVerifierContext(jwk)); + } catch (Exception e) { + throw new VerificationException("A potential JWK was retrieved but found invalid", e); + } + } + + return verifiers; + } + + private void validateHttpsIssuerUri(String issuerUri) throws VerificationException { + if (!issuerUri.startsWith("https://")) { + throw new VerificationException( + "HTTPS URI required to retrieve JWT VC Issuer Metadata" + ); + } + } + + private List fetchIssuerMetadataJwks(String issuerUri) throws VerificationException { + // Build full URL to JWT VC metadata endpoint + + issuerUri = normalizeUri(issuerUri); + String jwtVcIssuerUri = issuerUri + .concat(JWT_VC_ISSUER_END_POINT); // Append well-known path + + // Fetch and parse metadata + + JwtVcMetadata issuerMetadata; + JsonNode issuerMetadataNode = fetchData(jwtVcIssuerUri); + + try { + issuerMetadata = SdJwtUtils.mapper.treeToValue(issuerMetadataNode, JwtVcMetadata.class); + } catch (JsonProcessingException e) { + throw new VerificationException("Failed to parse exposed JWT VC Metadata", e); + } + + // Validate metadata + + String exposedIssuerUri = normalizeUri(issuerMetadata.getIssuer()); + + if (!issuerUri.equals(exposedIssuerUri)) { + throw new VerificationException(String.format( + "Unexpected metadata's issuer. Expected=%s, Got=%s", + issuerUri, exposedIssuerUri + )); + } + + // Extract exposed JWKS (including dereferencing if necessary) + + String jwksUri = issuerMetadata.getJwksUri(); + JSONWebKeySet jwks = issuerMetadata.getJwks(); + + if (jwks == null && jwksUri != null) { + // Dereference JWKS URI + JsonNode jwksNode = fetchData(jwksUri); + + // Parse fetched JWKS + try { + jwks = SdJwtUtils.mapper.treeToValue(jwksNode, JSONWebKeySet.class); + } catch (JsonProcessingException e) { + throw new VerificationException("Failed to parse exposed JWKS", e); + } + } + + if (jwks == null || jwks.getKeys() == null) { + throw new VerificationException( + String.format("Could not resolve issuer JWKs with URI: %s", issuerUri)); + } + + return Arrays.asList(jwks.getKeys()); + } + + private JsonNode fetchData(String uri) throws VerificationException { + try { + return Objects.requireNonNull(httpDataFetcher.fetchJsonData(uri)); + } catch (Exception exception) { + throw new VerificationException( + String.format("Could not fetch data from URI: %s", uri), + exception + ); + } + } + + private String normalizeUri(String uri) { + // Remove any trailing slash + return uri.replaceAll("/$", ""); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/PresentationRequirements.java b/core/src/main/java/org/keycloak/sdjwt/consumer/PresentationRequirements.java new file mode 100644 index 000000000000..bda0175e2ff3 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/PresentationRequirements.java @@ -0,0 +1,43 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.VerificationException; + +/** + * Presentation requirements to constrain the kind of credential expected. + * + *

    + * This mirrors the idea of the expressive + * DIF Presentation Definition, + * while enabling simpler alternatives. + *

    + * + * @author Ingrid Kamga + */ +public interface PresentationRequirements { + + /** + * Ensures that the configured requirements are satisfied by the presentation. + * + * @param disclosedPayload The fully disclosed Issuer-signed JWT of the presented token. + * @throws VerificationException if the configured requirements are not satisfied. + */ + void checkIfSatisfiedBy(JsonNode disclosedPayload) throws VerificationException; +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumer.java b/core/src/main/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumer.java new file mode 100644 index 000000000000..4087fd226e00 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumer.java @@ -0,0 +1,78 @@ +/* + * 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.sdjwt.consumer; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.util.ArrayList; +import java.util.List; + +/** + * A component for consuming (verifying) SD-JWT presentations. + * + *

    + * The purpose is to streamline SD-JWT VP verification beyond signature + * and disclosure checks of {@link org.keycloak.sdjwt.SdJwtVerificationContext} + *

    + * + * @author Ingrid Kamga + */ +public class SdJwtPresentationConsumer { + + /** + * Verify SD-JWT presentation against specific requirements. + * + * @param sdJwtVP the presentation to verify + * @param presentationRequirements the requirements on presented claims + * @param trustedSdJwtIssuers trusted issuers for the verification + * @param issuerSignedJwtVerificationOpts policy for Issuer-signed JWT verification + * @param keyBindingJwtVerificationOpts policy for Key-binding JWT verification + * @throws VerificationException if the verification fails for some reason + */ + public void verifySdJwtPresentation( + SdJwtVP sdJwtVP, + PresentationRequirements presentationRequirements, + List trustedSdJwtIssuers, + IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts, + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts + ) throws VerificationException { + // Retrieve verifying keys for Issuer-signed JWT + IssuerSignedJWT issuerSignedJWT = sdJwtVP.getIssuerSignedJWT(); + List issuerVerifyingKeys = new ArrayList<>(); + for (TrustedSdJwtIssuer trustedSdJwtIssuer : trustedSdJwtIssuers) { + List keys = trustedSdJwtIssuer + .resolveIssuerVerifyingKeys(issuerSignedJWT); + issuerVerifyingKeys.addAll(keys); + } + + // Verify the SD-JWT token cryptographically + // Pass presentation requirements to enforce that the presented token meets them + sdJwtVP.getSdJwtVerificationContext() + .verifyPresentation( + issuerVerifyingKeys, + issuerSignedJwtVerificationOpts, + keyBindingJwtVerificationOpts, + presentationRequirements + ); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinition.java b/core/src/main/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinition.java new file mode 100644 index 000000000000..e874a9cd34c9 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinition.java @@ -0,0 +1,104 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.VerificationException; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple presentation definition of the kind of credential expected. + * + *

    + * The credential's type and required claims are configured using regex patterns. + * The values of these fields are JSON-ified prior to matching the regex pattern. + *

    + * + * @author Ingrid Kamga + */ +public class SimplePresentationDefinition implements PresentationRequirements { + + private final Map requirements; + + public SimplePresentationDefinition(Map requirements) { + this.requirements = requirements; + } + + /** + * Checks if the provided JSON payload satisfies all required field patterns. + * + *

    + * For each required field, the corresponding JSON field value in the disclosed Issuer-signed JWT's payload + * is matched against the associated regex pattern. If any required field is missing or does not match the + * pattern, a {@link VerificationException} is thrown. + *

    + * + * @param disclosedPayload The fully disclosed Issuer-signed JWT of the presented token. + * @throws VerificationException If any required field is missing or fails the pattern check. + */ + @Override + public void checkIfSatisfiedBy(JsonNode disclosedPayload) throws VerificationException { + for (Map.Entry requirement : requirements.entrySet()) { + String field = requirement.getKey(); + Pattern pattern = requirement.getValue(); + + // Retrieve the value of the required field from the payload + JsonNode presented = disclosedPayload.get(field); + + // Check if the required field is present in the payload + if (presented == null || presented.isNull()) { + throw new VerificationException( + String.format("A required field was not presented: `%s`", field) + ); + } + + // Extract the JSON representation of the field's value + String json = presented.toString(); + + // Match the field value against the configured regex pattern + Matcher matcher = pattern.matcher(json); + if (!matcher.matches()) { + throw new VerificationException(String.format( + "Pattern matching failed for required field: `%s`. Expected pattern: /%s/, but got: %s", + field, pattern.pattern(), json + )); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map requirements = new HashMap<>(); + + public Builder addClaimRequirement(String field, String regexPattern) { + this.requirements.put(field, Pattern.compile(regexPattern)); + return this; + } + + public SimplePresentationDefinition build() { + return new SimplePresentationDefinition(requirements); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/StaticTrustedSdJwtIssuer.java b/core/src/main/java/org/keycloak/sdjwt/consumer/StaticTrustedSdJwtIssuer.java new file mode 100644 index 000000000000..2490b229c878 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/StaticTrustedSdJwtIssuer.java @@ -0,0 +1,42 @@ +/* + * 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.sdjwt.consumer; + +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.IssuerSignedJWT; + +import java.util.List; + +/** + * A trusted Issuer for running SD-JWT VP verification. + * + * @author Ingrid Kamga + */ +public class StaticTrustedSdJwtIssuer implements TrustedSdJwtIssuer { + + private final List signatureVerifierContexts; + + public StaticTrustedSdJwtIssuer(List signatureVerifierContexts) { + this.signatureVerifierContexts = signatureVerifierContexts; + } + + @Override + public List resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT) { + return signatureVerifierContexts; + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/consumer/TrustedSdJwtIssuer.java b/core/src/main/java/org/keycloak/sdjwt/consumer/TrustedSdJwtIssuer.java new file mode 100644 index 000000000000..f72020a50f3f --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/consumer/TrustedSdJwtIssuer.java @@ -0,0 +1,43 @@ +/* + * 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.sdjwt.consumer; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.IssuerSignedJWT; + +import java.util.List; + +/** + * A trusted Issuer for running SD-JWT VP verification. + * + * @author Ingrid Kamga + */ +public interface TrustedSdJwtIssuer { + + /** + * Resolves potential verifying keys to validate the Issuer-signed JWT. + * The method ensures that the resolved public keys can be trusted. + * + * @param issuerSignedJWT The Issuer-signed JWT to validate. + * @return trusted verifying keys + * @throws VerificationException if no trustworthy verifying key could be resolved + */ + List resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT) + throws VerificationException; +} diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java new file mode 100644 index 000000000000..9c70d17bf0c4 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java @@ -0,0 +1,53 @@ +/* + * 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.sdjwt.vp; + +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.sdjwt.SdJws; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + * + */ +public class KeyBindingJWT extends SdJws { + + public static final String TYP = "kb+jwt"; + + public KeyBindingJWT(JsonNode payload, SignatureSignerContext signer, String jwsType) { + super(payload, signer, jwsType); + } + + public static KeyBindingJWT of(String jwsString) { + return new KeyBindingJWT(jwsString); + } + + public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) { + return new KeyBindingJWT(payload, signer, jwsType); + } + + private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) { + super(payload, jwsInput); + } + + private KeyBindingJWT(String jwsString) { + super(jwsString); + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java new file mode 100644 index 000000000000..6f43f91a38ba --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java @@ -0,0 +1,140 @@ +/* + * 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.sdjwt.vp; + +/** + * Options for Key Binding JWT verification. + * + * @author Ingrid Kamga + */ +public class KeyBindingJwtVerificationOpts { + /** + * Specifies the Verifier's policy whether to check Key Binding + */ + private final boolean keyBindingRequired; + + /** + * Specifies the maximum age (in seconds) of an issued Key Binding + */ + private final int allowedMaxAge; + + private final String nonce; + private final String aud; + + private final boolean validateExpirationClaim; + private final boolean validateNotBeforeClaim; + + public KeyBindingJwtVerificationOpts( + boolean keyBindingRequired, + int allowedMaxAge, + String nonce, + String aud, + boolean validateExpirationClaim, + boolean validateNotBeforeClaim) { + this.keyBindingRequired = keyBindingRequired; + this.allowedMaxAge = allowedMaxAge; + this.nonce = nonce; + this.aud = aud; + this.validateExpirationClaim = validateExpirationClaim; + this.validateNotBeforeClaim = validateNotBeforeClaim; + } + + public boolean isKeyBindingRequired() { + return keyBindingRequired; + } + + public int getAllowedMaxAge() { + return allowedMaxAge; + } + + public String getNonce() { + return nonce; + } + + public String getAud() { + return aud; + } + + public boolean mustValidateExpirationClaim() { + return validateExpirationClaim; + } + + public boolean mustValidateNotBeforeClaim() { + return validateNotBeforeClaim; + } + + public static KeyBindingJwtVerificationOpts.Builder builder() { + return new KeyBindingJwtVerificationOpts.Builder(); + } + + public static class Builder { + private boolean keyBindingRequired = true; + private int allowedMaxAge = 5 * 60; + private String nonce; + private String aud; + private boolean validateExpirationClaim = true; + private boolean validateNotBeforeClaim = true; + + public Builder withKeyBindingRequired(boolean keyBindingRequired) { + this.keyBindingRequired = keyBindingRequired; + return this; + } + + public Builder withAllowedMaxAge(int allowedMaxAge) { + this.allowedMaxAge = allowedMaxAge; + return this; + } + + public Builder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + public Builder withAud(String aud) { + this.aud = aud; + return this; + } + + public Builder withValidateExpirationClaim(boolean validateExpirationClaim) { + this.validateExpirationClaim = validateExpirationClaim; + return this; + } + + public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) { + this.validateNotBeforeClaim = validateNotBeforeClaim; + return this; + } + + public KeyBindingJwtVerificationOpts build() { + if (keyBindingRequired && (aud == null || nonce == null || nonce.isEmpty())) { + throw new IllegalArgumentException( + "Missing `nonce` and `aud` claims for replay protection" + ); + } + + return new KeyBindingJwtVerificationOpts( + keyBindingRequired, + allowedMaxAge, + nonce, + aud, + validateExpirationClaim, + validateNotBeforeClaim + ); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java new file mode 100644 index 000000000000..6285df854add --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java @@ -0,0 +1,330 @@ +/* + * 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.sdjwt.vp; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; +import org.keycloak.sdjwt.SdJwt; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.SdJwtVerificationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Francis Pouatcha + */ +public class SdJwtVP { + private final String sdJwtVpString; + private final IssuerSignedJWT issuerSignedJWT; + + private final Map claims; + private final Map disclosures; + private final Map recursiveDigests; + private final List ghostDigests; + private final String hashAlgorithm; + + private final Optional keyBindingJWT; + private final SdJwtVerificationContext sdJwtVerificationContext; + + public Map getClaims() { + return claims; + } + + public IssuerSignedJWT getIssuerSignedJWT() { + return issuerSignedJWT; + } + + public Map getDisclosures() { + return disclosures; + } + + public Collection getDisclosuresString() { + return disclosures.values(); + } + + public Map getRecursiveDigests() { + return recursiveDigests; + } + + public Collection getGhostDigests() { + return ghostDigests; + } + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public Optional getKeyBindingJWT() { + return keyBindingJWT; + } + + private SdJwtVP(String sdJwtVpString, String hashAlgorithm, IssuerSignedJWT issuerSignedJWT, + Map claims, Map disclosures, Map recursiveDigests, + List ghostDigests, Optional keyBindingJWT) { + this.sdJwtVpString = sdJwtVpString; + this.hashAlgorithm = hashAlgorithm; + this.issuerSignedJWT = issuerSignedJWT; + this.claims = Collections.unmodifiableMap(claims); + this.disclosures = Collections.unmodifiableMap(disclosures); + this.recursiveDigests = Collections.unmodifiableMap(recursiveDigests); + this.ghostDigests = Collections.unmodifiableList(ghostDigests); + this.keyBindingJWT = keyBindingJWT; + + // Instantiate context for verification + this.sdJwtVerificationContext = new SdJwtVerificationContext( + this.sdJwtVpString, + this.issuerSignedJWT, + this.disclosures, + this.keyBindingJWT.orElse(null) + ); + } + + public static SdJwtVP of(String sdJwtString) { + int disclosureStart = sdJwtString.indexOf(SdJwt.DELIMITER); + int disclosureEnd = sdJwtString.lastIndexOf(SdJwt.DELIMITER); + + if (disclosureStart == -1) { + throw new IllegalArgumentException("SD-JWT is malformed, expected to contain a '" + SdJwt.DELIMITER + "'"); + } + + String issuerSignedJWTString = sdJwtString.substring(0, disclosureStart); + String disclosuresString = ""; + + if (disclosureEnd > disclosureStart) { + disclosuresString = sdJwtString.substring(disclosureStart + 1, disclosureEnd); + } + + IssuerSignedJWT issuerSignedJWT = IssuerSignedJWT.fromJws(issuerSignedJWTString); + + ObjectNode issuerPayload = (ObjectNode) issuerSignedJWT.getPayload(); + String hashAlgorithm = Optional.ofNullable(issuerPayload.get(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM)) + .map(JsonNode::asText) + .orElse(JavaAlgorithm.SHA256.toLowerCase()); + + Map claims = new HashMap<>(); + Map disclosures = new HashMap<>(); + + List split = Arrays.stream(disclosuresString.split(SdJwt.DELIMITER)) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + + for (String disclosure : split) { + String disclosureDigest = SdJwtUtils.hashAndBase64EncodeNoPad(disclosure.getBytes(), hashAlgorithm); + if (disclosures.containsKey(disclosureDigest)) { + throw new IllegalArgumentException("Duplicate disclosure digest"); + } + disclosures.put(disclosureDigest, disclosure); + ArrayNode disclosureData; + try { + disclosureData = (ArrayNode) SdJwtUtils.mapper.readTree(Base64Url.decode(disclosure)); + claims.put(disclosureDigest, disclosureData); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid disclosure data"); + } + } + Set allDigests = claims.keySet(); + + Map recursiveDigests = new HashMap<>(); + List ghostDigests = new ArrayList<>(); + allDigests.stream() + .forEach(disclosureDigest -> { + JsonNode node = findNode(issuerPayload, disclosureDigest); + node = processDisclosureDigest(node, disclosureDigest, claims, recursiveDigests, ghostDigests); + }); + + Optional keyBindingJWT = Optional.empty(); + if (sdJwtString.length() > disclosureEnd + 1) { + String keyBindingJWTString = sdJwtString.substring(disclosureEnd + 1); + keyBindingJWT = Optional.of(KeyBindingJWT.of(keyBindingJWTString)); + } + + // Drop the key binding String if any. As it is held by the keyBindingJwtObject + String sdJWtVPString = sdJwtString.substring(0, disclosureEnd + 1); + + return new SdJwtVP(sdJWtVPString, hashAlgorithm, issuerSignedJWT, claims, disclosures, recursiveDigests, + ghostDigests, keyBindingJWT); + + } + + private static JsonNode processDisclosureDigest(JsonNode node, String disclosureDigest, + Map claims, + Map recursiveDigests, + List ghostDigests) { + if (node == null) { // digest is nested in another disclosure + Set> entrySet = claims.entrySet(); + for (Entry entry : entrySet) { + if (entry.getKey().equals(disclosureDigest)) { + continue; + } + node = findNode(entry.getValue(), disclosureDigest); + if (node != null) { + recursiveDigests.put(disclosureDigest, entry.getKey()); + break; + } + } + } + if (node == null) { // No digest found for disclosure. + ghostDigests.add(disclosureDigest); + } + return node; + } + + public JsonNode getCnfClaim() { + return issuerSignedJWT.getCnfClaim().orElse(null); + } + + public String present(List disclosureDigests, JsonNode keyBindingClaims, + SignatureSignerContext holdSignatureSignerContext, String jwsType) { + StringBuilder sb = new StringBuilder(); + if (disclosureDigests == null || disclosureDigests.isEmpty()) { + // disclose everything + sb.append(sdJwtVpString); + } else { + sb.append(issuerSignedJWT.toJws()); + sb.append(SdJwt.DELIMITER); + for (String disclosureDigest : disclosureDigests) { + sb.append(disclosures.get(disclosureDigest)); + sb.append(SdJwt.DELIMITER); + } + } + String unboundPresentation = sb.toString(); + if (keyBindingClaims == null || holdSignatureSignerContext == null) { + return unboundPresentation; + } + String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm()); + keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash); + KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType); + sb.append(keyBindingJWT.toJws()); + return sb.toString(); + } + + /** + * Verifies SD-JWT presentation. + * + * @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller + * is responsible for establishing trust in that the keys belong + * to the intended issuer. + * @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. + * @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification. + * Must, among others, specify the Verifier's policy whether + * to check Key Binding. + * @throws VerificationException if verification failed + */ + public void verify( + List issuerVerifyingKeys, + IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts, + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts + ) throws VerificationException { + sdJwtVerificationContext.verifyPresentation( + issuerVerifyingKeys, + issuerSignedJwtVerificationOpts, + keyBindingJwtVerificationOpts, + null + ); + } + + /** + * Retrieve verification context for advanced scenarios. + */ + public SdJwtVerificationContext getSdJwtVerificationContext() { + return sdJwtVerificationContext; + } + + // Recursively searches the node with the given value. + // Returns the node if found, null otherwise. + private static JsonNode findNode(JsonNode node, String value) { + if (node == null) { + return null; + } + if (node.isValueNode()) { + if (node.asText().equals(value)) { + return node; + } else { + return null; + } + } + if (node.isArray() || node.isObject()) { + for (JsonNode child : node) { + JsonNode found = findNode(child, value); + if (found != null) { + return found; + } + } + } + return null; + } + + @Override + public String toString() { + return sdJwtVpString; + } + + public String verbose() { + StringBuilder sb = new StringBuilder(); + sb.append("Issuer Signed JWT: "); + sb.append(issuerSignedJWT.getPayload()); + sb.append("\n"); + disclosures.forEach((digest, disclosure) -> { + sb.append("\n"); + sb.append("Digest: "); + sb.append(digest); + sb.append("\n"); + sb.append("Disclosure: "); + sb.append(disclosure); + sb.append("\n"); + sb.append("Content: "); + sb.append(claims.get(digest)); + sb.append("\n"); + }); + sb.append("\n"); + sb.append("Recursive Digests: "); + sb.append(recursiveDigests); + sb.append("\n"); + sb.append("\n"); + sb.append("Ghost Digests: "); + sb.append(ghostDigests); + sb.append("\n"); + sb.append("\n"); + if (keyBindingJWT.isPresent()) { + sb.append("Key Binding JWT: "); + sb.append("\n"); + sb.append(keyBindingJWT.get().getPayload().toString()); + sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java index e80f81f240ee..b4e9d0652113 100644 --- a/core/src/main/java/org/keycloak/util/JWKSUtils.java +++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java @@ -17,6 +17,7 @@ package org.keycloak.util; +import com.fasterxml.jackson.databind.JsonNode; import org.jboss.logging.Logger; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; @@ -27,11 +28,13 @@ import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKParser; +import org.keycloak.jose.jwk.OKPPublicJWK; import org.keycloak.jose.jwk.RSAPublicJWK; import org.keycloak.jose.jws.crypto.HashUtils; import java.io.IOException; import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -76,9 +79,13 @@ public static PublicKeysWrapper getKeyWrappersForUse(JSONWebKeySet keySet, JWK.U logger.debugf("Ignoring JWK key '%s'. Missing required field 'use'.", jwk.getKeyId()); } else if ((requestedUse.asString().equals(jwk.getPublicKeyUse()) || (jwk.getPublicKeyUse() == null && useRequestedUseWhenNull)) && parser.isKeyTypeSupported(jwk.getKeyType())) { - KeyWrapper keyWrapper = wrap(jwk, parser); - keyWrapper.setUse(getKeyUse(requestedUse.asString())); - result.add(keyWrapper); + try { + KeyWrapper keyWrapper = wrap(jwk, parser); + keyWrapper.setUse(getKeyUse(requestedUse.asString())); + result.add(keyWrapper); + } catch (RuntimeException e) { + logger.debugf(e, "Ignoring JWK key '%s'. Failed to load key.", jwk.getKeyId()); + } } } return new PublicKeysWrapper(result); @@ -125,6 +132,9 @@ private static KeyWrapper wrap(JWK jwk, JWKParser parser) { if (jwk.getAlgorithm() != null) { keyWrapper.setAlgorithm(jwk.getAlgorithm()); } + if (jwk.getOtherClaims().get(OKPPublicJWK.CRV) != null) { + keyWrapper.setCurve((String) jwk.getOtherClaims().get(OKPPublicJWK.CRV)); + } keyWrapper.setType(jwk.getKeyType()); keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse())); keyWrapper.setPublicKey(parser.toPublicKey()); @@ -136,20 +146,30 @@ public static String computeThumbprint(JWK key) { } // TreeMap uses the natural ordering of the keys. - // Therefore, it follows the way of hash value calculation for a public key defined by RFC 7678 + // Therefore, it follows the way of hash value calculation for a public key defined by RFC 7638 public static String computeThumbprint(JWK key, String hashAlg) { - Map members = new TreeMap<>(); - members.put(JWK.KEY_TYPE, key.getKeyType()); + String kty = key.getKeyType(); + String[] requiredMembers = JWK_THUMBPRINT_REQUIRED_MEMBERS.get(kty); - for (String member : JWK_THUMBPRINT_REQUIRED_MEMBERS.get(key.getKeyType())) { - members.put(member, (String) key.getOtherClaims().get(member)); + // e.g. `oct`, see RFC 7638 Section 3.2 + if (requiredMembers == null) { + throw new UnsupportedOperationException("Unsupported key type: " + kty); } + Map members = new TreeMap<>(); + members.put(JWK.KEY_TYPE, kty); + try { + JsonNode node = JsonSerialization.writeValueAsNode(key); + for (String member : requiredMembers) { + members.put(member, node.get(member).asText()); + } + byte[] bytes = JsonSerialization.writeValueAsBytes(members); byte[] hash = HashUtils.hash(hashAlg, bytes); return Base64Url.encode(hash); } catch (IOException ex) { + logger.debugf(ex, "Failed to compute JWK thumbprint for key '%s'.", key.getKeyId()); return null; } } diff --git a/core/src/main/java/org/keycloak/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java index e321d11151f5..faa0d71c7a17 100755 --- a/core/src/main/java/org/keycloak/util/JsonSerialization.java +++ b/core/src/main/java/org/keycloak/util/JsonSerialization.java @@ -24,6 +24,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.io.InputStream; @@ -38,9 +40,10 @@ public class JsonSerialization { public static final ObjectMapper mapper = new ObjectMapper(); public static final ObjectMapper prettyMapper = new ObjectMapper(); - public static final ObjectMapper sysPropertiesAwareMapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); static { + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); prettyMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); @@ -67,6 +70,10 @@ public static byte[] writeValueAsBytes(Object obj) throws IOException { return mapper.writeValueAsBytes(obj); } + public static JsonNode writeValueAsNode(Object obj) { + return mapper.valueToTree(obj); + } + public static T readValue(byte[] bytes, Class type) throws IOException { return mapper.readValue(bytes, type); } @@ -76,7 +83,7 @@ public static T readValue(String bytes, Class type) throws IOException { } public static T readValue(InputStream bytes, Class type) throws IOException { - return readValue(bytes, type, false); + return mapper.readValue(bytes, type); } public static T readValue(String string, TypeReference type) throws IOException { @@ -87,14 +94,6 @@ public static T readValue(InputStream bytes, TypeReference type) throws I return mapper.readValue(bytes, type); } - public static T readValue(InputStream bytes, Class type, boolean replaceSystemProperties) throws IOException { - if (replaceSystemProperties) { - return sysPropertiesAwareMapper.readValue(bytes, type); - } else { - return mapper.readValue(bytes, type); - } - } - /** * Creates an {@link ObjectNode} based on the given {@code pojo}, copying all its properties to the resulting {@link ObjectNode}. * diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 41432d68e7ae..9103c38d3dd3 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -42,6 +42,16 @@ public class TokenUtil { public static final String TOKEN_TYPE_DPOP = "DPoP"; + // Mentioned in the token-exchange specification https://datatracker.ietf.org/doc/html/rfc8693#name-successful-response + public static final String TOKEN_TYPE_NA = "N_A"; + + // 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; + + // https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken + public static final String TOKEN_TYPE_JWT_LOGOUT_TOKEN = "logout+jwt"; + public static final String TOKEN_TYPE_KEYCLOAK_ID = "Serialized-ID"; public static final String TOKEN_TYPE_ID = "ID"; diff --git a/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java b/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java new file mode 100644 index 000000000000..497257469d1a --- /dev/null +++ b/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java @@ -0,0 +1,157 @@ +/* + * 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.jose.jwk; + +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.EdECPublicKey; +import java.security.spec.EdECPoint; +import java.security.spec.EdECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.NamedParameterSpec; +import java.util.Optional; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyUse; + +/** + *

    Class that uses Java 15+ EdEC classes and implements the EdECUtils interface.

    + * + * @author rmartinc + */ +class EdECUtilsImpl implements EdECUtils { + + public EdECUtilsImpl() { + } + + @Override + public boolean isEdECSupported() { + return true; + } + + @Override + public JWK okp(String kid, String algorithm, Key key, KeyUse keyUse) { + EdECPublicKey eddsaPublicKey = (EdECPublicKey) key; + + OKPPublicJWK k = new OKPPublicJWK(); + + kid = kid != null ? kid : KeyUtils.createKeyId(key); + + k.setKeyId(kid); + k.setKeyType(KeyType.OKP); + k.setAlgorithm(algorithm); + k.setPublicKeyUse(keyUse == null ? JWKBuilder.DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName()); + k.setCrv(eddsaPublicKey.getParams().getName()); + + Optional x = edPublicKeyInJwkRepresentation(eddsaPublicKey); + k.setX(x.orElse("")); + + return k; + } + + @Override + public PublicKey createOKPPublicKey(JWK jwk) { + String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X); + String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV); + // JWK representation "x" of a public key + int bytesLength = 0; + if (Algorithm.Ed25519.equals(crv)) { + bytesLength = 32; + } else if (Algorithm.Ed448.equals(crv)) { + bytesLength = 57; + } else { + throw new RuntimeException("Invalid JWK representation of OKP type algorithm"); + } + + byte[] decodedX = Base64Url.decode(x); + if (decodedX.length != bytesLength) { + throw new RuntimeException("Invalid JWK representation of OKP type public key"); + } + + // x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even + boolean isOddX = false; + if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000 + isOddX = true; + } + + // MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0 + decodedX[decodedX.length - 1] &= 127; // 0b01111111 + + // both x and y-coordinate in twisted Edwards curve are always 0 or natural number + BigInteger y = new BigInteger(1, reverseBytes(decodedX)); + NamedParameterSpec spec = new NamedParameterSpec(crv); + EdECPoint ep = new EdECPoint(isOddX, y); + EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep); + + PublicKey publicKey = null; + try { + publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return publicKey; + } + + private static Optional edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) { + EdECPoint edEcPoint = eddsaPublicKey.getPoint(); + BigInteger yCoordinate = edEcPoint.getY(); + + // JWK representation "x" of a public key + int bytesLength = 0; + if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) { + bytesLength = 32; + } else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) { + bytesLength = 57; + } else { + return Optional.ofNullable(null); + } + + // consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate. + byte[] yCoordinateLittleEndianBytes = new byte[bytesLength]; + + // convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027) + byte[] yCoordinateLittleEndian = reverseBytes(yCoordinate.toByteArray()); + System.arraycopy(yCoordinateLittleEndian, 0, yCoordinateLittleEndianBytes, 0, yCoordinateLittleEndian.length); + + // set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037) + if (edEcPoint.isXOdd()) { + yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000 + } + + return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes)); + } + + private static byte[] reverseBytes(byte[] array) { + if (array == null || array.length == 0) { + return null; + } + + int length = array.length; + byte[] reversedArray = new byte[length]; + + for (int i = 0; i < length; i++) { + reversedArray[length - 1 - i] = array[i]; + } + + return reversedArray; + } +} diff --git a/core/src/test/java/org/keycloak/HashTest.java b/core/src/test/java/org/keycloak/HashTest.java new file mode 100644 index 000000000000..e3c8dda69a9f --- /dev/null +++ b/core/src/test/java/org/keycloak/HashTest.java @@ -0,0 +1,51 @@ +/* + * 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; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.jose.jws.crypto.HashUtils; + +/** + * @author Marek Posolda + */ +public class HashTest { + + @Test + public void testSha256Hash() throws Exception { + testSha256Hash("myCodeVerifier", StandardCharsets.ISO_8859_1, "rxrlnYFwTggx1TzJeKXupM_TAJia_vbHD35PIlq9tRg"); + testSha256Hash("myCodeVerifier", StandardCharsets.UTF_8, "rxrlnYFwTggx1TzJeKXupM_TAJia_vbHD35PIlq9tRg"); + + testSha256Hash("Some1[^&*$#", StandardCharsets.ISO_8859_1, "lXO5GHk4DoCxiStRrpJgQ-cOnJQmJTb2gh3HJ3Ueq9U"); + testSha256Hash("Some1[^&*$#", StandardCharsets.UTF_8, "lXO5GHk4DoCxiStRrpJgQ-cOnJQmJTb2gh3HJ3Ueq9U"); + + // This will differ between ISO-8851-1 and UTF8 due the special character + testSha256Hash("krák", StandardCharsets.ISO_8859_1, "XD3Gb_rLS49onF_rOsTWc7SLa27Vny8AHgmoEOmJx5s"); + testSha256Hash("krák", StandardCharsets.UTF_8, "QKvM6HItSe5Yi0rQqQgIFhyKhQJCNr4H60eP3YgcjpU"); + } + + private void testSha256Hash(String codeVerifier, Charset charset, String expectedHash) { + String hash1 = HashUtils.sha256UrlEncodedHash(codeVerifier, charset); + Assert.assertEquals(hash1, expectedHash); + } +} diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java index acdf0c8ecae9..2e8579f5d13c 100755 --- a/core/src/test/java/org/keycloak/JsonParserTest.java +++ b/core/src/test/java/org/keycloak/JsonParserTest.java @@ -23,7 +23,6 @@ import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; -import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.ClientPoliciesRepresentation; import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; import org.keycloak.representations.idm.ClientPolicyConditionRepresentation; @@ -97,30 +96,6 @@ public void testUnwrap() throws Exception { Assert.assertNotNull(nested.get("foo")); } - @Test - public void testParsingSystemProps() throws IOException { - System.setProperty("my.host", "foo"); - System.setProperty("con.pool.size", "200"); - System.setProperty("allow.any.hostname", "true"); - System.setProperty("socket.timeout.millis", "6000"); - System.setProperty("connection.timeout.millis", "7000"); - System.setProperty("connection.ttl.millis", "500"); - - InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json"); - - AdapterConfig config = JsonSerialization.readValue(is, AdapterConfig.class, true); - Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl()); - Assert.assertEquals("external", config.getSslRequired()); - Assert.assertEquals("angular-product${non.existing}", config.getResource()); - Assert.assertTrue(config.isPublicClient()); - Assert.assertTrue(config.isAllowAnyHostname()); - Assert.assertEquals(100, config.getCorsMaxAge()); - Assert.assertEquals(200, config.getConnectionPoolSize()); - Assert.assertEquals(6000L, config.getSocketTimeout()); - Assert.assertEquals(7000L, config.getConnectionTimeout()); - Assert.assertEquals(500L, config.getConnectionTTL()); - } - static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}"); @Test @@ -263,4 +238,4 @@ private void assertClaimValue(ClaimsRepresentation.ClaimValue claimVal, B } } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/keycloak/RSAVerifierTest.java b/core/src/test/java/org/keycloak/RSAVerifierTest.java index f930f0d2fc67..ce8ee21be247 100755 --- a/core/src/test/java/org/keycloak/RSAVerifierTest.java +++ b/core/src/test/java/org/keycloak/RSAVerifierTest.java @@ -22,22 +22,33 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; -import org.keycloak.common.util.Time; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; import org.keycloak.rule.CryptoInitRule; import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; /** * @author Bill Burke * @version $Revision: 1 $ */ public abstract class RSAVerifierTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); // private static X509Certificate[] idpCertificates; private static KeyPair idpPair; private static KeyPair badPair; @@ -45,13 +56,9 @@ public abstract class RSAVerifierTest { // private static X509Certificate[] clientCertificateChain; private AccessToken token; - @ClassRule - public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); - @BeforeClass public static void setupCerts() - throws Exception - { + throws Exception { // CryptoIntegration.init(ClassLoader.getSystemClassLoader()); badPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); @@ -62,12 +69,12 @@ public void initTest() { token = new AccessToken(); token.type(TokenUtil.TOKEN_TYPE_BEARER) - .subject("CN=Client") - .issuer("http://localhost:8080/auth/realm") - .addAccess("service").addRole("admin"); + .subject("CN=Client") + .issuer("http://localhost:8080/auth/realm") + .addAccess("service").addRole("admin"); } - @Test + @Test public void testSimpleVerification() throws Exception { String encoded = new JWSBuilder() .jsonContent(token) @@ -78,30 +85,56 @@ public void testSimpleVerification() throws Exception { Assert.assertEquals("CN=Client", token.getSubject()); } + @Test + public void testVerificationWithAddedX5cAndJwk() throws Exception { + KeyPair caKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + X509Certificate caCertificate = CertificateUtils.generateV1SelfSignedCertificate(caKeyPair, "root"); + X509Certificate idpCertificate = CertificateUtils.generateV3Certificate(idpPair, + caKeyPair.getPrivate(), + caCertificate, + "idp"); + JWK jwk = JWKBuilder.create().rsa(idpPair.getPublic()); + + String encoded = new JWSBuilder() + .jwk(jwk) + .x5c(Arrays.asList(new X509Certificate[]{idpCertificate, caCertificate})) + .jsonContent(token) + .rsa256(idpPair.getPrivate()); + TokenVerifier tokenVerifier = TokenVerifier.create(encoded, JsonWebToken.class); + verifySkeletonKeyToken(encoded); + Assert.assertTrue(token.getResourceAccess("service").getRoles().contains("admin")); + Assert.assertEquals("CN=Client", token.getSubject()); + + List x5c = tokenVerifier.getHeader().getX5c(); + Assert.assertEquals(2, x5c.size()); + Assert.assertEquals(Base64.encodeBytes(idpCertificate.getEncoded()), x5c.get(0)); + Assert.assertEquals(Base64.encodeBytes(caCertificate.getEncoded()), x5c.get(1)); + Assert.assertEquals(JsonSerialization.mapper.convertValue(jwk, Map.class), + JsonSerialization.mapper.convertValue(tokenVerifier.getHeader().getKey(), Map.class)); + } + private AccessToken verifySkeletonKeyToken(String encoded) throws VerificationException { return RSATokenVerifier.verifyToken(encoded, idpPair.getPublic(), "http://localhost:8080/auth/realm"); } - // @Test - public void testSpeed() throws Exception - { - // Took 44 seconds with 50000 iterations - byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token); + // @Test + public void testSpeed() throws Exception { + // Took 44 seconds with 50000 iterations + byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token); - long start = System.currentTimeMillis(); - int count = 50000; - for (int i = 0; i < count; i++) - { - String encoded = new JWSBuilder() - .content(tokenBytes) - .rsa256(idpPair.getPrivate()); + long start = System.currentTimeMillis(); + int count = 50000; + for (int i = 0; i < count; i++) { + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); - verifySkeletonKeyToken(encoded); + verifySkeletonKeyToken(encoded); - } - long end = System.currentTimeMillis() - start; - System.out.println("took: " + end); - } + } + long end = System.currentTimeMillis() - start; + System.out.println("took: " + end); + } @Test public void testBadSignature() { @@ -120,7 +153,7 @@ public void testBadSignature() { @Test public void testNotBeforeGood() throws Exception { - token.notBefore(Time.currentTime() - 100); + token.nbf(Time.currentTime() - 100L); String encoded = new JWSBuilder() .jsonContent(token) @@ -136,7 +169,7 @@ public void testNotBeforeGood() throws Exception { @Test public void testNotBeforeBad() { - token.notBefore(Time.currentTime() + 100); + token.nbf(Time.currentTime() + 100L); String encoded = new JWSBuilder() .jsonContent(token) @@ -153,7 +186,7 @@ public void testNotBeforeBad() { @Test public void testExpirationGood() throws Exception { - token.expiration(Time.currentTime() + 100); + token.exp(Time.currentTime() + 100L); String encoded = new JWSBuilder() .jsonContent(token) @@ -169,7 +202,7 @@ public void testExpirationGood() throws Exception { @Test public void testExpirationBad() { - token.expiration(Time.currentTime() - 100); + token.exp(Time.currentTime() - 100L); String encoded = new JWSBuilder() .jsonContent(token) @@ -187,8 +220,8 @@ public void testExpirationBad() { public void testTokenAuth() { token = new AccessToken(); token.subject("CN=Client") - .issuer("http://localhost:8080/auth/realms/demo") - .addAccess("service").addRole("admin").verifyCaller(true); + .issuer("http://localhost:8080/auth/realms/demo") + .addAccess("service").addRole("admin").verifyCaller(true); token.setEmail("bill@jboss.org"); String encoded = new JWSBuilder() @@ -234,10 +267,10 @@ public void testAudience() throws Exception { private void verifyAudience(String encodedToken, String expectedAudience) throws VerificationException { TokenVerifier.create(encodedToken, AccessToken.class) - .publicKey(idpPair.getPublic()) - .realmUrl("http://localhost:8080/auth/realm") - .audience(expectedAudience) - .verify(); + .publicKey(idpPair.getPublic()) + .realmUrl("http://localhost:8080/auth/realm") + .audience(expectedAudience) + .verify(); } diff --git a/core/src/test/java/org/keycloak/jose/HmacTest.java b/core/src/test/java/org/keycloak/jose/HmacTest.java index b9aedda0df67..b8041e1d7576 100755 --- a/core/src/test/java/org/keycloak/jose/HmacTest.java +++ b/core/src/test/java/org/keycloak/jose/HmacTest.java @@ -21,14 +21,12 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; -import org.keycloak.common.util.BouncyIntegration; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.rule.CryptoInitRule; import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; import javax.crypto.spec.SecretKeySpec; import java.util.UUID; diff --git a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java index cae36c518de9..26c00bd0aa9c 100644 --- a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java +++ b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java @@ -82,37 +82,37 @@ public void testArray() throws IOException { @Test public void isActiveReturnFalseWhenBeforeTimeInFuture() { - int currentTime = Time.currentTime(); - int futureTime = currentTime + 10; + long currentTime = Time.currentTime(); + long futureTime = currentTime + 10; JsonWebToken jsonWebToken = new JsonWebToken(); - jsonWebToken.notBefore(futureTime); + jsonWebToken.nbf(futureTime); assertFalse(jsonWebToken.isActive()); } @Test public void isActiveReturnTrueWhenBeforeTimeInPast() { - int currentTime = Time.currentTime(); - int pastTime = currentTime - 10; + long currentTime = Time.currentTime(); + long pastTime = currentTime - 10; JsonWebToken jsonWebToken = new JsonWebToken(); - jsonWebToken.notBefore(pastTime); + jsonWebToken.nbf(pastTime); assertTrue(jsonWebToken.isActive()); } @Test public void isActiveShouldReturnTrueWhenBeforeTimeInFutureWithinTimeSkew() { - int notBeforeTime = Time.currentTime() + 5; + long notBeforeTime = Time.currentTime() + 5; int allowedClockSkew = 10; JsonWebToken jsonWebToken = new JsonWebToken(); - jsonWebToken.notBefore(notBeforeTime); + jsonWebToken.nbf(notBeforeTime); assertTrue(jsonWebToken.isActive(allowedClockSkew)); } @Test public void isActiveShouldReturnFalseWhenWhenBeforeTimeInFutureOutsideTimeSkew() { - int notBeforeTime = Time.currentTime() + 10; + long notBeforeTime = Time.currentTime() + 10; int allowedClockSkew = 5; JsonWebToken jsonWebToken = new JsonWebToken(); - jsonWebToken.notBefore(notBeforeTime); + jsonWebToken.nbf(notBeforeTime); assertFalse(jsonWebToken.isActive(allowedClockSkew)); } diff --git a/core/src/test/java/org/keycloak/jose/jwk/JWKTest.java b/core/src/test/java/org/keycloak/jose/jwk/JWKTest.java index 92ea511eafd2..8bfff750c631 100644 --- a/core/src/test/java/org/keycloak/jose/jwk/JWKTest.java +++ b/core/src/test/java/org/keycloak/jose/jwk/JWKTest.java @@ -17,18 +17,14 @@ package org.keycloak.jose.jwk; -import java.util.Arrays; -import java.util.List; - import org.junit.ClassRule; import org.junit.Test; +import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.KeyType; -import org.keycloak.crypto.KeyUse; -import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.rule.CryptoInitRule; import org.keycloak.util.JsonSerialization; @@ -42,14 +38,15 @@ import java.security.cert.X509Certificate; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECPoint; +import java.util.Arrays; +import java.util.List; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.keycloak.common.util.CertificateUtils.generateV1SelfSignedCertificate; +import static org.keycloak.common.util.CertificateUtils.generateV3Certificate; /** * This is not tested in keycloak-core. The subclasses should be created in the crypto modules to make sure it is tested with corresponding modules (bouncycastle VS bouncycastle-fips) @@ -143,6 +140,11 @@ private void testPublicEs256(String algorithm) throws Exception { ECGenParameterSpec ecSpec = new ECGenParameterSpec(algorithm); keyGen.initialize(ecSpec, randomGen); KeyPair keyPair = keyGen.generateKeyPair(); + KeyPair keyPair2 = keyGen.generateKeyPair(); + X509Certificate certificate = generateV1SelfSignedCertificate(keyPair, "root"); + X509Certificate certificate2 = generateV3Certificate(keyPair2, keyPair.getPrivate(), certificate, "child"); + certificate.verify(keyPair.getPublic()); + certificate2.verify(keyPair.getPublic()); ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); @@ -178,6 +180,36 @@ private void testPublicEs256(String algorithm) throws Exception { verify(data, sign, JavaAlgorithm.ES256, publicKeyFromJwk); } + @Test + public void testCertificateGenerationWithRsaAndEc() throws Exception { + KeyPairGenerator keyGenRsa = CryptoIntegration.getProvider().getKeyPairGen(KeyType.RSA); + KeyPairGenerator keyGenEc = CryptoIntegration.getProvider().getKeyPairGen(KeyType.EC); + SecureRandom randomGen = new SecureRandom(); + ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); + keyGenEc.initialize(ecSpec, randomGen); + KeyPair keyPairRsa = keyGenRsa.generateKeyPair(); + KeyPair keyPairEc = keyGenEc.generateKeyPair(); + X509Certificate certificateRsa = generateV1SelfSignedCertificate(keyPairRsa, "root"); + X509Certificate certificateEc = generateV3Certificate(keyPairEc, keyPairRsa.getPrivate(), certificateRsa, "child"); + certificateRsa.verify(keyPairRsa.getPublic()); + certificateEc.verify(keyPairRsa.getPublic()); + } + + @Test + public void testCertificateGenerationWithEcAndRsa() throws Exception { + KeyPairGenerator keyGenRsa = CryptoIntegration.getProvider().getKeyPairGen(KeyType.RSA); + KeyPairGenerator keyGenEc = CryptoIntegration.getProvider().getKeyPairGen(KeyType.EC); + SecureRandom randomGen = new SecureRandom(); + ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); + keyGenEc.initialize(ecSpec, randomGen); + KeyPair keyPairRsa = keyGenRsa.generateKeyPair(); + KeyPair keyPairEc = keyGenEc.generateKeyPair(); + X509Certificate certificateEc = generateV1SelfSignedCertificate(keyPairEc, "root"); + X509Certificate certificateRsa = generateV3Certificate(keyPairRsa, keyPairEc.getPrivate(), certificateEc, "child"); + certificateRsa.verify(keyPairEc.getPublic()); + certificateEc.verify(keyPairEc.getPublic()); + } + @Test public void publicEs256P256() throws Exception { testPublicEs256("secp256r1"); @@ -194,15 +226,15 @@ public void publicEs256P384() throws Exception { } @Test - public void parse() { + public void parseRsa() { String jwkJson = "{" + - " \"kty\": \"RSA\"," + - " \"alg\": \"RS256\"," + - " \"use\": \"sig\"," + - " \"kid\": \"3121adaa80ace09f89d80899d4a5dc4ce33d0747\"," + - " \"n\": \"soFDjoZ5mQ8XAA7reQAFg90inKAHk0DXMTizo4JuOsgzUbhcplIeZ7ks83hsEjm8mP8lUVaHMPMAHEIp3gu6Xxsg-s73ofx1dtt_Fo7aj8j383MFQGl8-FvixTVobNeGeC0XBBQjN8lEl-lIwOa4ZoERNAShplTej0ntDp7TQm0=\"," + - " \"e\": \"AQAB\"" + - " }"; + " \"kty\": \"RSA\"," + + " \"alg\": \"RS256\"," + + " \"use\": \"sig\"," + + " \"kid\": \"3121adaa80ace09f89d80899d4a5dc4ce33d0747\"," + + " \"n\": \"soFDjoZ5mQ8XAA7reQAFg90inKAHk0DXMTizo4JuOsgzUbhcplIeZ7ks83hsEjm8mP8lUVaHMPMAHEIp3gu6Xxsg-s73ofx1dtt_Fo7aj8j383MFQGl8-FvixTVobNeGeC0XBBQjN8lEl-lIwOa4ZoERNAShplTej0ntDp7TQm0=\"," + + " \"e\": \"AQAB\"" + + " }"; PublicKey key = JWKParser.create().parse(jwkJson).toPublicKey(); assertEquals("RSA", key.getAlgorithm()); @@ -210,20 +242,41 @@ public void parse() { } @Test - public void emptyEcOverclaim() throws Exception { - JWKBuilder builder = JWKBuilder.create(); - KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); - KeyPair keyPair = generator.generateKeyPair(); - JWK jwk = builder.ec(keyPair.getPublic(), KeyUse.ENC); - JWKParser parser = new JWKParser(jwk); - - try { - parser.toPublicKey(); - } catch (NullPointerException e) { - fail("NullPointerException is thrown: " + e.getMessage()); - } catch (RuntimeException e) { - // Other runtime exception is expected. - } + public void parseEc() { + + String jwkJson = "{\n" + + " \"kty\": \"EC\",\n" + + " \"use\": \"sig\",\n" + + " \"crv\": \"P-384\",\n" + + " \"kid\": \"KTGEM0qFeO9VGjTLjmXiE_R_eSBUkU87xmytygI1pFQ\",\n" + + " \"x\": \"_pYSppQj0JkrXFQdJPOTiktUxy_giDnqc-PEmNShrWrZm8Ol6E5qB3m1kmZJ7HUF\",\n" + + " \"y\": \"BVlstiJytsgOxrsC1VuNYdx86KKMeJg5WvJhEi-5kMpF2aMHZqbJCcIq0uRdzi7Q\",\n" + + " \"alg\": \"ES256\"\n" + + "}"; + + JWKParser sut = JWKParser.create().parse(jwkJson); + + PublicKey pub = sut.toPublicKey(); + assertNotNull(pub); + assertTrue( pub.getAlgorithm().startsWith("EC")); + assertEquals("X.509", pub.getFormat()); + } + + @Test + public void toPublicKey_EC() { + + ECPublicJWK ecJwk = new ECPublicJWK(); + ecJwk.setKeyType(KeyType.EC); + ecJwk.setCrv("P-256"); + ecJwk.setX("zHXlTZt3yU_oNnLIjgpt-ZaiStrYIzR2oxxq53J0uIs"); + ecJwk.setY("cOsAvnh6olE8KHWPHmB-pJawRWmTtbChmWtSeWZRJdc"); + + JWKParser sut = JWKParser.create(ecJwk); + + PublicKey pub = sut.toPublicKey(); + assertNotNull(pub); + assertTrue(pub.getAlgorithm().startsWith("EC")); + assertEquals("X.509", pub.getFormat()); } private byte[] sign(byte[] data, String javaAlgorithm, PrivateKey key) throws Exception { diff --git a/core/src/test/java/org/keycloak/sdjwt/ArrayElementDisclosureTest.java b/core/src/test/java/org/keycloak/sdjwt/ArrayElementDisclosureTest.java new file mode 100644 index 000000000000..606046339e98 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/ArrayElementDisclosureTest.java @@ -0,0 +1,78 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Francis Pouatcha + */ +public class ArrayElementDisclosureTest { + + @Test + public void testSdJwtWithUndiclosedArrayElements6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw") + .withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA") + .build(); + + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), + "sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json"); + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testSdJwtWithUndiclosedAndDecoyArrayElements6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw") + .withUndisclosedArrayElt("nationalities", 0, "Qg_O64zqAxe412a108iroA") + .withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA") + .withDecoyArrayElt("nationalities", 1, "5bPs1IquZNa0hkaFzzzZNw") + .build(); + + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .build(); + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), + "sdjwt/s6.1-issuer-payload-decoy-array-ellement.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/ArrayElementSerializationTest.java b/core/src/test/java/org/keycloak/sdjwt/ArrayElementSerializationTest.java new file mode 100644 index 000000000000..b7e6ef377afe --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/ArrayElementSerializationTest.java @@ -0,0 +1,57 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.node.TextNode; + +/** + * @author Francis Pouatcha + */ +public class ArrayElementSerializationTest { + + @Before + public void setUp() throws Exception { + SdJwtUtils.arrayEltSpaced = false; + } + + @After + public void tearDown() throws Exception { + SdJwtUtils.arrayEltSpaced = true; + } + + @Test + public void testToBase64urlEncoded() { + // Create an instance of UndisclosedArrayElement with the specified fields + // "lklxF5jMYlGTPUovMNIvCA", "FR" + UndisclosedArrayElement arrayElementDisclosure = UndisclosedArrayElement.builder() + .withSalt(new SdJwtSalt("lklxF5jMYlGTPUovMNIvCA")) + .withArrayElement(new TextNode("FR")).build(); + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, arrayElementDisclosure.getDisclosureString()); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/DisclosureRedListTest.java b/core/src/test/java/org/keycloak/sdjwt/DisclosureRedListTest.java new file mode 100644 index 000000000000..94e275f493a8 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/DisclosureRedListTest.java @@ -0,0 +1,66 @@ +/* + * 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.sdjwt; + +import org.junit.Test; + +public class DisclosureRedListTest { + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInObjectClaim() { + DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("vct") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInArrayClaim() { + DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedArrayElt("iat", 0, "2GLC42sKQveCfGfryNRN9w") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInDecoyArrayClaim() { + DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withDecoyArrayElt("exp", 0, "2GLC42sKQveCfGfryNRN9w") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedIss() { + DisclosureSpec.builder().withUndisclosedClaim("iss").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInObjectNbf() { + DisclosureSpec.builder().withUndisclosedClaim("nbf").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedCnf() { + DisclosureSpec.builder().withUndisclosedClaim("cnf").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedStatus() { + DisclosureSpec.builder().withUndisclosedClaim("status").build(); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/IssuerSignedJWTTest.java b/core/src/test/java/org/keycloak/sdjwt/IssuerSignedJWTTest.java new file mode 100644 index 000000000000..207fe917dcda --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/IssuerSignedJWTTest.java @@ -0,0 +1,133 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Francis Pouatcha + */ +public class IssuerSignedJWTTest { + /** + * If issuer decides to disclose everything, paylod of issuer signed JWT should + * be same as the claim set. + * + * This is essential for backward compatibility with non sd based jwt issuance. + * + * @throws IOException + */ + @Test + public void testIssuerSignedJWTPayloadWithValidClaims() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + List claims = new ArrayList<>(); + claimSet.fields().forEachRemaining(entry -> { + claims.add( + VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + IssuerSignedJWT jwt = IssuerSignedJWT.builder().withClaims(claims).build(); + + assertEquals(claimSet, jwt.getPayload()); + } + + @Test + public void testIssuerSignedJWTPayloadThrowsExceptionForDuplicateClaims() throws IOException { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + List claims = new ArrayList<>(); + + // First fill claims + claimSet.fields().forEachRemaining(entry -> { + claims.add( + VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + // First fill claims + claimSet.fields().forEachRemaining(entry -> { + claims.add( + VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + // All claims are duplicate. + assertTrue(claims.size() == claimSet.size() * 2); + + // Expecting exception + assertThrows(IllegalArgumentException.class, () -> IssuerSignedJWT.builder().withClaims(claims).build()); + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw").build(); + + SdJwt sdJwt = SdJwt.builder().withDisclosureSpec(disclosureSpec).withClaimSet(claimSet).build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims3_3() { + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json"); + + // Merge both + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .build(); + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/JsonClaimsetTest.java b/core/src/test/java/org/keycloak/sdjwt/JsonClaimsetTest.java new file mode 100644 index 000000000000..33249a1f7056 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/JsonClaimsetTest.java @@ -0,0 +1,78 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +/** + * @author Francis Pouatcha + */ +public class JsonClaimsetTest { + + @Test + public void testRead61ClaimSet() throws IOException { + InputStream is = getClass().getClassLoader().getResourceAsStream("sdjwt/s6.1-holder-claims.json"); + JsonNode claimSet = SdJwtUtils.mapper.readTree(is); + + // Test reading a String + String expected_sub_claim = "user_42"; + JsonNode sub_claim = claimSet.get("sub"); + assertEquals(JsonNodeType.STRING, sub_claim.getNodeType()); + assertEquals(expected_sub_claim, sub_claim.asText()); + + // Test reading a boolean + JsonNode phone_number_verified_claim = claimSet.get("phone_number_verified"); + Boolean expected_phone_number_verified_claim = true; + assertEquals(JsonNodeType.BOOLEAN, phone_number_verified_claim.getNodeType()); + assertEquals(expected_phone_number_verified_claim, phone_number_verified_claim.asBoolean()); + + // Test reading an object + JsonNode address_claim = claimSet.get("address"); + assertEquals(JsonNodeType.OBJECT, address_claim.getNodeType()); + JsonNode street_address_claim = address_claim.get("street_address"); + assertEquals(JsonNodeType.STRING, street_address_claim.getNodeType()); + String expected_street_address_claim = "123 Main St"; + assertEquals(expected_street_address_claim, street_address_claim.asText()); + + // Test reading a number + JsonNode updated_at_claim = claimSet.get("updated_at"); + int expected_updated_at_claim = 1570000000; + assertEquals(JsonNodeType.NUMBER, updated_at_claim.getNodeType()); + assertEquals(expected_updated_at_claim, updated_at_claim.asInt()); + + // Test reading an array + JsonNode nationalities_claim = claimSet.get("nationalities"); + assertEquals(JsonNodeType.ARRAY, nationalities_claim.getNodeType()); + assertEquals(2, nationalities_claim.size()); + JsonNode element_0_nationalities_claim = nationalities_claim.get(0); + assertEquals(JsonNodeType.STRING, element_0_nationalities_claim.getNodeType()); + String expected_element_0_nationalities_claim = "US"; + assertEquals(expected_element_0_nationalities_claim, element_0_nationalities_claim.asText()); + JsonNode element_1_nationalities_claim = nationalities_claim.get(1); + assertEquals(JsonNodeType.STRING, element_1_nationalities_claim.getNodeType()); + String expected_element_1_nationalities_claim = "DE"; + assertEquals(expected_element_1_nationalities_claim, element_1_nationalities_claim.asText()); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/JsonNodeComparisonTest.java b/core/src/test/java/org/keycloak/sdjwt/JsonNodeComparisonTest.java new file mode 100644 index 000000000000..9967a3029bbe --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/JsonNodeComparisonTest.java @@ -0,0 +1,39 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Francis Pouatcha + */ +public class JsonNodeComparisonTest { + @Test + public void testJsonNodeEquality() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + JsonNode node1 = mapper.readTree("{\"name\":\"John\", \"age\":30}"); + JsonNode node2 = mapper.readTree("{\"age\":30, \"name\":\"John\"}"); + + assertEquals("JsonNode objects should be equal", node1, node2); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJWTSamplesTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJWTSamplesTest.java new file mode 100644 index 000000000000..db54482fd0c2 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJWTSamplesTest.java @@ -0,0 +1,175 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Francis Pouatcha + */ +public class SdJWTSamplesTest { + @Test + public void testS7_1_FlatSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + // produce the main sdJwt, adding nested sdJwts + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("address", "2GLC42sKQveCfGfryNRN9w") + .build(); + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.1-issuer-payload.json"); + + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testS7_2_StructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + SdJwt addrSdJWT = SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder().build(); + // produce the main sdJwt, adding nested sdJwts + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + } + + @Test + public void testS7_2b_PartialDisclosureOfStructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + SdJwt addrSdJWT = SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder().build(); + // produce the main sdJwt, adding nested sdJwts + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2b-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + } + + @Test + public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + SdJwt addrSdJWT = SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .build(); + // produce the main sdJwt, adding nested sdJwts + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java new file mode 100644 index 000000000000..b26f49167f2a --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java @@ -0,0 +1,186 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.rule.CryptoInitRule; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +public abstract class SdJwsTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + static TestSettings testSettings = TestSettings.getInstance(); + + private JsonNode createPayload() { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("sub", "test"); + node.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + node.put("name", "Test User"); + return node; + } + + @Test + public void testVerifySignature_Positive() throws Exception { + SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") { + }; + sdJws.verifySignature(testSettings.holderVerifierContext); + } + + @Test + public void testVerifySignature_WrongPublicKey() { + SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") { + }; + assertThrows(VerificationException.class, () -> sdJws.verifySignature(testSettings.issuerVerifierContext)); + } + + @Test + public void testVerifyExpClaim_ExpiredJWT() { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("exp", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()); + SdJws sdJws = new SdJws(payload) { + }; + assertThrows(VerificationException.class, sdJws::verifyExpClaim); + } + + @Test + public void testVerifyExpClaim_Positive() throws Exception { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + SdJws sdJws = new SdJws(payload) { + }; + sdJws.verifyExpClaim(); + } + + @Test + public void testVerifyNotBeforeClaim_Negative() { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("nbf", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + SdJws sdJws = new SdJws(payload) { + }; + assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim); + } + + @Test + public void testVerifyNotBeforeClaim_Positive() throws Exception { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("nbf", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()); + SdJws sdJws = new SdJws(payload) { + }; + sdJws.verifyNotBeforeClaim(); + } + + @Test + public void testPayloadJwsConstruction() { + SdJws sdJws = new SdJws(createPayload()) { + }; + assertNotNull(sdJws.getPayload()); + } + + @Test(expected = IllegalStateException.class) + public void testUnsignedJwsConstruction() { + SdJws sdJws = new SdJws(createPayload()) { + }; + sdJws.toJws(); + } + + @Test + public void testSignedJwsConstruction() { + SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") { + }; + assertNotNull(sdJws.toJws()); + } + + + + @Test + public void testVerifyIssClaim_Negative() { + List allowedIssuers = Arrays.asList(new String[]{"issuer1@sdjwt.com", "issuer2@sdjwt.com"}); + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("iss", "unknown-issuer@sdjwt.com"); + SdJws sdJws = new SdJws(payload) {}; + VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyIssClaim(allowedIssuers)); + assertEquals("Unknown 'iss' claim value: unknown-issuer@sdjwt.com", exception.getMessage()); + } + + @Test + public void testVerifyIssClaim_Positive() throws VerificationException { + List allowedIssuers = Arrays.asList(new String[]{"issuer1@sdjwt.com", "issuer2@sdjwt.com"}); + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("iss", "issuer1@sdjwt.com"); + SdJws sdJws = new SdJws(payload) {}; + sdJws.verifyIssClaim(allowedIssuers); + } + + @Test + public void testVerifyVctClaim_Negative() { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("vct", "IdentityCredential"); + SdJws sdJws = new SdJws(payload) {}; + VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyVctClaim(Collections.singletonList("PassportCredential"))); + assertEquals("Unknown 'vct' claim value: IdentityCredential", exception.getMessage()); + } + + @Test + public void testVerifyVctClaim_Positive() throws VerificationException { + JsonNode payload = createPayload(); + ((ObjectNode) payload).put("vct", "IdentityCredential"); + SdJws sdJws = new SdJws(payload) {}; + sdJws.verifyVctClaim(Collections.singletonList("IdentityCredential")); + } + + @Test + public void shouldValidateAgeSinceIssued() throws VerificationException { + long now = Instant.now().getEpochSecond(); + SdJws sdJws = exampleSdJws(now); + sdJws.verifyAge(180); + } + + @Test + public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() { + long now = Instant.now().getEpochSecond(); + SdJws sdJws = exampleSdJws(now - 1000); // that will be too old + VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180)); + assertEquals("jwt is too old", exception.getMessage()); + } + + private SdJws exampleSdJws(long iat) { + ObjectNode payload = SdJwtUtils.mapper.createObjectNode(); + payload.set("iat", SdJwtUtils.mapper.valueToTree(iat)); + + return new SdJws(payload) { + }; + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java new file mode 100644 index 000000000000..b1ce1fe8fdc0 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java @@ -0,0 +1,144 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.rule.CryptoInitRule; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests for SdJwtFacade. + * + * @author Rodrick Awambeng + */ +public abstract class SdJwtFacadeTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + private static final String HASH_ALGORITHM = "sha-256"; + private static final String JWS_TYPE = "JWS_TYPE"; + + private SdJwtFacade sdJwtFacade; + + private JsonNode claimSet; + private DisclosureSpec disclosureSpec; + + @Before + public void setUp() { + SignatureSignerContext signer = TestSettings.getInstance().getIssuerSignerContext(); + + sdJwtFacade = new SdJwtFacade(signer, HASH_ALGORITHM, JWS_TYPE); + + claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-holder-claims.json"); + disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("sub", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .build(); + } + + @Test + public void shouldCreateSdJwtSuccessfully() { + SdJwt createdSdJwt = sdJwtFacade.createSdJwt(claimSet, disclosureSpec); + + assertNotNull(createdSdJwt); + } + + @Test + public void shouldVerifySdJwtSuccessfullyWithValidKeys() { + claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-claims.json"); + + SdJwt sdJwt = sdJwtFacade.createSdJwt(claimSet, disclosureSpec); + + List verifyingKeys = Collections.singletonList( + createSignatureVerifierContext("doc-signer-05-25-2022", "ES256", true) + ); + IssuerSignedJwtVerificationOpts verificationOpts = createVerificationOptions(); + + try { + sdJwtFacade.verifySdJwt(sdJwt, verifyingKeys, verificationOpts); + } catch (VerificationException e) { + fail("Verification failed: " + e.getMessage()); + } + } + + @Test + public void shouldReturnSdJwtString() { + SdJwt sdJwt = sdJwtFacade.createSdJwt(claimSet, disclosureSpec); + + String sdJwtString = sdJwtFacade.getSdJwtString(sdJwt); + + assertNotNull(sdJwtString); + assertEquals(sdJwt.toString(), sdJwtString); + } + + @Test + public void shouldFailVerificationWithInvalidKeys() { + claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-claims.json"); + SdJwt sdJwt = sdJwtFacade.createSdJwt(claimSet, disclosureSpec); + + List invalidKeys = Collections.singletonList( + createSignatureVerifierContext("invalid-key-id", "invalid-algorithm", false) + ); + IssuerSignedJwtVerificationOpts verificationOpts = createVerificationOptions(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwtFacade.verifySdJwt(sdJwt, invalidKeys, verificationOpts) + ); + + assertTrue(exception.getMessage().contains("Signature could not be verified")); + } + + private SignatureVerifierContext createSignatureVerifierContext(String kid, String algorithm, boolean verificationResult) { + return new SignatureVerifierContext() { + @Override + public String getKid() { + return kid; + } + + @Override + public String getAlgorithm() { + return algorithm; + } + + @Override + public boolean verify(byte[] data, byte[] signature) { + return verificationResult; + } + }; + } + + private IssuerSignedJwtVerificationOpts createVerificationOptions() { + return new IssuerSignedJwtVerificationOpts(true, true, false); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtTest.java new file mode 100644 index 000000000000..34c96a422cb2 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtTest.java @@ -0,0 +1,105 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.keycloak.crypto.SignatureSignerContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Francis Pouatcha + */ +public class SdJwtTest { + + @Test + public void settingsTest() { + SignatureSignerContext issuerSignerContext = TestSettings.getInstance().getIssuerSignerContext(); + assertNotNull(issuerSignerContext); + } + + @Test + public void testA1_Example2_with_nested_disclosure_and_decoy_claims() { + DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("locality", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("region", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("country", "lklxF5jMYlGTPUovMNIvCA") + .withDecoyClaim("2GLC42sKQveCfGfryNRN9w") + .withDecoyClaim("eluV5Og3gSNII8EYnsxA_A") + .withDecoyClaim("6Ij7tM-a5iVPGboS5tmvVA") + .withDecoyClaim("eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("sub", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("phone_number", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "yytVbdAPGcgl2rI4C9GSog") + .withDecoyClaim("AJx-095VPrpTtN4QMOqROA") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-holder-claims.json"); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + + // produce the nested sdJwt + SdJwt addrSdJWT = SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + JsonNode addPayload = addrSdJWT.asNestedPayload(); + JsonNode expectedAddrPayload = TestUtils.readClaimSet(getClass(), + "sdjwt/a1.example2-address-payload.json"); + assertEquals(expectedAddrPayload, addPayload); + + // Verify nested claim has 4 disclosures + assertEquals(4, addrSdJWT.getDisclosures().size()); + + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + // Read claims added by the issuer & merge both + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + // produce the main sdJwt, adding nested sdJwts + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + // Verify all claims are present. + // 10 disclosures from 16 digests (6 decoy claims & decoy array elements) + assertEquals(10, sdJwt.getDisclosures().size()); + } + +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtUtilsTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtUtilsTest.java new file mode 100644 index 000000000000..5808b10ef6e3 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtUtilsTest.java @@ -0,0 +1,124 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; +import org.keycloak.jose.jws.crypto.HashUtils; + +/** + * @author Francis Pouatcha + */ +public class SdJwtUtilsTest { + /** + * Verify hash production and base 64 url encoding + * Verify algorithm denomination for keycloak encoding. + */ + @Test + public void testHashDisclosure() { + String expected = "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY"; + byte[] hash = HashUtils.hash("SHA-256", "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0".getBytes()); + assertEquals(expected, SdJwtUtils.encodeNoPad(hash)); + } + + /** + * Verify hash production and base 64 url encoding + * Verify algorithm denomination for keycloak encoding. + */ + @Test + public void testHashDisclosure2() { + String expected = "w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs"; + byte[] hash = HashUtils.hash("SHA-256", "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0".getBytes()); + assertEquals(expected, SdJwtUtils.encodeNoPad(hash)); + } + + /** + * Test the base64 URL encoding of this json string from the spec, + * with whitespace between array elements. + * + * ["_26bc4LT-ac6q2KI6cBW5es", "family_name", "Möbius"] + * + * shall produce + * WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0 + * + * There is no padding in the expected string. + * + * see + * https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1 + * + * @throws IOException + */ + @Test + public void testBase64urlEncodedObjectWhiteSpacedJsonArray() { + String input = "[\"_26bc4LT-ac6q2KI6cBW5es\", \"family_name\", \"Möbius\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes())); + } + + /** + * As we are expexting json serializer to behave differently + * + * https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1 + * + * @throws IOException + */ + @Test + public void testBase64urlEncodedObjectNoWhiteSpacedJsonArray() { + // Test the base64 URL encoding of this json string from the spec, + // no whitespace between array elements + String input = "[\"_26bc4LT-ac6q2KI6cBW5es\",\"family_name\",\"Möbius\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes())); + } + + @Test + public void testBase64urlEncodedArrayElementWhiteSpacedJsonArray() { + String input = "[\"lklxF5jMYlGTPUovMNIvCA\", \"FR\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes())); + } + + @Test + public void testBase64urlEncodedArrayElementNoWhiteSpacedJsonArray() { + String input = "[\"lklxF5jMYlGTPUovMNIvCA\",\"FR\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes())); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java new file mode 100644 index 000000000000..283f6ea59eb7 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java @@ -0,0 +1,492 @@ +/* + * 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.sdjwt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.rule.CryptoInitRule; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.keycloak.crypto.SignatureSignerContext; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * @author Ingrid Kamga + */ +public abstract class SdJwtVerificationTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + static ObjectMapper mapper = new ObjectMapper(); + static TestSettings testSettings = TestSettings.getInstance(); + + @Test + public void settingsTest() { + SignatureSignerContext issuerSignerContext = testSettings.issuerSigContext; + assertNotNull(issuerSignerContext); + } + + @Test + public void testSdJwtVerification_FlatSdJwt() throws VerificationException { + for (String hashAlg : Arrays.asList("sha-256", "sha-384", "sha-512")) { + SdJwt sdJwt = exampleFlatSdJwtV1() + .withHashAlgorithm(hashAlg) + .build(); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + } + } + + @Test + public void testSdJwtVerification_EnforceIdempotence() throws VerificationException { + SdJwt sdJwt = exampleFlatSdJwtV1().build(); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + } + + @Test + public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException { + SdJwt sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build(); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + } + + @Test + public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception { + SdJwt sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build(); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + } + + @Test + public void testSdJwtVerification_RecursiveSdJwt() throws Exception { + SdJwt sdJwt = exampleRecursiveSdJwtV1().build(); + + sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ); + } + + @Test + public void sdJwtVerificationShouldFail_OnInsecureHashAlg() { + SdJwt sdJwt = exampleFlatSdJwtV1() + .withHashAlgorithm("sha-224") // not deemed secure + .build(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertEquals("Unexpected or insecure hash algorithm: sha-224", exception.getMessage()); + } + + @Test + public void sdJwtVerificationShouldFail_WithWrongVerifier() { + SdJwt sdJwt = exampleFlatSdJwtV1().build(); + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + Collections.singletonList(testSettings.holderVerifierContext), // wrong verifier + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertThat(exception.getMessage(), is("Invalid Issuer-Signed JWT: Signature could not be verified")); + } + + @Test + public void sdJwtVerificationShouldFail_IfExpired() { + long now = Instant.now().getEpochSecond(); + + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); + claimSet.put("exp", now - 1000); // expired 1000 seconds ago + + // Exp claim is plain + SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build(); + // Exp claim is undisclosed + SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet())) + .withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A") + .build()).build(); + + for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) { + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts() + .withValidateExpirationClaim(true) + .build() + ) + ); + + assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage()); + assertEquals("JWT has expired", exception.getCause().getMessage()); + } + } + + @Test + public void sdJwtVerificationShouldFail_IfExpired_CaseExpInvalid() { + // exp: null + ObjectNode claimSet1 = mapper.createObjectNode(); + claimSet1.put("given_name", "John"); + + // exp: invalid + ObjectNode claimSet2 = mapper.createObjectNode(); + claimSet1.put("given_name", "John"); + claimSet1.put("exp", "should-not-be-a-string"); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .build(); + + SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build(); + SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build(); + + for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) { + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts() + .withValidateExpirationClaim(true) + .build() + ) + ); + + assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage()); + assertEquals("Missing or invalid 'exp' claim", exception.getCause().getMessage()); + } + } + + @Test + public void sdJwtVerificationShouldFail_IfIssuedInTheFuture() { + long now = Instant.now().getEpochSecond(); + + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); + claimSet.put("iat", now + 1000); // issued in the future + + // Exp claim is plain + SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build(); + // Exp claim is undisclosed + SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet())) + .withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A") + .build()).build(); + + for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) { + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts() + .withValidateIssuedAtClaim(true) + .build() + ) + ); + + assertEquals("Issuer-Signed JWT: Invalid `iat` claim", exception.getMessage()); + assertEquals("JWT issued in the future", exception.getCause().getMessage()); + } + } + + @Test + public void sdJwtVerificationShouldFail_IfNbfInvalid() { + long now = Instant.now().getEpochSecond(); + + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); + claimSet.put("nbf", now + 1000); // now will be too soon to accept the jwt + + // Exp claim is plain + SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build(); + // Exp claim is undisclosed + SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet())) + .withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A") + .build()).build(); + + for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) { + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts() + .withValidateNotBeforeClaim(true) + .build() + ) + ); + + assertEquals("Issuer-Signed JWT: Invalid `nbf` claim", exception.getMessage()); + assertEquals("JWT is not yet valid", exception.getCause().getMessage()); + } + } + + @Test + public void sdJwtVerificationShouldFail_IfSdArrayElementIsNotString() throws JsonProcessingException { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); + claimSet.set("_sd", mapper.readTree("[123]")); + + SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertEquals("Unexpected non-string element inside _sd array: 123", exception.getMessage()); + } + + @Test + public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() { + for (String forbiddenClaimName : Arrays.asList("_sd", "...")) { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put(forbiddenClaimName, "Value"); + + SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withUndisclosedClaim(forbiddenClaimName, "eluV5Og3gSNII8EYnsxA_A") + .build()).build(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertEquals("Disclosure claim name must not be '_sd' or '...'", exception.getMessage()); + } + } + + @Test + public void sdJwtVerificationShouldFail_IfDuplicateDigestValue() { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); // this same field will also be nested + + SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .build()).build(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertTrue(exception.getMessage().startsWith("A digest was encountered more than once:")); + } + + @Test + public void sdJwtVerificationShouldFail_IfDuplicateSaltValue() { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("given_name", "John"); + claimSet.put("family_name", "Doe"); + + String salt = "eluV5Og3gSNII8EYnsxA_A"; + SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder() + .withUndisclosedClaim("given_name", salt) + // We are reusing the same salt value, and that is the problem + .withUndisclosedClaim("family_name", salt) + .build()).build(); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwt.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build() + ) + ); + + assertEquals("A salt value was reused: " + salt, exception.getMessage()); + } + + private List defaultIssuerVerifyingKeys() { + return Collections.singletonList(testSettings.issuerVerifierContext); + } + + private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() { + return IssuerSignedJwtVerificationOpts.builder() + .withValidateIssuedAtClaim(false) + .withValidateExpirationClaim(false) + .withValidateNotBeforeClaim(false); + } + + private SdJwt.Builder exampleFlatSdJwtV1() { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c"); + claimSet.put("given_name", "John"); + claimSet.put("family_name", "Doe"); + claimSet.put("email", "john.doe@example.com"); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .build(); + + return SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .withSigner(testSettings.issuerSigContext); + } + + private SdJwt.Builder exampleFlatSdJwtV2(ObjectNode claimSet, DisclosureSpec disclosureSpec) { + return SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .withSigner(testSettings.issuerSigContext); + } + + private SdJwt exampleAddrSdJwt() { + ObjectNode addressClaimSet = mapper.createObjectNode(); + addressClaimSet.put("street_address", "Rue des Oliviers"); + addressClaimSet.put("city", "Paris"); + addressClaimSet.put("country", "France"); + + DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("city", "G02NSrQfjFXQ7Io09syajA") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .build(); + + return SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + } + + private SdJwt.Builder exampleSdJwtWithUndisclosedNestedFieldsV1() { + SdJwt addrSdJWT = exampleAddrSdJwt(); + + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c"); + claimSet.put("given_name", "John"); + claimSet.put("family_name", "Doe"); + claimSet.put("email", "john.doe@example.com"); + claimSet.set("address", addrSdJWT.asNestedPayload()); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + return SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .withNestedSdJwt(addrSdJWT) + .withSigner(testSettings.issuerSigContext); + } + + private SdJwt.Builder exampleSdJwtWithUndisclosedArrayElementsV1() throws JsonProcessingException { + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c"); + claimSet.put("given_name", "John"); + claimSet.put("family_name", "Doe"); + claimSet.put("email", "john.doe@example.com"); + claimSet.set("nationalities", mapper.readTree("[\"US\", \"DE\"]")); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA") + .withDecoyArrayElt("nationalities", 2, "G02NSrQfjFXQ7Io09syajA") + .build(); + + return SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .withSigner(testSettings.issuerSigContext); + } + + private SdJwt.Builder exampleRecursiveSdJwtV1() { + SdJwt addrSdJWT = exampleAddrSdJwt(); + + ObjectNode claimSet = mapper.createObjectNode(); + claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c"); + claimSet.put("given_name", "John"); + claimSet.put("family_name", "Doe"); + claimSet.put("email", "john.doe@example.com"); + claimSet.set("address", addrSdJWT.asNestedPayload()); + + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + // Making the whole address object selectively disclosable makes the process recursive + .withUndisclosedClaim("address", "BZFzhQsdPfZY1WSL-1GXKg") + .build(); + + return SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .withNestedSdJwt(addrSdJWT) + .withSigner(testSettings.issuerSigContext); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/TestSettings.java b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java new file mode 100644 index 000000000000..8ebcc81da11f --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java @@ -0,0 +1,235 @@ +/* + * 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.sdjwt; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.util.HashMap; +import java.util.Map; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.crypto.ECDSASignatureSignerContext; +import org.keycloak.crypto.ECDSASignatureVerifierContext; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Import test-settings from: + * + * open wallet foundation labs + * + * @author Francis Pouatcha + */ +public class TestSettings { + public final SignatureSignerContext holderSigContext; + public final SignatureSignerContext issuerSigContext; + public final SignatureVerifierContext holderVerifierContext; + public final SignatureVerifierContext issuerVerifierContext; + + private static TestSettings instance = null; + + public static TestSettings getInstance() { + if (instance == null) { + instance = new TestSettings(); + } + return instance; + } + + public SignatureSignerContext getIssuerSignerContext() { + return issuerSigContext; + } + + public SignatureSignerContext getHolderSignerContext() { + return holderSigContext; + } + + public SignatureVerifierContext getIssuerVerifierContext() { + return issuerVerifierContext; + } + + public SignatureVerifierContext getHolderVerifierContext() { + return holderVerifierContext; + } + + // private constructor + private TestSettings() { + JsonNode testSettings = TestUtils.readClaimSet(getClass(), "sdjwt/test-settings.json"); + JsonNode keySettings = testSettings.get("key_settings"); + + holderSigContext = initSigContext(keySettings, "holder_key", "ES256", "holder"); + issuerSigContext = initSigContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022"); + + holderVerifierContext = initVerifierContext(keySettings, "holder_key", "ES256", "holder"); + issuerVerifierContext = initVerifierContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022"); + } + + private static SignatureSignerContext initSigContext(JsonNode keySettings, String keyName, String algorithm, + String kid) { + JsonNode keySetting = keySettings.get(keyName); + KeyPair keyPair = readKeyPair(keySetting); + return getSignatureSignerContext(keyPair, algorithm, kid); + } + + private static SignatureVerifierContext initVerifierContext(JsonNode keySettings, String keyName, String algorithm, + String kid) { + JsonNode keySetting = keySettings.get(keyName); + KeyPair keyPair = readKeyPair(keySetting); + return getSignatureVerifierContext(keyPair.getPublic(), algorithm, kid); + } + + private static KeyPair readKeyPair(JsonNode keySetting) { + String curveName = keySetting.get("crv").asText(); + String base64UrlEncodedD = keySetting.get("d").asText(); + String base64UrlEncodedX = keySetting.get("x").asText(); + String base64UrlEncodedY = keySetting.get("y").asText(); + return readEcdsaKeyPair(curveName, base64UrlEncodedD, base64UrlEncodedX, base64UrlEncodedY); + } + + public static SignatureVerifierContext verifierContextFrom(JsonNode keyData, String algorithm) { + PublicKey publicKey = readPublicKey(keyData); + return getSignatureVerifierContext(publicKey, algorithm, KeyUtils.createKeyId(publicKey)); + } + + private static PublicKey readPublicKey(JsonNode keyData) { + if (keyData.has("jwk")) { + keyData = keyData.get("jwk"); + } + String curveName = keyData.get("crv").asText(); + String base64UrlEncodedX = keyData.get("x").asText(); + String base64UrlEncodedY = keyData.get("y").asText(); + return readEcdsaPublic(curveName, base64UrlEncodedX, base64UrlEncodedY); + } + + private static PublicKey readEcdsaPublic(String curveName, String base64UrlEncodedX, + String base64UrlEncodedY) { + + ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName)); + + byte[] xBytes = Base64Url.decode(base64UrlEncodedX); + byte[] yBytes = Base64Url.decode(base64UrlEncodedY); + + try { + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + + // Generate ECPrivateKey + + // Instantiate ECPoint + BigInteger xValue = new BigInteger(1, xBytes); + BigInteger yValue = new BigInteger(1, yBytes); + ECPoint point = new ECPoint(xValue, yValue); + + // Generate ECPublicKey + return keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static KeyPair readEcdsaKeyPair(String curveName, String base64UrlEncodedD, String base64UrlEncodedX, + String base64UrlEncodedY) { + + ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName)); + + byte[] dBytes = Base64Url.decode(base64UrlEncodedD); + byte[] xBytes = Base64Url.decode(base64UrlEncodedX); + byte[] yBytes = Base64Url.decode(base64UrlEncodedY); + + try { + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + + // Generate ECPrivateKey + BigInteger dValue = new BigInteger(1, dBytes); + PrivateKey privateKey = keyFactory.generatePrivate(new ECPrivateKeySpec(dValue, ecSpec)); + + // Instantiate ECPoint + BigInteger xValue = new BigInteger(1, xBytes); + BigInteger yValue = new BigInteger(1, yBytes); + ECPoint point = new ECPoint(xValue, yValue); + + // Generate ECPublicKey + PublicKey publicKey = keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec)); + return new KeyPair(publicKey, privateKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final Map ECDSA_KEY_SPECS = new HashMap<>(); + + private static ECParameterSpec getECParameterSpec(String paramSpecName) { + return ECDSA_KEY_SPECS.computeIfAbsent(paramSpecName, TestSettings::generateEcdsaKeySpec); + } + + // generate key spec + private static ECParameterSpec generateEcdsaKeySpec(String paramSpecName) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(paramSpecName); + keyPairGenerator.initialize(ecGenParameterSpec); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return ((java.security.interfaces.ECPublicKey) keyPair.getPublic()).getParams(); + } catch (Exception e) { + throw new RuntimeException("Error obtaining ECParameterSpec for P-256 curve", e); + } + } + + private static SignatureSignerContext getSignatureSignerContext(KeyPair keyPair, String algorithm, String kid) { + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setPrivateKey(keyPair.getPrivate()); + keyWrapper.setPublicKey(keyPair.getPublic()); + keyWrapper.setType(keyPair.getPublic().getAlgorithm()); + keyWrapper.setUse(KeyUse.SIG); + keyWrapper.setKid(kid); + return new ECDSASignatureSignerContext(keyWrapper); + } + + private static SignatureVerifierContext getSignatureVerifierContext(PublicKey publicKey, String algorithm, + String kid) { + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setPublicKey(publicKey); + keyWrapper.setType(publicKey.getAlgorithm()); + keyWrapper.setUse(KeyUse.SIG); + keyWrapper.setKid(kid); + return new ECDSASignatureVerifierContext(keyWrapper); + } + + private static final Map ECDSA_CURVE_2_SPECS_NAMES = new HashMap<>(); + + private static final void curveToSpecName() { + ECDSA_CURVE_2_SPECS_NAMES.put("P-256", "secp256r1"); + } + + static { + curveToSpecName(); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/TestUtils.java b/core/src/test/java/org/keycloak/sdjwt/TestUtils.java new file mode 100644 index 000000000000..d0da34dabc92 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/TestUtils.java @@ -0,0 +1,62 @@ +/* + * 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.sdjwt; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Francis Pouatcha + */ +public class TestUtils { + public static JsonNode readClaimSet(Class klass, String path) { + // try-with-resources closes inputstream! + try (InputStream is = klass.getClassLoader().getResourceAsStream(path)) { + return SdJwtUtils.mapper.readTree(is); + } catch (IOException e) { + throw new RuntimeException("Error reading file at path: " + path, e); + } + } + + public static String readFileAsString(Class klass, String filePath) { + StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + (new InputStreamReader(klass.getClassLoader().getResourceAsStream(filePath))))) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); // Appends line without a newline character + } + } catch (IOException e) { + throw new RuntimeException("Error reading file at path: " + filePath, e); + } + return stringBuilder.toString(); + } + + public static String splitStringIntoLines(String input, int lineLength) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i += lineLength) { + int end = Math.min(input.length(), i + lineLength); + result.append(input, i, end).append("\n"); + } + return result.toString(); + } + +} diff --git a/core/src/test/java/org/keycloak/sdjwt/UndisclosedClaimTest.java b/core/src/test/java/org/keycloak/sdjwt/UndisclosedClaimTest.java new file mode 100644 index 000000000000..8883a809c9a1 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/UndisclosedClaimTest.java @@ -0,0 +1,58 @@ +/* + * 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.sdjwt; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.node.TextNode; + +/** + * @author Francis Pouatcha + */ +public class UndisclosedClaimTest { + + @Before + public void setUp() throws Exception { + SdJwtUtils.arrayEltSpaced = false; + } + + @After + public void tearDown() throws Exception { + SdJwtUtils.arrayEltSpaced = true; + } + + @Test + public void testToBase64urlEncoded() { + // Create an instance of UndisclosedClaim with the specified fields + UndisclosedClaim undisclosedClaim = UndisclosedClaim.builder() + .withClaimName("family_name") + .withSalt(new SdJwtSalt("_26bc4LT-ac6q2KI6cBW5es")) + .withClaimValue(new TextNode("Möbius")) + .build(); + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, undisclosedClaim.getDisclosureStrings().get(0)); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuerTest.java b/core/src/test/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuerTest.java new file mode 100644 index 000000000000..7a4aeb8e2aa5 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/consumer/JwtVcMetadataTrustedSdJwtIssuerTest.java @@ -0,0 +1,419 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.rule.CryptoInitRule; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJws; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.TestUtils; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.rmi.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * @author Ingrid Kamga + */ +public abstract class JwtVcMetadataTrustedSdJwtIssuerTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Test + public void shouldResolveIssuerVerifyingKeys() throws Exception { + String issuerUri = "https://issuer.example.com"; + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUri, mockHttpDataFetcher()); + + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt(); + List keys = trustedIssuer + .resolveIssuerVerifyingKeys(issuerSignedJWT); + + // There three keys exposed on the metadata endpoint. + assertEquals(3, keys.size()); + } + + @Test + public void shouldResolveKeys_WhenIssuerTrustedOnRegexPattern() throws Exception { + Pattern issuerUriRegex = Pattern.compile("https://.*\\.example\\.com"); + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUriRegex, mockHttpDataFetcher()); + + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt(); + trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT); + } + + @Test + public void shouldResolveKeys_WhenJwtSpecifiesKid() throws VerificationException, JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUri, mockHttpDataFetcher()); + + // This JWT specifies a key ID in its header + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt"); + + // Act + List keys = trustedIssuer + .resolveIssuerVerifyingKeys(issuerSignedJWT); + + // Despite three keys exposed on the metadata endpoint, + // only the key matching the JWT's kid is resolved. + assertEquals(1, keys.size()); + assertEquals("doc-signer-05-25-2022", keys.get(0).getKid()); + } + + @Test + public void shouldRejectNonHttpsIssuerURIs() { + String issuerUri = "http://issuer.example.com"; // not https + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new JwtVcMetadataTrustedSdJwtIssuer(issuerUri, mockHttpDataFetcher()) + ); + + assertThat(exception.getMessage(), endsWith("HTTPS URI required to retrieve JWT VC Issuer Metadata")); + } + + @Test + public void shouldRejectNonHttpsIssuerURIs_EvenIfIssuerBuiltWithRegexPattern() throws JsonProcessingException { + Pattern issuerUriRegex = Pattern.compile(".*\\.example\\.com"); + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUriRegex, mockHttpDataFetcher()); + + String issuerUri = "http://issuer.example.com"; // not https + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwtWithIssuer(issuerUri); + VerificationException exception = assertThrows(VerificationException.class, + () -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT) + ); + + assertThat(exception.getMessage(), endsWith("HTTPS URI required to retrieve JWT VC Issuer Metadata")); + } + + @Test + public void shouldRejectJwtsWithUnexpectedIssuerClaims() throws JsonProcessingException { + String regex = "https://.*\\.example\\.com"; + Pattern issuerUriRegex = Pattern.compile(regex); + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUriRegex, mockHttpDataFetcher()); + + String issuerUri = "https://trial.authlete.com"; // does not match the regex above + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwtWithIssuer(issuerUri); + VerificationException exception = assertThrows(VerificationException.class, + () -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT) + ); + + assertThat(exception.getMessage(), + endsWith(String.format("Unexpected Issuer URI claim. Expected=/%s/, Got=%s", regex, issuerUri))); + } + + @Test + public void shouldFailOnInvalidJwkExposed() throws JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + metadata.set("jwks", SdJwtUtils.mapper.readTree( + "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"kty\": \"EC\",\n" + + " \"x\": \"invalid\",\n" + + " \"y\": \"invalid\"\n" + + " }\n" + + " ]\n" + + "}" + )); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadata(issuerUri, metadata), + "A potential JWK was retrieved but found invalid", + "Unsupported or invalid JWK" + ); + } + + @Test + public void shouldFailOnUnexpectedIssuerExposed() throws JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", "https://another-issuer.example.com"); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadata(issuerUri, metadata), + "Unexpected metadata's issuer", + null + ); + } + + @Test + public void shouldFailOnMalformedJwtVcMetadataExposed() throws JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + // This is malformed because the issuer must be a string + metadata.set("issuer", SdJwtUtils.mapper.readTree("{}")); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadata(issuerUri, metadata), + "Failed to parse exposed JWT VC Metadata", + null + ); + } + + @Test + public void shouldFailOnMalformedJwksExposed() throws JsonProcessingException { + List malformedJwks = Arrays.asList( + SdJwtUtils.mapper.readTree("[]"), + SdJwtUtils.mapper.readTree("{\"keys\": {}}") + ); + + for (JsonNode jwks : malformedJwks) { + String issuerUri = "https://issuer.example.com"; + + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + metadata.put("jwks_uri", "https://issuer.example.com/api/vci/jwks"); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks), + "Failed to parse exposed JWKS", + null + ); + } + } + + @Test + public void shouldFailOnMissingJwksOnMetadataEndpoint() throws JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + + // There are no means to resolve JWKS with these metadata + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadata(issuerUri, metadata), + String.format("Could not resolve issuer JWKs with URI: %s", issuerUri), + null + ); + } + + @Test + public void shouldFailOnEmptyPublishedJwkList() throws Exception { + // This JWKS to publish has an empty list of keys, which is unexpected. + JsonNode jwks = SdJwtUtils.mapper.readTree("{\"keys\": []}"); + + // JWT VC Metadata + String issuerUri = "https://issuer.example.com"; + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + metadata.set("jwks", jwks); + + // Act and assert + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks), + String.format("Issuer JWKs were unexpectedly resolved to an empty list. Issuer URI: %s", issuerUri), + null + ); + } + + @Test + public void shouldFailOnNoPublishedJwkMatchingJwtKid() throws Exception { + // Alter kid fields of all JWKs to publish, so as none is a match + JsonNode jwks = exampleJwks(); + for (JsonNode jwk : jwks.get("keys")) { + ((ObjectNode) jwk).put("kid", jwk.get("kid").asText() + "-wont-match"); + } + + // JWT VC Metadata + String issuerUri = "https://issuer.example.com"; + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + metadata.set("jwks", jwks); + + // This JWT specifies a key ID in its header + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt"); + String kid = issuerSignedJWT.getHeader().getKeyId(); + + // Act and assert + genericTestShouldFail( + issuerSignedJWT, + mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks), + String.format("No published JWK was found to match kid: %s", kid), + null + ); + } + + @Test + public void shouldFailOnPublishedJwksWithDuplicateKid() throws JsonProcessingException { + // This JWT specifies a key ID in its header + + IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt"); + + // Set the same kid to all JWKs to publish, which is problematic + + String kid = issuerSignedJWT.getHeader().getKeyId(); + JsonNode jwks = exampleJwks(); + for (JsonNode jwk : jwks.get("keys")) { + ((ObjectNode) jwk).put("kid", kid); + } + + // JWT VC Metadata + + String issuerUri = "https://issuer.example.com"; + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + metadata.set("jwks", jwks); + + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerUri, mockHttpDataFetcherWithMetadata(issuerUri, metadata)); + + // Act and assert + + VerificationException exception = assertThrows(VerificationException.class, + () -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT) + ); + + assertThat(exception.getMessage(), + endsWith(String.format("Cannot choose between multiple exposed JWKs with same kid: %s", kid))); + } + + @Test + public void shouldFailOnIOErrorWhileFetchingMetadata() throws JsonProcessingException { + String issuerUri = "https://issuer.example.com"; + + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", issuerUri); + + HttpDataFetcher mockFetcher = mockHttpDataFetcherWithMetadata( + // HTTP can only mock an unrelated issuer + "https://another-issuer.example.com", + metadata + ); + + genericTestShouldFail( + exampleIssuerSignedJwt(), + mockFetcher, + String.format("Could not fetch data from URI: %s", issuerUri), + null + ); + } + + private void genericTestShouldFail( + IssuerSignedJWT issuerSignedJWT, + HttpDataFetcher mockFetcher, + String errorMessage, + String causeErrorMessage + ) { + TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer( + issuerSignedJWT.getPayload().get(SdJws.CLAIM_NAME_ISSUER).asText(), + mockFetcher + ); + + VerificationException exception = assertThrows(VerificationException.class, + () -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT) + ); + + assertTrue(exception.getMessage().contains(errorMessage)); + if (causeErrorMessage != null) { + assertTrue(exception.getCause().getMessage().contains(causeErrorMessage)); + } + } + + private IssuerSignedJWT exampleIssuerSignedJwt() { + return exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb.txt"); + } + + private IssuerSignedJWT exampleIssuerSignedJwt(String sdJwtVector) { + return exampleIssuerSignedJwt(sdJwtVector, "https://issuer.example.com"); + } + + private IssuerSignedJWT exampleIssuerSignedJwtWithIssuer(String issuerUri) { + return exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb.txt", issuerUri); + } + + private IssuerSignedJWT exampleIssuerSignedJwt(String sdJwtVector, String issuerUri) { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), sdJwtVector); + IssuerSignedJWT issuerSignedJWT = SdJwtVP.of(sdJwtVPString).getIssuerSignedJWT(); + ((ObjectNode) issuerSignedJWT.getPayload()).put("iss", issuerUri); + return issuerSignedJWT; + } + + private JsonNode exampleJwks() throws JsonProcessingException { + return SdJwtUtils.mapper.readTree( + TestUtils.readFileAsString(getClass(), + "sdjwt/s30.1-jwt-vc-metadata-jwks.json" + ) + ); + } + + private HttpDataFetcher mockHttpDataFetcher() throws JsonProcessingException { + ObjectNode metadata = SdJwtUtils.mapper.createObjectNode(); + metadata.put("issuer", "https://issuer.example.com"); + metadata.put("jwks_uri", "https://issuer.example.com/api/vci/jwks"); + + return mockHttpDataFetcherWithMetadata( + metadata.get("issuer").asText(), + metadata + ); + } + + private HttpDataFetcher mockHttpDataFetcherWithMetadata( + String issuer, JsonNode metadata + ) throws JsonProcessingException { + return mockHttpDataFetcherWithMetadataAndJwks(issuer, metadata, exampleJwks()); + } + + private HttpDataFetcher mockHttpDataFetcherWithMetadataAndJwks( + String issuer, JsonNode metadata, JsonNode jwks + ) { + return uri -> { + if (!uri.startsWith(issuer)) { + throw new UnknownHostException("Unavailable URI"); + } + + if (uri.endsWith("/.well-known/jwt-vc-issuer")) { + return metadata; + } else if (uri.endsWith("/api/vci/jwks")) { + return jwks; + } else { + throw new UnknownHostException("Unavailable URI"); + } + }; + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java b/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java new file mode 100644 index 000000000000..b3829c77daa8 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java @@ -0,0 +1,119 @@ +/* + * 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.sdjwt.consumer; + +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.rule.CryptoInitRule; +import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; +import org.keycloak.sdjwt.TestSettings; +import org.keycloak.sdjwt.TestUtils; +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * @author Ingrid Kamga + */ +public abstract class SdJwtPresentationConsumerTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + SdJwtPresentationConsumer sdJwtPresentationConsumer = new SdJwtPresentationConsumer(); + static TestSettings testSettings = TestSettings.getInstance(); + + @Test + public void shouldVerifySdJwtPresentation() throws VerificationException { + sdJwtPresentationConsumer.verifySdJwtPresentation( + exampleSdJwtVP(), + examplePresentationRequirements(), + exampleTrustedSdJwtIssuers(), + defaultIssuerSignedJwtVerificationOpts(), + defaultKeyBindingJwtVerificationOpts() + ); + } + + @Test + public void shouldFail_IfPresentationRequirementsNotMet() { + SimplePresentationDefinition definition = SimplePresentationDefinition.builder() + .addClaimRequirement("vct", ".*diploma.*") + .build(); + + VerificationException exception = assertThrows(VerificationException.class, + () -> sdJwtPresentationConsumer.verifySdJwtPresentation( + exampleSdJwtVP(), + definition, + exampleTrustedSdJwtIssuers(), + defaultIssuerSignedJwtVerificationOpts(), + defaultKeyBindingJwtVerificationOpts() + ) + ); + + assertTrue(exception.getMessage() + .contains("A required field was not presented: `vct`")); + } + + private SdJwtVP exampleSdJwtVP() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt"); + return SdJwtVP.of(sdJwtVPString); + } + + private PresentationRequirements examplePresentationRequirements() { + return SimplePresentationDefinition.builder() + .addClaimRequirement("sub", "\"user_[0-9]+\"") + .addClaimRequirement("given_name", ".*") + .build(); + } + + private List exampleTrustedSdJwtIssuers() { + return Arrays.asList( + new StaticTrustedSdJwtIssuer( + Collections.singletonList(testSettings.holderVerifierContext) + ), + new StaticTrustedSdJwtIssuer( + Collections.singletonList(testSettings.issuerVerifierContext) + ) + ); + } + + private IssuerSignedJwtVerificationOpts defaultIssuerSignedJwtVerificationOpts() { + return IssuerSignedJwtVerificationOpts.builder() + .withValidateIssuedAtClaim(false) + .withValidateNotBeforeClaim(false) + .build(); + } + + private KeyBindingJwtVerificationOpts defaultKeyBindingJwtVerificationOpts() { + return KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .withAllowedMaxAge(Integer.MAX_VALUE) + .withNonce("1234567890") + .withAud("https://verifier.example.org") + .withValidateExpirationClaim(false) + .withValidateNotBeforeClaim(false) + .build(); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinitionTest.java b/core/src/test/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinitionTest.java new file mode 100644 index 000000000000..aa4e682434d9 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/consumer/SimplePresentationDefinitionTest.java @@ -0,0 +1,82 @@ +/* + * 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.sdjwt.consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.TestUtils; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +/** + * @author Ingrid Kamga + */ +public class SimplePresentationDefinitionTest { + + private final ObjectMapper mapper = SdJwtUtils.mapper; + + @Test + public void testCheckIfSatisfiedBy() throws VerificationException, JsonProcessingException { + SimplePresentationDefinition definition = SimplePresentationDefinition.builder() + .addClaimRequirement("vct", ".*identity_credential.*") + .addClaimRequirement("given_name", "\"John\"") + .addClaimRequirement("cat", "123") + .addClaimRequirement("addr", ".*\"(Douala|Berlin)\".*") + .addClaimRequirement("colors", "\\[\"red\",.*") + .build(); + + definition.checkIfSatisfiedBy(exampleDisclosedPayload()); + } + + @Test + public void testCheckIfSatisfiedBy_shouldFailOnRequiredFieldMissing() { + SimplePresentationDefinition definition = SimplePresentationDefinition.builder() + .addClaimRequirement("family_name", ".*") + .build(); + + VerificationException exception = assertThrows(VerificationException.class, + () -> definition.checkIfSatisfiedBy(exampleDisclosedPayload())); + + assertEquals("A required field was not presented: `family_name`", exception.getMessage()); + } + + @Test + public void testCheckIfSatisfiedBy_shouldFailOnNonMatchingPattern() { + SimplePresentationDefinition definition = SimplePresentationDefinition.builder() + .addClaimRequirement("vct", ".*diploma.*") + .build(); + + VerificationException exception = assertThrows(VerificationException.class, + () -> definition.checkIfSatisfiedBy(exampleDisclosedPayload())); + + assertThat(exception.getMessage(), startsWith("Pattern matching failed for required field")); + } + + private JsonNode exampleDisclosedPayload() throws JsonProcessingException { + String content = TestUtils.readFileAsString(getClass(), + "sdjwt/s7.4-sample-disclosed-issuer-payload.json"); + return mapper.readTree(content); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java new file mode 100644 index 000000000000..6cb7de33208d --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java @@ -0,0 +1,48 @@ +/* + * 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.sdjwt.sdjwtvp; + +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; +import org.junit.Test; + +public class KeyBindingJwtVerificationOptsTest { + + @Test(expected = IllegalArgumentException.class) + public void buildShouldFail_IfKeyBindingRequired_AndNonceNotSpecified() { + KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void buildShouldFail_IfKeyBindingRequired_AndNonceEmpty() { + KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .withNonce("") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void buildShouldFail_IfKeyBindingRequired_AndAudNotSpecified() { + KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .withNonce("12345678") + .build(); + } + +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java new file mode 100644 index 000000000000..2ae3488431c5 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java @@ -0,0 +1,221 @@ +/* + * 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.sdjwt.sdjwtvp; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.rule.CryptoInitRule; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwt; +import org.keycloak.sdjwt.TestSettings; +import org.keycloak.sdjwt.TestUtils; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * @author Francis Pouatcha + */ +public abstract class SdJwtVPTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + // Additional tests can be written to cover edge cases, error conditions, + // and any other functionality specific to the SdJwt class. + @Test + public void testIssuerSignedJWTWithUndiclosedClaims3_3() { + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json"); + + // Merge both + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withSigner(TestSettings.getInstance().getIssuerSignerContext()) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + String sdJwtString = sdJwt.toSdJwtString(); + + SdJwtVP actualSdJwt = SdJwtVP.of(sdJwtString); + + String expectedString = TestUtils.readFileAsString(getClass(), "sdjwt/s3.3-unsecured-sd-jwt.txt"); + SdJwtVP expecteSdJwt = SdJwtVP.of(expectedString); + + TestCompareSdJwt.compare(expecteSdJwt, actualSdJwt); + + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims6_1() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.1-issued-payload.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(0, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testA1_Example2_with_nested_disclosure_and_decoy_claims() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/a1.example2-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(10, sdJwtVP.getDisclosures().size()); + assertEquals(0, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(5, sdJwtVP.getDisclosures().size()); + assertEquals(4, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_GhostDisclosures() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt+ghost.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(8, sdJwtVP.getDisclosures().size()); + assertEquals(4, sdJwtVP.getRecursiveDigests().size()); + assertEquals(3, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_VerifyIssuerSignaturePositive() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getIssuerVerifierContext()); + } + + @Test(expected = VerificationException.class) + public void testS7_3_VerifyIssuerSignatureNegative() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getHolderVerifierContext()); + } + + @Test + public void testS6_2_PresentationPositive() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + String presentation = sdJwtVP.present(null, keyBindingClaims, + TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from settings + presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getHolderVerifierContext()); + + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + } + + @Test(expected = VerificationException.class) + public void testS6_2_PresentationNegative() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + String presentation = sdJwtVP.present(null, keyBindingClaims, + TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + + // Verify with wrong public key from settings (iisuer) + presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getIssuerVerifierContext()); + } + + @Test + public void testS6_2_PresentationPartialDisclosure() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + // disclose only the given_name + String presentation = sdJwtVP.present(Arrays.asList("jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"), + keyBindingClaims, TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + } + + + @Test + public void testOf_validInput() { + String sdJwtString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtString); + + assertNotNull(sdJwtVP); + assertEquals(4, sdJwtVP.getDisclosures().size()); + } + + @Test + public void testOf_MalformedSdJwt_ThrowsIllegalArgumentException() { + // Given + String malformedSdJwt = "issuer-signed-jwt"; + + // When & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SdJwtVP.of(malformedSdJwt)); + assertEquals("SD-JWT is malformed, expected to contain a '~'", exception.getMessage()); + } + +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java new file mode 100644 index 000000000000..5a3a4cb1d7c9 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java @@ -0,0 +1,458 @@ +/* + * 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.sdjwt.sdjwtvp; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.rule.CryptoInitRule; +import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; +import org.keycloak.sdjwt.SdJwt; +import org.keycloak.sdjwt.TestSettings; +import org.keycloak.sdjwt.TestUtils; +import org.keycloak.sdjwt.vp.KeyBindingJWT; +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +/** + * @author Ingrid Kamga + */ +public abstract class SdJwtVPVerificationTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + // This testsuite relies on a range of test vectors (`sdjwt/s20.*-sdjwt+kb*.txt`) + // manually crafted to fit different cases. External tools were typically used, + // including mkjwk.org for generating keys, jwt.io for creating signatures, and + // base64.guru for manipulating the Base64-encoded disclosures. + + static ObjectMapper mapper = new ObjectMapper(); + static TestSettings testSettings = TestSettings.getInstance(); + + @Test + public void testVerif_s20_1_sdjwt_with_kb() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ); + } + + @Test + public void testVerif_s20_8_sdjwt_with_kb__AltCnfCurves() throws VerificationException { + List entries = Arrays.asList( + "sdjwt/s20.8-sdjwt+kb--es384.txt", + "sdjwt/s20.8-sdjwt+kb--es512.txt" + ); + + for (String entry : entries) { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ); + } + } + + @Test + public void testVerif_s20_8_sdjwt_with_kb__CnfRSA() throws VerificationException { + List entries = Arrays.asList( + "sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt", + "sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt", + "sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt", + "sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt" + ); + + for (String entry : entries) { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ); + } + } + + @Test + public void testVerifKeyBindingNotRequired() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts() + .withKeyBindingRequired(false) + .build() + ); + } + + @Test + public void testShouldFail_IfExtraDisclosureWithNoDigest() { + testShouldFailGeneric( + // One disclosure has no digest throughout Issuer-signed JWT + "sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "At least one disclosure is not protected by digest", + null + ); + } + + @Test + public void testShouldFail_IfFieldDisclosureLengthIncorrect() { + testShouldFailGeneric( + // One field disclosure has only two elements + "sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "A field disclosure must contain exactly three elements", + null + ); + } + + @Test + public void testShouldFail_IfArrayElementDisclosureLengthIncorrect() { + testShouldFailGeneric( + // One array element disclosure has more than two elements + "sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "An array element disclosure must contain exactly two elements", + null + ); + } + + @Test + public void testShouldFail_IfKeyBindingRequiredAndMissing() { + testShouldFailGeneric( + // This sd-jwt has no key binding jwt + "sdjwt/s6.2-presented-sdjwtvp.txt", + defaultKeyBindingJwtVerificationOpts() + .withKeyBindingRequired(true) + .build(), + "Missing Key Binding JWT", + null + ); + } + + @Test + public void testShouldFail_IfKeyBindingJwtSignatureInvalid() { + testShouldFailGeneric( + // Messed up with the kb signature + "sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "Key binding JWT invalid", + "VerificationException: Invalid jws signature" + ); + } + + @Test + public void testShouldFail_IfNoCnfClaim() { + testShouldFailGeneric( + // This test vector has no cnf claim in Issuer-signed JWT + "sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "No cnf claim in Issuer-signed JWT for key binding", + null + ); + } + + @Test + public void testShouldFail_IfWrongKbTyp() { + testShouldFailGeneric( + // Key Binding JWT's header: {"kid": "holder", "typ": "unexpected", "alg": "ES256"} + "sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "Key Binding JWT is not of declared typ kb+jwt", + null + ); + } + + @Test + public void testShouldFail_IfReplayChecksFail_Nonce() { + testShouldFailGeneric( + "sdjwt/s20.1-sdjwt+kb.txt", + defaultKeyBindingJwtVerificationOpts() + .withNonce("abcd") // kb's nonce is "1234567890" + .build(), + "Key binding JWT: Unexpected `nonce` value", + null + ); + } + + @Test + public void testShouldFail_IfReplayChecksFail_Aud() { + testShouldFailGeneric( + "sdjwt/s20.1-sdjwt+kb.txt", + defaultKeyBindingJwtVerificationOpts() + .withAud("abcd") // kb's aud is "https://verifier.example.org" + .build(), + "Key binding JWT: Unexpected `aud` value", + null + ); + } + + @Test + public void testShouldFail_IfKbSdHashWrongFormat() { + ObjectNode kbPayload = exampleKbPayload(); + + // This hash is not a string + kbPayload.set("sd_hash", mapper.valueToTree(1234)); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts().build(), + "Key binding JWT: Claim `sd_hash` missing or not a string", + null + ); + } + + @Test + public void testShouldFail_IfKbSdHashInvalid() { + ObjectNode kbPayload = exampleKbPayload(); + + // This hash makes no sense + kbPayload.put("sd_hash", "c3FmZHFmZGZlZXNkZmZi"); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts().build(), + "Key binding JWT: Invalid `sd_hash` digest", + null + ); + } + + @Test + public void testShouldFail_IfKbIssuedInFuture() { + long now = Instant.now().getEpochSecond(); + + ObjectNode kbPayload = exampleKbPayload(); + kbPayload.set("iat", mapper.valueToTree(now + 1000)); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts().build(), + "Key binding JWT: Invalid `iat` claim", + "jwt issued in the future" + ); + } + + @Test + public void testShouldFail_IfKbTooOld() { + long issuerSignedJwtIat = 1683000000; // same value in test vector + + ObjectNode kbPayload = exampleKbPayload(); + // This KB-JWT is then issued more than 60s ago + kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120)); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts() + .withAllowedMaxAge(60) + .build(), + "Key binding JWT is too old", + null + ); + } + + @Test + public void testShouldFail_IfKbExpired() { + long now = Instant.now().getEpochSecond(); + + ObjectNode kbPayload = exampleKbPayload(); + kbPayload.set("exp", mapper.valueToTree(now - 1000)); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts() + .withValidateExpirationClaim(true) + .build(), + "Key binding JWT: Invalid `exp` claim", + "jwt has expired" + ); + } + + @Test + public void testShouldFail_IfKbNotBeforeTimeYet() { + long now = Instant.now().getEpochSecond(); + + ObjectNode kbPayload = exampleKbPayload(); + kbPayload.set("nbf", mapper.valueToTree(now + 1000)); + + testShouldFailGeneric2( + kbPayload, + defaultKeyBindingJwtVerificationOpts() + .withValidateNotBeforeClaim(true) + .build(), + "Key binding JWT: Invalid `nbf` claim", + "jwt not valid yet" + ); + } + + @Test + public void testShouldFail_IfCnfNotJwk() { + // The cnf claim is not of type jwk + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + UnsupportedOperationException exception = assertThrows( + UnsupportedOperationException.class, + () -> sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ) + ); + + assertEquals("Only cnf/jwk claim supported", exception.getMessage()); + } + + @Test + public void testShouldFail_IfCnfJwkCantBeParsed() { + testShouldFailGeneric( + // The cnf/jwk object has an unrecognized key type + "sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "Could not process cnf/jwk", + "Unsupported or invalid JWK" + ); + } + + @Test + public void testShouldFail_IfCnfJwkCantBeParsed2() { + testShouldFailGeneric( + // HMAC cnf/jwk parsing is not supported + "sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt", + defaultKeyBindingJwtVerificationOpts().build(), + "Could not process cnf/jwk", + "Unsupported or invalid JWK" + ); + } + + private void testShouldFailGeneric( + String testFilePath, + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts, + String exceptionMessage, + String exceptionCauseMessage + ) { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), testFilePath); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + keyBindingJwtVerificationOpts + ) + ); + + assertEquals(exceptionMessage, exception.getMessage()); + if (exceptionCauseMessage != null) { + assertThat(exception.getCause().getMessage(), containsString(exceptionCauseMessage)); + } + } + + /** + * This test helper allows replacing the key binding JWT of base + * sample `sdjwt/s20.1-sdjwt+kb.txt` to cover different scenarios. + */ + private void testShouldFailGeneric2( + JsonNode kbPayloadSubstitute, + KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts, + String exceptionMessage, + String exceptionCauseMessage + ) { + KeyBindingJWT keyBindingJWT = KeyBindingJWT.from( + kbPayloadSubstitute, + testSettings.holderSigContext, + KeyBindingJWT.TYP + ); + + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of( + sdJwtVPString.substring(0, sdJwtVPString.lastIndexOf(SdJwt.DELIMITER) + 1) + + keyBindingJWT.toJws() + ); + + VerificationException exception = assertThrows( + VerificationException.class, + () -> sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + keyBindingJwtVerificationOpts + ) + ); + + assertEquals(exceptionMessage, exception.getMessage()); + if (exceptionCauseMessage != null) { + assertEquals(exceptionCauseMessage, exception.getCause().getMessage()); + } + } + + private List defaultIssuerVerifyingKeys() { + return Collections.singletonList(testSettings.issuerVerifierContext); + } + + private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() { + return IssuerSignedJwtVerificationOpts.builder() + .withValidateIssuedAtClaim(false) + .withValidateNotBeforeClaim(false); + } + + private KeyBindingJwtVerificationOpts.Builder defaultKeyBindingJwtVerificationOpts() { + return KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .withAllowedMaxAge(Integer.MAX_VALUE) + .withNonce("1234567890") + .withAud("https://verifier.example.org") + .withValidateExpirationClaim(false) + .withValidateNotBeforeClaim(false); + } + + private ObjectNode exampleKbPayload() { + ObjectNode payload = mapper.createObjectNode(); + payload.put("nonce", "1234567890"); + payload.put("aud", "https://verifier.example.org"); + payload.put("sd_hash", "X9RrrfWt_70gHzOcovGSIt4Fms9Tf2g2hjlWVI_cxZg"); + payload.set("iat", mapper.valueToTree(1702315679)); + + return payload; + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java new file mode 100644 index 000000000000..9b62aaee8068 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java @@ -0,0 +1,114 @@ +/* + * 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.sdjwt.sdjwtvp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This class will try to test conformity to the spec by comparing json objects. + * We are facing the situation that: + * - json produced are not normalized. But we can compare them by matching their + * content once loaded into a json object. + * - ecdsa signature contains random component. We can't compare them directly. + * Even if we had the same input byte + * - The no rationale for ordering the disclosures. So we can only make sure + * each of them is present and that the json content matches. + * Warning: in other to produce the same disclosure strings and hashes like in + * the spec, i had to produce + * the same print. This is by no way reliable enough to be used to test + * conformity to the spec. + * + * @author Francis Pouatcha + */ +public class TestCompareSdJwt { + + public static void compare(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) { + try { + compareIssuerSignedJWT(expectedSdJwt.getIssuerSignedJWT(), actualSdJwt.getIssuerSignedJWT()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + compareDisclosures(expectedSdJwt, actualSdJwt); + + } + + private static void compareIssuerSignedJWT(IssuerSignedJWT e, IssuerSignedJWT a) + throws JsonMappingException, JsonProcessingException { + + assertEquals(e.getPayload(), a.getPayload()); + + List expectedJwsStrings = Arrays.asList(e.toJws().split("\\.")); + List actualJwsStrings = Arrays.asList(a.toJws().split("\\.")); + + // compare json content of header + assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0))); + + // compare payload + assertEquals(toJsonNode(expectedJwsStrings.get(1)), toJsonNode(actualJwsStrings.get(1))); + + // We wont compare signatures. + } + + private static void compareDisclosures(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) { + Set expectedDisclosures = expectedSdJwt.getDisclosuresString().stream() + .map(TestCompareSdJwt::toJsonNode) + .collect(Collectors.toSet()); + Set actualDisclosures = actualSdJwt.getDisclosuresString().stream() + .map(TestCompareSdJwt::toJsonNode) + .collect(Collectors.toSet()); + + assertEquals(expectedDisclosures.size(), actualDisclosures.size()); + + boolean foundEqualPair = false; + for (JsonNode a : expectedDisclosures) { + for (JsonNode b : actualDisclosures) { + if (a.equals(b)) { + foundEqualPair = true; + break; + } + } + } + + assertTrue("The set should contain equal elements", foundEqualPair); + } + + private static JsonNode toJsonNode(String base64EncodedString) { + try { + return SdJwtUtils.mapper.readTree(Base64Url.decode(base64EncodedString)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/core/src/test/java/org/keycloak/util/JWKSUtilsTest.java b/core/src/test/java/org/keycloak/util/JWKSUtilsTest.java index 983a391accda..b769a87e0f88 100644 --- a/core/src/test/java/org/keycloak/util/JWKSUtilsTest.java +++ b/core/src/test/java/org/keycloak/util/JWKSUtilsTest.java @@ -22,6 +22,7 @@ import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.PublicKeysWrapper; +import org.keycloak.jose.jwk.ECPublicJWK; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.rule.CryptoInitRule; @@ -29,6 +30,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; public abstract class JWKSUtilsTest { @@ -36,6 +38,35 @@ public abstract class JWKSUtilsTest { @ClassRule public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + @Test + public void publicEcMatches() throws Exception { + String keyA = "{" + + " \"kty\": \"EC\"," + + " \"use\": \"sig\"," + + " \"crv\": \"P-384\"," + + " \"kid\": \"key-a\"," + + " \"x\": \"KVZ5h_W0-8fXmUrxmyRpO_9vwwI7urXfyxGdxm1hpEuhPj2hhDxivnb2BhNvtC6O\"," + + " \"y\": \"1J3JVw_zR3uB3biAE7fs3V_4tJy2M1JinzWj9a4je5GSoW6zgGV4bk85OcuyUAhj\"," + + " \"alg\": \"ES384\"" + + " }"; + + ECPublicJWK ecPublicKey = JsonSerialization.readValue(keyA, ECPublicJWK.class); + JWK publicKey = JsonSerialization.readValue(keyA, JWK.class); + + assertEquals(JWKSUtils.computeThumbprint(publicKey), JWKSUtils.computeThumbprint(ecPublicKey)); + } + + @Test + public void unsupportedKeyType() throws Exception { + String keyA = "{" + + " \"kty\": \"OCT\"," + + " \"use\": \"sig\"" + + " }"; + + JWK publicKey = JsonSerialization.readValue(keyA, JWK.class); + assertThrows(UnsupportedOperationException.class, () -> JWKSUtils.computeThumbprint(publicKey)); + } + @Test public void publicRs256() throws Exception { diff --git a/core/src/test/java/org/keycloak/util/PemUtilsTest.java b/core/src/test/java/org/keycloak/util/PemUtilsTest.java index 1daf36b2446a..83449a0589a8 100644 --- a/core/src/test/java/org/keycloak/util/PemUtilsTest.java +++ b/core/src/test/java/org/keycloak/util/PemUtilsTest.java @@ -12,6 +12,7 @@ import org.junit.Test; import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.PemException; import org.keycloak.common.util.PemUtils; import org.keycloak.rule.CryptoInitRule; @@ -61,7 +62,10 @@ public void testEncodeAndDecodeGeneratedObjects() { public void testDecodeObjectsInPEMFormat() { String privateKey1 = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y="; String publicKey1 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; - + String publicKeyEC = "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElyCs9XI47lFR5l4WafsZZrAiUmEr\n" + + "+kYeStgx3tyPntt3YNfs6kAVNozI4aJqdqDjITJWatHm6boJ0BRLPNphRA==\n" + + "-----END PUBLIC KEY-----"; String cert1 = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="; String cert2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="; @@ -84,6 +88,7 @@ public void testDecodeObjectsInPEMFormat() { testPrivateKeyEncodeDecode(privateKey1); testPublicKeyEncodeDecode(publicKey1); + testPublicKeyEncodeDecode(publicKeyEC); testPrivateKeyEncodeDecode(PemUtils.removeBeginEnd(privateKey2).replace("\n", "")); testCertificateEncodeDecode(cert1); testCertificateEncodeDecode(cert2); @@ -114,6 +119,50 @@ public void testPrivateKeyInPKCS8Format() { String pk = PemUtils.removeBeginEnd(privateKeyPkcs8).replace("\n", ""); PrivateKey decodedPrivateKey2 = PemUtils.decodePrivateKey(pk); Assert.assertEquals(decodedPrivateKey1, decodedPrivateKey2); + + String ecPrivateKeyPkcs8 = "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgO1oavi4kqVFc/rxj\n" + + "24SJivHXq7buWX58U0tswYikPwyhRANCAASCIp6nVvOk9flbUrMW7JPDmyaXCnDc\n" + + "Q2uMfvxVWIJzBuhG6VDoeFPk3yf2EN5t7Q8FU5jPSp6gJz9xbaFYYLL6\n" + + "-----END PRIVATE KEY-----"; + + PrivateKey decodedEcPrivateKey = PemUtils.decodePrivateKey(ecPrivateKeyPkcs8); + Assert.assertEquals("EC", decodedEcPrivateKey.getAlgorithm()); + } + + @Test + public void testDecodeCertificateBundle() { + String certBundleEC = "-----BEGIN CERTIFICATE-----\n" + + "MIIBUTCB96ADAgECAggYMJVpV/BvyTAKBggqhkjOPQQDAjARMQ8wDQYDVQQDEwZz\n" + + "dWItY2EwIBcNMDAwMTAxMDkwMDAwWhgPMjEwMDAxMDEwOTAwMDBaMBUxEzARBgNV\n" + + "BAMTCmVuZC1lbnRpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASCIp6nVvOk\n" + + "9flbUrMW7JPDmyaXCnDcQ2uMfvxVWIJzBuhG6VDoeFPk3yf2EN5t7Q8FU5jPSp6g\n" + + "Jz9xbaFYYLL6ozMwMTAOBgNVHQ8BAf8EBAMCBaAwHwYDVR0jBBgwFoAU3etTPCDC\n" + + "f31HxBuYWWjF9ImW4ccwCgYIKoZIzj0EAwIDSQAwRgIhAKpP+HBEvUWEfjdr2qD2\n" + + "sw/bVLtW1HnpqVnQm2i/kDp2AiEA6F+kKyMNu+jGKmzj0Pf6v0cj0c+f00bqoJdk\n" + + "h+GXGnM=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIBejCCAR+gAwIBAgIIGDCVaVflNG8wCgYIKoZIzj0EAwIwDTELMAkGA1UEAxMC\n" + + "Y2EwIBcNMDAwMTAxMDkwMDAwWhgPMjEwMDAxMDEwOTAwMDBaMBExDzANBgNVBAMT\n" + + "BnN1Yi1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI4bNe/0VXXojhjdh76p\n" + + "89esSheOT5WEBVQnJUvDBDSRoxRiFx2BEdPaVn8L4cCbaZIxLsoJusOJadm7Eltc\n" + + "h3qjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" + + "BBTd61M8IMJ/fUfEG5hZaMX0iZbhxzAfBgNVHSMEGDAWgBQ9q0KnjYuFWTSXf4YM\n" + + "Taz6vbNVRTAKBggqhkjOPQQDAgNJADBGAiEA3y9pa2JMhtM898f6NOZhezoHzj1a\n" + + "2JQIZRLQbOTjk0wCIQCg9A8414teP9whzRGSxM4eJNExdfHeJBYjDD345EW0vg==\n" + + "-----END CERTIFICATE-----"; + + X509Certificate[] certs = PemUtils.decodeCertificates(certBundleEC); + Assert.assertEquals(2, certs.length); + Assert.assertEquals("CN=end-entity", certs[0].getSubjectX500Principal().getName()); + Assert.assertEquals("CN=sub-ca", certs[1].getSubjectX500Principal().getName()); + + String invalidCertBundle = "foo\n"; + Assert.assertThrows(PemException.class, () -> { + PemUtils.decodeCertificates(invalidCertBundle); + }); + } private void testPrivateKeyEncodeDecode(String origPrivateKeyPem) { @@ -125,7 +174,7 @@ private void testPrivateKeyEncodeDecode(String origPrivateKeyPem) { private void testPublicKeyEncodeDecode(String origPublicKeyPem) { PublicKey decodedPublicKey = PemUtils.decodePublicKey(origPublicKeyPem); String encodedPublicKey = PemUtils.encodeKey(decodedPublicKey); - assertEquals(origPublicKeyPem, encodedPublicKey); + assertEquals(PemUtils.removeBeginEnd(origPublicKeyPem), encodedPublicKey); } private void testCertificateEncodeDecode(String origCertPem) { diff --git a/core/src/test/resources/sdjwt/a1.example2-address-payload.json b/core/src/test/resources/sdjwt/a1.example2-address-payload.json new file mode 100644 index 000000000000..c0c870942718 --- /dev/null +++ b/core/src/test/resources/sdjwt/a1.example2-address-payload.json @@ -0,0 +1,12 @@ +{ + "_sd": [ + "IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8", + "Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo", + "QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM", + "UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg", + "cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo", + "oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA", + "qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74", + "uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4" + ] +} diff --git a/core/src/test/resources/sdjwt/a1.example2-holder-claims.json b/core/src/test/resources/sdjwt/a1.example2-holder-claims.json new file mode 100644 index 000000000000..2442ceef0b36 --- /dev/null +++ b/core/src/test/resources/sdjwt/a1.example2-holder-claims.json @@ -0,0 +1,14 @@ +{ + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "given_name": "太郎", + "family_name": "山田", + "email": "\"unusual email address\"@example.jp", + "phone_number": "+81-80-1234-5678", + "address": { + "street_address": "東京都港区芝公園4丁目2−8", + "locality": "東京都", + "region": "港区", + "country": "JP" + }, + "birthdate": "1940-01-01" +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json b/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json new file mode 100644 index 000000000000..27e72e26ada5 --- /dev/null +++ b/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json @@ -0,0 +1,5 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000 +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json b/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json new file mode 100644 index 000000000000..1ae5d7e15570 --- /dev/null +++ b/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json @@ -0,0 +1,28 @@ +{ + "_sd": [ + "9hf5niUdeWrPmaU5mz727OELoKHX5TDZjrBVHCVzqcg", + "Kfv8UXTNDG2NWPv6CtT5QAa-w5-ugOfICaoap474crk", + "Kuet1yAa0HIQvYnOVd59hcViO9Ug6J2kSfqYRBeowvE", + "MMldOFFzB2d0umlmpTIaGerhWdU_PpYfLvKhh_f_9aY", + "X6ZAYOII2vPN40V7xExZwVwz7yRmLNcVwt5DL8RLv4g", + "ihDxP1pJ59-iRb-aft25j3cqC1ShChhO_sWC02gVUGw", + "s0BKYsLWxQQeU8tVlltM7MKsIRTrEIa1PkJmqxBBf5U", + "vg70gfzXO8HR7ERDkL46S6Ior1ey0DvZoEUHupJwoxc" + ], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "address": { + "_sd": [ + "IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8", + "Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo", + "QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM", + "UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg", + "cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo", + "oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA", + "qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74", + "uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4" + ] + }, + "_sd_alg": "sha-256" +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/a1.example2-sdjwt.txt b/core/src/test/resources/sdjwt/a1.example2-sdjwt.txt new file mode 100644 index 000000000000..23537c6c5c3d --- /dev/null +++ b/core/src/test/resources/sdjwt/a1.example2-sdjwt.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiOWhmNW5pVWRlV3JQbWFVNW16NzI3T0VMb0tIWDVURFpqckJWSENWenFjZyIsIktmdjhVWFROREcyTldQdjZDdFQ1UUFhLXc1LXVnT2ZJQ2FvYXA0NzRjcmsiLCJLdWV0MXlBYTBISVF2WW5PVmQ1OWhjVmlPOVVnNkoya1NmcVlSQmVvd3ZFIiwiTU1sZE9GRnpCMmQwdW1sbXBUSWFHZXJoV2RVX1BwWWZMdktoaF9mXzlhWSIsIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCJpaER4UDFwSjU5LWlSYi1hZnQyNWozY3FDMVNoQ2hoT19zV0MwMmdWVUd3IiwiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSIsInZnNzBnZnpYTzhIUjdFUkRrTDQ2UzZJb3IxZXkwRHZab0VVSHVwSndveGMiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJJWk95Sm4wRDE3YUs4NDVpWjhpNWhEbG9Ta2FydGlVbHZwX2hLak5iQXQ4IiwiTHN4X3R3LVV3RVpfSHNLOFFjSURrQ241V3ZmZTVCbXZjbldRQjZpa3FxbyIsIlFOUWQzX3k4bzZxdGRKZ3VRRHVBM3pkZmRZei1XZ0xTYWplMDZzMlVtV00iLCJVV3p2Q0JVUll4NGRTa2VCQ3B0Z0x0dWRGYkxnbkpvQmdtYUhCLTc2bE9nIiwiY3g0dG9FYjFxQVJrQWY4TnVEMEFUazNvTTZtOGEwcThuQVZGRHRCZGZvbyIsIm9VdUU5MERVQ3gzWHVfSDV6UU1CRXFBZGJNcmxBWjdRb0s1eklKX0IxbUEiLCJxZDJHNVRHSC02TTFxTnk4b3VoWGZzRTdVNnZXRE9ucDJGVXZvdkFhVzc0IiwidU5Ib1dZaFhzWmhWSkNORTJEcXktenF0N3Q2OWdKS3k1UWFGdjdHck1YNCJdfSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMH0.MEQCIDiR0hG5E-jCC6YEr1nrTJSOwIn7FL8FmQWhJfFgkjRrAiAPPnEfgnBRiad2RyfNjIx6UzGV2TP0SYLhNTm6syGMjw~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAi5aSq6YOOIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIuWxseeUsCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0~WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--explicit-kid.txt b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--explicit-kid.txt new file mode 100644 index 000000000000..3b1095a6f5dd --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--explicit-kid.txt @@ -0,0 +1,6 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0Iiwia2lkIjoiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.N0xUjkyxK6q-uvDF0bLpOSq8XI-QXZ9iI5U4w4GSx9NwDZQfg4P9SffgjQ11LwZKKfLprNernp53-oRBWaOuDA + +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~ + +eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiSEtwV1JKMmtqaXluSjBBbDlNTFJ5dmFjSS1HSVpmMHN5SUVvUnB2VktESSJ9.YFBWGvdAq8UIz7Y3b2lVMaQAFCkS02qkClGOPsn9qE-xDOgqT6VYx2D9-nSAU69dvkTdq6ynPMutlCYNtvtZ6w diff --git a/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt new file mode 100644 index 000000000000..26b6d3b0c183 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslB00 diff --git a/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt new file mode 100644 index 000000000000..b273353d0556 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt b/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt new file mode 100644 index 000000000000..7f3582df43dc --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt @@ -0,0 +1,2 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiYWxnIjoiSFMyNTYifQ.qFD5kLKnWxuEwldxGxXRKfi3uuEokEBCglYKidyYHDM6mYrNIyYdjcCQaQ4Ll_KVpo7aLbzkAExxIZRtN3FwVQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt b/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt new file mode 100644 index 000000000000..82ec49adc7a8 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJ1bmV4cGVjdGVkIiwiYWxnIjoiRVMyNTYifQ.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.8LSkT5EJ4UTukkMeNDyo01yQn2hr2ipdCjXII4B8Jb56y1ZvqiE_r6fEUY1DoUa3tvKY21XzF0SCsUgCuY5PVg diff --git a/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt b/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt new file mode 100644 index 000000000000..4fe3076eb2b8 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt @@ -0,0 +1,5 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImhlbGxvIiwgIndvcmxkIl0 +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt new file mode 100644 index 000000000000..3e9176a81e78 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt @@ -0,0 +1,7 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifSx7Ii4uLiI6Im5vZkFmeDhTcWV2d3EwYWJWalJrV3BOai01NjBkU3dUUzdMbUJLR3FrZ2MifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.sMAP6gUt1TLwdNtT-U06qbC4qZWB8i0gadzAHA5fvB-LpXTccHPZTsG9TIlgh8-vgYOnqr6t36XaHnU4217LpQ +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiQ00iXQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0 +~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt new file mode 100644 index 000000000000..b210c360fc5f --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiLCJxSjROWDR3RWk1SGl6VUg4QjZ4cGZtMmxqZkVtTzlGRF9YRmtvWFd1WFdRIl0sImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDAsInN1YiI6InVzZXJfNDIiLCJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0seyIuLi4iOiI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.-lSmU8_PXTnSr1wbAkoW3Xwa_VOX-dL4MlREkWjXtOHzSJ7DnDUpv_cJSh5eub3VGqxjbHnzqz0VOoLhRx47pw +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZpZWxkIl0 +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt new file mode 100644 index 000000000000..07e9d34753e1 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJvY3QiLCJrIjoiR0F3ZXkwSlh6U0kzTUlPLS03eUt2b2R5ZC1Yam5XR2M2OWtNT1NXZ2RoNUtQeVlPdmNCQzJNa18zZndjcEFmWGVqZEQ4TVpNUE0yY2JVdWc0RERZZGQzb1ZnVjNmYlRnRnlEdDZpYTQ3SExoeUkybFNDOXJIQ1Foa0NrczRDejNyTFBtbjhGcU1BenFFQmRxQmpmTTdxOVBvTVBvRHl3cS1iU3FpcTBnQVhrbG9nMlA2OXVpa2MxX0F3dDJRdk14ZC12SGVxVGVOb2RKVGlKUllDOTQwcW5HTXNzdlhodTVsU0tKQVNuLWRzamhaX25FQlhhbmUxZGlSZFlFY2daWDJJa196amhIa044dTBJMTNDd2Y2MS1fdHJjVFRkZG9Oal9KZkVMNGpuRHJTdVBNWFFXYzNYUFBXN193U1pGMGFEdndpWnV4YnpXVjRiVVdjS1Q1Nlh3IiwiYWxnIjoiSFMyNTYifX19.hIazN1P8S71Q0mnPaOjlN6buVyFpFlwW2B1W0RDebJdpcnb-ms8sCOx5NNi8aK_5KfCkvCECfVhNVAcQpOIyFw + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt new file mode 100644 index 000000000000..269d56388821 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt @@ -0,0 +1,3 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7ImtpZCI6ImRmZDFhYTk3LTZkOGQtNDU3NS1hMGZlLTM0Yjk2ZGUyYmZhZCJ9fQ.BLt9LcdgL-0HM1TV2OLLuJq9U1f8vlqha8I-WlcA-Je6e5U84HmWhYEgaBHOtt4NNrzAC-dk2xSxXjjr8aemTw +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt new file mode 100644 index 000000000000..cf65a6314de7 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt @@ -0,0 +1,6 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQ1oiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.MfjyETGLaL8zJ7xYiWsfhFhvEFCA2Epj7BMsZKboOtBdHw-_ap1bjUnVY_3IDvoRLmyDzb6_AUj-OJ1IQS9_Lw + + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt new file mode 100644 index 000000000000..13bc13735595 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMjU2IiwibiI6ImdnUE91THM4R3U5WWVObmpfazZMQmFnOV9zNFNjdk1QRjZ4Z2t2M2ZnQnhQNVo5VWNBWUJ5Z09YczQ4UnRabzlCczhvS1NlQndXMWdHUDN2Szl1Q09iZ1cyd0JfSDQ0WG1qVk1MZnRsYnlTVzA0aHpmU3lzQWlBQ0tkdzJLZHFncEJEVDQyWHd1bEVBMV9KM3NGcWZRNkdacUwzUWRUaUpDNFZuSllSTERES0UwY2otWXlTVTZCcktfUG9WYWxqLWdOR2ZvYkRRaVJzeU0wSTVlVWpvTlM2SmxPYjlSTDlkdUh5SUdER3FrVE5kblFiaVI4Wm1NQVpyOHBPaS03WUU1dGVMVmpJUXRlOHdVcERWdk9MVXA5eVdZOE1RbjNLMUk5UTdBMGZ2bEVveTFnd1FMMkV3U29Oc1NEUDkzS1ZJdnVyYlQ1ZzVuakttZHRnZTBXSll6USJ9fX0.4LrL9rQm5GgwBT_IePfjcvwJpYgkE-s5mTUyr81kX5NblcOPdAexvojfPpnfZ2qOsv6axYkQwD3aRS5gG3oYqA + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoia2ItODdTMFlERGZUX2V5Z0pIenNyZERkZzE0dUJqXzh2MnhHa2E4WGlIMCJ9.bQXPEjBuN8I803XvX1ZK4F6FakaAb6tCo4Km5xfLXIV9ukCHySwUMRrLoP5XPVcVxBytJEJpkQ997ahs2ux3b-UN-yoBOOR6Kwc31hV7BdWU8GnSbH-6gxmB0WJPvh3fBfNfQzfTfIsTLjS9becnPoIt-1PIBQzJXGG0SHut4hjdnHEOtvnbaVwhN6Facil7A5xXoLhNsk-WBKmdBL89aFlLfpO7i1I_87uCnZXspcQ6c7kETaQReZQtJNitQrYLiFwgIv8cyiFbwPQVKwJ4XAQpt9N2I50XwTE6dhUbdAxdjRzqgoxZ-gWXMWksouyH3wrN2nKAJEs3Ya-uz1JYBQ diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt new file mode 100644 index 000000000000..69847b709de7 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMzg0IiwibiI6ImdaMGExcTgzU24zdjhFWm84bXFYR3NhLUZGa0NNd05qWkx4SmktSU5kMnlxYnNmM1hVVThUZ2ZoVDdRS1FiVDctZmRTeWVKUkI1TTBnb3ZOOGZfdE8wQXN3aUF2M181bTRwYVBSbVhJRzNyZ0JkY2JXRnhFTDlQV0lHN3h3am13LXV0Mi1ZdnFRYkEzSTFERGdGV1Z1c0h5RzRFZUtaamVOR0hNYnhqaXRsSVZ4TV9ya09MTmVUSENDM2hJcDVzdWZ6VlJ1eXhuTFJsdXFoa09FcWdRTDJoWEdTYmx4QnJGY0h6bUU0cnIxc1c4eDlYYnR3QjhMQnJRYjBhM0gzdnpzNVJDTm5UUEh4ejhXaUx5TURlT1hjYUVTWGdnRjNmcFhLUGVOQkNfcnB5bWxIWWFRUGtuTFVRd0NiVEJESnNYYTB6b240UlVRZUpMS1hMVWJPRXU5dyJ9fX0.k3dMFI6QerNlkQV_6bEyQ94eOY6Mbu9Zk5GPdp6K96FnUk3PbKeurdxFK92lC3rXWROOSmlYXXOszagLvEnbkA + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiMlYtMkVqRGNfVWxLeDF1dFdpbEs1SUlMekEzOE55MU1icDBrdXN3Tm54TSJ9.dBBEuOCZOZiwWAUDc8JwzAfxsNj-ronJyykwRF0yuwNnVJoyq-t6YBTrXEKkLcN8iqu0xSvyAHS5cZIk0K49K9HoD9Qs37-KcotghULkaN-e4vaLnNe4xQOP3ujejU9Gby_QOpZ880cxzD1-6TktmpC7mIHs5laI9kJYDn56aQAZ781IPGH0YAgl7c_VSlMyt5wdAOX8xYpPwZ9HtpBEwwQ-ivw1XxngDbwnAVxXwGp0SAM8eq3z9T6L3ABoi-RSD4TtwUFvnLjPbC2-R_dZGCutGR-NVj-km88HZethOFt78KGaGGHm9Qyiw7-C23zSAniRqZM54O3JaZxmImVlEQ diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt new file mode 100644 index 000000000000..779880217bf0 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTNTEyIiwibiI6InVycnl3WlUzVTUwWWJBbDgxY2dnNnNYMkVQRk5JeFFEVkJWYU5pR2NkRVYzbGdBTVl6a3NJYi1UdGZXSVpKMG9VZnBieF9lV1Y3Znd4blZYUHBFRkdPcGNuZUlKLXBPdV9CV1dMTi1PMl9BNFRudHctUnNzNk5RTFZDdmFvSW05YlZYbDZocWNwbDI1dFNJRlExb0w4Y1hxdXUzcDQxUlZJYXVZSC1PNnRZVndQdEwxeEg5WEp2Ym1OM2ZXMjZNdEpVYVIxZWpicVp1UE9xM1hBMXkwRmU5NkJqYXdRYXRCelprekxoMDhCWHdzTkpCbFA2a2F1aTRVU3F4QkdwWVczOElQTjNQOVdSMEo4akNtVEl1d2dwQmVOUFhQSFA1U2FPdEhtUE1KeEZxNUtIY3lWN0s3OHI4LXFZRFlWSHJieGVHZ01GanFZZXRVZ3A4UHVQbGsxdyJ9fX0.LLARtgBTQPHJynt_of4J7Api8YBM_YtA8EJpF1_ZYu72BGINv5vQjPjX4ZAVzOsNZS5E4uv4RfS0q4Wxl6BNwQ + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiSHBaVTZDLTVSZGhEdUkwckJUTGxvQU9ZcWI4MzRScHpwYVpNQ2c3Qm03WSJ9.X5fd-bLsd_tGP83pfiS4KCCnNgO4WGfB7Sa7339RdmvbzDFPYiwFuyBq_ROAzqBU_B9NDRbRxPQGHNV_I2hYUHj-zIwIYLwS5-VkKPTWunEaL19KGLqi4uPI4ZX_1n4al5PyupDWY2EXt90Xf35KOHpVtaupYz7Z7ZWPi2uG338FD-BXiPgsBCloABdvkdq8EGx6XleBev3S43cW33f-Zozw75L1-WgF_cnObVnFT_7_nOk4N8InGU46SyL_CyeCo-_LXdKN_tDZ2Mi6AEBKwJoD3WY6sf_uI49d1o1USs4AR9PcedbwQKDV-RzF_XQRqD6TZfOEvT6KJtyVUOU2DQ diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt new file mode 100644 index 000000000000..7613f3b63c5a --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJpWGtzVXQ5ZXY2NFdRdmdmM3llM0VTWXZFcGxkWkNkNGlBNUgwbVBzLUkyRjNmUjNLd1E2M0dWd2FuTDdHNlk3Z1REZ2YyZzE3SjhpVHVic210WGttUnFHdzNNb0JoLVVLYncwYjUyZTZtZm0wbG5wT24yMElXamxKcXpaVDROUTZ2eFpqMkdXbEx0bUhvUTBpM0JTQTliMW5CVDBkMHVNYk9kZHlRS0plOEtRTGJIZUoyOXF3UEFzQWl5X3R5czBkd3d2R1dmV2VKUHVvTTNfY2pxcTRXQ0Z3ZDllNXlXejU2VFFXZFljSDZ3dFB6RzM4R0JPc3hOdmtTSW53NXhyWXZlOHZzcjNLY0tISjYySDE2NHhZU2ZNQ3BiYXVhSkFTY3hrcnUwRUwwUXVxVzB6dzZKdjRiZVZITUtqSXo1NWxHYTV4QmFybWxxSkZBRnl5MWwwclEifX19.WMEOfYaPIQFTY79rNRHeqoz4eTDyrhXJOtm_zncW5aJ10vTzSA7tsVvREzn6fajP21EI-ZNWuTzD3Ji88OI6SA + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJSUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiRWRER05ldkJ5YlVCMnpSYnpBZXl1Z2hHa2l3UG5tZVRCQS10QlhGVHpocyJ9.Bhec_71SLidxezU4HqkvrtWPF16pHkrPos3OL9y1rOR0ACgZ2KEigFr7pIn59_be60xi-EeNvAo1zt0N5uILBd3jkKjRmpC2MO2ZkIgKterJN_MEcCXlOQZc48QoDJIBuvmXq5wmIZMVfUJTw9i2PhtfaX49K5Fpmf3s9Iv4WnJLY7wVswiIYNFckKxal9agTCKNxZ5SAyz_3mZ3VJYeSG7d9IjhQ3w7w19jcsdaL635qt_Vf75dDodLZjlh1N0VhRqxbQj2sl4NbrC3Ezr7JXcSdUipn5vjRSgV4g8-ws-EF2NMhwPR4Ut_HSXNpge2NMqJcaTjXnmmX6RQesdGyA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt new file mode 100644 index 000000000000..1de3b9555cc3 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJDNnJDUnJSUmM3Z3ZtUFU4S3pOVTEweXRIRW1oRVFhaTY5SkxBeDU3U0FrcHlsNlpxazVXV2FlcUFUNVdDajV0IiwiY3J2IjoiUC0zODQiLCJ4IjoiS2EyazVKRjBSZkVQMFlVU2lFODNmZ1VVS3VIRC16bWQtdXlkYXJMN1JKVjFtdGd3MkhjNU80d0ZJQm85Zk9KOSIsInkiOiJFdEIwSGV1dTlubmZjcDlCLXdGaUdWN3dCT1plTUpMTGVPaHpfUFRiUUxhdUgyTEcwQ25fRlFYajJRZURGOGwxIn19fQ.udOcVKk1WTxg5XldomVczJY2Dptiz4sFf8OQADUC0PaYzOwIl5CjMuTHhs1K-tORGfIO7nPAe_VCLC0jXaSzgQ + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiaFN3cXVsbDJ4VWMtNDQwdkhLX0RUdnVXZWxDUDBlSVo0d0JOcXNOeVAySSJ9.TA93w_A3IBornn6Gu81oNjT2M-evVz6_TyCWTX-ZfL9uXkeiP44hRn0irCwCy0krtHrq49EyZxXLM2o9qRGYw1cDi68u2gYMEHLiXZzXu51q0ckQ2pjsTYDE2pqrSOZT diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt new file mode 100644 index 000000000000..c6804e1e1079 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJBQm0xYmVXQ2xTS2ZwRTBwYk94aGVqd21XekhjdmVrMGo3N3RrWWkxYUZtclktLUtxcWxDV3FlQnZpcUwzaU5ZcnFLTEx4ZGxOOS1nNXlHZjZjUHl1MUVIIiwiY3J2IjoiUC01MjEiLCJ4IjoiQU1uY3B1bTBmZ240V2hfZUswbTFhNWdzX2MyelpVb2hGLUlvQTZ1OUhGejlyZVlxX3c4d1VMZFZaNXdySHp2MGFyOG94MmRXZWp1WDIza0FLVm8wdUZJRSIsInkiOiJBYldlOVBOd0VFUlNrU0pZRXJBektNeWNVczAwLXBlZVl2MlVFd1FYZlM5ZFZ5ZVJGMGxiU2E5WlYtZzVlWGJuRXpuUk5sa2xLcHVaeTdncVppUENPRGkyIn19fQ.QoxsCI_hP2bazbr9sS2uE93vQ1DhD8Qdrjg0csou00I8XVKbmccLlHuKHALGYEqhFWVIQ5pCSL2XCkxnz-t5uQ + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWmM4aGNvVlBvN0VQTktQZzRjd05VdTFXMGtieWFwOC1zck5xWEpyUkhzOCJ9.APIud1JXrH0BLSD3TLoLQvkGS-48zqYcaB6sANnxXRDMlzHiqdqr_FnGD0QcY_VJcD_8EMhUvlrGty0qfSWMPDkHADyZIQIPTsz-5lCbPV6WU5IILprmov_PloxC-JNz58lo7Ak5hbnqJ2wZ6UAqN98XV2DMgIv84UcyezXLy23uszWm diff --git a/core/src/test/resources/sdjwt/s3.3-holder-claims.json b/core/src/test/resources/sdjwt/s3.3-holder-claims.json new file mode 100644 index 000000000000..f953b06a3a9d --- /dev/null +++ b/core/src/test/resources/sdjwt/s3.3-holder-claims.json @@ -0,0 +1,17 @@ +{ + "vct": "https://credentials.example.com/identity_credential", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "is_over_18": true, + "is_over_21": true, + "is_over_65": true +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s3.3-issuer-claims.json b/core/src/test/resources/sdjwt/s3.3-issuer-claims.json new file mode 100644 index 000000000000..bf99ac221964 --- /dev/null +++ b/core/src/test/resources/sdjwt/s3.3-issuer-claims.json @@ -0,0 +1,14 @@ +{ + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "vct": "https://credentials.example.com/identity_credential", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s3.3-issuer-payload.json b/core/src/test/resources/sdjwt/s3.3-issuer-payload.json new file mode 100644 index 000000000000..4c5c737d2c3f --- /dev/null +++ b/core/src/test/resources/sdjwt/s3.3-issuer-payload.json @@ -0,0 +1,26 @@ +{ + "_sd": [ + "09vKrJMOlyTWM0sjpu_pdOBVBQ2M1y3KhpH515nXkpY", + "2rsjGbaC0ky8mT0pJrPioWTq0_daw1sX76poUlgCwbI", + "EkO8dhW0dHEJbvUHlE_VCeuC9uRELOieLZhh7XbUTtA", + "IlDzIKeiZdDwpqpK6ZfbyphFvz5FgnWa-sN6wqQXCiw", + "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", + "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI", + "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", + "jdrTE8YcbY4EifugihiAe_BPekxJQZICeiUQwY9QqxI", + "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4" + ], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "vct": "https://credentials.example.com/identity_credential", + "_sd_alg": "sha-256", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt b/core/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt new file mode 100644 index 000000000000..94169248ea7b --- /dev/null +++ b/core/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt @@ -0,0 +1,28 @@ +eyJhbGciOiAiRVMyNTYiLCAia2lkIjogImRvYy1zaWduZXItMDUtMjUtMjAyMiIsICJ0 +eXAiOiAidmMrc2Qtand0In0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9C +VkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9k +YXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9p +ZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNO +NndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQ +WWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJ +IiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAi +amRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5 +eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAi +aHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4 +cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxl +LmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJj +bmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRD +QUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJa +eGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.YHjaS +waBy-6hBYBre1F1ehiHNp69F9jnP2Hve3g0gNTzG_6GxV-E9rPR5m_CCo1SgDk0GaE5S +II6FBprkwDP-Q~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLC +AiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgI +kRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VA +ZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251b +WJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIi +wgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2 +FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOi +AiVVMifV0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOT +QwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLC +B0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnV +lXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s30.1-jwt-vc-metadata-jwks.json b/core/src/test/resources/sdjwt/s30.1-jwt-vc-metadata-jwks.json new file mode 100644 index 000000000000..65e2a703dbda --- /dev/null +++ b/core/src/test/resources/sdjwt/s30.1-jwt-vc-metadata-jwks.json @@ -0,0 +1,31 @@ +{ + "keys": [ + { + "kid": "doc-signer-05-25-2022", + "kty": "EC", + "d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g", + "crv": "P-256", + "x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ", + "y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" + }, + { + "kty": "EC", + "crv": "P-256", + "kid": "J1FwJP87C6-QN_WSIOmJAQc6n5CQ_bZdaFJ5GDnW1Rk", + "x5c": [ + "MIIBXTCCAQSgAwIBAgIGAYyR2cIZMAoGCCqGSM49BAMCMDYxNDAyBgNVBAMMK0oxRndKUDg3QzYtUU5fV1NJT21KQVFjNm41Q1FfYlpkYUZKNUdEblcxUmswHhcNMjMxMjIyMTQwNjU2WhcNMjQxMDE3MTQwNjU2WjA2MTQwMgYDVQQDDCtKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAopVeboJpYRycw1YKkkROXfCpKEKl9Y1YPFhOGj4xTg2UOunxTxSIVkT94qFVIuu1hkEoE2NxelZo3+yTFUODDAKBggqhkjOPQQDAgNHADBEAiBnFjScBcvERleLjMCu5NbxJKkNsa/gQhkXTfDmbq+T3gIgVazbsVdQvZgluc9nJYQxWlzXT9i6f+wgUKx0KCYbj3A=" + ], + "x": "AopVeboJpYRycw1YKkkROXfCpKEKl9Y1YPFhOGj4xTg", + "y": "NlDrp8U8UiFZE_eKhVSLrtYZBKBNjcXpWaN_skxVDgw", + "alg": "ES256" + }, + { + "kty": "EC", + "crv": "P-256", + "kid": "ZbAAKwhynrqBnYlHdEkBIvNJFZZH_bRg1KIopKfZ6O8", + "x": "eshMYyyoEsH_Eb85a7o76msXFPokfvNaeyY3u5qDm3M", + "y": "q0lGMn_UXiWJdgJtSCNzh9zPC6s7qKqQMo4V1i-69jA", + "alg": "ES256" + } + ] +} diff --git a/core/src/test/resources/sdjwt/s6.1-holder-claims.json b/core/src/test/resources/sdjwt/s6.1-holder-claims.json new file mode 100644 index 000000000000..38d3eb088b99 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.1-holder-claims.json @@ -0,0 +1,20 @@ +{ + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s6.1-issued-payload.txt b/core/src/test/resources/sdjwt/s6.1-issued-payload.txt new file mode 100644 index 000000000000..12bd7390db55 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.1-issued-payload.txt @@ -0,0 +1,29 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb +IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ +akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL +dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1 +SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB +TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2 +Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr +b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn +bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu +Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog +InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15 +VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1 +ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog +InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y +NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH +ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG +MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK +wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgI +mdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZh +bWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWl +sIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhR +IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4Z +TQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngt +MDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjog +IjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFu +eXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZR +IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5 +YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92T +U5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json b/core/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json new file mode 100644 index 000000000000..dc441c0bf80e --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json @@ -0,0 +1,19 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + { "...": "mXYRA4kcMm9hHUX-dCc44jKpyrNiEtJo2IqLk5YzRik" }, + { "...": "XkluhXNRk-Gmh8zBHo4Ad3drmukEbmm4CECMCefdG24" }, + { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } + ] +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json b/core/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json new file mode 100644 index 000000000000..5dfd80435020 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json @@ -0,0 +1,18 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + "US", + { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } + ] +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s6.1-issuer-payload.json b/core/src/test/resources/sdjwt/s6.1-issuer-payload.json new file mode 100644 index 000000000000..45e7510be372 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.1-issuer-payload.json @@ -0,0 +1,18 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s6.2-key-binding-claims.json b/core/src/test/resources/sdjwt/s6.2-key-binding-claims.json new file mode 100644 index 000000000000..7d9d320a6a85 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.2-key-binding-claims.json @@ -0,0 +1,5 @@ +{ + "nonce": "1234567890", + "aud": "https://verifier.example.org", + "iat": 1702315679 +} diff --git a/core/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt b/core/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt new file mode 100644 index 000000000000..7553c0466363 --- /dev/null +++ b/core/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt @@ -0,0 +1,23 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb +IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ +akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL +dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1 +SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB +TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2 +Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr +b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn +bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu +Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog +InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15 +VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1 +ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog +InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y +NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH +ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG +MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK +wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgI +mZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFk +ZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5 +IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMi +fV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s7-holder-claims.json b/core/src/test/resources/sdjwt/s7-holder-claims.json new file mode 100644 index 000000000000..532e51ff913c --- /dev/null +++ b/core/src/test/resources/sdjwt/s7-holder-claims.json @@ -0,0 +1,9 @@ +{ + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE" + } +} diff --git a/core/src/test/resources/sdjwt/s7-issuer-claims.json b/core/src/test/resources/sdjwt/s7-issuer-claims.json new file mode 100644 index 000000000000..399212e9b975 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7-issuer-claims.json @@ -0,0 +1,5 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000 +} diff --git a/core/src/test/resources/sdjwt/s7.1-issuer-payload.json b/core/src/test/resources/sdjwt/s7.1-issuer-payload.json new file mode 100644 index 000000000000..fb7807812a9c --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.1-issuer-payload.json @@ -0,0 +1,8 @@ +{ + "_sd": ["fOBUSQvo46yQO-wRwXBcGqvnbKIueISEL961_Sjd4do"], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "_sd_alg": "sha-256" +} diff --git a/core/src/test/resources/sdjwt/s7.2-issuer-payload.json b/core/src/test/resources/sdjwt/s7.2-issuer-payload.json new file mode 100644 index 000000000000..18b48088d4b0 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.2-issuer-payload.json @@ -0,0 +1,15 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "_sd": [ + "6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0", + "9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM", + "KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88", + "WN9r9dCBJ8HTCsS2jKASxTjEyW5m5x65_Z_2ro2jfXM" + ] + }, + "_sd_alg": "sha-256" +} diff --git a/core/src/test/resources/sdjwt/s7.2b-issuer-payload.json b/core/src/test/resources/sdjwt/s7.2b-issuer-payload.json new file mode 100644 index 000000000000..57e0380aa076 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.2b-issuer-payload.json @@ -0,0 +1,15 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "_sd": [ + "6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0", + "9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM", + "KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88" + ], + "country": "DE" + }, + "_sd_alg": "sha-256" +} diff --git a/core/src/test/resources/sdjwt/s7.3-issuer-payload.json b/core/src/test/resources/sdjwt/s7.3-issuer-payload.json new file mode 100644 index 000000000000..976f8f09dbd1 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.3-issuer-payload.json @@ -0,0 +1,8 @@ +{ + "_sd": ["HvrKX6fPV0v9K_yCVFBiLFHsMaxcD_114Em6VT8x1lg"], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "_sd_alg": "sha-256" +} diff --git a/core/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt b/core/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt new file mode 100644 index 000000000000..b35df402b293 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s7.3-sdjwt.txt b/core/src/test/resources/sdjwt/s7.3-sdjwt.txt new file mode 100644 index 000000000000..87001644cec9 --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.3-sdjwt.txt @@ -0,0 +1 @@ +eyJraWQiOiJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.Bhhy9eDenyBCsh9F064pxs5SK0Tbfc0o4xXlzmYv50qODKShpfsiC8__W1SXrF687JsbXmbD7VA5Db7iMoPwbg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~ \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/s7.4-sample-disclosed-issuer-payload.json b/core/src/test/resources/sdjwt/s7.4-sample-disclosed-issuer-payload.json new file mode 100644 index 000000000000..db9822a7ee1c --- /dev/null +++ b/core/src/test/resources/sdjwt/s7.4-sample-disclosed-issuer-payload.json @@ -0,0 +1,7 @@ +{ + "vct": "https://credentials.example.com/identity_credential", + "given_name": "John", + "cat": 123, + "addr": {"city": "Douala", "country": "CM"}, + "colors": ["red", "green"] +} \ No newline at end of file diff --git a/core/src/test/resources/sdjwt/test-settings.json b/core/src/test/resources/sdjwt/test-settings.json new file mode 100644 index 000000000000..5f5121a9adc8 --- /dev/null +++ b/core/src/test/resources/sdjwt/test-settings.json @@ -0,0 +1,31 @@ +{ + "identifiers": { + "issuer": "https://example.com/issuer", + "verifier": "https://example.com/verifier" + }, + "key_settings": { + "key_size": 256, + "kty": "EC", + "issuer_key": { + "kid": "doc-signer-05-25-2022", + "kty": "EC", + "d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g", + "crv": "P-256", + "x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ", + "y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" + }, + "holder_key": { + "kid": "holder", + "kty": "EC", + "d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + }, + "key_binding_nonce": "1234567890", + "expiry_seconds": 86400000, + "random_seed": 0, + "iat": 1683000000, + "exp": 1883000000 +} diff --git a/core/src/test/resources/sdjwt/test-settings.yml b/core/src/test/resources/sdjwt/test-settings.yml new file mode 100644 index 000000000000..5795afb84541 --- /dev/null +++ b/core/src/test/resources/sdjwt/test-settings.yml @@ -0,0 +1,32 @@ +# from: https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml +identifiers: + issuer: "https://example.com/issuer" + verifier: "https://example.com/verifier" + +key_settings: + key_size: 256 + + kty: EC + + issuer_key: + kty: EC + d: Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g + crv: P-256 + x: b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ + y: Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8 + + holder_key: + kty: EC + d: 5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I + crv: P-256 + x: TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc + y: ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ + +key_binding_nonce: "1234567890" + +expiry_seconds: 86400000 # 1000 days + +random_seed: 0 + +iat: 1683000000 # Tue May 02 2023 04:00:00 GMT+0000 +exp: 1883000000 # Sat Sep 01 2029 23:33:20 GMT+0000 \ No newline at end of file diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java index 36fb50b8caad..f623de862768 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java @@ -22,6 +22,8 @@ import org.bouncycastle.crypto.Wrapper; import org.bouncycastle.crypto.engines.AESWrapEngine; import org.bouncycastle.crypto.params.KeyParameter; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -32,14 +34,14 @@ public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Wrapper encrypter = new AESWrapEngine(); encrypter.init(false, new KeyParameter(encryptionKey.getEncoded())); return encrypter.unwrap(encodedCek, 0, encodedCek.length); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { Wrapper encrypter = new AESWrapEngine(); encrypter.init(true, new KeyParameter(encryptionKey.getEncoded())); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java index e7aa605f8659..98ca18c9015e 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java @@ -49,6 +49,7 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.keycloak.crypto.JavaAlgorithm; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -67,7 +68,7 @@ import java.util.List; /** - * The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate} + * The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link X509Certificate} * * @author Bill Burke * @author Giriraj Sharma @@ -76,19 +77,16 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider { /** - * Generates version 3 {@link java.security.cert.X509Certificate}. + * Generates version 3 {@link X509Certificate}. * - * @param keyPair the key pair + * @param keyPair the key pair * @param caPrivateKey the CA private key - * @param caCert the CA certificate - * @param subject the subject name - * + * @param caCert the CA certificate + * @param subject the subject name * @return the x509 certificate - * - * @throws Exception the exception */ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert, - String subject) throws Exception { + String subject) { try { X500Name subjectDN = new X500Name("CN=" + subject); @@ -114,7 +112,7 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva // Authority Key Identifier certGen.addExtension(Extension.authorityKeyIdentifier, false, - x509ExtensionUtils.createAuthorityKeyIdentifier(subjPubKeyInfo)); + x509ExtensionUtils.createAuthorityKeyIdentifier(caCert)); // Key Usage certGen.addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign @@ -131,7 +129,17 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); // Content Signer - ContentSigner sigGen = new JcaContentSignerBuilder("SHA1WithRSAEncryption").setProvider(BouncyIntegration.PROVIDER).build(caPrivateKey); + ContentSigner sigGen; + switch (caCert.getPublicKey().getAlgorithm()) + { + case "EC": + sigGen = new JcaContentSignerBuilder("SHA256WithECDSA").setProvider(BouncyIntegration.PROVIDER) + .build(caPrivateKey); + break; + default: + sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider(BouncyIntegration.PROVIDER) + .build(caPrivateKey); + } // Certificate return new JcaX509CertificateConverter().setProvider(BouncyIntegration.PROVIDER).getCertificate(certGen.build(sigGen)); @@ -141,13 +149,11 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva } /** - * Generate version 1 self signed {@link java.security.cert.X509Certificate}.. + * Generate version 1 self signed {@link X509Certificate}.. * * @param caKeyPair the CA key pair - * @param subject the subject name - * + * @param subject the subject name * @return the x509 certificate - * * @throws Exception the exception */ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) { @@ -155,12 +161,16 @@ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String } public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 10); + return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime()); + } + + @Override + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) { try { X500Name subjectDN = new X500Name("CN=" + subject); Date validityStartDate = new Date(System.currentTimeMillis() - 100000); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.YEAR, 10); - Date validityEndDate = new Date(calendar.getTime().getTime()); SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded()); X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate, @@ -174,16 +184,36 @@ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String } /** - * Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}. + * Creates the content signer for generation of Version 1 {@link X509Certificate}. * * @param privateKey the private key - * * @return the content signer */ private ContentSigner createSigner(PrivateKey privateKey) { try { - JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption") - .setProvider(BouncyIntegration.PROVIDER); + JcaContentSignerBuilder signerBuilder; + switch (privateKey.getAlgorithm()) { + case "RSA": { + signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider(BouncyIntegration.PROVIDER); + break; + } + case "EC": + case "ECDSA": { + signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA") + .setProvider(BouncyIntegration.PROVIDER); + break; + } + case JavaAlgorithm.Ed25519: + case JavaAlgorithm.Ed448: { + signerBuilder = new JcaContentSignerBuilder(privateKey.getAlgorithm()) + .setProvider(BouncyIntegration.PROVIDER); + break; + } + default: { + throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm())); + } + } return signerBuilder.build(privateKey); } catch (Exception e) { throw new RuntimeException("Could not create content signer.", e); @@ -192,7 +222,7 @@ private ContentSigner createSigner(PrivateKey privateKey) { @Override public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException { - + Extensions certExtensions = new JcaX509CertificateHolder(cert).getExtensions(); if (certExtensions == null) throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found"); @@ -212,6 +242,7 @@ public List getCertificatePolicyList(X509Certificate cert) throws Genera /** * Retrieves a list of CRL distribution points from CRLDP v3 certificate extension * See CRL validation + * * @param cert * @return * @throws IOException @@ -225,7 +256,7 @@ public List getCRLDistributionPoints(X509Certificate cert) throws IOExce List distributionPointUrls = new LinkedList<>(); DEROctetString octetString; try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) { - octetString = (DEROctetString)crldpExtensionInputStream.readObject(); + octetString = (DEROctetString) crldpExtensionInputStream.readObject(); } byte[] octets = octetString.getOctets(); @@ -251,28 +282,26 @@ public List getCRLDistributionPoints(X509Certificate cert) throws IOExce } public X509Certificate createServicesTestCertificate(String dn, - Date startDate, - Date expiryDate, - KeyPair keyPair, - String... certificatePolicyOid) { + Date startDate, + Date expiryDate, + KeyPair keyPair, + String... certificatePolicyOid) { // Cert data X500Name subjectDN = new X500Name(dn); X500Name issuerDN = new X500Name(dn); SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance( - ASN1Sequence.getInstance(keyPair.getPublic().getEncoded())); + ASN1Sequence.getInstance(keyPair.getPublic().getEncoded())); BigInteger serialNumber = new BigInteger(130, new SecureRandom()); // Build the certificate X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate, - subjectDN, subjPubKeyInfo); + subjectDN, subjPubKeyInfo); - if (certificatePolicyOid != null) - { - try - { - for (Extension certExtension: certPolicyExtensions(certificatePolicyOid)) + if (certificatePolicyOid != null) { + try { + for (Extension certExtension : certPolicyExtensions(certificatePolicyOid)) certGen.addExtension(certExtension); } catch (CertIOException e) { throw new IllegalStateException(e); @@ -282,11 +311,11 @@ public X509Certificate createServicesTestCertificate(String dn, // Sign the cert with the private key try { ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA") - .setProvider(BouncyIntegration.PROVIDER) - .build(keyPair.getPrivate()); + .setProvider(BouncyIntegration.PROVIDER) + .build(keyPair.getPrivate()); X509Certificate x509Certificate = new JcaX509CertificateConverter() - .setProvider(BouncyIntegration.PROVIDER) - .getCertificate(certGen.build(contentSigner)); + .setProvider(BouncyIntegration.PROVIDER) + .getCertificate(certGen.build(contentSigner)); return x509Certificate; } catch (CertificateException | OperatorCreationException e) { @@ -297,11 +326,9 @@ public X509Certificate createServicesTestCertificate(String dn, private List certPolicyExtensions(String... certificatePolicyOid) { List certificatePolicies = new LinkedList<>(); - if (certificatePolicyOid != null && certificatePolicyOid.length > 0) - { + if (certificatePolicyOid != null && certificatePolicyOid.length > 0) { List policyInfoList = new LinkedList<>(); - for (String oid: certificatePolicyOid) - { + for (String oid : certificatePolicyOid) { policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid))); } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java index 9cf773175bc5..142380f9d63b 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java @@ -1,17 +1,26 @@ package org.keycloak.crypto.def; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigInteger; - import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERSequenceGenerator; import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; import org.keycloak.common.crypto.ECDSACryptoProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; + public class BCECDSACryptoProvider implements ECDSACryptoProvider { @@ -60,5 +69,22 @@ public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int return concatenatedSignatureValue; } - + @Override + public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) { + try { + BCECPrivateKey bcecPrivateKey = new BCECPrivateKey(ecPrivateKey, BouncyCastleProvider.CONFIGURATION); + + ECPoint q = bcecPrivateKey.getParameters().getG().multiply(bcecPrivateKey.getD()); + + ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(q, bcecPrivateKey.getParameters()); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPublicKey) keyFactory.generatePublic(publicKeySpec); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Key algorithm not supported.", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("Received an invalid key spec.", e); + } + } + } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java new file mode 100644 index 000000000000..d9a1db967c75 --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java @@ -0,0 +1,278 @@ +/* + * 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.crypto.def; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; + +import org.bouncycastle.crypto.Wrapper; +import org.bouncycastle.crypto.agreement.kdf.ConcatenationKDFGenerator; +import org.bouncycastle.crypto.engines.AESWrapEngine; +import org.bouncycastle.crypto.params.KDFParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.util.DigestFactory; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class BCEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(false, new KeyParameter(derivedKey)); + return encrypter.unwrap(encodedCek, 0, encodedCek.length); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(true, new KeyParameter(derivedKey)); + byte[] cekBytes = keyStorage.getCekBytes(); + return encrypter.wrap(cekBytes, 0, cekBytes.length); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveOtherInfo(int keyDataLength, String algorithmID, byte[] agreementPartyUInfo, + byte[] agreementPartyVInfo) { + byte[] algorithmId = encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))); + byte[] partyUInfo = encodeDataLengthData(agreementPartyUInfo); + byte[] partyVInfo = encodeDataLengthData(agreementPartyVInfo); + byte[] suppPubInfo = toByteArray(keyDataLength); + byte[] suppPrivInfo = emptyBytes(); + return concat(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) + throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + byte[] z = deriveSharedSecret(publicKey, privateKey); + byte[] otherInfo = deriveOtherInfo(keyDataLength, algorithmID, agreementPartyUInfo, agreementPartyVInfo); + KDFParameters param = new KDFParameters(z, otherInfo); + ConcatenationKDFGenerator concatKdf = new ConcatenationKDFGenerator(DigestFactory.createSHA256()); + concatKdf.init(param); + int derivedKeyLength = keyDataLength / 8; + byte[] derivedKeyBytes = new byte[derivedKeyLength]; + concatKdf.generateBytes(derivedKeyBytes, 0, derivedKeyLength); + return derivedKeyBytes; + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + String name = nistToSecCurveName(crv); + try { + ECPoint point = new ECPoint(x, y); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(pubKeySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveSharedSecret(Key publicKey, Key privateKey) + throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException { + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + keyAgreement.init(privateKey); + keyAgreement.doPhase(publicKey, true); + return keyAgreement.generateSecret(); + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static String nistToSecCurveName(String nistCurveName) { + switch (nistCurveName) { + case "P-256": + return "secp256r1"; + case "P-384": + return "secp384r1"; + case "P-521": + return "secp521r1"; + default: + throw new IllegalArgumentException("Unsupported curve"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java index 114d22054662..4bde6faa454c 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java @@ -18,6 +18,7 @@ package org.keycloak.crypto.def; +import org.bouncycastle.asn1.ASN1IA5String; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERIA5String; @@ -52,17 +53,16 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.keycloak.jose.jwe.JWEUtils; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.models.KeycloakSession; import org.keycloak.utils.OCSPProvider; import java.io.IOException; -import java.math.BigInteger; import java.net.URI; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.SecureRandom; import java.security.cert.CRLReason; import java.security.cert.CertPath; import java.security.cert.CertPathValidatorException; @@ -119,26 +119,24 @@ protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate ce JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber()); - // Create a nounce extension to protect against replay attacks - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt())); + URI responderURI = responderURIs.get(0); - DEROctetString derString = new DEROctetString(nounce.toByteArray()); - Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString); - Extensions extensions = new Extensions(nounceExtension); + try { + // Create a nonce extension to protect against replay attacks + DEROctetString requestNonce = new DEROctetString(new DEROctetString(JWEUtils.generateSecret(16))); + Extension nonceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, requestNonce); + Extensions extensions = new Extensions(nonceExtension); - OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build(); + OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID).setRequestExtensions(extensions).build(); - URI responderURI = responderURIs.get(0); - logger.log(Level.INFO, "OCSP Responder {0}", responderURI); + logger.log(Level.INFO, "OCSP Responder {0}", responderURI); - try { OCSPResp resp = getResponse(session, ocspReq, responderURI); logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()}); switch (resp.getStatus()) { case OCSPResp.SUCCESSFUL: if (resp.getResponseObject() instanceof BasicOCSPResp) { - return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject()); + return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, requestNonce, (BasicOCSPResp)resp.getResponseObject()); } else { throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response."); } @@ -171,7 +169,7 @@ protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate ce } } - private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse) + private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, DEROctetString requestNonce, BasicOCSPResp basicOcspResponse) throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { SingleResp expectedResponse = null; for (SingleResp singleResponse : basicOcspResponse.getResponses()) { @@ -182,7 +180,7 @@ private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCert } if (expectedResponse != null) { - verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date); + verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, requestNonce, date); return singleResponseToRevocationStatus(expectedResponse); } else { throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request"); @@ -200,7 +198,7 @@ private boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) { idLeft.getSerialNumber().equals(idRight.getSerialNumber()); } - private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { + private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, DEROctetString requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { List certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts())); X509Certificate signingCert = null; @@ -331,15 +329,18 @@ private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate iss throw new CertPathValidatorException("Error verifying OCSP Response\'s signature"); } else { Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); - if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) { + if (responseNonce == null && requestNonce != null) { + logger.log(Level.FINE, "No OCSP nonce in response"); + } + if (responseNonce != null && requestNonce != null && !requestNonce.equals(responseNonce.getExtnValue())) { throw new CertPathValidatorException("Nonces do not match."); } else { // See Sun's OCSP implementation. // https://www.ietf.org/rfc/rfc2560.txt, if nextUpdate is not set, // the responder is indicating that newer update is avilable all the time long current = date == null ? System.currentTimeMillis() : date.getTime(); - Date stop = new Date(current + (long) TIME_SKEW); - Date start = new Date(current - (long) TIME_SKEW); + Date stop = new Date(current + TIME_SKEW); + Date start = new Date(current - TIME_SKEW); Iterator iter = Arrays.asList(basicOcspResponse.getResponses()).iterator(); SingleResp singleRes = null; @@ -436,7 +437,7 @@ protected List getResponderURIs(X509Certificate cert) throws Certificate if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) { // See https://www.ietf.org/rfc/rfc2560.txt, 3.1 Certificate Content if (ad.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) { - DERIA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName()); + ASN1IA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName()); responderURIs.add(value.getString()); } } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java index 82e88c410a6b..d39a685babde 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java @@ -17,6 +17,8 @@ package org.keycloak.crypto.def; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.keycloak.common.util.DerUtils; import org.keycloak.common.util.PemException; @@ -24,6 +26,7 @@ import java.io.StringWriter; import java.security.PrivateKey; +import java.security.PublicKey; /** * Encodes Key or Certificates to PEM format string @@ -59,6 +62,22 @@ protected String encode(Object obj) { } } + @Override + public PublicKey decodePublicKey(String pem) { + try { + // try to decode using SubjectPublicKeyInfo which allows to know the key type + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem)); + if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) { + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } + } catch (Exception e) { + // error reading PEM object just go to previous RSA forced key + } + + // assume RSA if it cannot be decoded from BC knowing the key + return decodePublicKey(pem, "RSA"); + } + @Override public PrivateKey decodePrivateKey(String pem) { if (pem == null) { diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java index 13581eae64e7..5b17a6945026 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java @@ -23,6 +23,8 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.ASN1UTF8String; +import org.bouncycastle.asn1.BERTags; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.x500.AttributeTypeAndValue; import org.bouncycastle.asn1.x500.RDN; @@ -147,34 +149,41 @@ public Object extractUserIdentity(X509Certificate[] certs) { return obj; } - byte[] otherNameBytes = (byte[]) obj; + // From Java 21, the 3rd entry can be present with the type-id as String and 4th entry with the value (either in String or byte format). + // See javadoc of X509Certificate.getSubjectAlternativeNames in Java 21. For the sake of simplicity, we just ignore those additional String entries and + // always parse it from byte (2nd entry) as we still need to support Java 17 and it is not reliable anyway that entries are present in Java 21. + if (obj instanceof byte[]) { + byte[] otherNameBytes = (byte[]) obj; - try { - ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes)); - ASN1Encodable asn1otherName = asn1Stream.readObject(); - asn1otherName = unwrap(asn1otherName); + try { + ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes)); + ASN1Encodable asn1otherName = asn1Stream.readObject(); + asn1otherName = unwrap(asn1otherName); - ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName); + ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName); - if (asn1Sequence != null) { - ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0); - ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid)); - tempOid = oid.getId(); + if (asn1Sequence != null) { + ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0); + ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid)); + tempOid = oid.getId(); - ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1); - DERUTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded)); + ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1); + ASN1UTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded)); - tempOtherName = principalName.getString(); + tempOtherName = principalName.getString(); - // We found UPN among the 'otherName' principal. We don't need to look other - if (UPN_OID.equals(tempOid)) { - foundUpn = true; - break; + // We found UPN among the 'otherName' principal. We don't need to look other + if (UPN_OID.equals(tempOid)) { + foundUpn = true; + break; + } } - } - } catch (Exception e) { - logger.error("Failed to parse subjectAltName", e); + } catch (Exception e) { + logger.error("Failed to parse subjectAltName", e); + } + } else { + logger.tracef("Ignoring the Subject alternative name entry. Entry number: %d, value: %s", i + 1, obj); } } @@ -195,8 +204,8 @@ public Object extractUserIdentity(X509Certificate[] certs) { private ASN1Encodable unwrap(ASN1Encodable encodable) { while (encodable instanceof ASN1TaggedObject) { - ASN1TaggedObject taggedObj = (ASN1TaggedObject) encodable; - encodable = taggedObj.getObject(); + ASN1TaggedObject taggedObj = ASN1TaggedObject.getInstance(encodable, BERTags.CONTEXT_SPECIFIC); + encodable = taggedObj.getBaseObject().toASN1Primitive(); } return encodable; diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java index bd0d0c9c9258..e4f93954a93c 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java @@ -60,6 +60,10 @@ public DefaultCryptoProvider() { providers.put(CryptoConstants.RSA1_5, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/PKCS1Padding")); providers.put(CryptoConstants.RSA_OAEP, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); providers.put(CryptoConstants.RSA_OAEP_256, new DefaultRsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + providers.put(CryptoConstants.ECDH_ES, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new BCEcdhEsAlgorithmProvider()); if (existingBc == null) { Security.addProvider(this.bcProvider); @@ -75,6 +79,10 @@ public Provider getBouncyCastleProvider() { return bcProvider; } + @Override + public int order() { + return 200; + } @Override public T getAlgorithmProvider(Class clazz, String algorithmType) { @@ -175,7 +183,7 @@ public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException, NoS @Override public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException, NoSuchProviderException { return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName), BouncyIntegration.PROVIDER); - + } @Override diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java index b0bc523e7ca7..f8875e2277c9 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java @@ -3,7 +3,9 @@ import java.security.Key; import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -16,14 +18,14 @@ public DefaultRsaKeyEncryptionJWEAlgorithmProvider(String jcaAlgorithmName) { } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(encodedCek); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.ENCRYPT_MODE, publicKey); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2Parameters.java b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2Parameters.java new file mode 100644 index 000000000000..ff3f868055bf --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2Parameters.java @@ -0,0 +1,48 @@ +package org.keycloak.crypto.hash; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +public class Argon2Parameters { + + public static String DEFAULT_TYPE = "id"; + public static String DEFAULT_VERSION = "1.3"; + public static int DEFAULT_HASH_LENGTH = 32; + public static int DEFAULT_MEMORY = 7168; + public static int DEFAULT_ITERATIONS = 5; + public static int DEFAULT_PARALLELISM = 1; + + private static Map types = new LinkedHashMap<>(); + + static { + types.put("id", org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_id); + types.put("d", org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_d); + types.put("i", org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_i); + } + + private static Map versions = new LinkedHashMap<>(); + + static { + versions.put("1.3", org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_VERSION_13); + versions.put("1.0", org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_VERSION_10); + } + + public static Set listTypes() { + return types.keySet(); + } + + public static Set listVersions() { + return versions.keySet(); + } + + public static int getTypeValue(String type) { + return types.get(type); + } + + public static int getVersionValue(String version) { + return versions.get(version); + } + + +} diff --git a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java new file mode 100644 index 000000000000..242393c43009 --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java @@ -0,0 +1,154 @@ +package org.keycloak.crypto.hash; + +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.credential.hash.Salt; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.models.credential.dto.PasswordCredentialData; +import org.keycloak.models.credential.dto.PasswordSecretData; +import org.keycloak.tracing.TracingProviderUtil; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; + +import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.MEMORY_KEY; +import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.PARALLELISM_KEY; +import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.TYPE_KEY; +import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.VERSION_KEY; + +public class Argon2PasswordHashProvider implements PasswordHashProvider { + + private static final Logger logger = Logger.getLogger(Argon2PasswordHashProvider.class); + private final String version; + private final String type; + private final int hashLength; + private final int memory; + private final int iterations; + private final int parallelism; + private final Semaphore cpuCoreSemaphore; + + public Argon2PasswordHashProvider(String version, String type, int hashLength, int memory, int iterations, int parallelism, Semaphore cpuCoreSemaphore) { + this.version = version; + this.type = type; + this.hashLength = hashLength; + this.memory = memory; + this.iterations = iterations; + this.parallelism = parallelism; + this.cpuCoreSemaphore = cpuCoreSemaphore; + } + + @Override + public boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential) { + PasswordCredentialData data = credential.getPasswordCredentialData(); + + return iterations == data.getHashIterations() && + checkCredData(TYPE_KEY, type, data) && + checkCredData(VERSION_KEY, version, data) && + checkCredData(Argon2PasswordHashProviderFactory.HASH_LENGTH_KEY, hashLength, data) && + checkCredData(MEMORY_KEY, memory, data) && + checkCredData(PARALLELISM_KEY, parallelism, data); + } + + /** + * Password hashing iterations from password policy is intentionally ignored for now for two reasons. 1) default + * iterations are 210K, which is way too large for Argon2, and 2) it makes little sense to configure iterations only + * for Argon2, which should be combined with configuring memory, which is not currently configurable in password + * policy. + */ + @Override + public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) { + if (iterations == -1) { + iterations = this.iterations; + } else if (iterations > 100) { + logger.warn("Iterations for Argon should be less than 100, using default"); + iterations = this.iterations; + } + + byte[] salt = Salt.generateSalt(); + String encoded = encode(rawPassword, salt, version, type, hashLength, parallelism, memory, iterations); + + Map> additionalParameters = new HashMap<>(); + additionalParameters.put(VERSION_KEY, Collections.singletonList(version)); + additionalParameters.put(TYPE_KEY, Collections.singletonList(type)); + additionalParameters.put(Argon2PasswordHashProviderFactory.HASH_LENGTH_KEY, Collections.singletonList(Integer.toString(hashLength))); + additionalParameters.put(MEMORY_KEY, Collections.singletonList(Integer.toString(memory))); + additionalParameters.put(PARALLELISM_KEY, Collections.singletonList(Integer.toString(parallelism))); + + return PasswordCredentialModel.createFromValues(Argon2PasswordHashProviderFactory.ID, salt, iterations, additionalParameters, encoded); + } + + @Override + public boolean verify(String rawPassword, PasswordCredentialModel credential) { + PasswordCredentialData data = credential.getPasswordCredentialData(); + MultivaluedHashMap parameters = data.getAdditionalParameters(); + PasswordSecretData secretData = credential.getPasswordSecretData(); + + String version = parameters.getFirst(VERSION_KEY); + String type = parameters.getFirst(TYPE_KEY); + int hashLength = Integer.parseInt(parameters.getFirst(Argon2PasswordHashProviderFactory.HASH_LENGTH_KEY)); + int parallelism = Integer.parseInt(parameters.getFirst(PARALLELISM_KEY)); + int memory = Integer.parseInt(parameters.getFirst(MEMORY_KEY)); + int iterations = data.getHashIterations(); + + String encoded = encode(rawPassword, secretData.getSalt(), version, type, hashLength, parallelism, memory, iterations); + return encoded.equals(secretData.getValue()); + } + + @Override + public String credentialHashingStrength(PasswordCredentialModel credential) { + MultivaluedHashMap parameters = credential.getPasswordCredentialData().getAdditionalParameters(); + return String.format("Argon2%s-%s[m=%s,t=%d,p=%s]", parameters.getFirst(TYPE_KEY), parameters.getFirst(VERSION_KEY), parameters.getFirst(MEMORY_KEY), credential.getPasswordCredentialData().getHashIterations(), parameters.getFirst(PARALLELISM_KEY)); + } + + private String encode(String rawPassword, byte[] salt, String version, String type, int hashLength, int parallelism, int memory, int iterations) { + var tracing = TracingProviderUtil.getTracingProvider(); + try { + return tracing.trace(Argon2PasswordHashProvider.class, "encode", span -> { + try { + cpuCoreSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + org.bouncycastle.crypto.params.Argon2Parameters parameters = new org.bouncycastle.crypto.params.Argon2Parameters.Builder(Argon2Parameters.getTypeValue(type)) + .withVersion(Argon2Parameters.getVersionValue(version)) + .withSalt(salt) + .withParallelism(parallelism) + .withMemoryAsKB(memory) + .withIterations(iterations).build(); + + Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(parameters); + + byte[] result = new byte[hashLength]; + generator.generateBytes(rawPassword.toCharArray(), result); + return Base64.encodeBytes(result); + }); + } finally { + cpuCoreSemaphore.release(); + } + } + + private boolean checkCredData(String key, int expectedValue, PasswordCredentialData data) { + String s = data.getAdditionalParameters().getFirst(key); + Integer v = s != null ? Integer.parseInt(s) : null; + return v != null && expectedValue == v; + } + + private boolean checkCredData(String key, String expectedValue, PasswordCredentialData data) { + String s = data.getAdditionalParameters().getFirst(key); + return expectedValue.equals(s); + } + + @Override + public void close() { + } +} diff --git a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java new file mode 100644 index 000000000000..53a28907e713 --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java @@ -0,0 +1,137 @@ +package org.keycloak.crypto.hash; + +import org.keycloak.Config; +import org.keycloak.common.Profile; +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.credential.hash.PasswordHashProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Semaphore; + +public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFactory, EnvironmentDependentProviderFactory { + + public static final String ID = "argon2"; + public static final String TYPE_KEY = "type"; + public static final String VERSION_KEY = "version"; + public static final String HASH_LENGTH_KEY = "hashLength"; + public static final String MEMORY_KEY = "memory"; + public static final String ITERATIONS_KEY = "iterations"; + public static final String PARALLELISM_KEY = "parallelism"; + public static final String CPU_CORES_KEY = "cpuCores"; + + /** + * The Argon2 password hashing is CPU bound, so it doesn't make sense to hash more values concurrently than there are cores on the machine. + * When we run more, this only leads to an increased memory usage and to throttling of the process in containerized environments + * when a CPU limit is imposed. The throttling would have a negative impact on other concurrent non-hashing activities of Keycloak. + */ + private Semaphore cpuCoreSemaphore; + + private String version; + private String type; + private int hashLength; + private int memory; + private int iterations; + private int parallelism; + + @Override + public PasswordHashProvider create(KeycloakSession session) { + return new Argon2PasswordHashProvider(version, type, hashLength, memory, iterations, parallelism, cpuCoreSemaphore); + } + + @Override + public void init(Config.Scope config) { + version = config.get(VERSION_KEY, Argon2Parameters.DEFAULT_VERSION); + type = config.get(TYPE_KEY, Argon2Parameters.DEFAULT_TYPE); + hashLength = config.getInt(HASH_LENGTH_KEY, Argon2Parameters.DEFAULT_HASH_LENGTH); + memory = config.getInt(MEMORY_KEY, Argon2Parameters.DEFAULT_MEMORY); + iterations = config.getInt(ITERATIONS_KEY, Argon2Parameters.DEFAULT_ITERATIONS); + parallelism = config.getInt(PARALLELISM_KEY, Argon2Parameters.DEFAULT_PARALLELISM); + cpuCoreSemaphore = new Semaphore(config.getInt(CPU_CORES_KEY, Runtime.getRuntime().availableProcessors())); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public List getConfigMetadata() { + ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); + + builder.property() + .name(VERSION_KEY) + .type("string") + .helpText("Version") + .options(new LinkedList<>(Argon2Parameters.listVersions())) + .defaultValue(Argon2Parameters.DEFAULT_VERSION) + .add(); + + builder.property() + .name(TYPE_KEY) + .type("string") + .helpText("Type") + .options(new LinkedList<>(Argon2Parameters.listTypes())) + .defaultValue(Argon2Parameters.DEFAULT_TYPE) + .add(); + + builder.property() + .name(HASH_LENGTH_KEY) + .type("int") + .helpText("Hash length") + .defaultValue(Argon2Parameters.DEFAULT_HASH_LENGTH) + .add(); + + builder.property() + .name(MEMORY_KEY) + .type("int") + .helpText("Memory size (KB)") + .defaultValue(Argon2Parameters.DEFAULT_MEMORY) + .add(); + + builder.property() + .name(ITERATIONS_KEY) + .type("int") + .helpText("Iterations") + .defaultValue(Argon2Parameters.DEFAULT_ITERATIONS) + .add(); + + builder.property() + .name(PARALLELISM_KEY) + .type("int") + .helpText("Parallelism") + .defaultValue(Argon2Parameters.DEFAULT_PARALLELISM) + .add(); + + builder.property() + .name(CPU_CORES_KEY) + .type("int") + .helpText("Maximum parallel CPU cores to use for hashing") + .add(); + + return builder.build(); + } + + @Override + public boolean isSupported(Config.Scope config) { + return !Profile.isFeatureEnabled(Profile.Feature.FIPS); + } + + @Override + public int order() { + return 300; + } +} diff --git a/crypto/default/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory b/crypto/default/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory new file mode 100644 index 000000000000..8a50e534b5bb --- /dev/null +++ b/crypto/default/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory @@ -0,0 +1 @@ +org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory \ No newline at end of file diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java new file mode 100644 index 000000000000..15afc3f2538d --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java @@ -0,0 +1,81 @@ +/* + * 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.crypto.def.test; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.crypto.def.BCECDSACryptoProvider; +import org.keycloak.rule.CryptoInitRule; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class BCECDSACryptoProviderTest { + + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"secp256r1"}, {"secp384r1"}, {"secp521r1"} + }); + } + + private String curve; + + public BCECDSACryptoProviderTest(String curve) { + this.curve = curve; + } + + @Test + public void getPublicFromPrivate() { + KeyPair testKey = generateECKey(curve); + + BCECDSACryptoProvider bcecdsaCryptoProvider = new BCECDSACryptoProvider(); + assertEquals("The derived key should be equal to the originally generated one.", + testKey.getPublic(), + bcecdsaCryptoProvider.getPublicFromPrivate((ECPrivateKey) testKey.getPrivate())); + + } + + public static KeyPair generateECKey(String curve) { + + try { + KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA"); + ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve); + kpg.initialize(parameterSpec); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java new file mode 100644 index 000000000000..d57c5faa2f11 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,142 @@ +/* + * 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.crypto.def.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.Environment; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.def.BCEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class BCEcdhEsAlgorithmProviderTest { + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + * @throws IllegalStateException exception + * @throws InvalidKeyException exception + */ + @Test + public void deriveKey() throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeyException, IllegalStateException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = BCEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + ECPoint point = new ECPoint(x, y); + String name = nistToSecCurveName(crv); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(pubKeySpec); + } + + private PrivateKey getPrivateKey(String crv, String dStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + String name = nistToSecCurveName(crv); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPrivateKeySpec privKeySpec = new ECPrivateKeySpec(d, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(privKeySpec); + } + + private static String nistToSecCurveName(String nistCurveName) { + switch (nistCurveName) { + case "P-256": + return "secp256r1"; + case "P-384": + return "secp384r1"; + case "P-521": + return "secp521r1"; + default: + throw new IllegalArgumentException("Unsupported curve"); + } + } +} \ No newline at end of file diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/CryptoPerfTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/CryptoPerfTest.java index 2dad3fe46a5e..da5b6e4108a0 100644 --- a/crypto/default/src/test/java/org/keycloak/crypto/def/test/CryptoPerfTest.java +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/CryptoPerfTest.java @@ -138,7 +138,7 @@ private void perfTestPasswordHashins(int iterations, int derivedKeySize, String perfTest(new Runnable() { @Override public void run() { - provider.encode("password", -1); + provider.encodedCredential("password", -1); } }, "testPbkdf512", 1); } diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java new file mode 100644 index 000000000000..8e390b05acbc --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java @@ -0,0 +1,35 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.consumer.JwtVcMetadataTrustedSdJwtIssuerTest; + +/** + * @author Ingrid Kamga + */ +public class DefaultCryptoJwtVcMetadataTrustedSdJwtIssuerTest extends JwtVcMetadataTrustedSdJwtIssuerTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java new file mode 100644 index 000000000000..e99837421a08 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java @@ -0,0 +1,35 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.SdJwsTest; + +/** + * @author Ingrid Kamga + */ +public class DefaultCryptoSdJwsTest extends SdJwsTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtFacadeTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtFacadeTest.java new file mode 100644 index 000000000000..8f38acecc62a --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtFacadeTest.java @@ -0,0 +1,34 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.SdJwtFacadeTest; + +/** + * @author Rodrick Awambeng + */ +public class DefaultCryptoSdJwtFacadeTest extends SdJwtFacadeTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtPresentationConsumerTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtPresentationConsumerTest.java new file mode 100644 index 000000000000..f20ed4403992 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtPresentationConsumerTest.java @@ -0,0 +1,35 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.consumer.SdJwtPresentationConsumerTest; + +/** + * @author Ingrid Kamga + */ +public class DefaultCryptoSdJwtPresentationConsumerTest extends SdJwtPresentationConsumerTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java new file mode 100644 index 000000000000..9c0fb4119ba9 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java @@ -0,0 +1,36 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.sdjwtvp.SdJwtVPTest; + +/** + * Test with default security provider and non-fips bouncycastle + * + * @author Francis Pouatcha + */ +public class DefaultCryptoSdJwtVPTest extends SdJwtVPTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java new file mode 100644 index 000000000000..24c976c0dcd6 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java @@ -0,0 +1,35 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.sdjwtvp.SdJwtVPVerificationTest; + +/** + * @author Ingrid Kamga + */ +public class DefaultCryptoSdJwtVPVerificationTest extends SdJwtVPVerificationTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java new file mode 100644 index 000000000000..9ea805af24e9 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java @@ -0,0 +1,35 @@ +/* + * 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.crypto.def.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.SdJwtVerificationTest; + +/** + * @author Ingrid Kamga + */ +public class DefaultCryptoSdJwtVerificationTest extends SdJwtVerificationTest { + + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } +} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java index ffc16e196103..0a5a38395882 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java @@ -20,7 +20,9 @@ import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage.KeyUse; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -31,14 +33,14 @@ public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = Cipher.getInstance("AESWrap_128"); cipher.init(Cipher.UNWRAP_MODE, encryptionKey); return cipher.unwrap(encodedCek, "AES", Cipher.SECRET_KEY).getEncoded(); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = Cipher.getInstance("AESWrap_128"); cipher.init(Cipher.WRAP_MODE, encryptionKey); return cipher.wrap(keyStorage.getCEKKey(KeyUse.ENCRYPTION, false)); diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtils.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtils.java deleted file mode 100644 index 88add31161e4..000000000000 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtils.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2016 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.crypto.elytron; - -import java.io.IOException; -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.DateTimeException; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import javax.security.auth.x500.X500Principal; - -import org.jboss.logging.Logger; -import org.keycloak.common.crypto.CertificateUtilsProvider; -import org.wildfly.security.asn1.ASN1; -import org.wildfly.security.asn1.DERDecoder; -import org.wildfly.security.x500.X500; -import org.wildfly.security.x500.cert.AuthorityKeyIdentifierExtension; -import org.wildfly.security.x500.cert.BasicConstraintsExtension; -import org.wildfly.security.x500.cert.CertificatePoliciesExtension; -import org.wildfly.security.x500.cert.CertificatePoliciesExtension.PolicyInformation; -import org.wildfly.security.x500.cert.ExtendedKeyUsageExtension; -import org.wildfly.security.x500.cert.KeyUsage; -import org.wildfly.security.x500.cert.KeyUsageExtension; -import org.wildfly.security.x500.cert.SubjectKeyIdentifierExtension; -import org.wildfly.security.x500.cert.X509CertificateBuilder; -import org.wildfly.security.x500.cert.X509CertificateExtension; - -/** - * The Class CertificateUtils provides utility functions for generation - * and usage of X.509 certificates - * - * @author David Anderson - */ -public class ElytronCertificateUtils implements CertificateUtilsProvider { - - Logger log = Logger.getLogger(getClass()); - - /** - * Generates version 3 {@link java.security.cert.X509Certificate}. - * - * @param keyPair the key pair - * @param caPrivateKey the CA private key - * @param caCert the CA certificate - * @param subject the subject name - * - * @return the x509 certificate - * - * @throws Exception the exception - */ - @Override - public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, - X509Certificate caCert, - String subject) throws Exception { - try { - - X500Principal subjectdn = subjectToX500Principle(subject); - X500Principal issuerdn = subjectdn; - if (caCert != null) { - issuerdn = caCert.getSubjectX500Principal(); - } - - // Validity - ZonedDateTime notBefore = ZonedDateTime.ofInstant(new Date(System.currentTimeMillis()).toInstant(), - ZoneId.systemDefault()); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.YEAR, 3); - Date validityEndDate = new Date(calendar.getTime().getTime()); - ZonedDateTime notAfter = ZonedDateTime.ofInstant(validityEndDate.toInstant(), - ZoneId.systemDefault()); - // Serial Number - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - BigInteger serialNumber = BigInteger.valueOf(Math.abs(random.nextInt())); - // Extended Key Usage - ArrayList ekuList = new ArrayList(); - ekuList.add(X500.OID_KP_EMAIL_PROTECTION); - ekuList.add(X500.OID_KP_SERVER_AUTH); - - X509CertificateBuilder cbuilder = new X509CertificateBuilder() - .setSubjectDn(subjectdn) - .setIssuerDn(issuerdn) - - .setNotValidBefore(notBefore) - .setNotValidAfter(notAfter) - - .setSigningKey(keyPair.getPrivate()) - .setPublicKey(keyPair.getPublic()) - - .setSerialNumber(serialNumber) - - .setSignatureAlgorithmName("SHA256withRSA") - - .setSigningKey(caPrivateKey) - - // Subject Key Identifier Extension - .addExtension(new SubjectKeyIdentifierExtension(keyPair.getPublic().getEncoded())) - - // Authority Key Identifier - .addExtension(new AuthorityKeyIdentifierExtension(keyPair.getPublic().getEncoded(), null, null)) - - // Key Usage - .addExtension( - new KeyUsageExtension(KeyUsage.digitalSignature, KeyUsage.keyCertSign, KeyUsage.cRLSign)) - - .addExtension(new ExtendedKeyUsageExtension(false, ekuList)) - - // Basic Constraints - .addExtension(new BasicConstraintsExtension(true, true, 0)); - - return cbuilder.build(); - - } catch (Exception e) { - throw new RuntimeException("Error creating X509v3Certificate.", e); - } - } - - /** - * Generate version 1 self signed {@link java.security.cert.X509Certificate}.. - * - * @param caKeyPair the CA key pair - * @param subject the subject name - * - * @return the x509 certificate - * - * @throws Exception the exception - */ - @Override - public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) { - return generateV1SelfSignedCertificate(caKeyPair, subject, BigInteger.valueOf(System.currentTimeMillis())); - } - - @Override - public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, - BigInteger serialNumber) { - try { - - X500Principal subjectdn = subjectToX500Principle(subject); - - ZonedDateTime notBefore = ZonedDateTime.ofInstant( - (new Date(System.currentTimeMillis() - 100000)).toInstant(), - ZoneId.systemDefault()); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.YEAR, 10); - Date validityEndDate = new Date(calendar.getTime().getTime()); - ZonedDateTime notAfter = ZonedDateTime.ofInstant(validityEndDate.toInstant(), - ZoneId.systemDefault()); - - X509CertificateBuilder cbuilder = new X509CertificateBuilder() - .setSubjectDn(subjectdn) - .setIssuerDn(subjectdn) - .setNotValidBefore(notBefore) - .setNotValidAfter(notAfter) - - .setSigningKey(caKeyPair.getPrivate()) - .setPublicKey(caKeyPair.getPublic()) - - .setSerialNumber(serialNumber) - - .setSignatureAlgorithmName("SHA256withRSA"); - - return cbuilder.build(); - - } catch (Exception e) { - throw new RuntimeException("Error creating X509v1Certificate.", e); - } - } - - - // Some subject names will not conform to the RFC format - private static X500Principal subjectToX500Principle(String subject) { - if(!subject.startsWith("CN=")) { - subject = "CN="+subject; - } - return new X500Principal(subject); - } - - @Override - public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException { - byte[] policy = cert.getExtensionValue("2.5.29.32"); - - System.out.println("Policy: " + new String(policy)); - DERDecoder decPolicy = new DERDecoder(policy); - - int type = decPolicy.peekType(); - System.out.println("type " + type); - - DERDecoder der = new DERDecoder(decPolicy.decodeOctetString()); - - List policyList =new ArrayList<>(); - - while (der.hasNextElement()) { - switch (der.peekType()) { - case ASN1.SEQUENCE_TYPE: - der.startSequence(); - break; - case ASN1.OBJECT_IDENTIFIER_TYPE: - policyList.add(der.decodeObjectIdentifier()); - der.endSequence(); - break; - default: - der.skipElement(); - - } - } - - return policyList; - } - - @Override - public List getCRLDistributionPoints(X509Certificate cert) throws IOException { - byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); - if (data == null) { - return Collections.emptyList(); - } - List distPointUrls = new ArrayList<>(); - DERDecoder der = new DERDecoder(data); - - der = new DERDecoder(der.decodeOctetString()); - - while ( der.hasNextElement() ) { - switch (der.peekType()) { - case ASN1.SEQUENCE_TYPE: - der.startSequence(); - break; - case ASN1.UTF8_STRING_TYPE: - distPointUrls.add(der.decodeUtf8String()); - break; - case 0xa0: // Decode CRLDistributionPoint FullName list - der.startExplicit(0xa0); - break; - case 0x86: // Decode CRLDistributionPoint FullName - der.decodeImplicit(0x86); - distPointUrls.add(der.decodeOctetStringAsString()); - log.debug("Adding Dist point name: " + distPointUrls.get(distPointUrls.size()-1)); - break; - default: - der.skipElement(); - } - // Check to see if there is another sequence to process - try { - if(!der.hasNextElement() && der.peekType() == ASN1.SEQUENCE_TYPE) { - der.startSequence(); - } else if (!der.hasNextElement() && der.peekType() == 0xa0) { - der.startExplicit(0xa0); - } - - } catch(Exception e) { - // Just log this error. Likely the Dist points have been parsed, but - // the end of the cert is failing to parse. - log.warn("There is an issue parsing the certificate for Distribution Points", e); - - } - } - - return distPointUrls; - } - - @Override - public X509Certificate createServicesTestCertificate(String dn, Date startDate, Date expiryDate, KeyPair keyPair, - String... certificatePolicyOid) { - - try { - X500Principal subjectdn = subjectToX500Principle(dn); - X500Principal issuerdn = subjectToX500Principle(dn); - - ZonedDateTime notValidBefore = ZonedDateTime.ofInstant(startDate.toInstant(), ZoneId.systemDefault()); - ZonedDateTime notValidAfter = ZonedDateTime.ofInstant(expiryDate.toInstant(), ZoneId.systemDefault()); - - X509CertificateBuilder cbuilder = new X509CertificateBuilder() - .setSubjectDn(subjectdn) - .setIssuerDn(issuerdn) - - .setNotValidBefore(notValidBefore) - .setNotValidAfter(notValidAfter) - - .setSigningKey(keyPair.getPrivate()) - .setPublicKey(keyPair.getPublic()) - - .addExtension(createPoliciesExtension(certificatePolicyOid)) - - .setSignatureAlgorithmName("SHA256withRSA"); - - return cbuilder.build(); - } catch ( DateTimeException | CertificateException e ) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - private X509CertificateExtension createPoliciesExtension(String[] certificatePolicyOid) { - - List policyList = new ArrayList<>(); - for(String policyOid : certificatePolicyOid) { - policyList.add(new PolicyInformation(policyOid)); - - } - - return new CertificatePoliciesExtension(false, policyList); - - } - -} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java new file mode 100644 index 000000000000..273e0b205b0f --- /dev/null +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java @@ -0,0 +1,347 @@ +/* + * Copyright 2016 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.crypto.elytron; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.DateTimeException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import javax.security.auth.x500.X500Principal; + +import org.jboss.logging.Logger; +import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.wildfly.security.asn1.ASN1; +import org.wildfly.security.asn1.DERDecoder; +import org.wildfly.security.x500.GeneralName; +import org.wildfly.security.x500.X500; +import org.wildfly.security.x500.cert.AuthorityKeyIdentifierExtension; +import org.wildfly.security.x500.cert.BasicConstraintsExtension; +import org.wildfly.security.x500.cert.CertificatePoliciesExtension; +import org.wildfly.security.x500.cert.CertificatePoliciesExtension.PolicyInformation; +import org.wildfly.security.x500.cert.ExtendedKeyUsageExtension; +import org.wildfly.security.x500.cert.KeyUsage; +import org.wildfly.security.x500.cert.KeyUsageExtension; +import org.wildfly.security.x500.cert.SubjectKeyIdentifierExtension; +import org.wildfly.security.x500.cert.X509CertificateBuilder; +import org.wildfly.security.x500.cert.X509CertificateExtension; +import org.wildfly.security.x500.cert.util.KeyUtil; + +/** + * The Class CertificateUtils provides utility functions for generation + * and usage of X.509 certificates + * + * @author David Anderson + */ +public class ElytronCertificateUtilsProvider implements CertificateUtilsProvider { + + Logger log = Logger.getLogger(getClass()); + + /** + * Generates version 3 {@link java.security.cert.X509Certificate}. + * + * @param keyPair the key pair + * @param caPrivateKey the CA private key + * @param caCert the CA certificate + * @param subject the subject name + * + * @return the x509 certificate + * + * @throws Exception the exception + */ + @Override + public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, + X509Certificate caCert, + String subject) throws Exception { + try { + + X500Principal subjectdn = subjectToX500Principle(subject); + X500Principal issuerdn = caCert.getSubjectX500Principal(); + + // Validity + ZonedDateTime notBefore = ZonedDateTime.ofInstant(new Date(System.currentTimeMillis()).toInstant(), + ZoneId.systemDefault()); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 3); + Date validityEndDate = new Date(calendar.getTime().getTime()); + ZonedDateTime notAfter = ZonedDateTime.ofInstant(validityEndDate.toInstant(), + ZoneId.systemDefault()); + // Serial Number + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + BigInteger serialNumber = BigInteger.valueOf(Math.abs(random.nextInt())); + // Extended Key Usage + ArrayList ekuList = new ArrayList(); + ekuList.add(X500.OID_KP_EMAIL_PROTECTION); + ekuList.add(X500.OID_KP_SERVER_AUTH); + + X509CertificateBuilder cbuilder = new X509CertificateBuilder() + .setSubjectDn(subjectdn) + .setIssuerDn(issuerdn) + + .setNotValidBefore(notBefore) + .setNotValidAfter(notAfter) + + .setPublicKey(keyPair.getPublic()) + + .setSerialNumber(serialNumber) + + .setSigningKey(caPrivateKey) + + // Subject Key Identifier Extension + .addExtension(new SubjectKeyIdentifierExtension(KeyUtil.getKeyIdentifier(keyPair.getPublic()))) + + // Authority Key Identifier + .addExtension(new AuthorityKeyIdentifierExtension( + KeyUtil.getKeyIdentifier(caCert.getPublicKey()), + Collections.singletonList(new GeneralName.DirectoryName(caCert.getIssuerX500Principal().getName())), + caCert.getSerialNumber() + )) + + // Key Usage + .addExtension( + new KeyUsageExtension(KeyUsage.digitalSignature, KeyUsage.keyCertSign, KeyUsage.cRLSign)) + + .addExtension(new ExtendedKeyUsageExtension(false, ekuList)) + + // Basic Constraints + .addExtension(new BasicConstraintsExtension(true, true, 0)); + + switch (caPrivateKey.getAlgorithm()){ + case "EC": + cbuilder.setSignatureAlgorithmName("SHA256withECDSA"); + break; + default: + cbuilder.setSignatureAlgorithmName("SHA256withRSA"); + } + + return cbuilder.build(); + + } catch (Exception e) { + throw new RuntimeException("Error creating X509v3Certificate.", e); + } + } + + /** + * Generate version 1 self signed {@link java.security.cert.X509Certificate}.. + * + * @param caKeyPair the CA key pair + * @param subject the subject name + * + * @return the x509 certificate + * + * @throws Exception the exception + */ + @Override + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) { + return generateV1SelfSignedCertificate(caKeyPair, subject, BigInteger.valueOf(System.currentTimeMillis())); + } + + @Override + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, + BigInteger serialNumber) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 10); + return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime()); + } + + @Override + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) { + try { + + X500Principal subjectdn = subjectToX500Principle(subject); + + ZonedDateTime notBefore = ZonedDateTime.ofInstant( + (new Date(System.currentTimeMillis() - 100000)).toInstant(), + ZoneId.systemDefault()); + + ZonedDateTime notAfter = ZonedDateTime.ofInstant(validityEndDate.toInstant(), + ZoneId.systemDefault()); + + X509CertificateBuilder cbuilder = new X509CertificateBuilder() + .setSubjectDn(subjectdn) + .setIssuerDn(subjectdn) + .setNotValidBefore(notBefore) + .setNotValidAfter(notAfter) + + .setSigningKey(caKeyPair.getPrivate()) + .setPublicKey(caKeyPair.getPublic()) + + .setSerialNumber(serialNumber); + + switch (caKeyPair.getPrivate().getAlgorithm()){ + case "EC": + cbuilder.setSignatureAlgorithmName("SHA256withECDSA"); + break; + default: + cbuilder.setSignatureAlgorithmName("SHA256withRSA"); + } + + return cbuilder.build(); + + } catch (Exception e) { + throw new RuntimeException("Error creating X509v1Certificate.", e); + } + } + + + // Some subject names will not conform to the RFC format + private static X500Principal subjectToX500Principle(String subject) { + if(!subject.startsWith("CN=")) { + subject = "CN="+subject; + } + return new X500Principal(subject); + } + + @Override + public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException { + byte[] policy = cert.getExtensionValue("2.5.29.32"); + + System.out.println("Policy: " + new String(policy)); + DERDecoder decPolicy = new DERDecoder(policy); + + int type = decPolicy.peekType(); + System.out.println("type " + type); + + DERDecoder der = new DERDecoder(decPolicy.decodeOctetString()); + + List policyList =new ArrayList<>(); + + while (der.hasNextElement()) { + switch (der.peekType()) { + case ASN1.SEQUENCE_TYPE: + der.startSequence(); + break; + case ASN1.OBJECT_IDENTIFIER_TYPE: + policyList.add(der.decodeObjectIdentifier()); + der.endSequence(); + break; + default: + der.skipElement(); + + } + } + + return policyList; + } + + @Override + public List getCRLDistributionPoints(X509Certificate cert) throws IOException { + byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); + if (data == null) { + return Collections.emptyList(); + } + List distPointUrls = new ArrayList<>(); + DERDecoder der = new DERDecoder(data); + + der = new DERDecoder(der.decodeOctetString()); + + while ( der.hasNextElement() ) { + switch (der.peekType()) { + case ASN1.SEQUENCE_TYPE: + der.startSequence(); + break; + case ASN1.UTF8_STRING_TYPE: + distPointUrls.add(der.decodeUtf8String()); + break; + case 0xa0: // Decode CRLDistributionPoint FullName list + der.startExplicit(0xa0); + break; + case 0x86: // Decode CRLDistributionPoint FullName + der.decodeImplicit(0x86); + distPointUrls.add(der.decodeOctetStringAsString()); + log.debug("Adding Dist point name: " + distPointUrls.get(distPointUrls.size()-1)); + break; + default: + der.skipElement(); + } + // Check to see if there is another sequence to process + try { + if(!der.hasNextElement() && der.peekType() == ASN1.SEQUENCE_TYPE) { + der.startSequence(); + } else if (!der.hasNextElement() && der.peekType() == 0xa0) { + der.startExplicit(0xa0); + } + + } catch(Exception e) { + // Just log this error. Likely the Dist points have been parsed, but + // the end of the cert is failing to parse. + log.warn("There is an issue parsing the certificate for Distribution Points", e); + + } + } + + return distPointUrls; + } + + @Override + public X509Certificate createServicesTestCertificate(String dn, Date startDate, Date expiryDate, KeyPair keyPair, + String... certificatePolicyOid) { + + try { + X500Principal subjectdn = subjectToX500Principle(dn); + X500Principal issuerdn = subjectToX500Principle(dn); + + ZonedDateTime notValidBefore = ZonedDateTime.ofInstant(startDate.toInstant(), ZoneId.systemDefault()); + ZonedDateTime notValidAfter = ZonedDateTime.ofInstant(expiryDate.toInstant(), ZoneId.systemDefault()); + + X509CertificateBuilder cbuilder = new X509CertificateBuilder() + .setSubjectDn(subjectdn) + .setIssuerDn(issuerdn) + + .setNotValidBefore(notValidBefore) + .setNotValidAfter(notValidAfter) + + .setSigningKey(keyPair.getPrivate()) + .setPublicKey(keyPair.getPublic()) + + .addExtension(createPoliciesExtension(certificatePolicyOid)) + + .setSignatureAlgorithmName("SHA256withRSA"); + + return cbuilder.build(); + } catch ( DateTimeException | CertificateException e ) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private X509CertificateExtension createPoliciesExtension(String[] certificatePolicyOid) { + + List policyList = new ArrayList<>(); + for(String policyOid : certificatePolicyOid) { + policyList.add(new PolicyInformation(policyOid)); + + } + + return new CertificatePoliciesExtension(false, policyList); + + } + +} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java index b5f296ebc3b8..855de2d3ed6a 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.math.BigInteger; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import org.jboss.logging.Logger; import org.keycloak.common.crypto.ECDSACryptoProvider; @@ -51,7 +53,7 @@ public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) th seq.endSequence(); return seq.getEncoded(); - + } @Override @@ -60,8 +62,8 @@ public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int DERDecoder der = new DERDecoder(derEncodedSignatureValue); der.startSequence(); - byte[] r = convertToBytes(der.decodeInteger(),len); - byte[] s = convertToBytes(der.decodeInteger(),len); + byte[] r = convertToBytes(der.decodeInteger(), len); + byte[] s = convertToBytes(der.decodeInteger(), len); der.endSequence(); byte[] concatenatedSignatureValue = new byte[signLength]; @@ -71,13 +73,18 @@ public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int return concatenatedSignatureValue; } + @Override + public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) { + throw new UnsupportedOperationException("Elytron Crypto Provider currently does not support extraction of EC Public Keys."); + } + // If byte array length doesn't match expected length, copy to new // byte array of the expected length private byte[] convertToBytes(BigInteger decodeInteger, int len) { byte[] bytes = decodeInteger.toByteArray(); - if(len < bytes.length) { + if (len < bytes.length) { log.debug("Decoded integer byte length greater than expected."); byte[] t = new byte[len]; System.arraycopy(bytes, bytes.length - len, t, 0, len); diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java new file mode 100644 index 000000000000..436d7c8125df --- /dev/null +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java @@ -0,0 +1,252 @@ +/* + * 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.crypto.elytron; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.spec.SecretKeySpec; + +import org.jose4j.jwe.kdf.ConcatKeyDerivationFunction; +import org.jose4j.keys.EcKeyUtil; +import org.jose4j.keys.EllipticCurves; +import org.jose4j.lang.JoseException; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class ElytronEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + Cipher cipher = Cipher.getInstance(getAesWrapAlgorithm(header.getAlgorithm())); + cipher.init(Cipher.UNWRAP_MODE, new SecretKeySpec(derivedKey, "AES")); + return cipher.unwrap(encodedCek, "AES", Cipher.SECRET_KEY).getEncoded(); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + Cipher cipher = Cipher.getInstance(getAesWrapAlgorithm(header.getAlgorithm())); + cipher.init(Cipher.WRAP_MODE, new SecretKeySpec(derivedKey, "AES")); + byte[] cekBytes = keyStorage.getCekBytes(); + return cipher.wrap(new SecretKeySpec(cekBytes, "AES")); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) + throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + byte[] z = deriveSharedSecret(publicKey, privateKey); + byte[] suppPrivInfo = emptyBytes(); + ConcatKeyDerivationFunction concatKdf = new ConcatKeyDerivationFunction("SHA-256"); + return concatKdf.kdf(z, keyDataLength, encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))), + encodeDataLengthData(agreementPartyUInfo), encodeDataLengthData(agreementPartyVInfo), + toByteArray(keyDataLength), suppPrivInfo); + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + try { + return ecKeyUtil.publicKey(x, y, EllipticCurves.getSpec(crv)); + } catch (JoseException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveSharedSecret(Key publicKey, Key privateKey) + throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException { + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + keyAgreement.init(privateKey); + keyAgreement.doPhase(publicKey, true); + return keyAgreement.generateSecret(); + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String getAesWrapAlgorithm(String alg) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return "AESWrap_128"; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return "AESWrap_192"; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return "AESWrap_256"; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } +} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java index 42431bc48aca..94e0b6f15252 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java @@ -20,7 +20,9 @@ import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -36,14 +38,14 @@ public ElytronRsaKeyEncryptionJWEAlgorithmProvider(String jcaAlgorithmName) { } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(encodedCek); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.ENCRYPT_MODE, publicKey); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronUserIdentityExtractorProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronUserIdentityExtractorProvider.java index 57ddba578cd1..a28de70b03bb 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronUserIdentityExtractorProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronUserIdentityExtractorProvider.java @@ -16,7 +16,7 @@ */ package org.keycloak.crypto.elytron; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.Principal; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -143,7 +143,7 @@ public Object extractUserIdentity(X509Certificate[] certs) { while(!Character.isLetterOrDigit(sb[0])) { sb = Arrays.copyOfRange(sb, 1, sb.length); } - subjectName = new String(sb, "UTF-8"); + subjectName = new String(sb, StandardCharsets.UTF_8); upnOidFound = true; } break; @@ -176,7 +176,7 @@ public Object extractUserIdentity(X509Certificate[] certs) { } } - } catch (CertificateParsingException | UnsupportedEncodingException e) { + } catch (CertificateParsingException e) { log.error("Failed to parse Subject Name:",e); } diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java index d544cd95ec9c..6c5d0e036611 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java @@ -30,11 +30,13 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; +import java.security.spec.AlgorithmParameterSpec; import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; @@ -59,6 +61,10 @@ public WildFlyElytronProvider() { providers.put(CryptoConstants.RSA1_5, new ElytronRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/PKCS1Padding")); providers.put(CryptoConstants.RSA_OAEP, new ElytronRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); providers.put(CryptoConstants.RSA_OAEP_256, new ElytronRsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + providers.put(CryptoConstants.ECDH_ES, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new ElytronEcdhEsAlgorithmProvider()); } @Override @@ -66,6 +72,11 @@ public Provider getBouncyCastleProvider() { return null; } + @Override + public int order() { + return 200; + } + @Override public T getAlgorithmProvider(Class clazz, String algorithm) { Object o = providers.get(algorithm); @@ -77,7 +88,7 @@ public T getAlgorithmProvider(Class clazz, String algorithm) { @Override public CertificateUtilsProvider getCertificateUtils() { - return new ElytronCertificateUtils(); + return new ElytronCertificateUtilsProvider(); } @Override @@ -141,7 +152,7 @@ public Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchPaddingEx public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException { return SecretKeyFactory.getInstance(keyAlgorithm); } - + @Override public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException { return KeyStore.getInstance(format.toString()); @@ -166,8 +177,28 @@ public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException { @Override public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException { - return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName)); - + String javaAlgorithm = JavaAlgorithm.getJavaAlgorithm(sigAlgName); + + switch (javaAlgorithm) { + case JavaAlgorithm.PS256, JavaAlgorithm.PS384, JavaAlgorithm.PS512: + var signature = Signature.getInstance("RSASSA-PSS"); + + int digestLength = Integer.parseInt(javaAlgorithm.substring(3, 6)); + MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + digestLength); + AlgorithmParameterSpec params = new PSSParameterSpec( + ps.getDigestAlgorithm(), "MGF1", ps, digestLength / 8, 1); + + try { + signature.setParameter(params); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + + return signature; + + default: + return Signature.getInstance(javaAlgorithm); + } } @Override diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/CRLDistributionPointTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/CRLDistributionPointTest.java index 6f5fb4df3297..f3ee9a1494d6 100644 --- a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/CRLDistributionPointTest.java +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/CRLDistributionPointTest.java @@ -27,14 +27,12 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import javax.security.auth.x500.X500Principal; import org.junit.Test; -import org.keycloak.common.util.PemUtils; -import org.keycloak.crypto.elytron.ElytronCertificateUtils; +import org.keycloak.crypto.elytron.ElytronCertificateUtilsProvider; import org.wildfly.security.x500.GeneralName; import org.wildfly.security.x500.cert.CRLDistributionPoint; import org.wildfly.security.x500.cert.CRLDistributionPoint.DistributionPointName; @@ -55,7 +53,7 @@ public void getCrlDistPoint() throws CertificateException, NoSuchAlgorithmExcept expect.add("http://crl0.test0.com"); - ElytronCertificateUtils bcutil = new ElytronCertificateUtils(); + ElytronCertificateUtilsProvider bcutil = new ElytronCertificateUtilsProvider(); List crldp = bcutil.getCRLDistributionPoints(cert); assertArrayEquals(expect.toArray(), crldp.toArray()); @@ -70,7 +68,7 @@ public void getCrlDistPointMultiNames() throws CertificateException, NoSuchAlgor expect.add("http://crl0.test0.com"); expect.add("http://crl0.test1.com"); - ElytronCertificateUtils bcutil = new ElytronCertificateUtils(); + ElytronCertificateUtilsProvider bcutil = new ElytronCertificateUtilsProvider(); List crldp = bcutil.getCRLDistributionPoints(cert); assertArrayEquals(expect.toArray(), crldp.toArray()); @@ -87,7 +85,7 @@ public void getMultiCrlDistPointMultiNames() throws CertificateException, NoSuch expect.add("http://crl1.test0.com"); expect.add("http://crl1.test1.com"); - ElytronCertificateUtils bcutil = new ElytronCertificateUtils(); + ElytronCertificateUtilsProvider bcutil = new ElytronCertificateUtilsProvider(); List crldp = bcutil.getCRLDistributionPoints(cert); assertArrayEquals(expect.toArray(), crldp.toArray()); @@ -101,7 +99,7 @@ public void revokedCertCRLDistTest() throws CertificateException, IOException { expect.add("http://localhost:8889/empty.crl"); expect.add("http://localhost:8889/intermediate-ca.crl"); - ElytronCertificateUtils bcutil = new ElytronCertificateUtils(); + ElytronCertificateUtilsProvider bcutil = new ElytronCertificateUtilsProvider(); List crldp = bcutil.getCRLDistributionPoints(cert); assertArrayEquals(expect.toArray(), crldp.toArray()); diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java new file mode 100644 index 000000000000..b07eeceb1627 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,104 @@ +/* + * 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.crypto.elytron.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; + +import org.jose4j.keys.EcKeyUtil; +import org.jose4j.keys.EllipticCurves; +import org.jose4j.lang.JoseException; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.elytron.ElytronEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class ElytronEcdhEsAlgorithmProviderTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + * @throws IllegalStateException exception + * @throws InvalidKeyException exception + */ + @Test + public void deriveKey() throws JoseException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = ElytronEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException, + JoseException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) throws JoseException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + return ecKeyUtil.publicKey(x, y, EllipticCurves.getSpec(crv)); + } + + private PrivateKey getPrivateKey(String crv, String dStr) throws JoseException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + return ecKeyUtil.privateKey(d, EllipticCurves.getSpec(crv)); + } +} \ No newline at end of file diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java new file mode 100644 index 000000000000..b10565c3c12e --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoJwtVcMetadataTrustedSdJwtIssuerTest.java @@ -0,0 +1,26 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.consumer.JwtVcMetadataTrustedSdJwtIssuerTest; + +/** + * @author Ingrid Kamga + */ +public class ElytronCryptoJwtVcMetadataTrustedSdJwtIssuerTest extends JwtVcMetadataTrustedSdJwtIssuerTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java new file mode 100644 index 000000000000..5d7f820a129f --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java @@ -0,0 +1,29 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.SdJwsTest; + +/** + * @author Ingrid Kamga + */ +public class ElytronCryptoSdJwsTest extends SdJwsTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtFacadeTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtFacadeTest.java new file mode 100644 index 000000000000..59477f113fd5 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtFacadeTest.java @@ -0,0 +1,25 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.SdJwtFacadeTest; + +/** + * @author Rodrick Awambeng + */ +public class ElytronCryptoSdJwtFacadeTest extends SdJwtFacadeTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtPresentationConsumerTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtPresentationConsumerTest.java new file mode 100644 index 000000000000..6b23fcb629ae --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtPresentationConsumerTest.java @@ -0,0 +1,26 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.consumer.SdJwtPresentationConsumerTest; + +/** + * @author Ingrid Kamga + */ +public class ElytronCryptoSdJwtPresentationConsumerTest extends SdJwtPresentationConsumerTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java new file mode 100644 index 000000000000..75aa0633fa83 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java @@ -0,0 +1,27 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.sdjwtvp.SdJwtVPTest; + +/** + * Test with Elytron provider + * + * @author Francis Pouatcha + */ +public class ElytronCryptoSdJwtVPTest extends SdJwtVPTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java new file mode 100644 index 000000000000..8db7f26072e2 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java @@ -0,0 +1,29 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.junit.Assume; +import org.junit.Before; +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.sdjwtvp.SdJwtVPVerificationTest; + +/** + * @author Ingrid Kamga + */ +public class ElytronCryptoSdJwtVPVerificationTest extends SdJwtVPVerificationTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java new file mode 100644 index 000000000000..8910c557a3ef --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java @@ -0,0 +1,26 @@ +/* + * 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.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.SdJwtVerificationTest; + +/** + * @author Ingrid Kamga + */ +public class ElytronCryptoSdJwtVerificationTest extends SdJwtVerificationTest { +} diff --git a/crypto/fips1402/pom.xml b/crypto/fips1402/pom.xml index 7691a0f12880..dab8c4724be3 100644 --- a/crypto/fips1402/pom.xml +++ b/crypto/fips1402/pom.xml @@ -30,12 +30,6 @@ Keycloak Crypto FIPS 140-2 Integration - - 17 - 17 - 17 - - org.keycloak diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java index 0f505d9218f3..5a1bd610bea9 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java @@ -49,6 +49,7 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.keycloak.crypto.JavaAlgorithm; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -67,7 +68,7 @@ import java.util.List; /** - * The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate} + * The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link X509Certificate} * * @author Bill Burke * @author Giriraj Sharma @@ -76,19 +77,17 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{ /** - * Generates version 3 {@link java.security.cert.X509Certificate}. + * Generates version 3 {@link X509Certificate}. * * @param keyPair the key pair * @param caPrivateKey the CA private key * @param caCert the CA certificate * @param subject the subject name - * + * * @return the x509 certificate - * - * @throws Exception the exception */ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert, - String subject) throws Exception { + String subject) { try { X500Name subjectDN = new X500Name("CN=" + subject); @@ -114,7 +113,7 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva // Authority Key Identifier certGen.addExtension(Extension.authorityKeyIdentifier, false, - x509ExtensionUtils.createAuthorityKeyIdentifier(subjPubKeyInfo)); + x509ExtensionUtils.createAuthorityKeyIdentifier(caCert)); // Key Usage certGen.addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign @@ -131,7 +130,16 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); // Content Signer - ContentSigner sigGen = new JcaContentSignerBuilder("SHA1WithRSAEncryption").setProvider(BouncyIntegration.PROVIDER).build(caPrivateKey); + ContentSigner sigGen; + switch (caCert.getPublicKey().getAlgorithm()){ + case "EC": + sigGen = new JcaContentSignerBuilder("SHA256WithECDSA").setProvider(BouncyIntegration.PROVIDER) + .build(caPrivateKey); + break; + default: + sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider(BouncyIntegration.PROVIDER) + .build(caPrivateKey); + } // Certificate return new JcaX509CertificateConverter().setProvider(BouncyIntegration.PROVIDER).getCertificate(certGen.build(sigGen)); @@ -141,13 +149,13 @@ public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPriva } /** - * Generate version 1 self signed {@link java.security.cert.X509Certificate}.. + * Generate version 1 self signed {@link X509Certificate}.. * * @param caKeyPair the CA key pair * @param subject the subject name - * + * * @return the x509 certificate - * + * * @throws Exception the exception */ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) { @@ -155,12 +163,16 @@ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String } public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 10); + return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime()); + } + + @Override + public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) { try { X500Name subjectDN = new X500Name("CN=" + subject); Date validityStartDate = new Date(System.currentTimeMillis() - 100000); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.YEAR, 10); - Date validityEndDate = new Date(calendar.getTime().getTime()); SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded()); X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate, @@ -174,7 +186,7 @@ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String } /** - * Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}. + * Creates the content signer for generation of Version 1 {@link X509Certificate}. * * @param privateKey the private key * @@ -182,8 +194,28 @@ public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String */ private ContentSigner createSigner(PrivateKey privateKey) { try { - JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption") - .setProvider(BouncyIntegration.PROVIDER); + JcaContentSignerBuilder signerBuilder; + switch (privateKey.getAlgorithm()) { + case "RSA": { + signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider(BouncyIntegration.PROVIDER); + break; + } + case "EC": { + signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA") + .setProvider(BouncyIntegration.PROVIDER); + break; + } + case JavaAlgorithm.Ed25519: + case JavaAlgorithm.Ed448: { + signerBuilder = new JcaContentSignerBuilder(privateKey.getAlgorithm()) + .setProvider(BouncyIntegration.PROVIDER); + break; + } + default: { + throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm())); + } + } return signerBuilder.build(privateKey); } catch (Exception e) { throw new RuntimeException("Could not create content signer.", e); @@ -192,7 +224,7 @@ private ContentSigner createSigner(PrivateKey privateKey) { @Override public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException { - + Extensions certExtensions = new JcaX509CertificateHolder(cert).getExtensions(); if (certExtensions == null) throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found"); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java index 1b6dfd35b76c..401bb5d35244 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java @@ -1,17 +1,26 @@ package org.keycloak.crypto.fips; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigInteger; - import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERSequenceGenerator; import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.jcajce.spec.ECDomainParameterSpec; +import org.bouncycastle.math.ec.ECPoint; import org.keycloak.common.crypto.ECDSACryptoProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + public class BCFIPSECDSACryptoProvider implements ECDSACryptoProvider { @@ -60,5 +69,27 @@ public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int return concatenatedSignatureValue; } - + @Override + public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) { + try { + ECParameterSpec parameterSpec = ecPrivateKey.getParams(); + ECDomainParameterSpec domainParameterSpec = new ECDomainParameterSpec(parameterSpec); + + ECPoint q = domainParameterSpec.getDomainParameters().getG().multiply(ecPrivateKey.getS()).normalize(); + ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec( + new java.security.spec.ECPoint( + q.getAffineXCoord().toBigInteger(), + q.getAffineYCoord().toBigInteger()), + domainParameterSpec); + + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Key algorithm not supported.", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("Received an invalid key spec.", e); + } + } + + } diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java new file mode 100644 index 000000000000..5506bd14141c --- /dev/null +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java @@ -0,0 +1,262 @@ +/* + * 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.crypto.fips; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.KeyUnwrapper; +import org.bouncycastle.crypto.KeyWrapper; +import org.bouncycastle.crypto.SymmetricKey; +import org.bouncycastle.crypto.SymmetricSecretKey; +import org.bouncycastle.crypto.asymmetric.AsymmetricECPrivateKey; +import org.bouncycastle.crypto.asymmetric.AsymmetricECPublicKey; +import org.bouncycastle.crypto.asymmetric.ECDomainParameters; +import org.bouncycastle.crypto.fips.FipsAES; +import org.bouncycastle.crypto.fips.FipsAES.WrapParameters; +import org.bouncycastle.crypto.fips.FipsAgreement; +import org.bouncycastle.crypto.fips.FipsEC; +import org.bouncycastle.crypto.fips.FipsKDF; +import org.bouncycastle.crypto.fips.FipsKDF.AgreementKDFPRF; +import org.bouncycastle.jcajce.spec.ECDomainParameterSpec; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class BCFIPSEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, derivedKey); + FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); + KeyUnwrapper unwrapper = factory.createKeyUnwrapper(aesKey, FipsAES.KW); + return unwrapper.unwrap(encodedCek, 0, encodedCek.length); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + byte[] inputKeyBytes = keyStorage.getCekBytes(); // bytes making up the key to be wrapped + byte[] keyBytes = derivedKey; // bytes making up AES key doing the wrapping + SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); + FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); + KeyWrapper wrapper = factory.createKeyWrapper(aesKey, FipsAES.KW); + return wrapper.wrap(inputKeyBytes, 0, inputKeyBytes.length); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BCFIPS"); + SecureRandom randomGen = SecureRandom.getInstance("DEFAULT", "BCFIPS"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveOtherInfo(int keyDataLength, String algorithmID, byte[] agreementPartyUInfo, + byte[] agreementPartyVInfo) { + byte[] algorithmId = encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))); + byte[] partyUInfo = encodeDataLengthData(agreementPartyUInfo); + byte[] partyVInfo = encodeDataLengthData(agreementPartyVInfo); + byte[] suppPubInfo = toByteArray(keyDataLength); + byte[] suppPrivInfo = emptyBytes(); + return concat(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) { + byte[] otherInfo = deriveOtherInfo(keyDataLength, algorithmID, agreementPartyUInfo, agreementPartyVInfo); + FipsEC.DHAgreementFactory factory = new FipsEC.DHAgreementFactory(); + FipsAgreement agree = factory.createAgreement( + new AsymmetricECPrivateKey(FipsEC.ALGORITHM, privateKey.getEncoded()), + FipsEC.DH.withKDF(FipsKDF.CONCATENATION.withPRF(AgreementKDFPRF.SHA256), otherInfo, keyDataLength / 8)); + return agree.calculate(new AsymmetricECPublicKey(FipsEC.ALGORITHM, publicKey.getEncoded())); + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + try { + ECPoint point = new ECPoint(x, y); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePublic(pubKeySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalArgumentException(e); + } + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java index 37f8d26a14d1..36ba06fb7cac 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java @@ -52,17 +52,16 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.keycloak.jose.jwe.JWEUtils; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.models.KeycloakSession; import org.keycloak.utils.OCSPProvider; import java.io.IOException; -import java.math.BigInteger; import java.net.URI; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.SecureRandom; import java.security.cert.CRLReason; import java.security.cert.CertPath; import java.security.cert.CertPathValidatorException; @@ -92,7 +91,7 @@ public class BCFIPSOCSPProvider extends OCSPProvider { private final static Logger logger = Logger.getLogger(BCFIPSOCSPProvider.class.getName()); - protected OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException, InterruptedException { + protected OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException { byte[] data = getEncodedOCSPResponse(session, ocspReq.getEncoded(), responderUri); return new OCSPResp(data); } @@ -116,28 +115,27 @@ protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate ce DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().build(); DigestCalculator digCalc = dcp.get(CertificateID.HASH_SHA1); + JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber()); - // Create a nounce extension to protect against replay attacks - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt())); + URI responderURI = responderURIs.get(0); - DEROctetString derString = new DEROctetString(nounce.toByteArray()); - Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString); - Extensions extensions = new Extensions(nounceExtension); + try { + // Create a nonce extension to protect against replay attacks + DEROctetString requestNonce = new DEROctetString(new DEROctetString(JWEUtils.generateSecret(16))); + Extension nonceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, requestNonce); + Extensions extensions = new Extensions(nonceExtension); - OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build(); + OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID).setRequestExtensions(extensions).build(); - URI responderURI = responderURIs.get(0); - logger.log(Level.INFO, "OCSP Responder {0}", responderURI); + logger.log(Level.INFO, "OCSP Responder {0}", responderURI); - try { OCSPResp resp = getResponse(session, ocspReq, responderURI); logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()}); switch (resp.getStatus()) { case OCSPResp.SUCCESSFUL: if (resp.getResponseObject() instanceof BasicOCSPResp) { - return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject()); + return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, requestNonce, (BasicOCSPResp)resp.getResponseObject()); } else { throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response."); } @@ -157,7 +155,7 @@ protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate ce throw new CertPathValidatorException("OCSP request is malformed. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED); } } - catch(IOException | InterruptedException e) { + catch(IOException e) { logger.log(Level.FINE, "OCSP Responder \"{0}\" failed to return a valid OCSP response\n{1}", new Object[] {responderURI, e.getMessage()}); throw new CertPathValidatorException("OCSP check failed", e); @@ -170,7 +168,7 @@ protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate ce } } - private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse) + private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, DEROctetString requestNonce, BasicOCSPResp basicOcspResponse) throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { SingleResp expectedResponse = null; for (SingleResp singleResponse : basicOcspResponse.getResponses()) { @@ -181,7 +179,7 @@ private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCert } if (expectedResponse != null) { - verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date); + verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, requestNonce, date); return singleResponseToRevocationStatus(expectedResponse); } else { throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request"); @@ -199,7 +197,7 @@ private boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) { idLeft.getSerialNumber().equals(idRight.getSerialNumber()); } - private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { + private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, DEROctetString requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException { List certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts())); X509Certificate signingCert = null; @@ -330,7 +328,10 @@ private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate iss throw new CertPathValidatorException("Error verifying OCSP Response\'s signature"); } else { Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); - if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) { + if (responseNonce == null && requestNonce != null) { + logger.log(Level.FINE, "No OCSP nonce in response"); + } + if (responseNonce != null && requestNonce != null && !requestNonce.equals(responseNonce.getExtnValue())) { throw new CertPathValidatorException("Nonces do not match."); } else { // See Sun's OCSP implementation. diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java index b0b065b95a71..40171a15205a 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java @@ -18,12 +18,12 @@ package org.keycloak.crypto.fips; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.keycloak.common.util.BouncyIntegration; -import org.keycloak.common.util.DerUtils; import org.keycloak.common.util.PemException; import org.keycloak.common.crypto.PemUtilsProvider; import org.keycloak.common.util.PemUtils; @@ -31,9 +31,8 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; -import java.security.KeyFactory; import java.security.PrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; +import java.security.PublicKey; /** * Encodes Key or Certificates to PEM format string @@ -69,6 +68,22 @@ protected String encode(Object obj) { } } + @Override + public PublicKey decodePublicKey(String pem) { + try { + // try to decode using SubjectPublicKeyInfo which allows to know the key type + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem)); + if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) { + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } + } catch (Exception e) { + // error reading PEM object just go to previous RSA forced key + } + + // assume RSA if it cannot be decoded from BC knowing the key + return decodePublicKey(pem, "RSA"); + } + @Override public PrivateKey decodePrivateKey(String pem) { if (pem == null) { diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java index 6cc13ad5db72..4d09ce15a0c2 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java @@ -58,7 +58,7 @@ class X500NameRDNExtractorBCProvider extends X500NameRDNExtractor { private ASN1ObjectIdentifier x500NameStyle; Function x500Name; - + public X500NameRDNExtractorBCProvider(String attrName, Function x500Name) { this.x500NameStyle = BCStyle.INSTANCE.attrNameToOID(attrName); this.x500Name = x500Name; @@ -147,34 +147,41 @@ public Object extractUserIdentity(X509Certificate[] certs) { return obj; } - byte[] otherNameBytes = (byte[]) obj; + // From Java 21, the 3rd entry can be present with the type-id as String and 4th entry with the value (either in String or byte format). + // See javadoc of X509Certificate.getSubjectAlternativeNames in Java 21. For the sake of simplicity, we just ignore those additional String entries and + // always parse it from byte (2nd entry) as we still need to support Java 17 and it is not reliable anyway that entries are present in Java 21. + if (obj instanceof byte[]) { + byte[] otherNameBytes = (byte[]) obj; - try { - ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes)); - ASN1Encodable asn1otherName = asn1Stream.readObject(); - asn1otherName = unwrap(asn1otherName); + try { + ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes)); + ASN1Encodable asn1otherName = asn1Stream.readObject(); + asn1otherName = unwrap(asn1otherName); - ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName); + ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName); - if (asn1Sequence != null) { - ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0); - ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid)); - tempOid = oid.getId(); + if (asn1Sequence != null) { + ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0); + ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid)); + tempOid = oid.getId(); - ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1); - DERUTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded)); + ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1); + DERUTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded)); - tempOtherName = principalName.getString(); + tempOtherName = principalName.getString(); - // We found UPN among the 'otherName' principal. We don't need to look other - if (UPN_OID.equals(tempOid)) { - foundUpn = true; - break; + // We found UPN among the 'otherName' principal. We don't need to look other + if (UPN_OID.equals(tempOid)) { + foundUpn = true; + break; + } } - } - } catch (Exception e) { - logger.error("Failed to parse subjectAltName", e); + } catch (Exception e) { + logger.error("Failed to parse subjectAltName", e); + } + } else { + logger.tracef("Ignoring the Subject alternative name entry. Entry number: %d, value: %s", i + 1, obj); } } diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java index 925b1d0f1246..5796e4f9d60a 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java @@ -86,6 +86,10 @@ public FIPS1402Provider() { providers.put(CryptoConstants.RSA1_5, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_PKCS1v1_5)); providers.put(CryptoConstants.RSA_OAEP, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_OAEP)); providers.put(CryptoConstants.RSA_OAEP_256, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_OAEP.withDigest(FipsSHS.Algorithm.SHA256))); + providers.put(CryptoConstants.ECDH_ES, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new BCFIPSEcdhEsAlgorithmProvider()); Security.insertProviderAt(new KeycloakFipsSecurityProvider(bcFipsProvider), 1); if (existingBcFipsProvider == null) { @@ -106,6 +110,11 @@ public Provider getBouncyCastleProvider() { return bcFipsProvider; } + @Override + public int order() { + return 200; + } + @Override public T getAlgorithmProvider(Class clazz, String algorithm) { Object o = providers.get(algorithm); @@ -191,12 +200,12 @@ public Cipher getAesCbcCipher() throws NoSuchAlgorithmException, NoSuchProviderE public Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { return Cipher.getInstance("AES/GCM/NoPadding", BouncyIntegration.PROVIDER); } - + @Override public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException, NoSuchProviderException { return SecretKeyFactory.getInstance(keyAlgorithm, BouncyIntegration.PROVIDER); } - + @Override public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException, NoSuchProviderException { return KeyStore.getInstance(format.toString(), BouncyIntegration.PROVIDER); @@ -218,11 +227,11 @@ public CertStore getCertStore(CollectionCertStoreParameters certStoreParams) thr public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException, NoSuchProviderException { return CertPathBuilder.getInstance("PKIX", BouncyIntegration.PROVIDER); } - + @Override public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException, NoSuchProviderException { return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName), BouncyIntegration.PROVIDER); - + } @Override @@ -323,13 +332,13 @@ private void checkSecureRandom(Runnable insertBcFipsProvider) { } catch (NoSuchAlgorithmException nsae) { // Fallback to regular SecureRandom + // We could delete this once https://issues.redhat.com/browse/RHEL-3478 is fixed SecureRandom secRandom = new SecureRandom(); String origStrongAlgs = Security.getProperty("securerandom.strongAlgorithms"); String usedAlg = secRandom.getAlgorithm() + ":" + secRandom.getProvider().getName(); log.debugf("Strong secure random not available. Tried algorithms: %s. Using algorithm as a fallback for strong secure random: %s", origStrongAlgs, usedAlg); - String strongAlgs = origStrongAlgs == null ? usedAlg : usedAlg + "," + origStrongAlgs; - Security.setProperty("securerandom.strongAlgorithms", strongAlgs); + Security.setProperty("securerandom.strongAlgorithms", usedAlg); try { // Need to insert BCFIPS provider to security providers with "strong algorithm" available @@ -338,8 +347,6 @@ private void checkSecureRandom(Runnable insertBcFipsProvider) { log.debugf("Initialized BCFIPS secured random"); } catch (NoSuchAlgorithmException | NoSuchProviderException nsaee) { throw new IllegalStateException("Not possible to initiate BCFIPS secure random", nsaee); - } finally { - Security.setProperty("securerandom.strongAlgorithms", origStrongAlgs != null ? origStrongAlgs : ""); } } } diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java index d563be8bdb7c..59f527ca8059 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java @@ -7,7 +7,9 @@ import org.bouncycastle.crypto.SymmetricKey; import org.bouncycastle.crypto.SymmetricSecretKey; import org.bouncycastle.crypto.fips.FipsAES; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -17,7 +19,7 @@ public class FIPSAesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { byte[] keyBytes = encryptionKey.getEncoded(); // bytes making up AES key doing the wrapping SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); @@ -26,7 +28,7 @@ public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { byte[] inputKeyBytes = keyStorage.getCekBytes(); // bytes making up the key to be wrapped byte[] keyBytes = encryptionKey.getEncoded(); // bytes making up AES key doing the wrapping SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java index a9a23c08e546..4fc6e7b25ef5 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java @@ -8,7 +8,9 @@ import org.bouncycastle.crypto.asymmetric.AsymmetricRSAPrivateKey; import org.bouncycastle.crypto.asymmetric.AsymmetricRSAPublicKey; import org.bouncycastle.crypto.fips.FipsRSA; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -27,7 +29,7 @@ public FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WrapParameters wrapParam } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { AsymmetricRSAPrivateKey rsaPrivateKey = new AsymmetricRSAPrivateKey(FipsRSA.ALGORITHM, privateKey.getEncoded()); @@ -41,7 +43,7 @@ public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { AsymmetricRSAPublicKey rsaPubKey = new AsymmetricRSAPublicKey(FipsRSA.ALGORITHM, publicKey.getEncoded()); byte[] inputKeyBytes = keyStorage.getCekBytes(); diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java new file mode 100644 index 000000000000..d3e41dd9a490 --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java @@ -0,0 +1,90 @@ +/* + * 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.crypto.fips.test; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.fips.BCFIPSECDSACryptoProvider; +import org.keycloak.keys.AbstractEcKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdhKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; +import org.keycloak.rule.CryptoInitRule; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class BCFIPSECDSACryptoProviderTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {Algorithm.ES256}, {Algorithm.ES384}, {Algorithm.ES512} + }); + } + + private String algorithm; + + public BCFIPSECDSACryptoProviderTest(String algorithm) { + this.algorithm = algorithm; + } + + @Test + public void getPublicFromPrivate() { + KeyPair testKey = generateECKey(algorithm); + + BCFIPSECDSACryptoProvider bcfipsecdsaCryptoProvider = new BCFIPSECDSACryptoProvider(); + ECPublicKey derivedKey = bcfipsecdsaCryptoProvider.getPublicFromPrivate((ECPrivateKey) testKey.getPrivate()); + assertEquals("The derived key should be equal to the originally generated one.", + testKey.getPublic(), + derivedKey); + } + + public static KeyPair generateECKey(String algorithm) { + + try { + KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA"); + String domainParamNistRep = GeneratedEcdsaKeyProviderFactory.convertJWSAlgorithmToECDomainParmNistRep(algorithm); + if (domainParamNistRep == null) { + domainParamNistRep = GeneratedEcdhKeyProviderFactory.convertJWEAlgorithmToECDomainParmNistRep(algorithm); + } + String curve = AbstractEcKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep); + ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve); + kpg.initialize(parameterSpec); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java new file mode 100644 index 000000000000..6124220908a2 --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,117 @@ +/* + * 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.crypto.fips.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.asymmetric.ECDomainParameters; +import org.bouncycastle.jcajce.spec.ECDomainParameterSpec; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.fips.BCFIPSEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class BCFIPSEcdhEsAlgorithmProviderTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + */ + @Test + public void deriveKey() throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = BCFIPSEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + ECPoint point = new ECPoint(x, y); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePublic(pubKeySpec); + } + + private PrivateKey getPrivateKey(String crv, String dStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPrivateKeySpec privKeySpec = new ECPrivateKeySpec(d, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePrivate(privKeySpec); + } +} diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402JWETest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402JWETest.java index df609c0c3833..dbc86b496900 100644 --- a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402JWETest.java +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402JWETest.java @@ -2,6 +2,7 @@ import org.junit.Assume; import org.junit.Before; +import org.junit.Test; import org.keycloak.common.util.Environment; import org.keycloak.jose.JWETest; @@ -17,4 +18,20 @@ public void before() { // Run this test just if java is in FIPS mode Assume.assumeTrue("Java is not in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); } + + @Test + @Override + public void testRSA1_5_A128GCM() throws Exception { + // https://www.bouncycastle.org/download/bouncy-castle-java-fips/#release-notes + // The provider blocks RSA with PKCS1.5 encryption + Assume.assumeFalse("approved_only is set", Boolean.getBoolean("org.bouncycastle.fips.approved_only")); + super.testRSA1_5_A128GCM(); + } + + @Test + @Override + public void testRSA1_5_A128CBCHS256() throws Exception { + Assume.assumeFalse("approved_only is set", Boolean.getBoolean("org.bouncycastle.fips.approved_only")); + super.testRSA1_5_A128CBCHS256(); + } } diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index d175607fce28..40825859ef61 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -42,15 +42,15 @@ org.keycloak - keycloak-model-legacy + keycloak-model-storage org.keycloak - keycloak-model-legacy-private + keycloak-model-storage-private org.keycloak - keycloak-model-legacy-services + keycloak-model-storage-services org.keycloak @@ -86,6 +86,10 @@ org.keycloak keycloak-sssd-federation + + org.keycloak + keycloak-ipatuura-federation + diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml index 8749833cd6fd..5388210a0050 100755 --- a/dependencies/server-min/pom.xml +++ b/dependencies/server-min/pom.xml @@ -63,10 +63,6 @@ org.keycloak keycloak-server-spi-private - - org.keycloak - keycloak-js-adapter-jar - diff --git a/distribution/adapters/jetty94-adapter-zip/assembly.xml b/distribution/adapters/jetty94-adapter-zip/assembly.xml deleted file mode 100644 index bbb90d9830f4..000000000000 --- a/distribution/adapters/jetty94-adapter-zip/assembly.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - war-dist - - - zip - tar.gz - - false - - - - - - keycloak.mod - - modules - - - ${project.build.directory}/modules - - - - - - false - true - true - - org.keycloak:keycloak-jetty94-adapter - - - org.eclipse.jetty:jetty-server - org.eclipse.jetty:jetty-util - org.eclipse.jetty:jetty-security - - lib/keycloak - - - diff --git a/distribution/adapters/jetty94-adapter-zip/keycloak.mod b/distribution/adapters/jetty94-adapter-zip/keycloak.mod deleted file mode 100644 index 4da630848fde..000000000000 --- a/distribution/adapters/jetty94-adapter-zip/keycloak.mod +++ /dev/null @@ -1,28 +0,0 @@ -# -# Keycloak Jetty Adapter -# - -[depend] -server -security - -[lib] - - -lib/keycloak/*.jar - diff --git a/distribution/adapters/jetty94-adapter-zip/pom.xml b/distribution/adapters/jetty94-adapter-zip/pom.xml deleted file mode 100644 index 7e980c961d95..000000000000 --- a/distribution/adapters/jetty94-adapter-zip/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-jetty94-adapter-dist - pom - Keycloak Jetty 9.4.x Adapter Distro - - - - - org.keycloak - keycloak-jetty94-adapter - - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - - - diff --git a/distribution/adapters/pom.xml b/distribution/adapters/pom.xml deleted file mode 100755 index dc45b59bf0b0..000000000000 --- a/distribution/adapters/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - keycloak-distribution-parent - org.keycloak - 999.0.0-SNAPSHOT - - - Adapters Distribution Parent - - 4.0.0 - - keycloak-adapters-distribution-parent - pom - - - wildfly-adapter - tomcat-adapter-zip - jetty94-adapter-zip - - diff --git a/distribution/adapters/shared-cli/adapter-elytron-install.cli b/distribution/adapters/shared-cli/adapter-elytron-install.cli deleted file mode 100644 index edabdd16d367..000000000000 --- a/distribution/adapters/shared-cli/adapter-elytron-install.cli +++ /dev/null @@ -1,63 +0,0 @@ -if (outcome != success) of /extension=org.keycloak.keycloak-adapter-subsystem:read-resource - /extension=org.keycloak.keycloak-adapter-subsystem/:add(module=org.keycloak.keycloak-adapter-subsystem) -else - echo Keycloak OpenID Connect Extension already installed -end-if - -if (outcome != success) of /subsystem=keycloak:read-resource - /subsystem=keycloak:add -else - echo Keycloak OpenID Connect Subsystem already installed -end-if - -if (outcome != success) of /subsystem=elytron/custom-realm=KeycloakOIDCRealm:read-resource - /subsystem=elytron/custom-realm=KeycloakOIDCRealm:add(class-name=org.keycloak.adapters.elytron.KeycloakSecurityRealm, module=org.keycloak.keycloak-wildfly-elytron-oidc-adapter) -else - echo Keycloak OpenID Connect Realm already installed -end-if - -if (outcome != success) of /subsystem=elytron/security-domain=KeycloakDomain:read-resource - /subsystem=elytron/security-domain=KeycloakDomain:add(default-realm=KeycloakOIDCRealm,permission-mapper=default-permission-mapper,security-event-listener=local-audit,realms=[{realm=KeycloakOIDCRealm}]) -else - echo Keycloak Security Domain already installed. Trying to install Keycloak OpenID Connect Realm. - /subsystem=elytron/security-domain=KeycloakDomain:list-add(name=realms, value={realm=KeycloakOIDCRealm}) -end-if - -if (outcome != success) of /subsystem=elytron/constant-realm-mapper=keycloak-oidc-realm-mapper:read-resource - /subsystem=elytron/constant-realm-mapper=keycloak-oidc-realm-mapper:add(realm-name=KeycloakOIDCRealm) -else - echo Keycloak OpenID Connect Realm Mapper already installed -end-if - -if (outcome != success) of /subsystem=elytron/service-loader-http-server-mechanism-factory=keycloak-oidc-http-server-mechanism-factory:read-resource - /subsystem=elytron/service-loader-http-server-mechanism-factory=keycloak-oidc-http-server-mechanism-factory:add(module=org.keycloak.keycloak-wildfly-elytron-oidc-adapter) -else - echo Keycloak OpenID Connect HTTP Mechanism already installed -end-if - -if (outcome != success) of /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:read-resource - /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:add(http-server-mechanism-factories=[keycloak-oidc-http-server-mechanism-factory, global]) -else - echo Keycloak HTTP Mechanism Factory already installed. Trying to install Keycloak OpenID Connect HTTP Mechanism Factory. - /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:list-add(name=http-server-mechanism-factories, value=keycloak-oidc-http-server-mechanism-factory) -end-if - - -if (outcome != success) of /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:read-resource - /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:add(security-domain=KeycloakDomain,http-server-mechanism-factory=keycloak-http-server-mechanism-factory,mechanism-configurations=[{mechanism-name=KEYCLOAK,mechanism-realm-configurations=[{realm-name=KeycloakOIDCRealm,realm-mapper=keycloak-oidc-realm-mapper}]}]) -else - echo Keycloak HTTP Authentication Factory already installed. Trying to install Keycloak OpenID Connect Mechanism Configuration - /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:list-add(name=mechanism-configurations, value={mechanism-name=KEYCLOAK,mechanism-realm-configurations=[{realm-name=KeycloakOIDCRealm,realm-mapper=keycloak-oidc-realm-mapper}]}) -end-if - -if (outcome != success) of /subsystem=undertow/application-security-domain=other:read-resource - /subsystem=undertow/application-security-domain=other:add(http-authentication-factory=keycloak-http-authentication) -else - echo Undertow already configured with Keycloak -end-if - -if (outcome != success) of /subsystem=ejb3/application-security-domain=other:read-resource - /subsystem=ejb3/application-security-domain=other:add(security-domain=KeycloakDomain) -else - echo EJB already configured with Keycloak -end-if \ No newline at end of file diff --git a/distribution/adapters/shared-cli/adapter-install.cli b/distribution/adapters/shared-cli/adapter-install.cli deleted file mode 100644 index b4a396ba7a84..000000000000 --- a/distribution/adapters/shared-cli/adapter-install.cli +++ /dev/null @@ -1,4 +0,0 @@ -/subsystem=security/security-domain=keycloak/:add -/subsystem=security/security-domain=keycloak/authentication=classic/:add(login-modules=[{ "code" => "org.keycloak.adapters.jboss.KeycloakLoginModule","flag" => "required"}]) -/extension=org.keycloak.keycloak-adapter-subsystem/:add(module=org.keycloak.keycloak-adapter-subsystem) -/subsystem=keycloak:add \ No newline at end of file diff --git a/distribution/adapters/tomcat-adapter-zip/assembly.xml b/distribution/adapters/tomcat-adapter-zip/assembly.xml deleted file mode 100755 index a5a3c5601b69..000000000000 --- a/distribution/adapters/tomcat-adapter-zip/assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - war-dist - - - zip - tar.gz - - false - - - - false - true - true - - org.keycloak:keycloak-tomcat-adapter - - - org.apache.tomcat:tomcat-servlet-api - org.apache.tomcat:tomcat-catalina - - - - - diff --git a/distribution/adapters/tomcat-adapter-zip/pom.xml b/distribution/adapters/tomcat-adapter-zip/pom.xml deleted file mode 100755 index c6f4d90b26c6..000000000000 --- a/distribution/adapters/tomcat-adapter-zip/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-tomcat-adapter-dist - pom - Keycloak Tomcat Adapter Distro - - - - - org.keycloak - keycloak-tomcat-adapter - - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - - - diff --git a/distribution/adapters/wildfly-adapter/assembly.xml b/distribution/adapters/wildfly-adapter/assembly.xml deleted file mode 100755 index 411e8cdeb851..000000000000 --- a/distribution/adapters/wildfly-adapter/assembly.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - server-dist - - - zip - tar.gz - - - false - - - - target/${project.build.finalName} - - true - - **/module.xml - - - - target/${project.build.finalName} - - false - - docs/schema/** - README.md - - - - target/${project.build.finalName} - - - bin/*.sh - - 0755 - - - target/${project.build.finalName} - - - themes/** - - 0444 - - - src/main/modules - modules - - layers.conf - - - - - - ../shared-cli/adapter-install.cli - bin - - - cli/adapter-install-offline.cli - bin - - - ../shared-cli/adapter-elytron-install.cli - bin - - - cli/adapter-elytron-install-offline.cli - bin - - - - diff --git a/distribution/adapters/wildfly-adapter/cli/adapter-elytron-install-offline.cli b/distribution/adapters/wildfly-adapter/cli/adapter-elytron-install-offline.cli deleted file mode 100644 index b4ca831c911b..000000000000 --- a/distribution/adapters/wildfly-adapter/cli/adapter-elytron-install-offline.cli +++ /dev/null @@ -1,65 +0,0 @@ -embed-server --server-config=${server.config:standalone.xml} - -if (outcome != success) of /extension=org.keycloak.keycloak-adapter-subsystem:read-resource - /extension=org.keycloak.keycloak-adapter-subsystem/:add(module=org.keycloak.keycloak-adapter-subsystem) -else - echo Keycloak OpenID Connect Extension already installed -end-if - -if (outcome != success) of /subsystem=keycloak:read-resource - /subsystem=keycloak:add -else - echo Keycloak OpenID Connect Subsystem already installed -end-if - -if (outcome != success) of /subsystem=elytron/custom-realm=KeycloakOIDCRealm:read-resource - /subsystem=elytron/custom-realm=KeycloakOIDCRealm:add(class-name=org.keycloak.adapters.elytron.KeycloakSecurityRealm, module=org.keycloak.keycloak-wildfly-elytron-oidc-adapter) -else - echo Keycloak OpenID Connect Realm already installed -end-if - -if (outcome != success) of /subsystem=elytron/security-domain=KeycloakDomain:read-resource - /subsystem=elytron/security-domain=KeycloakDomain:add(default-realm=KeycloakOIDCRealm,permission-mapper=default-permission-mapper,security-event-listener=local-audit,realms=[{realm=KeycloakOIDCRealm}]) -else - echo Keycloak Security Domain already installed. Trying to install Keycloak OpenID Connect Realm. - /subsystem=elytron/security-domain=KeycloakDomain:list-add(name=realms, value={realm=KeycloakOIDCRealm}) -end-if - -if (outcome != success) of /subsystem=elytron/constant-realm-mapper=keycloak-oidc-realm-mapper:read-resource - /subsystem=elytron/constant-realm-mapper=keycloak-oidc-realm-mapper:add(realm-name=KeycloakOIDCRealm) -else - echo Keycloak OpenID Connect Realm Mapper already installed -end-if - -if (outcome != success) of /subsystem=elytron/service-loader-http-server-mechanism-factory=keycloak-oidc-http-server-mechanism-factory:read-resource - /subsystem=elytron/service-loader-http-server-mechanism-factory=keycloak-oidc-http-server-mechanism-factory:add(module=org.keycloak.keycloak-wildfly-elytron-oidc-adapter) -else - echo Keycloak OpenID Connect HTTP Mechanism already installed -end-if - -if (outcome != success) of /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:read-resource - /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:add(http-server-mechanism-factories=[keycloak-oidc-http-server-mechanism-factory, global]) -else - echo Keycloak HTTP Mechanism Factory already installed. Trying to install Keycloak OpenID Connect HTTP Mechanism Factory. - /subsystem=elytron/aggregate-http-server-mechanism-factory=keycloak-http-server-mechanism-factory:list-add(name=http-server-mechanism-factories, value=keycloak-oidc-http-server-mechanism-factory) -end-if - - -if (outcome != success) of /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:read-resource - /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:add(security-domain=KeycloakDomain,http-server-mechanism-factory=keycloak-http-server-mechanism-factory,mechanism-configurations=[{mechanism-name=KEYCLOAK,mechanism-realm-configurations=[{realm-name=KeycloakOIDCRealm,realm-mapper=keycloak-oidc-realm-mapper}]}]) -else - echo Keycloak HTTP Authentication Factory already installed. Trying to install Keycloak OpenID Connect Mechanism Configuration - /subsystem=elytron/http-authentication-factory=keycloak-http-authentication:list-add(name=mechanism-configurations, value={mechanism-name=KEYCLOAK,mechanism-realm-configurations=[{realm-name=KeycloakOIDCRealm,realm-mapper=keycloak-oidc-realm-mapper}]}) -end-if - -if (outcome != success) of /subsystem=undertow/application-security-domain=other:read-resource - /subsystem=undertow/application-security-domain=other:add(http-authentication-factory=keycloak-http-authentication) -else - echo Undertow already configured with Keycloak -end-if - -if (outcome != success) of /subsystem=ejb3/application-security-domain=other:read-resource - /subsystem=ejb3/application-security-domain=other:add(security-domain=KeycloakDomain) -else - echo EJB already configured with Keycloak -end-if \ No newline at end of file diff --git a/distribution/adapters/wildfly-adapter/cli/adapter-install-offline.cli b/distribution/adapters/wildfly-adapter/cli/adapter-install-offline.cli deleted file mode 100644 index 5243b2d2469a..000000000000 --- a/distribution/adapters/wildfly-adapter/cli/adapter-install-offline.cli +++ /dev/null @@ -1,5 +0,0 @@ -embed-server --server-config=${server.config:standalone.xml} -/subsystem=security/security-domain=keycloak/:add -/subsystem=security/security-domain=keycloak/authentication=classic/:add(login-modules=[{ "code" => "org.keycloak.adapters.jboss.KeycloakLoginModule","flag" => "required"}]) -/extension=org.keycloak.keycloak-adapter-subsystem/:add(module=org.keycloak.keycloak-adapter-subsystem) -/subsystem=keycloak:add \ No newline at end of file diff --git a/distribution/adapters/wildfly-adapter/pom.xml b/distribution/adapters/wildfly-adapter/pom.xml deleted file mode 100644 index 96ad4d4de431..000000000000 --- a/distribution/adapters/wildfly-adapter/pom.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - 4.0.0 - - keycloak-adapters-distribution-parent - org.keycloak - 999.0.0-SNAPSHOT - - - - 23.0.2.Final - 1.2.13.Final - 15.0.1.Final - 5.1.3.Final - assembly.xml - - - keycloak-wildfly-adapter-dist - pom - Keycloak Adapter Overlay Distribution - - - - - org.keycloak - keycloak-adapter-feature-pack - zip - - - - - - - org.wildfly.build - wildfly-server-provisioning-maven-plugin - ${wildfly.build-tools.version} - - - server-provisioning - - build - - compile - - server-provisioning.xml - true - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assemble - package - - single - - - - ${assemblyFile} - - true - ${project.build.finalName} - false - ${project.build.directory} - ${project.build.directory}/assembly/work - - - - - - - diff --git a/distribution/adapters/wildfly-adapter/server-provisioning.xml b/distribution/adapters/wildfly-adapter/server-provisioning.xml deleted file mode 100644 index 5f4ff950732c..000000000000 --- a/distribution/adapters/wildfly-adapter/server-provisioning.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/distribution/api-docs-dist/pom.xml b/distribution/api-docs-dist/pom.xml index 0a010eff8a98..fe2100febe96 100755 --- a/distribution/api-docs-dist/pom.xml +++ b/distribution/api-docs-dist/pom.xml @@ -31,7 +31,6 @@ Keycloak ${project.version} - 17 @@ -40,13 +39,23 @@ keycloak-dependencies-server-all pom + + io.quarkus + quarkus-micrometer + provided + org.keycloak - keycloak-admin-client + keycloak-admin-client-tests org.keycloak - keycloak-authz-client + keycloak-authz-client-tests + + + io.quarkus.resteasy.reactive + resteasy-reactive + provided @@ -58,16 +67,18 @@ net.java.dev.jna jna + - - com.googlecode.owasp-java-html-sanitizer - owasp-java-html-sanitizer + com.google.guava + guava - org.infinispan.protostream - protostream-processor - ${infinispan.protostream.processor.version} + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.infinispan + infinispan-component-annotations @@ -82,15 +93,21 @@ 2400m UTF-8 true - ${maven.compiler.source} true + true + + org.keycloak:* + + + org.keycloak:keycloak-operator + aggregate-javadoc compile - javadoc + aggregate @@ -113,21 +130,6 @@ - - org.apache.maven.plugins - maven-javadoc-plugin - - - aggregate-javadoc - - true - - org.keycloak:* - - - - - org.apache.maven.plugins maven-deploy-plugin diff --git a/distribution/downloads/src/main/resources/files b/distribution/downloads/src/main/resources/files index de4a49fac59b..219a5b3a84a1 100644 --- a/distribution/downloads/src/main/resources/files +++ b/distribution/downloads/src/main/resources/files @@ -1,13 +1,9 @@ mvn:keycloak-quarkus-dist:keycloak mvn:keycloak-api-docs-dist:keycloak-api-docs -mvn:keycloak-jetty94-adapter-dist:keycloak-oidc-jetty94-adapter -mvn:keycloak-tomcat-adapter-dist:keycloak-oidc-tomcat-adapter - -mvn:keycloak-saml-jetty94-adapter-dist:keycloak-saml-jetty94-adapter -mvn:keycloak-saml-tomcat-adapter-dist:keycloak-saml-tomcat-adapter - mvn:documentation/keycloak-documentation:keycloak-documentation npm:js/libs/keycloak-admin-client/target/keycloak-keycloak-admin-client-$$VERSION$$.tgz:keycloak-admin-client-$$VERSION$$.tgz -npm:js/libs/keycloak-js/target/keycloak-js-$$VERSION$$.tgz:keycloak-js-$$VERSION$$.tgz +npm:js/libs/ui-shared/target/keycloak-keycloak-ui-shared-$$VERSION$$.tgz:keycloak-ui-shared-$$VERSION$$.tgz +npm:js/apps/account-ui/target/keycloak-keycloak-account-ui-$$VERSION$$.tgz:keycloak-account-ui-$$VERSION$$.tgz +npm:js/apps/admin-ui/target/keycloak-keycloak-admin-ui-$$VERSION$$.tgz:keycloak-admin-ui-$$VERSION$$.tgz diff --git a/distribution/feature-packs/adapter-feature-pack/assembly.xml b/distribution/feature-packs/adapter-feature-pack/assembly.xml deleted file mode 100644 index bf20b3ac2857..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/assembly.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - feature-pack - - zip - - false - - - target/${project.build.finalName} - - - - - - src/main/resources/licenses/keycloak - content/docs/licenses-keycloak - - licenses.xml - - - - target/licenses - content/docs/licenses-keycloak - - - diff --git a/distribution/feature-packs/adapter-feature-pack/feature-pack-build.xml b/distribution/feature-packs/adapter-feature-pack/feature-pack-build.xml deleted file mode 100644 index 50ea3d7636f1..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/feature-pack-build.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml deleted file mode 100755 index cd9b1baf68a9..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/pom.xml +++ /dev/null @@ -1,239 +0,0 @@ - - - - org.keycloak - feature-packs-parent - 999.0.0-SNAPSHOT - - - - 23.0.2.Final - 1.2.13.Final - 15.0.1.Final - 5.1.3.Final - org.wildfly:wildfly-feature-pack - - - - 4.0.0 - - org.keycloak - keycloak-adapter-feature-pack - - Keycloak Feature Pack: Adapter - pom - - - - org.keycloak - keycloak-core - - - * - * - - - - - org.keycloak - keycloak-common - - - * - * - - - - - org.keycloak - keycloak-crypto-default - - - * - * - - - - - org.keycloak - keycloak-adapter-core - - - * - * - - - - - org.keycloak - keycloak-jboss-adapter-core - - - * - * - - - - - org.keycloak - keycloak-wildfly-subsystem - - - * - * - - - - - org.keycloak - keycloak-wildfly-elytron-oidc-adapter - - - * - * - - - - - org.keycloak - keycloak-adapter-spi - - - * - * - - - - - org.keycloak - keycloak-undertow-adapter-spi - - - * - * - - - - - org.keycloak - keycloak-undertow-adapter - - - * - * - - - - - - - org.keycloak - keycloak-authz-client - - - * - * - - - - - org.keycloak - keycloak-policy-enforcer - - - * - * - - - - - - org.wildfly - wildfly-feature-pack - ${wildfly.version} - zip - - - org.bouncycastle - bcprov-jdk15on - - - org.bouncycastle - bcpkix-jdk15on - - - - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - - org.wildfly.build - wildfly-feature-pack-build-maven-plugin - ${wildfly.build-tools.version} - - - feature-pack-build - - build - - compile - - feature-pack-build.xml - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - true - ${project.build.finalName} - false - target/ - target/assembly/work - - - - - - org.keycloak - keycloak-distribution-licenses-maven-plugin - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/subsystems.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/subsystems.xml deleted file mode 100755 index b4560533858e..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/subsystems.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - logging.xml - ee.xml - io.xml - jmx.xml - naming.xml - request-controller.xml - security.xml - undertow.xml - keycloak-adapter.xml - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/template.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/template.xml deleted file mode 100755 index c041fea093f1..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/domain/template.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/subsystems.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/subsystems.xml deleted file mode 100644 index eebbd22d5089..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/subsystems.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - logging.xml - deployment-scanner.xml - ee.xml - io.xml - jmx.xml - naming.xml - request-controller.xml - security.xml - security-manager.xml - undertow.xml - keycloak-adapter.xml - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/template.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/template.xml deleted file mode 100644 index fd1ecca4ca8c..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/configuration/standalone/template.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/content/README.md b/distribution/feature-packs/adapter-feature-pack/src/main/resources/content/README.md deleted file mode 100644 index e7b122b46e57..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/content/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This directory intentionally left empty. The Feature Pack plugin barfs if there is no content -directory. But Git won't save an empty directory. Thus, we need this readme file. \ No newline at end of file diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/keycloak/licenses.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/keycloak/licenses.xml deleted file mode 100644 index bac7cdfbcc59..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/keycloak/licenses.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/rh-sso/licenses.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/rh-sso/licenses.xml deleted file mode 100644 index bac7cdfbcc59..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/licenses/rh-sso/licenses.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml deleted file mode 100755 index 4e2e48ac3492..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml deleted file mode 100755 index 36ce0f1831ab..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-subsystem/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-subsystem/main/module.xml deleted file mode 100755 index b64b3afc4374..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-subsystem/main/module.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-authz-client/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-authz-client/main/module.xml deleted file mode 100755 index 8da2620c6e90..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-authz-client/main/module.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-common/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-common/main/module.xml deleted file mode 100755 index 486bde3a527d..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-common/main/module.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml deleted file mode 100755 index d6a4d8c09c87..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml deleted file mode 100644 index afcabe3cc9f3..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-jboss-adapter-core/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-jboss-adapter-core/main/module.xml deleted file mode 100755 index 82a92bd52a85..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-jboss-adapter-core/main/module.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml deleted file mode 100755 index 96fb5cc54c60..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-undertow-adapter/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-undertow-adapter/main/module.xml deleted file mode 100755 index 6dcf78156b82..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-undertow-adapter/main/module.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml deleted file mode 100755 index 453f5bef349b..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-subsystem/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-subsystem/main/module.xml deleted file mode 100755 index 04569f861350..000000000000 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-wildfly-subsystem/main/module.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/feature-packs/pom.xml b/distribution/feature-packs/pom.xml deleted file mode 100644 index 4c66eecf261f..000000000000 --- a/distribution/feature-packs/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - keycloak-distribution-parent - org.keycloak - 999.0.0-SNAPSHOT - - - Feature Pack Builds - - 4.0.0 - - feature-packs-parent - pom - - - adapter-feature-pack - - diff --git a/distribution/galleon-feature-packs/pom.xml b/distribution/galleon-feature-packs/pom.xml index fc16e8511026..eb2026a1eeb2 100644 --- a/distribution/galleon-feature-packs/pom.xml +++ b/distribution/galleon-feature-packs/pom.xml @@ -32,7 +32,20 @@ saml-adapter-galleon-pack - saml-adapter-galleon-pack-layer-metadata-tests + + + + upstream-adapters + + + !eap8-adapters + + + + saml-adapter-galleon-pack-layer-metadata-tests + + + 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..37978951b2ee 100644 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/pom.xml @@ -103,7 +103,7 @@ org.keycloak - keycloak-saml-adapter-core-jakarta + keycloak-saml-adapter-core * @@ -136,7 +136,7 @@ org.keycloak - keycloak-saml-undertow-adapter + keycloak-saml-wildfly-elytron-adapter * @@ -147,29 +147,7 @@ org.keycloak - keycloak-saml-wildfly-elytron-jakarta-adapter - - - * - * - - - provided - - - org.keycloak - keycloak-saml-wildfly-jakarta-subsystem - - - * - * - - - provided - - - org.keycloak - keycloak-undertow-adapter-spi + keycloak-saml-wildfly-subsystem * @@ -207,6 +185,9 @@ ${basedir}/src/main/resources + + ${basedir}/src/main/layers/${saml.adapter.galleon.pack.metadata.dir} + @@ -241,7 +222,6 @@ compile - REQUIRED true false false @@ -289,6 +269,7 @@ org.wildfly.galleon-plugins wildfly-galleon-maven-plugin + REQUIRED wildfly-feature-pack-build-eap.xml @@ -296,4 +277,4 @@ - \ No newline at end of file + diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml similarity index 100% rename from distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml rename to distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-client-saml/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-client-saml/layer-spec.xml new file mode 100644 index 000000000000..f5572e7279ae --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-client-saml/layer-spec.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-saml/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-saml/layer-spec.xml similarity index 100% rename from distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-saml/layer-spec.xml rename to distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/metadata/layers/standalone/keycloak-saml/layer-spec.xml diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml new file mode 100644 index 000000000000..e130798553f6 --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml-ejb/layer-spec.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml/layer-spec.xml new file mode 100644 index 000000000000..145e0cfa960b --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-client-saml/layer-spec.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-saml/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-saml/layer-spec.xml new file mode 100644 index 000000000000..1d869f320b5c --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/layers/non-metadata/layers/standalone/keycloak-saml/layer-spec.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-client-saml/layer-spec.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-client-saml/layer-spec.xml deleted file mode 100644 index a6350b953362..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/layers/standalone/keycloak-client-saml/layer-spec.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/license/licenses.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/license/licenses.xml index 1ff56094a145..e10310b7e51c 100644 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/license/licenses.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/license/licenses.xml @@ -11,16 +11,6 @@ - - org.keycloak - keycloak-undertow-adapter-spi - - - Apache License 2.0 - https://raw.githubusercontent.com/keycloak/keycloak/999-SNAPSHOT/LICENSE.txt - - - org.keycloak keycloak-common @@ -73,7 +63,7 @@ org.keycloak - keycloak-saml-adapter-core-jakarta + keycloak-saml-adapter-core Apache License 2.0 @@ -103,17 +93,7 @@ org.keycloak - keycloak-saml-undertow-adapter - - - Apache License 2.0 - https://raw.githubusercontent.com/keycloak/keycloak/999-SNAPSHOT/LICENSE.txt - - - - - org.keycloak - keycloak-saml-wildfly-elytron-jakarta-adapter + keycloak-saml-wildfly-elytron-adapter Apache License 2.0 @@ -123,7 +103,7 @@ org.keycloak - keycloak-saml-wildfly-jakarta-subsystem + keycloak-saml-wildfly-subsystem Apache License 2.0 diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml index f995f6b8b9a3..e5257ae3051c 100755 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-spi/main/module.xml @@ -22,7 +22,6 @@ - diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml deleted file mode 100755 index ae472c9dbaa1..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-core/main/module.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml deleted file mode 100644 index 341f07412380..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml +++ /dev/null @@ -1,40 +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 deleted file mode 100755 index 0440e67ff9f1..000000000000 --- 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 +++ /dev/null @@ -1,43 +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/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/main/module.xml new file mode 100755 index 000000000000..465593f379ff --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-core/main/module.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-subsystem/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-subsystem/main/module.xml index b48b7a19bb6d..c01667f97bdb 100755 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-adapter-subsystem/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-subsystem/main/module.xml @@ -27,6 +27,6 @@ - + diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml deleted file mode 100755 index d89e36e07f4a..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml +++ /dev/null @@ -1,46 +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-wildfly-elytron-adapter/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml new file mode 100755 index 000000000000..5c144021fe46 --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml deleted file mode 100755 index a4a47421dc8d..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml +++ /dev/null @@ -1,52 +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-wildfly-jakarta-subsystem/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-jakarta-subsystem/main/module.xml deleted file mode 100755 index 11f364eaf463..000000000000 --- a/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-jakarta-subsystem/main/module.xml +++ /dev/null @@ -1,46 +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-wildfly-subsystem/main/module.xml b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml new file mode 100755 index 000000000000..42ee3ef21e06 --- /dev/null +++ b/distribution/galleon-feature-packs/saml-adapter-galleon-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/licenses-common/check-licenses.sh b/distribution/licenses-common/check-licenses.sh index beba47e2dacd..4bbc5bb66497 100755 --- a/distribution/licenses-common/check-licenses.sh +++ b/distribution/licenses-common/check-licenses.sh @@ -11,7 +11,7 @@ for i in `find $MODULES -name '*.jar'`; do if [ ! -f $LICENSES/$groupId\,$artifactId\,$version,* ]; then echo "Missing: " echo "group: $groupId" - echo "artifcat: $artifactId" + echo "artifact: $artifactId" echo "version: $version" echo "---------------------" fi diff --git a/distribution/licenses-common/download-license-files.sh b/distribution/licenses-common/download-license-files.sh index 1b3089043ec9..9e9efddf6021 100755 --- a/distribution/licenses-common/download-license-files.sh +++ b/distribution/licenses-common/download-license-files.sh @@ -64,7 +64,7 @@ do # Windows won't like it if : is used as a separator filename="$groupid,$artifactid,$version,$name.txt" echo "$filename" - curl -LsS -o "$output_dir/$filename" "$url" + curl -LfsS -o "$output_dir/$filename" "$url" done xmlstarlet sel -T -t -m "/licenseSummary/others/other/licenses/license" -v "../../description/text()" -o $'\t' -v "name/text()" -o $'\t' -v "url/text()" --nl "$xml" | \ @@ -73,7 +73,7 @@ do # Windows won't like it if : is used as a separator filename="$description,$name.txt" echo "$filename" - curl -LsS -o "$output_dir/$filename" "$url" + curl -LfsS -o "$output_dir/$filename" "$url" done echo "==> Normalizing license line endings" >&2 diff --git a/distribution/maven-plugins/osv-scanner.toml b/distribution/maven-plugins/osv-scanner.toml new file mode 100644 index 000000000000..683c17048988 --- /dev/null +++ b/distribution/maven-plugins/osv-scanner.toml @@ -0,0 +1,10 @@ +# Ignore false positives for https://securityscorecards.dev/viewer/?uri=github.com/keycloak/keycloak + +# Suppress TestNG alert: +# - TestNG is brought in as a transitive dependency via groovy-testng. +# - Test dependencies are not included in the server distribution. +# - The latest groovy-testng version doesn't address the CVE. + +[[IgnoredVulns]] +id = "GHSA-rc2q-x9mf-w3vf" +reason = "suppressed because TestNG, a transitive dependency from groovy-testng, isn’t included in the server distribution." diff --git a/distribution/maven-plugins/pom.xml b/distribution/maven-plugins/pom.xml index fe9b7db2e36c..0658ad683770 100644 --- a/distribution/maven-plugins/pom.xml +++ b/distribution/maven-plugins/pom.xml @@ -31,7 +31,8 @@ org.codehaus.groovy groovy-all - 2.4.12 + 3.0.21 + pom org.apache.maven @@ -42,7 +43,7 @@ org.apache.maven maven-core - 3.3.9 + 3.8.1 provided @@ -63,7 +64,7 @@ org.codehaus.gmavenplus gmavenplus-plugin - 1.6 + 3.0.2 compile-groovy diff --git a/distribution/pom.xml b/distribution/pom.xml index d5ba3d6a1533..d2b8affb0c08 100755 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -41,9 +41,7 @@ - adapters saml-adapters - feature-packs galleon-feature-packs licenses-common maven-plugins diff --git a/distribution/saml-adapters/jetty94-adapter-zip/assembly.xml b/distribution/saml-adapters/jetty94-adapter-zip/assembly.xml deleted file mode 100644 index 88267704d7da..000000000000 --- a/distribution/saml-adapters/jetty94-adapter-zip/assembly.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - war-dist - - - zip - tar.gz - - false - - - - - - keycloak.mod - - modules - - - ${project.build.directory}/modules - - - - - - false - true - true - - org.keycloak:keycloak-saml-jetty94-adapter - - - org.eclipse.jetty:jetty-server - org.eclipse.jetty:jetty-util - org.eclipse.jetty:jetty-security - - lib/keycloak - - - diff --git a/distribution/saml-adapters/jetty94-adapter-zip/keycloak.mod b/distribution/saml-adapters/jetty94-adapter-zip/keycloak.mod deleted file mode 100644 index 4da630848fde..000000000000 --- a/distribution/saml-adapters/jetty94-adapter-zip/keycloak.mod +++ /dev/null @@ -1,28 +0,0 @@ -# -# Keycloak Jetty Adapter -# - -[depend] -server -security - -[lib] - - -lib/keycloak/*.jar - diff --git a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml b/distribution/saml-adapters/jetty94-adapter-zip/pom.xml deleted file mode 100644 index 21315a13f1f7..000000000000 --- a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-saml-jetty94-adapter-dist - pom - Keycloak SAML Jetty 9.4.x Adapter Distro - - - - - org.keycloak - keycloak-saml-jetty94-adapter - - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - - - diff --git a/distribution/saml-adapters/pom.xml b/distribution/saml-adapters/pom.xml index e357db3f9b4c..e0bf923961d4 100755 --- a/distribution/saml-adapters/pom.xml +++ b/distribution/saml-adapters/pom.xml @@ -32,8 +32,6 @@ wildfly-adapter - jetty94-adapter-zip - tomcat-adapter-zip diff --git a/distribution/saml-adapters/tomcat-adapter-zip/assembly.xml b/distribution/saml-adapters/tomcat-adapter-zip/assembly.xml deleted file mode 100755 index 8fce0fa7343d..000000000000 --- a/distribution/saml-adapters/tomcat-adapter-zip/assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - war-dist - - - zip - tar.gz - - false - - - - false - true - true - - org.keycloak:keycloak-saml-tomcat-adapter - - - org.apache.tomcat:tomcat-servlet-api - org.apache.tomcat:tomcat-catalina - - - - - diff --git a/distribution/saml-adapters/tomcat-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat-adapter-zip/pom.xml deleted file mode 100755 index 3710b43611de..000000000000 --- a/distribution/saml-adapters/tomcat-adapter-zip/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../pom.xml - - - keycloak-saml-tomcat-adapter-dist - pom - Keycloak SAML Tomcat Adapter Distro - - - - - org.keycloak - keycloak-saml-tomcat-adapter - - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/pom.xml b/distribution/saml-adapters/wildfly-adapter/pom.xml index ab52d865dab5..b019240ae075 100755 --- a/distribution/saml-adapters/wildfly-adapter/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/pom.xml @@ -32,37 +32,7 @@ wildfly-modules - wildfly-jakarta-modules + wildfly-adapter-zip - - - - wildfly-saml-adapter-jakarta-build - - - - !todo-wildfly.jakarta.adapters - - - - wildfly-adapter-jakarta-zip - - - - - wildfly-saml-adapter-build - - - !wildfly.jakarta.adapters - - - - wildfly-adapter-zip - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/assembly.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/assembly.xml deleted file mode 100755 index e4b01db9e1fb..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/assembly.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - war-dist - - - zip - tar.gz - - false - - - - ${project.build.directory}/unpacked/modules - - **/** - - modules/system/add-ons/keycloak - - - ${project.build.directory}/unpacked/licenses - docs/licenses-keycloak - - - - - ../../shared-cli/adapter-install-saml.cli - bin - - - ../../shared-cli/adapter-install-saml-offline.cli - bin - - - ${project.build.directory}/shared-cli/adapter-elytron-install-saml.cli - bin - - - ${project.build.directory}/shared-cli/adapter-elytron-install-saml-offline.cli - bin - - - 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 deleted file mode 100755 index 794389bd1605..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-jakarta-zip/pom.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - 4.0.0 - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - - keycloak-saml-wildfly-adapter-jakarta-dist - pom - Keycloak SAML Wildfly Adapter Jakarta Distro - - - - - org.keycloak - keycloak-saml-wildfly-jakarta-modules - zip - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - unpack - prepare-package - - unpack - - - - - org.keycloak - keycloak-saml-wildfly-jakarta-modules - zip - ${project.build.directory}/unpacked - - - - - - - - maven-antrun-plugin - - - generate-jakarta-cli - prepare-package - - - - - - - - - - - - - run - - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml index dd99dca163e1..feb22b8d3894 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml @@ -30,7 +30,7 @@ **/** - modules/system/add-ons/keycloak + modules ${project.build.directory}/unpacked/licenses diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/assembly.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/assembly.xml deleted file mode 100755 index 9d38002cfe3d..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/assembly.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - dist - - - zip - - false - - - - src/main/resources/licenses/keycloak - licenses - - licenses.xml - - - - ${project.build.directory}/licenses - licenses - - - ${project.build.directory}/modules - modules - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/build.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/build.xml deleted file mode 100755 index 77c7555eaefc..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/build.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/lib.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/lib.xml deleted file mode 100755 index 5794c22ec01b..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/lib.xml +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "; - project.setProperty("current.maven.root", root); - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "; - if(path.indexOf('${') != -1) { - throw "Module resource root not found, make sure it is listed in build/pom.xml" + path; - } - if(attributes.get("jandex") == "true" ) { - root = root + "\n\t"; - } - project.setProperty("current.resource.root", root); - ]]> - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml deleted file mode 100755 index 634ddcbee246..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/pom.xml +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - 4.0.0 - - - keycloak-parent - org.keycloak - 999.0.0-SNAPSHOT - ../../../../pom.xml - - - keycloak-saml-wildfly-jakarta-modules - - Keycloak SAML Wildfly Jakarta Modules - pom - - - org.keycloak - keycloak-common - - - * - * - - - - - org.keycloak - keycloak-core - - - * - * - - - - - org.keycloak - keycloak-crypto-default - - - * - * - - - - - org.keycloak - keycloak-adapter-spi - - - * - * - - - - - org.keycloak - keycloak-undertow-adapter-spi - - - * - * - - - - - org.keycloak - keycloak-saml-core - - - * - * - - - - - org.keycloak - keycloak-saml-adapter-api-public - - - * - * - - - - - org.keycloak - keycloak-saml-adapter-core-jakarta - - - * - * - - - - - org.keycloak - keycloak-jboss-adapter-core - - - * - * - - - - - org.keycloak - keycloak-saml-undertow-adapter - - - * - * - - - - - org.keycloak - keycloak-saml-core-public - - - * - * - - - - - org.keycloak - keycloak-saml-wildfly-elytron-jakarta-adapter - - - * - * - - - - - org.keycloak - keycloak-saml-wildfly-jakarta-subsystem - - - * - * - - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - false - - - build-dist - - run - - compile - - - - - - - - - - - - org.jboss - jandex - 1.0.3.Final - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - org.apache.ant - ant-apache-bsf - 1.9.3 - - - org.apache.bsf - bsf-api - 3.1 - - - rhino - js - 1.7R2 - - - - - maven-assembly-plugin - - - assemble - package - - single - - - - assembly.xml - - - target - - - target/assembly/work - - false - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-resources - - validate - - copy-resources - - - ${project.build.directory}/modules/org/keycloak/keycloak-saml-adapter-subsystem - - - src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem - true - - - - - - - - org.keycloak - keycloak-distribution-licenses-maven-plugin - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/keycloak/licenses.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/keycloak/licenses.xml deleted file mode 100644 index bac7cdfbcc59..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/keycloak/licenses.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/rh-sso/licenses.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/rh-sso/licenses.xml deleted file mode 100644 index bac7cdfbcc59..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/licenses/rh-sso/licenses.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml deleted file mode 100644 index 25ae4fb1fad0..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml deleted file mode 100644 index 2c81303fed7a..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml deleted file mode 100644 index 438d779945f7..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml deleted file mode 100644 index 2bc2bfdad1b6..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml deleted file mode 100644 index 5f23b1b49e3f..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml deleted file mode 100644 index 1438ed986918..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml deleted file mode 100644 index b053066e9ab7..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core-jakarta/main/module.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml deleted file mode 100644 index 7fe1bfad52ed..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml deleted file mode 100644 index 563763513dfd..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml deleted file mode 100644 index 65d1a023897b..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml deleted file mode 100644 index c0427427c836..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml deleted file mode 100644 index f0af445e5a58..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-jakarta-adapter/main/module.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-jakarta-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-jakarta-subsystem/main/module.xml deleted file mode 100644 index 6ee84301a7ef..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-jakarta-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-jakarta-subsystem/main/module.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml index 9153837d0f9e..48329548cbf4 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml @@ -22,6 +22,16 @@ + + + + + + + + @@ -37,19 +47,10 @@ - - - - - - - - - @@ -72,10 +73,6 @@ - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/lib.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/lib.xml index 5794c22ec01b..005acd68eee0 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/lib.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/lib.xml @@ -31,20 +31,13 @@ - + - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml index 68cc6b70703f..8d7e26856ae9 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml @@ -44,26 +44,6 @@ - - org.keycloak - keycloak-core - - - * - * - - - - - org.keycloak - keycloak-crypto-default - - - * - * - - - org.keycloak keycloak-adapter-spi @@ -74,16 +54,6 @@ - - org.keycloak - keycloak-undertow-adapter-spi - - - * - * - - - org.keycloak keycloak-saml-core @@ -126,7 +96,7 @@ org.keycloak - keycloak-saml-undertow-adapter + keycloak-saml-core-public * @@ -136,7 +106,7 @@ org.keycloak - keycloak-saml-core-public + keycloak-saml-wildfly-elytron-adapter * @@ -146,7 +116,7 @@ org.keycloak - keycloak-saml-wildfly-elytron-adapter + keycloak-saml-wildfly-subsystem * @@ -156,7 +126,8 @@ org.keycloak - keycloak-saml-wildfly-subsystem + keycloak-saml-adapter-galleon-pack + zip * @@ -246,29 +217,6 @@ - - org.apache.maven.plugins - maven-resources-plugin - - - copy-resources - - validate - - copy-resources - - - ${project.build.directory}/modules/org/keycloak/keycloak-saml-adapter-subsystem - - - src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem - true - - - - - - org.keycloak keycloak-distribution-licenses-maven-plugin diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/licenses/rh-sso/licenses.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/licenses/rh-sso/licenses.xml deleted file mode 100644 index bac7cdfbcc59..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/licenses/rh-sso/licenses.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml deleted file mode 100755 index 6f50fa4f2303..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml deleted file mode 100755 index 695dc1a9ad2c..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml deleted file mode 100755 index 68cf8a57d401..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml deleted file mode 100644 index 526449f62b6d..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-crypto-default/main/module.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml deleted file mode 100755 index 40186d743f3d..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml deleted file mode 100755 index 1438ed986918..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml deleted file mode 100755 index d1db424f606a..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml deleted file mode 100755 index ae6407eb5c93..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml deleted file mode 100755 index 563763513dfd..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml deleted file mode 100755 index 64b079211b0b..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml deleted file mode 100755 index d4cefc52e380..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml deleted file mode 100755 index 093ffa8ee6ff..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml deleted file mode 100755 index aee496239db3..000000000000 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/bug-triage-prioritize.svg b/docs/bug-triage-prioritize.svg new file mode 100644 index 000000000000..5649051dbbf9 --- /dev/null +++ b/docs/bug-triage-prioritize.svg @@ -0,0 +1,639 @@ + + + + + + + + + + + + + priority-important + + No + + priority-regression + + priority-normal + + priority-low + + Regression? + + + No + + Yes + + + Some users blocked? + + Single user blocked? + + + Not blocking + + + Regression? + + + No + + Yes + + Documented? + + Many users blocked? + + + + + + Yes + + + No + + Yes + + + No + + No + + + No + + + + + Yes + + Prioritize + + + diff --git a/docs/bug-triage-verify.svg b/docs/bug-triage-verify.svg new file mode 100644 index 000000000000..243c03f43d9e --- /dev/null +++ b/docs/bug-triage-verify.svg @@ -0,0 +1,539 @@ + + + + + + + + + + + + + Triage + + question + + Update team/area + + Correct team/area? + + Ask for help? + + + No + + No + + + No + + Yes + + old-release + + Recent release? + + + No + + No + + missing-info + + + Sufficient info? + + No + + invalid + + + Valid bug? + + No + + + + + + + + Prioritize + + + diff --git a/docs/bug-triage.md b/docs/bug-triage.md new file mode 100644 index 000000000000..39b09c0fa126 --- /dev/null +++ b/docs/bug-triage.md @@ -0,0 +1,103 @@ +# Bug triage process + +## Actions + +When triaging a bug in most cases there is a single action required. Actions can be +executed on an GitHub Issue either by adding the label `action-` or +adding `~` as the last line of the comment. + +For example if an issue is missing information you could comment: + +``` +There is not enough information here, and I'm not following the steps-to-reproduce. + +~missing-info +``` + +You could also for example just add the label `action/priority-regression` to an issue. + +## Triaging bugs + +### Verify the issue + +The first step is to verify if the issue is a valid issue following these questions: + +![Bug triaging - Verify!](bug-triage-verify.svg "Bug Triage - Verify") + +If an issue is not valid add a comment with some explanation, and add `~` as the last line of the comment +to trigger corresponding action. + +In cases where it is clear that no additional comment is needed you can just add the `action/` label. For +example if the description only states `It doesn't work` then there's not much point in explaining what information is +missing. + +#### CVE reports on third-party libraries + +Known CVEs on third-party libraries will be automatically created as GitHub issues, labeled with `kind/cve`, `kind/bug`, and `status/triage`. The triager identifies the responsible team for the dependency and assigned the appropriate `team/...` label. This process is similar to the bug triage process previously mentioned. + +When evaluating the CVE report, assess the impact on the codebase by determining if we are vulnerable or affected. "Vulnerable" means that we use the code reported in the CVE, while "affected" means that we have the dependency with the CVE present but do not use the vulnerable code, making it impossible to exploit the CVE. If closing an issue as "not planned," include a proper explanation and the reason for closing it for future reference. + +### Prioritize the issue + +Second step is to prioritize the bug depending on how common the use-case is, if it's a regression, +or not blocking anything, for example a typo: + +![Bug triaging - Prioritize!](bug-triage-prioritize.svg "Bug Triage - Prioritize") + +When selecting the priority for an issue add the `action-` label to the issue. + + +## Missing information + +Bugs with insufficient information are assigned the labels `status/missing-info` and `status/auto-expire`, and the +`status/triage` label is removed. + +If the original reporter provides additional information the issue is automatically move back to triage by re-adding +the `status/triage` label. Otherwise, if the reporter does not provide additional information within 14 days the issue +is automatically closed. + +This effectively means that teams do not actively have to look at issues with missing information, since the issue +will be moved back to their triage backlog of more information is provided. + +To prevent an issue with missing information to be automatically closed, remove the `status/auto-expire` label. + + +## Low and normal priority issues + +In most cases these are not issues we will fix, and we will look for contributions (by adding the `help wanted` label) +to resolve them. As such the team does not actively monitor issues with these priorities. + +The bot will automatically bump the priority based on reactions added to the issue description: + +* Low priority bumped to normal if there are 10 or more reactions +* Normal priority bumped to important if there are 20 or more reactions + +To prevent an issue from being automatically bumped, remove the `status/auto-bump` label. + +If there are no updates to low or normal priority issues they will be automatically closed: + +* Low priority are closed after 90 days +* Normal priority are closed after 180 days + +To prevent an issue from being automatically closed, remove the `status/auto-expire` label. + + +## Changing priority + +To change the priority of an issue that has already been triaged simply add the new priority label. +The bot will take care of removing the existing priority label. It will also remove auto bumping and auto expiration +for the issue if the priority is set to important or blocker. + +The priority for an issue is also bumped to important if the `team/rh-iam` label is added. + + +## Backporting + +When triaging or fixing an issue consider if the fix should be backported. If it should be backported add the +corresponding `backport/` label. + +For convenience, use the `.github/scripts/pr-backport.sh` to help create the backport PRs. + +By adding a `backport/` label to the issue it is automatically added to the patch release project, and +additionally when merging the PR the `backport/` is automatically replaced with a `release/x.y.z` label. +Please do not add `release/x.y.z` manually! \ No newline at end of file diff --git a/docs/building.md b/docs/building.md index 2bb49bfa13d6..b40809c2d3b4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,10 +1,16 @@ ## Building from source -Ensure you have JDK 17 (or newer) and Git installed +Ensure you have **JDK 17** or **JDK 21** and Git installed java -version git --version +Newer versions of the JDK are not supported. If you have multiple JDK versions +installed, you can specify which one to use during the build by setting the `JAVA_HOME` +environment variable (this should be the directory containing `/bin/` or `/jre/`). + + JAVA_HOME=/path/to/jdk-21/ ./mvnw clean install + Instead of using a locally installed Maven, call the Maven wrapper script `mvnw` in the main folder of the project. This will use the Maven version which is supported by this project. @@ -47,6 +53,16 @@ To enable it by default, add it to the `MAVEN_OPTS` environment variable: export MAVEN_OPTS="-Dmaven.build.cache.enabled=true" +--- +**NOTE** + +To ensure that development in a branch does not break compatibility with existing releases, proto-schema-compatibility-maven-plugin checks may be run, which can cause builds to fail in proxy environments. +To avoid this, you can skip this check by adding the following property: + + -DskipProtoLock=true + +--- + ### Starting Keycloak To start Keycloak during development first build as specified above, then run: diff --git a/docs/cncf.md b/docs/cncf.md new file mode 100644 index 000000000000..2402bbe2e033 --- /dev/null +++ b/docs/cncf.md @@ -0,0 +1,35 @@ +# CNCF + +Keycloak is currently a CNCF incubating project. This document provides useful information and links around CNCF. + +## Useful links + +* [CLO Monitor](https://clomonitor.io/projects/cncf/keycloak) +* [CNCF Artwork](https://github.com/cncf/artwork/tree/master/projects/keycloak) +* [CNCF Landscape](https://landscape.cncf.io/?selected=keycloak) +* [CNCF Online Programs Guidelines](https://github.com/cncf/foundation/blob/main/online-programs-guidelines.md) +* [CNCF Service Desk](https://cncfservicedesk.atlassian.net/servicedesk/customer/portals) +* [DevStats](https://keycloak.devstats.cncf.io/) +* [Slack](https://cloud-native.slack.com/) (current channels [#keycloak](https://cloud-native.slack.com/archives/C056HC17KK9), [#keycloak-dev](https://cloud-native.slack.com/archives/C056XU905S6), and [#keycloak-maintainers](https://cloud-native.slack.com/archives/C056HBTMJVD)) +* [Services for CNCF Projects](https://www.cncf.io/services-for-projects/) +* [Schedule time with the CNCF Projects Team](http://project-meetings.cncf.io/) + +## Maintainers + +Maintainers should be listed in the following places: + +* [Keycloak Maintainers List](https://github.com/keycloak/keycloak/blob/main/MAINTAINERS.md) +* [CNCF Maintainers List](https://maintainers.cncf.io/) + +For new maintainers send email address of maintainer to cncf-maintainer-changes(at)cncf.io for access to maintainers +mailing list and service desk. + +Maintainers should be subscribed to the following communication channels: + +* cncf-keycloak-maintainers(at)lists.cncf.io +* keycloak-maintainers(at)googlegroups.com +* #keycloak-maintainers channel in [CNCF Slack](https://cloud-native.slack.com/) + +## Processes + +* [CNCF Project Proposal Process](https://github.com/cncf/toc/blob/main/process/project_proposals.md#introduction) diff --git a/docs/dependency-license-information.md b/docs/dependency-license-information.md index 404e96438700..ffd8d9dbe694 100644 --- a/docs/dependency-license-information.md +++ b/docs/dependency-license-information.md @@ -12,7 +12,7 @@ To manually determine a license, clone/checkout the source code at the tag or co ## How to store license info -Typically, each zip that gets distibuted to users needs to contain a license XML and individual license files, plus an html file generated at build time. +Typically, each zip that gets distributed to users needs to contain a license XML and individual license files, plus an html file generated at build time. The XML and individual files are maintained in git. When you change or add a dependency that is a part of: diff --git a/docs/documentation/License.html b/docs/documentation/License.html old mode 100755 new mode 100644 diff --git a/docs/documentation/README.md b/docs/documentation/README.md old mode 100755 new mode 100644 index 1b163653fe6f..34f98539617a --- a/docs/documentation/README.md +++ b/docs/documentation/README.md @@ -30,15 +30,15 @@ If you are using Windows, you need to run the following command with administrat To build Keycloak Documentation run: - mvn clean install -am -pl docs/documentation/dist -Pdocumentation + ./mvnw clean install -am -pl docs/documentation/dist -Pdocumentation Or to build a specific guide run: - mvn clean install -pl docs/documentation/GUIDE_DIR -Pdocumentation + ./mvnw clean install -pl docs/documentation/GUIDE_DIR -Pdocumentation By default, an archive version of the documentation is built. To build the latest build run: - mvn clean install ... -Platest,documentation + ./mvnw clean install ... -Platest,documentation You can then view the documentation by opening `docs/documentation/GUIDE_DIR/target/generated-docs/index.html`. diff --git a/docs/documentation/aggregation/pom.xml b/docs/documentation/aggregation/pom.xml index af92b7cddec8..778c61ef0ee9 100644 --- a/docs/documentation/aggregation/pom.xml +++ b/docs/documentation/aggregation/pom.xml @@ -26,12 +26,6 @@ ${project.version} pom - - org.keycloak.documentation - securing-apps - ${project.version} - pom - org.keycloak.documentation server-admin @@ -107,22 +101,6 @@ - - copy-securing_apps - process-resources - - copy-resources - - - ${project.build.outputDirectory}/securing_apps/ - - - ../securing_apps/target/generated-docs - **/** - - - - copy-server_admin process-resources diff --git a/docs/documentation/aggregation/src/index.html b/docs/documentation/aggregation/src/index.html index d5de0085c599..91ee2109e824 100644 --- a/docs/documentation/aggregation/src/index.html +++ b/docs/documentation/aggregation/src/index.html @@ -34,7 +34,6 @@
      -
    • Securing Apps
    • Server Admin
    • Server Development
    • Authorization Services
    • diff --git a/docs/documentation/api_documentation/topics/overview.adoc b/docs/documentation/api_documentation/topics/overview.adoc index a06996fbf7b3..76616bdfc9fc 100644 --- a/docs/documentation/api_documentation/topics/overview.adoc +++ b/docs/documentation/api_documentation/topics/overview.adoc @@ -1,6 +1,4 @@ -include::templates/making-open-source-more-inclusive.adoc[] - == {project_name} API Documentation === JavaDocs Documentation diff --git a/docs/documentation/authorization_services/images/getting-started/hello-world/authz-settings.png b/docs/documentation/authorization_services/images/getting-started/hello-world/authz-settings.png index 5214a2fbaa6a..e038eb654f88 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/hello-world/authz-settings.png and b/docs/documentation/authorization_services/images/getting-started/hello-world/authz-settings.png differ diff --git a/docs/documentation/authorization_services/images/getting-started/hello-world/create-client.png b/docs/documentation/authorization_services/images/getting-started/hello-world/create-client.png index e59680027c21..df6b80a70fdd 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/hello-world/create-client.png and b/docs/documentation/authorization_services/images/getting-started/hello-world/create-client.png differ diff --git a/docs/documentation/authorization_services/images/getting-started/hello-world/create-scope.png b/docs/documentation/authorization_services/images/getting-started/hello-world/create-scope.png index 55147e663fcb..4897878f94d9 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/hello-world/create-scope.png and b/docs/documentation/authorization_services/images/getting-started/hello-world/create-scope.png differ diff --git a/docs/documentation/authorization_services/images/getting-started/hello-world/create-user.png b/docs/documentation/authorization_services/images/getting-started/hello-world/create-user.png index 097e65703bbc..ee1e67bbdcc3 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/hello-world/create-user.png and b/docs/documentation/authorization_services/images/getting-started/hello-world/create-user.png differ diff --git a/docs/documentation/authorization_services/images/getting-started/hello-world/enable-authz.png b/docs/documentation/authorization_services/images/getting-started/hello-world/enable-authz.png index d7a6e4d39820..e1cce37dfe2c 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/hello-world/enable-authz.png and b/docs/documentation/authorization_services/images/getting-started/hello-world/enable-authz.png differ diff --git a/docs/documentation/authorization_services/images/getting-started/kc-start-page.png b/docs/documentation/authorization_services/images/getting-started/kc-start-page.png index e20eb9bdb95d..c4e53d2ec030 100644 Binary files a/docs/documentation/authorization_services/images/getting-started/kc-start-page.png and b/docs/documentation/authorization_services/images/getting-started/kc-start-page.png differ diff --git a/docs/documentation/authorization_services/images/keycloak_logo.png b/docs/documentation/authorization_services/images/keycloak_logo.png old mode 100755 new mode 100644 diff --git a/docs/documentation/authorization_services/images/permission/create-resource.png b/docs/documentation/authorization_services/images/permission/create-resource.png index e4b1c1d36c4b..487bd61b7c39 100644 Binary files a/docs/documentation/authorization_services/images/permission/create-resource.png and b/docs/documentation/authorization_services/images/permission/create-resource.png differ diff --git a/docs/documentation/authorization_services/images/permission/create-scope.png b/docs/documentation/authorization_services/images/permission/create-scope.png index 6ba0c886a36d..1db4839e0140 100644 Binary files a/docs/documentation/authorization_services/images/permission/create-scope.png and b/docs/documentation/authorization_services/images/permission/create-scope.png differ diff --git a/docs/documentation/authorization_services/images/permission/typed-resource-perm-example.png b/docs/documentation/authorization_services/images/permission/typed-resource-perm-example.png index 678bc4474789..1e04d4bf1590 100644 Binary files a/docs/documentation/authorization_services/images/permission/typed-resource-perm-example.png and b/docs/documentation/authorization_services/images/permission/typed-resource-perm-example.png differ diff --git a/docs/documentation/authorization_services/images/permission/view.png b/docs/documentation/authorization_services/images/permission/view.png index fa4a185c738f..5bdda9026318 100644 Binary files a/docs/documentation/authorization_services/images/permission/view.png and b/docs/documentation/authorization_services/images/permission/view.png differ diff --git a/docs/documentation/authorization_services/images/policy-evaluation-tool/policy-evaluation-tool.png b/docs/documentation/authorization_services/images/policy-evaluation-tool/policy-evaluation-tool.png index 079b0cb6b74c..b69bba96c39a 100644 Binary files a/docs/documentation/authorization_services/images/policy-evaluation-tool/policy-evaluation-tool.png and b/docs/documentation/authorization_services/images/policy-evaluation-tool/policy-evaluation-tool.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-aggregated.png b/docs/documentation/authorization_services/images/policy/create-aggregated.png index b7fe7d050f79..548b5cb65c2a 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-aggregated.png and b/docs/documentation/authorization_services/images/policy/create-aggregated.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-client-scope.png b/docs/documentation/authorization_services/images/policy/create-client-scope.png index 2412a3e84529..2241699bedcb 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-client-scope.png and b/docs/documentation/authorization_services/images/policy/create-client-scope.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-client.png b/docs/documentation/authorization_services/images/policy/create-client.png index f16e8c2ca995..27430f80b1e2 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-client.png and b/docs/documentation/authorization_services/images/policy/create-client.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-group-extend-children.png b/docs/documentation/authorization_services/images/policy/create-group-extend-children.png index 36c1cee1101c..eedd6fee8cfa 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-group-extend-children.png and b/docs/documentation/authorization_services/images/policy/create-group-extend-children.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-group.png b/docs/documentation/authorization_services/images/policy/create-group.png index 09691f3837ae..461800e428e1 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-group.png and b/docs/documentation/authorization_services/images/policy/create-group.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-regex.png b/docs/documentation/authorization_services/images/policy/create-regex.png index e9bb82677029..569a0110d0f3 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-regex.png and b/docs/documentation/authorization_services/images/policy/create-regex.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-role.png b/docs/documentation/authorization_services/images/policy/create-role.png index 0aece01caefc..a0aa2ef1c633 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-role.png and b/docs/documentation/authorization_services/images/policy/create-role.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-time.png b/docs/documentation/authorization_services/images/policy/create-time.png index 3cc3a62794e7..ee8b41338014 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-time.png and b/docs/documentation/authorization_services/images/policy/create-time.png differ diff --git a/docs/documentation/authorization_services/images/policy/create-user.png b/docs/documentation/authorization_services/images/policy/create-user.png index 5ece77b8e374..755d8c35bea6 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-user.png and b/docs/documentation/authorization_services/images/policy/create-user.png differ diff --git a/docs/documentation/authorization_services/images/policy/view.png b/docs/documentation/authorization_services/images/policy/view.png index 361ff3a25aa7..c39e64264596 100644 Binary files a/docs/documentation/authorization_services/images/policy/view.png and b/docs/documentation/authorization_services/images/policy/view.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/authz-export.png b/docs/documentation/authorization_services/images/resource-server/authz-export.png index 2ebd4acab182..59876dfdd699 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/authz-export.png and b/docs/documentation/authorization_services/images/resource-server/authz-export.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/authz-settings.png b/docs/documentation/authorization_services/images/resource-server/authz-settings.png index 695f2b7eeddd..e038eb654f88 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/authz-settings.png and b/docs/documentation/authorization_services/images/resource-server/authz-settings.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/client-create.png b/docs/documentation/authorization_services/images/resource-server/client-create.png index 4344ac163eda..4c7a315adf1d 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/client-create.png and b/docs/documentation/authorization_services/images/resource-server/client-create.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/client-enable-authz.png b/docs/documentation/authorization_services/images/resource-server/client-enable-authz.png index fa43196dfef0..1dbc9a35ac87 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/client-enable-authz.png and b/docs/documentation/authorization_services/images/resource-server/client-enable-authz.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/client-list.png b/docs/documentation/authorization_services/images/resource-server/client-list.png index d92fa79169d4..20613357f366 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/client-list.png and b/docs/documentation/authorization_services/images/resource-server/client-list.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/client-settings.png b/docs/documentation/authorization_services/images/resource-server/client-settings.png new file mode 100644 index 000000000000..3fd6531b573c Binary files /dev/null and b/docs/documentation/authorization_services/images/resource-server/client-settings.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/default-permission.png b/docs/documentation/authorization_services/images/resource-server/default-permission.png index f9632a742291..f3424d48cdf7 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/default-permission.png and b/docs/documentation/authorization_services/images/resource-server/default-permission.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/default-policy.png b/docs/documentation/authorization_services/images/resource-server/default-policy.png index 40f4ed431e4a..ecec90194d71 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/default-policy.png and b/docs/documentation/authorization_services/images/resource-server/default-policy.png differ diff --git a/docs/documentation/authorization_services/images/resource-server/default-resource.png b/docs/documentation/authorization_services/images/resource-server/default-resource.png index e9a6a9779ffb..59b789231166 100644 Binary files a/docs/documentation/authorization_services/images/resource-server/default-resource.png and b/docs/documentation/authorization_services/images/resource-server/default-resource.png differ diff --git a/docs/documentation/authorization_services/images/resource/create.png b/docs/documentation/authorization_services/images/resource/create.png index 7280ed78f2e2..366da990d62c 100644 Binary files a/docs/documentation/authorization_services/images/resource/create.png and b/docs/documentation/authorization_services/images/resource/create.png differ diff --git a/docs/documentation/authorization_services/images/resource/view.png b/docs/documentation/authorization_services/images/resource/view.png index eeee62d9199f..02b8a5f43ff2 100644 Binary files a/docs/documentation/authorization_services/images/resource/view.png and b/docs/documentation/authorization_services/images/resource/view.png differ diff --git a/docs/documentation/authorization_services/images/service/rs-uma-protection-role.png b/docs/documentation/authorization_services/images/service/rs-uma-protection-role.png index ef460cd1c8ab..89ef67eccc43 100644 Binary files a/docs/documentation/authorization_services/images/service/rs-uma-protection-role.png and b/docs/documentation/authorization_services/images/service/rs-uma-protection-role.png differ diff --git a/docs/documentation/authorization_services/topics.adoc b/docs/documentation/authorization_services/topics.adoc index a57dd845f85d..1c6806087bee 100644 --- a/docs/documentation/authorization_services/topics.adoc +++ b/docs/documentation/authorization_services/topics.adoc @@ -96,16 +96,6 @@ include::topics/service-rpt-overview.adoc[leveloffset=+2] include::topics/service-rpt-token-introspection.adoc[leveloffset=+3] -include::topics/service-client-api.adoc[leveloffset=+2] - include::topics/enforcer-overview.adoc[leveloffset=+1] -include::topics/enforcer-configuration.adoc[leveloffset=+2] - -include::topics/enforcer-claim-information-point.adoc[leveloffset=+2] - -include::topics/enforcer-authorization-context.adoc[leveloffset=+2] - include::topics/enforcer-js-adapter.adoc[leveloffset=+2] - -include::topics/enforcer-https.adoc[leveloffset=+2] diff --git a/docs/documentation/authorization_services/topics/auth-services-architecture.adoc b/docs/documentation/authorization_services/topics/auth-services-architecture.adoc index 09321d6eaace..efc6f76f3a8d 100644 --- a/docs/documentation/authorization_services/topics/auth-services-architecture.adoc +++ b/docs/documentation/authorization_services/topics/auth-services-architecture.adoc @@ -25,7 +25,7 @@ Provides implementations for different environments to actually enforce authoriz * **Policy Information Point (PIP)** + -Being based on {project_name} Authentication Server, you can obtain attributes from identities and runtime environment during the evaluation of authorization policies. +Being based on {project_name} Authentication Server, you can obtain attributes from identities and a runtime environment during the evaluation of authorization policies. == The authorization process @@ -74,7 +74,7 @@ what you want to protect (resource or scope) and the policies that must be satis === Policy enforcement -*Policy Enforcement* involves the necessary steps to actually enforce authorization decisions to a resource server. This is achieved by enabling a *Policy Enforcement Point* or PEP at the resource server that is capable of communicating with the authorization server, ask for authorization data and control access to protected resources based on the decisions and permissions returned by the server. +*Policy Enforcement* involves the necessary steps to actually enforce authorization decisions to a resource server. This is achieved by enabling a *Policy Enforcement Point* or PEP at the resource server that is capable of communicating with the authorization server, asking for authorization data and controlling access to protected resources based on the decisions and permissions returned by the server. image:images/pep-pattern-diagram.png[alt="PEP overview"] diff --git a/docs/documentation/authorization_services/topics/auth-services-terminology.adoc b/docs/documentation/authorization_services/topics/auth-services-terminology.adoc index d8b3efd84c9e..2de23a0d2bc0 100644 --- a/docs/documentation/authorization_services/topics/auth-services-terminology.adoc +++ b/docs/documentation/authorization_services/topics/auth-services-terminology.adoc @@ -25,7 +25,7 @@ Every resource has a unique identifier that can represent a single resource or a A resource's scope is a bounded extent of access that is possible to perform on a resource. In authorization policy terminology, a scope is one of the potentially many _verbs_ that can logically apply to a resource. -It usually indicates what can be done with a given resource. Example of scopes are view, edit, delete, and so on. However, scope can also be related to specific information provided by a resource. In this case, you can have a project resource and a cost scope, where the cost scope is used to define specific policies and permissions for users to access a project's cost. +It usually indicates what can be done with a given resource. Examples of scopes are view, edit, delete, and so on. However, scope can also be related to specific information provided by a resource. In this case, you can have a project resource and a cost scope, where the cost scope is used to define specific policies and permissions for users to access a project's cost. == Permission diff --git a/docs/documentation/authorization_services/topics/enforcer-authorization-context.adoc b/docs/documentation/authorization_services/topics/enforcer-authorization-context.adoc deleted file mode 100644 index 044139549991..000000000000 --- a/docs/documentation/authorization_services/topics/enforcer-authorization-context.adoc +++ /dev/null @@ -1,69 +0,0 @@ -[[_enforcer_authorization_context]] -= Obtaining the authorization context - -When policy enforcement is enabled, the permissions obtained from the server are available through `org.keycloak.AuthorizationContext`. -This class provides several methods you can use to obtain permissions and ascertain whether a permission was granted for a particular resource or scope. - -Obtaining the Authorization Context in a Servlet Container - -[source,java] ----- - HttpServletRequest request = // obtain javax.servlet.http.HttpServletRequest - AuthorizationContext authzContext = (AuthorizationContext) request.getAttribute(AuthorizationContext.class.getName()); ----- - -[NOTE] -The authorization context helps give you more control over the decisions made and returned by the server. For example, you can use it -to build a dynamic menu where items are hidden or shown depending on the permissions associated with a resource or scope. - -[source,java] ----- -if (authzContext.hasResourcePermission("Project Resource")) { - // user can access the Project Resource -} - -if (authzContext.hasResourcePermission("Admin Resource")) { - // user can access administration resources -} - -if (authzContext.hasScopePermission("urn:project.com:project:create")) { - // user can create new projects -} ----- - -The `AuthorizationContext` represents one of the main capabilities of {project_name} Authorization Services. From the examples above, you can see that the protected resource is not directly associated with the policies that govern them. - -Consider some similar code using role-based access control (RBAC): - -[source,java] ----- -if (User.hasRole('user')) { - // user can access the Project Resource -} - -if (User.hasRole('admin')) { - // user can access administration resources -} - -if (User.hasRole('project-manager')) { - // user can create new projects -} ----- - -Although both examples address the same requirements, they do so in different ways. In RBAC, roles only _implicitly_ define access for their resources. With {project_name}, you gain the capability to create more manageable code that focuses directly on your resources whether you are using RBAC, attribute-based access control (ABAC), or any other BAC variant. Either you have the permission for a given resource or scope, or you do not have that permission. - -Now, suppose your security requirements have changed and in addition to project managers, PMOs can also create new projects. - -Security requirements change, but with {project_name} there is no need to change your application code to address the new requirements. Once your application is based on the resource and scope identifier, you need only change the configuration of the permissions or policies associated with a particular resource in the authorization server. In this case, the permissions and policies associated with the `Project Resource` and/or the scope `urn:project.com:project:create` would be changed. - -= Using the AuthorizationContext to obtain an Authorization Client Instance - -The ```AuthorizationContext``` can also be used to obtain a reference to the <<_service_client_api, Authorization Client API>> configured to your application: - -[source,java] ----- - ClientAuthorizationContext clientContext = ClientAuthorizationContext.class.cast(authzContext); - AuthzClient authzClient = clientContext.getClient(); ----- - -In some cases, resource servers protected by the policy enforcer need to access the APIs provided by the authorization server. With an ```AuthzClient``` instance in hands, resource servers can interact with the server in order to create resources or check for specific permissions programmatically. diff --git a/docs/documentation/authorization_services/topics/enforcer-claim-information-point.adoc b/docs/documentation/authorization_services/topics/enforcer-claim-information-point.adoc deleted file mode 100644 index cd470b048bff..000000000000 --- a/docs/documentation/authorization_services/topics/enforcer-claim-information-point.adoc +++ /dev/null @@ -1,179 +0,0 @@ -[[_enforcer_claim_information_point]] -= Claim Information Point - -A Claim Information Point (CIP) is responsible for resolving claims and pushing these claims to the {project_name} server -in order to provide more information about the access context to policies. They can be defined as a configuration option -to the policy-enforcer in order to resolve claims from different sources, such as: - -* HTTP Request (parameters, headers, body, etc) -* External HTTP Service -* Static values defined in configuration -* Any other source by implementing the Claim Information Provider SPI - -When pushing claims to the {project_name} server, policies can base decisions not only on who a user is but also by taking -context and contents into account, based on who, what, why, when, where, and which for a given transaction. It is all about -Contextual-based Authorization and how to use runtime information in order to support fine-grained authorization decisions. - -== Obtaining information from the HTTP request - -Here are several examples showing how you can extract claims from an HTTP request: - -.keycloak.json -[source,json] ----- -{ - "paths": [ - { - "path": "/protected/resource", - "claim-information-point": { - "claims": { - "claim-from-request-parameter": "{request.parameter['a']}", - "claim-from-header": "{request.header['b']}", - "claim-from-cookie": "{request.cookie['c']}", - "claim-from-remoteAddr": "{request.remoteAddr}", - "claim-from-method": "{request.method}", - "claim-from-uri": "{request.uri}", - "claim-from-relativePath": "{request.relativePath}", - "claim-from-secure": "{request.secure}", - "claim-from-json-body-object": "{request.body['/a/b/c']}", - "claim-from-json-body-array": "{request.body['/d/1']}", - "claim-from-body": "{request.body}", - "claim-from-static-value": "static value", - "claim-from-multiple-static-value": ["static", "value"], - "param-replace-multiple-placeholder": "Test {keycloak.access_token['/custom_claim/0']} and {request.parameter['a']}" - } - } - } - ] -} ----- - -== Obtaining information from an external HTTP service - -Here are several examples showing how you can extract claims from an external HTTP Service: - -.keycloak.json -[source,json] ----- -{ - "paths": [ - { - "path": "/protected/resource", - "claim-information-point": { - "http": { - "claims": { - "claim-a": "/a", - "claim-d": "/d", - "claim-d0": "/d/0", - "claim-d-all": [ - "/d/0", - "/d/1" - ] - }, - "url": "http://mycompany/claim-provider", - "method": "POST", - "headers": { - "Content-Type": "application/x-www-form-urlencoded", - "header-b": [ - "header-b-value1", - "header-b-value2" - ], - "Authorization": "Bearer {keycloak.access_token}" - }, - "parameters": { - "param-a": [ - "param-a-value1", - "param-a-value2" - ], - "param-subject": "{keycloak.access_token['/sub']}", - "param-user-name": "{keycloak.access_token['/preferred_username']}", - "param-other-claims": "{keycloak.access_token['/custom_claim']}" - } - } - } - } - ] -} ----- - -== Static claims - -.keycloak.json -[source,json] ----- -{ - "paths": [ - { - "path": "/protected/resource", - "claim-information-point": { - "claims": { - "claim-from-static-value": "static value", - "claim-from-multiple-static-value": ["static", "value"] - } - } - } - ] -} ----- - -== Claim information provider SPI - -The Claim Information Provider SPI can be used by developers to support different claim information points in case none of the -built-ins providers are enough to address their requirements. - -For example, to implement a new CIP provider you need to implement `org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory` -and `ClaimInformationPointProvider` and also provide the file `META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory` -in your application`s classpath. - -Example of `org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory`: - -[source,java] ----- -public class MyClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory { - - @Override - public String getName() { - return "my-claims"; - } - - @Override - public void init(PolicyEnforcer policyEnforcer) { - - } - - @Override - public MyClaimInformationPointProvider create(Map config) { - return new MyClaimInformationPointProvider(config); - } -} ----- - -Every CIP provider must be associated with a name, as defined above in the `MyClaimInformationPointProviderFactory.getName` method. The name -will be used to map the configuration from the `claim-information-point` section in the `policy-enforcer` configuration to the implementation. - -When processing requests, the policy enforcer will call the MyClaimInformationPointProviderFactory.create method in order to obtain an -instance of MyClaimInformationPointProvider. When called, any configuration defined for this particular CIP provider -(via claim-information-point) is passed as a map. - -Example of `ClaimInformationPointProvider`: - -[source,java] ----- -public class MyClaimInformationPointProvider implements ClaimInformationPointProvider { - - private final Map config; - - public MyClaimInformationPointProvider(Map config) { - this.config = config; - } - - @Override - public Map> resolve(HttpFacade httpFacade) { - Map> claims = new HashMap<>(); - - // put whatever claim you want into the map - - return claims; - } -} ----- diff --git a/docs/documentation/authorization_services/topics/enforcer-configuration.adoc b/docs/documentation/authorization_services/topics/enforcer-configuration.adoc deleted file mode 100644 index 00492c0ad9cd..000000000000 --- a/docs/documentation/authorization_services/topics/enforcer-configuration.adoc +++ /dev/null @@ -1,132 +0,0 @@ -[[_enforcer_configuration]] -= Configuration - -The policy enforcer configuration uses a JSON format and most of the time you don't need to set anything if you want to -automatically resolve the protected paths based on the resources available from your resource server. - -If you want to manually define the resources being protected, you can use a slightly more verbose format: - -[source,json] ----- -{ - "enforcement-mode" : "ENFORCING", - "paths": [ - { - "path" : "/users/*", - "methods" : [ - { - "method": "GET", - "scopes" : ["urn:app.com:scopes:view"] - }, - { - "method": "POST", - "scopes" : ["urn:app.com:scopes:create"] - } - ] - } - ] -} ----- - -The following is a description of each configuration option: - -* *enforcement-mode* -+ -Specifies how policies are enforced. -+ -** *ENFORCING* -+ -(default mode) Requests are denied by default even when no policy is associated with a given resource. -+ -** *PERMISSIVE* -+ -Requests are allowed even when no policy is associated with a given resource. -+ -** *DISABLED* -+ -Completely disables the evaluation of policies and allows access to any resource. When `enforcement-mode` is `DISABLED`, -applications are still able to obtain all permissions granted by {project_name} through the <<_enforcer_authorization_context, Authorization Context>> -+ -* *on-deny-redirect-to* -+ -Defines a URL where a client request is redirected when an "access denied" message is obtained from the server. By default, the adapter responds with a 403 HTTP status code. -+ -* *path-cache* -+ -Defines how the policy enforcer should track associations between paths in your application and resources defined in {project_name}. The cache is needed to avoid -unnecessary requests to a {project_name} server by caching associations between paths and protected resources. -+ -** *lifespan* -+ -Defines the time in milliseconds when the entry should be expired. If not provided, default value is *30000*. A value equal to 0 can be set to completely disable the cache. A value equal to -1 can be set to disable the expiry of the cache. -+ -** *max-entries* -+ -Defines the limit of entries that should be kept in the cache. If not provided, default value is *1000*. -+ -* *paths* -+ -Specifies the paths to protect. This configuration is optional. If not defined, the policy enforcer discovers all paths by fetching the resources you defined to your application in {project_name}, where these resources are defined with `URIS` representing some paths in your application. -+ -** *name* -+ -The name of a resource on the server that is to be associated with a given path. When used in conjunction with a *path*, the policy enforcer ignores the resource's *URIS* property and uses the path you provided instead. -** *path* -+ -(required) A URI relative to the application's context path. If this option is specified, the policy enforcer queries the server for a resource with a *URI* with the same value. -Currently a very basic logic for path matching is supported. Examples of valid paths are: -+ -*** Wildcards: `/*` -*** Suffix: `/*.html` -*** Sub-paths: `/path/*` -*** Path parameters: /resource/{id} -*** Exact match: /resource -*** Patterns: /{version}/resource, /api/{version}/resource, /api/{version}/resource/* -+ -** *methods* -+ -The HTTP methods (for example, GET, POST, PATCH) to protect and how they are associated with the scopes for a given resource in the server. -+ -*** *method* -+ -The name of the HTTP method. -+ -*** *scopes* -+ -An array of strings with the scopes associated with the method. When you associate scopes with a specific method, the client trying to access a protected resource (or path) must provide an RPT that grants permission to all scopes specified in the list. For example, if you define a method _POST_ with a scope _create_, the RPT must contain a permission granting access to the _create_ scope when performing a POST to the path. -+ -*** *scopes-enforcement-mode* -+ -A string referencing the enforcement mode for the scopes associated with a method. Values can be *ALL* or *ANY*. If *ALL*, -all defined scopes must be granted in order to access the resource using that method. If *ANY*, at least one scope should be -granted in order to gain access to the resource using that method. By default, enforcement mode is set to *ALL*. -+ -** *enforcement-mode* -+ -Specifies how policies are enforced. -+ -*** *ENFORCING* -+ -(default mode) Requests are denied by default even when there is no policy associated with a given resource. -+ -*** *DISABLED* -+ -** *claim-information-point* -+ -Defines a set of one or more claims that must be resolved and pushed to the {project_name} server in order to make these claims available to policies. See <<_enforcer_claim_information_point, Claim Information Point>> for more details. -+ -* *lazy-load-paths* -+ -Specifies how the adapter should fetch the server for resources associated with paths in your application. If *true*, the policy -enforcer is going to fetch resources on-demand accordingly with the path being requested. This configuration is specially useful -when you do not want to fetch all resources from the server during deployment (in case you have provided no `paths`) or in case -you have defined only a sub set of `paths` and want to fetch others on-demand. -+ -* *http-method-as-scope* -+ -Specifies how scopes should be mapped to HTTP methods. If set to *true*, the policy enforcer will use the HTTP method from the current request to -check whether or not access should be granted. When enabled, make sure your resources in {project_name} are associated with scopes representing each HTTP method you are protecting. -+ -* *claim-information-point* -+ -Defines a set of one or more *global* claims that must be resolved and pushed to the {project_name} server in order to make these claims available to policies. See <<_enforcer_claim_information_point, Claim Information Point>> for more details. diff --git a/docs/documentation/authorization_services/topics/enforcer-https.adoc b/docs/documentation/authorization_services/topics/enforcer-https.adoc deleted file mode 100644 index 3356a653ef55..000000000000 --- a/docs/documentation/authorization_services/topics/enforcer-https.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[_enforcer_filter_using_https]] -= Configuring TLS/HTTPS - -When the server is using HTTPS, ensure your policy enforcer is configured as follows: - -[source,json] ----- -{ - "truststore": "path_to_your_trust_store", - "truststore-password": "trust_store_password" -} ----- - -The configuration above enables TLS/HTTPS to the Authorization Client, making possible to access a -{project_name} Server remotely using the HTTPS scheme. - -[NOTE] -It is strongly recommended that you enable TLS/HTTPS when accessing the {project_name} Server endpoints. diff --git a/docs/documentation/authorization_services/topics/enforcer-js-adapter.adoc b/docs/documentation/authorization_services/topics/enforcer-js-adapter.adoc index 882a289cb2f1..c131b417e72d 100644 --- a/docs/documentation/authorization_services/topics/enforcer-js-adapter.adoc +++ b/docs/documentation/authorization_services/topics/enforcer-js-adapter.adoc @@ -1,23 +1,37 @@ [[_enforcer_js_adapter]] -= JavaScript integration += JavaScript integration for Policy Enforcer The {project_name} Server comes with a JavaScript library you can use to interact with a resource server protected by a policy enforcer. This library is based on the {project_name} JavaScript adapter, which can be integrated to allow your client to obtain permissions from a {project_name} Server. -You can obtain this library from a running a {project_name} Server instance by including the following `script` tag in your web page: +You can obtain this library by installing it https://www.npmjs.com/package/keycloak-js[from NPM]: -[source,html,subs="attributes+"] +[source,bash] ---- - +npm install keycloak-js ---- + Next, you can create a `KeycloakAuthorization` instance as follows: [source,javascript] ---- -const keycloak = ... // obtain a Keycloak instance from keycloak.js library +import Keycloak from "keycloak-js"; +import KeycloakAuthorization from "keycloak-js/authz"; + +const keycloak = new Keycloak({ + url: "http://keycloak-server", + realm: "my-realm", + clientId: "my-app" +}); + const authorization = new KeycloakAuthorization(keycloak); + +await keycloak.init(); + +// Now you can use the authorization object to interact with the server. ---- -The *keycloak-authz.js* library provides two main features: + +The `keycloak-js/authz` library provides two main features: * Obtain permissions from the server using a permission ticket, if you are accessing a UMA protected resource server. @@ -35,8 +49,8 @@ responds with a *401* status code and a `WWW-Authenticate` header. [source,bash,subs="attributes+"] ---- HTTP/1.1 401 Unauthorized -WWW-Authenticate: UMA realm="${realm}", - as_uri="https://${host}:${port}{kc_realms_path}/${realm}", +WWW-Authenticate: UMA realm="${realm-name}", + as_uri="https://${host}:${port}{kc_realms_path}/${realm-name}", ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de" ---- @@ -48,22 +62,21 @@ and use the library to send an authorization request as follows: [source,javascript] ---- // prepare a authorization request with the permission ticket -const authorizationRequest = {}; -authorizationRequest.ticket = ticket; +const authorizationRequest = { ticket }; // send the authorization request, if successful retry the request -Identity.authorization.authorize(authorizationRequest).then(function (rpt) { +authorization.authorize(authorizationRequest).then((rpt) => { // onGrant -}, function () { +}, () => { // onDeny -}, function () { +}, () => { // onError }); ---- The `authorize` function is completely asynchronous and supports a few callback functions to receive notifications from the server: -* `onGrant`: The first argument of the function. If authorization was successful and the server returned an RPT with the requested permissions, the callback receives the RPT. +* `onGrant`: The first argument of the function. If authorization succeeds and the server returns an RPT with the requested permissions, the callback receives the RPT. * `onDeny`: The second argument of the function. Only called if the server has denied the authorization request. * `onError`: The third argument of the function. Only called if the server responds unexpectedly. @@ -71,13 +84,13 @@ Most applications should use the `onGrant` callback to retry a request after a 4 == Obtaining entitlements -The ```keycloak-authz.js``` library provides an `entitlement` function that you can use to obtain an RPT from the server by providing +The `keycloak-js/authz` library provides an `entitlement` function that you can use to obtain an RPT from the server by providing the resources and scopes your client wants to access. .Example about how to obtain an RPT with permissions for all resources and scopes the user can access [source,javascript] ---- -authorization.entitlement('my-resource-server-id').then(function (rpt) { +authorization.entitlement("my-resource-server-id").then((rpt) => { // onGrant callback function. // If authorization was successful you'll receive an RPT // with the necessary permissions to access the resource server @@ -87,13 +100,13 @@ authorization.entitlement('my-resource-server-id').then(function (rpt) { .Example about how to obtain an RPT with permissions for specific resources and scopes [source,javascript] ---- -authorization.entitlement('my-resource-server', { - "permissions": [ +authorization.entitlement("my-resource-server", { + permissions: [ { - "id" : "Some Resource" + id: "Some Resource" } ] -}).then(function (rpt) { +}).then((rpt) => { // onGrant }); ---- @@ -102,13 +115,13 @@ When using the `entitlement` function, you must provide the _client_id_ of the r The `entitlement` function is completely asynchronous and supports a few callback functions to receive notifications from the server: -* `onGrant`: The first argument of the function. If authorization was successful and the server returned an RPT with the requested permissions, the callback receives the RPT. +* `onGrant`: The first argument of the function. If authorization succeeds and the server returns an RPT with the requested permissions, the callback receives the RPT. * `onDeny`: The second argument of the function. Only called if the server has denied the authorization request. * `onError`: The third argument of the function. Only called if the server responds unexpectedly. == Authorization request -Both ```authorize``` and ```entitlement``` functions accept an authorization request object. This object can be set with the following +Both `authorize` and `entitlement` functions accept an authorization request object. This object can be set with the following properties: * *permissions* @@ -118,10 +131,10 @@ An array of objects representing the resource and scopes. For instance: [source,javascript] ---- const authorizationRequest = { - "permissions": [ + permissions: [ { - "id" : "Some Resource", - "scopes" : ["view", "edit"] + id: "Some Resource", + scopes: ["view", "edit"] } ] } diff --git a/docs/documentation/authorization_services/topics/enforcer-overview.adoc b/docs/documentation/authorization_services/topics/enforcer-overview.adoc index a3855866454a..8b912d1dc24f 100644 --- a/docs/documentation/authorization_services/topics/enforcer-overview.adoc +++ b/docs/documentation/authorization_services/topics/enforcer-overview.adoc @@ -1,29 +1,11 @@ [[_enforcer_overview]] = Policy enforcers -Policy Enforcement Point (PEP) is a design pattern and as such you can implement it in different ways. {project_name} provides all the necessary means -to implement PEPs for different platforms, environments, and programming languages. {project_name} Authorization Services presents a RESTful API, -and leverages OAuth2 authorization capabilities for fine-grained authorization using a centralized authorization server. +Policy Enforcement Point (PEP) is a design pattern and as such you can implement it in different ways. {project_name} provides all the necessary means to implement PEPs for different +platforms, environments, and programming languages. {project_name} Authorization Services presents a RESTful API and leverages OAuth2 authorization capabilities for fine-grained +authorization using a centralized authorization server. -image:images/pep-pattern-diagram.png[alt="PEP overview"] +The Policy enforcers available by {project_name} are: -A PEP is responsible for enforcing access decisions from the {project_name} server where these decisions are taken by evaluating the policies -associated with a protected resource. It acts as a filter or interceptor in your application in order to check whether or not a particular request -to a protected resource can be fulfilled based on the permissions granted by these decisions. - -{project_name} provides built-in support for enabling the *{project_name} Policy Enforcer* to Java applications with built-in support to secure JakartaEE-compliant frameworks and web containers. -If you are using Maven, you should configure the following dependency to your project: - -```xml - - org.keycloak - keycloak-policy-enforcer - ${keycloak.version} - -``` - -When you enable the policy enforcer all requests sent to your application are intercepted and access to protected resources will be granted -depending on the permissions granted by {project_name} to the identity making the request. - -Policy enforcement is strongly linked to your application's paths and the <<_resource_overview, resources>> you created for a resource server using the {project_name} Administration Console. By default, -when you create a resource server, {project_name} creates a <<_resource_server_default_config, default configuration>> for your resource server so you can enable policy enforcement quickly. +* {securing_apps_java_policy_enforcer_link}[{securing_apps_java_policy_enforcer_name}] - useful to be used in the Java client applications +* <<_enforcer_js_adapter, Javascript Policy enforcer>> - useful to be used in the applications secured by {project_name} Javascript adapter diff --git a/docs/documentation/authorization_services/topics/getting-started-overview.adoc b/docs/documentation/authorization_services/topics/getting-started-overview.adoc index d844fa2cdf85..53b831335af4 100644 --- a/docs/documentation/authorization_services/topics/getting-started-overview.adoc +++ b/docs/documentation/authorization_services/topics/getting-started-overview.adoc @@ -4,7 +4,7 @@ For certain applications, you can look at the following resources to quickly get started with {project_name} Authorization Services: -* {quickstartRepo_link}/tree/latest/jakarta/servlet-authz-client[Securing a JakartaEE Application in Wildfly] -* {quickstartRepo_link}/tree/latest/spring/rest-authz-resource-server[Securing a Spring Boot Application] +* {quickstartRepo_link}/tree/main/jakarta/servlet-authz-client[Securing a JakartaEE Application in Wildfly] +* {quickstartRepo_link}/tree/main/spring/rest-authz-resource-server[Securing a Spring Boot Application] * link:https://quarkus.io/guides/security-keycloak-authorization[Securing Quarkus Applications] -* {adapterguide_link_nodejs_adapter}[Securing Node.js Applications] \ No newline at end of file +* link:https://www.keycloak.org/securing-apps/nodejs-adapter[Keycloak Node.js adapter] diff --git a/docs/documentation/authorization_services/topics/permission-typed-resource-permission.adoc b/docs/documentation/authorization_services/topics/permission-typed-resource-permission.adoc index da0cd9fa0eaf..6b0651b872bc 100644 --- a/docs/documentation/authorization_services/topics/permission-typed-resource-permission.adoc +++ b/docs/documentation/authorization_services/topics/permission-typed-resource-permission.adoc @@ -10,7 +10,7 @@ Frequently, resources within an application can be categorized (or typed) based * Enforce a specific authentication method To create a typed resource permission, click <<_permission_create_resource_apply_resource_type, Apply to Resource Type>> when creating a new resource-based permission. With `Apply to Resource Type` set to `On`, -you can specify the type that you want to protect as well as the policies that are to be applied to govern access to all resources with type you have specified. +you can specify the type that you want to protect as well as the policies that are to be applied to govern access to all resources with the type you have specified. .Example of a typed resource permission image:images/permission/typed-resource-perm-example.png[alt="Example of a typed resource permission"] diff --git a/docs/documentation/authorization_services/topics/policy-aggregated-policy.adoc b/docs/documentation/authorization_services/topics/policy-aggregated-policy.adoc index 7c5f76814aab..06444be241c9 100644 --- a/docs/documentation/authorization_services/topics/policy-aggregated-policy.adoc +++ b/docs/documentation/authorization_services/topics/policy-aggregated-policy.adoc @@ -14,7 +14,7 @@ endif::[] image:images/policy/create-aggregated.png[alt="Add aggregated policy"] Let's suppose you have a resource called _Confidential Resource_ that can be accessed only by users from the _keycloak.org_ domain and from a certain range of IP addresses. -You can create a single policy with both conditions. However, you want to reuse the domain part of this policy to apply to permissions that operates regardless of the originating network. +You can create a single policy with both conditions. However, you want to reuse the domain part of this policy to apply to permissions that operate regardless of the originating network. You can create separate policies for both domain and network conditions and create a third policy based on the combination of these two policies. With an aggregated policy, you can freely combine other policies and then apply the new aggregated policy to any permission you want. diff --git a/docs/documentation/authorization_services/topics/policy-evaluation-api.adoc b/docs/documentation/authorization_services/topics/policy-evaluation-api.adoc index 5fbacbe7b9ec..cab4c47b99db 100644 --- a/docs/documentation/authorization_services/topics/policy-evaluation-api.adoc +++ b/docs/documentation/authorization_services/topics/policy-evaluation-api.adoc @@ -88,7 +88,7 @@ The `Identity` is built based on the OAuth2 Access Token that was sent along wit extracted from the original token. For example, if you are using a _Protocol Mapper_ to include a custom claim in an OAuth2 Access Token you can also access this claim from a policy and use it to build your conditions. -The `EvaluationContext` also gives you access to attributes related to both the execution and runtime environments. For now, there only a few built-in attributes. +The `EvaluationContext` also gives you access to attributes related to both the execution and runtime environments. For now, these are only a few built-in attributes. .Execution and Runtime Attributes |=== @@ -99,11 +99,11 @@ The `EvaluationContext` also gives you access to attributes related to both the | String. Format `MM/dd/yyyy hh:mm:ss` | kc.client.network.ip_address -| IPv4 address of the client +| IP address of the client, can be null if a valid IP is not provided. | String | kc.client.network.host -| Client's host name +| Client's host name, will be the IP address or whatever is provided by proxy headers | String | kc.client.id diff --git a/docs/documentation/authorization_services/topics/policy-group-policy.adoc b/docs/documentation/authorization_services/topics/policy-group-policy.adoc index 9efaead26f5c..38d5f81349f3 100644 --- a/docs/documentation/authorization_services/topics/policy-group-policy.adoc +++ b/docs/documentation/authorization_services/topics/policy-group-policy.adoc @@ -28,7 +28,7 @@ the user is a member of. If not defined, user's groups are obtained from your re * *Groups* + Allows you to select the groups that should be enforced by this policy when evaluating permissions. After adding a group, you can extend access to children of the group -by marking the checkbox *Extend to Children*. If left unmarked, access restrictions only applies to the selected group. +by marking the checkbox *Extend to Children*. If left unmarked, access restrictions only apply to the selected group. + * *Logic* + diff --git a/docs/documentation/authorization_services/topics/policy-role-policy.adoc b/docs/documentation/authorization_services/topics/policy-role-policy.adoc index 06510d3be6ae..4c3729afa572 100644 --- a/docs/documentation/authorization_services/topics/policy-role-policy.adoc +++ b/docs/documentation/authorization_services/topics/policy-role-policy.adoc @@ -31,6 +31,10 @@ Specifies which *realm* roles are permitted by this policy. + Specifies which *client* roles are permitted by this policy. To enable this field must first select a `Client`. + +* *Fetch Roles* ++ +By default, only the roles available from the token sent with the authorization requests are used to check if the user is granted with a role. If this setting is enabled, the policy will ignore roles from the token and check any role associated with the user instead. ++ * *Logic* + The logic of this policy to apply after the other conditions have been evaluated. diff --git a/docs/documentation/authorization_services/topics/policy-time-policy.adoc b/docs/documentation/authorization_services/topics/policy-time-policy.adoc index 6acd14ab987b..0577c1d2fa88 100644 --- a/docs/documentation/authorization_services/topics/policy-time-policy.adoc +++ b/docs/documentation/authorization_services/topics/policy-time-policy.adoc @@ -37,7 +37,7 @@ Defines the month that access must be granted. You can also specify a range of m Defines the year that access must be granted. You can also specify a range of years. In this case, permission is granted only if the current year is between or equal to the two values specified. * *Hour* + -Defines the hour that access must be granted. You can also specify a range of hours. In this case, permission is granted only if current hour is between or equal to the two values specified. +Defines the hour that access must be granted. You can also specify a range of hours. In this case, permission is granted only if the current hour is between or equal to the two values specified. * *Minute* + Defines the minute that access must be granted. You can also specify a range of minutes. In this case, permission is granted only if the current minute is between or equal to the two values specified. diff --git a/docs/documentation/authorization_services/topics/resource-server-create-client.adoc b/docs/documentation/authorization_services/topics/resource-server-create-client.adoc index 6e296f4d8f3b..f24128131863 100644 --- a/docs/documentation/authorization_services/topics/resource-server-create-client.adoc +++ b/docs/documentation/authorization_services/topics/resource-server-create-client.adoc @@ -35,4 +35,4 @@ http://${host}:${port}/my-resource-server . Click *Save*. The client is created and the client Settings page opens. A page similar to the following is displayed: + .Client Settings -image:images/resource-server/client-enable-authz.png[alt="Client Settings"] +image:images/resource-server/client-settings.png[alt="Client Settings"] diff --git a/docs/documentation/authorization_services/topics/resource-server-default-config.adoc b/docs/documentation/authorization_services/topics/resource-server-default-config.adoc index 66fcebb5a787..fa3ee6714440 100644 --- a/docs/documentation/authorization_services/topics/resource-server-default-config.adoc +++ b/docs/documentation/authorization_services/topics/resource-server-default-config.adoc @@ -49,4 +49,4 @@ The default resource is created with a **URI** that maps to any resource or path sure the default configuration doesn't conflict with your own settings. [NOTE] -The default configuration defines a resource that maps to all paths in your application. If you are about to write permissions to your own resources, be sure to remove the *Default Resource* or change its ```URIS``` fields to a more specific paths in your application. Otherwise, the policy associated with the default resource (which by default always grants access) will allow {project_name} to grant access to any protected resource. +The default configuration defines a resource that maps to all paths in your application. If you are about to write permissions to your own resources, be sure to remove the *Default Resource* or change its ```URIS``` fields to more specific paths in your application. Otherwise, the policy associated with the default resource (which by default always grants access) will allow {project_name} to grant access to any protected resource. diff --git a/docs/documentation/authorization_services/topics/resource-server-enable-authorization.adoc b/docs/documentation/authorization_services/topics/resource-server-enable-authorization.adoc index 3659e7bb2cc6..1dd0b26496c9 100644 --- a/docs/documentation/authorization_services/topics/resource-server-enable-authorization.adoc +++ b/docs/documentation/authorization_services/topics/resource-server-enable-authorization.adoc @@ -4,7 +4,8 @@ You can turn your OIDC client into a resource server and enable fine-grained authorization. .Procedure -. Toggle *Authorization Enabled* to *`On*. +. In the client settings page, scroll down to the *Capability Config* section. +. Toggle *Authorization Enabled* to *On*. . Click *Save*. + .Enabling authorization services @@ -68,7 +69,7 @@ Disables the evaluation of all policies and allows access to all resources. + * *Decision Strategy* + -This configurations changes how the policy evaluation engine decides whether or not a resource or scope should be granted based on the outcome from all evaluated permissions. `Affirmative` means that at least one permission must evaluate to a positive decision in order grant access to a resource and its scopes. `Unanimous` means that all permissions must evaluate to a positive decision in order for the final decision to be also positive. As an example, if two permissions for a same resource or scope are in conflict (one of them is granting access and the other is denying access), the permission to the resource or scope will be granted if the chosen strategy is `Affirmative`. Otherwise, a single deny from any permission will also deny access to the resource or scope. +This configuration changes how the policy evaluation engine decides whether or not a resource or scope should be granted based on the outcome from all evaluated permissions. `Affirmative` means that at least one permission must evaluate to a positive decision in order grant access to a resource and its scopes. `Unanimous` means that all permissions must evaluate to a positive decision in order for the final decision to be also positive. As an example, if two permissions for a same resource or scope are in conflict (one of them is granting access and the other is denying access), the permission to the resource or scope will be granted if the chosen strategy is `Affirmative`. Otherwise, a single deny from any permission will also deny access to the resource or scope. + * *Remote Resource Management* + diff --git a/docs/documentation/authorization_services/topics/resource-view.adoc b/docs/documentation/authorization_services/topics/resource-view.adoc index 3cf8433c86cc..45eec52f9eaf 100644 --- a/docs/documentation/authorization_services/topics/resource-view.adoc +++ b/docs/documentation/authorization_services/topics/resource-view.adoc @@ -9,8 +9,8 @@ image:images/resource/view.png[alt="Resources"] The resource list provides information about the protected resources, such as: * Type -* URIS * Owner +* URIs * Associated scopes, if any * Associated permissions diff --git a/docs/documentation/authorization_services/topics/service-authorization-discovery-document.adoc b/docs/documentation/authorization_services/topics/service-authorization-discovery-document.adoc index ec96c73a934c..d946b42f2cd4 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-discovery-document.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-discovery-document.adoc @@ -9,10 +9,10 @@ The discovery document can be obtained from: [source,bash,subs="attributes+"] ---- curl -X GET \ - http://${host}:${port}{kc_realms_path}/${realm}/.well-known/uma2-configuration + http://${host}:${port}{kc_realms_path}/${realm-name}/.well-known/uma2-configuration ---- -Where `${host}:${port}` is the hostname (or IP address) and port where {project_name} is running and `${realm}` is the name of +Where `${host}:${port}` is the hostname (or IP address) and port where {project_name} is running and `${realm-name}` is the name of a realm in {project_name}. As a result, you should get a response as follows: @@ -24,11 +24,11 @@ As a result, you should get a response as follows: // some claims are expected here // these are the main claims in the discovery document about Authorization Services endpoints location - "token_endpoint": "http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token", - "token_introspection_endpoint": "http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token/introspect", - "resource_registration_endpoint": "http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/resource_set", - "permission_endpoint": "http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/permission", - "policy_endpoint": "http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/uma-policy" + "token_endpoint": "http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token", + "token_introspection_endpoint": "http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token/introspect", + "resource_registration_endpoint": "http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set", + "permission_endpoint": "http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission", + "policy_endpoint": "http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy" } ---- @@ -37,7 +37,7 @@ Each of these endpoints expose a specific set of capabilities: * **token_endpoint** + A OAuth2-compliant Token Endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Through this -endpoint clients can send authorization requests and obtain an RPT with all permissions granted by {project_name}. +endpoint, clients can send authorization requests and obtain an RPT with all permissions granted by {project_name}. + * **token_introspection_endpoint** + diff --git a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-authentication.adoc b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-authentication.adoc index cd38856442c7..603624e7442a 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-authentication.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-authentication.adoc @@ -12,7 +12,7 @@ Clients should send an access token as a Bearer credential in an HTTP Authorizat [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" ---- @@ -31,7 +31,7 @@ Clients can use any of the client authentication methods supported by {project_n [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Basic cGhvdGg6L7Jl13RmfWgtkk==pOnNlY3JldA==" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" ---- diff --git a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-uma.adoc b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-uma.adoc index 334bcf125c3d..38bec15dda37 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-uma.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission-uma.adoc @@ -13,7 +13,7 @@ to user privacy where permissions are granted based on policies defined by the u * *Party-to-Party Authorization* + Resource owners (e.g.: regular end-users) can manage access to their resources and authorize other parties (e.g: regular end-users) -to access these resources. This is different than OAuth2 where consent is given to a client application acting on behalf of a user, with UMA +to access these resources. This is different from OAuth2 where consent is given to a client application acting on behalf of a user, with UMA resource owners are allowed to consent access to other users, in a completely asynchronous manner. + * *Resource Sharing* diff --git a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission.adoc b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission.adoc index 3f79ff647b8a..ea3a9c2f9e78 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-obtaining-permission.adoc @@ -29,13 +29,13 @@ indicates that the `claim_token` parameter references an access token. The `http + * **rpt** + -This parameter is *optional*. A previously issued RPT which permissions should also be evaluated and added in a new one. This parameter +This parameter is *optional*. A previously issued RPT whose permissions should also be evaluated and added in a new one. This parameter allows clients in possession of an RPT to perform incremental authorization where permissions are added on demand. + * **permission** + This parameter is *optional*. A string representing a set of one or more resources and scopes the client is seeking access. This parameter can be defined multiple times -in order to request permission for multiple resource and scopes. This parameter is an extension to `urn:ietf:params:oauth:grant-type:uma-ticket` grant type in order to allow clients to send authorization requests without a +in order to request permission for multiple resources and scopes. This parameter is an extension to `urn:ietf:params:oauth:grant-type:uma-ticket` grant type in order to allow clients to send authorization requests without a permission ticket. The format of the string must be: `RESOURCE_ID#SCOPE_ID`. For instance: `Resource A#Scope A`, `Resource A#Scope A, Scope B, Scope C`, `Resource A`, `#Scope A`. + * **permission_resource_format** @@ -58,7 +58,7 @@ identifier is included. + * **response_permissions_limit** + -This parameter is *optional*. An integer N that defines a limit for the amount of permissions an RPT can have. When used together with +This parameter is *optional*. An integer N that defines a limit for the amount of permissions an RPT can have. When used together with an `rpt` parameter, only the last N requested permissions will be kept in the RPT. + * **submit_request** @@ -105,7 +105,7 @@ Example of an authorization request when a client is seeking access to two resou [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "audience={resource_server_client_id}" \ @@ -120,7 +120,7 @@ and explicitly granted to the requesting user by other owners are evaluated. [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "audience={resource_server_client_id}" @@ -132,7 +132,7 @@ the resource server as part of the authorization process: [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "ticket=${permission_ticket} diff --git a/docs/documentation/authorization_services/topics/service-authorization-pushing-claims.adoc b/docs/documentation/authorization_services/topics/service-authorization-pushing-claims.adoc index a943f938a3fb..f99d51034877 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-pushing-claims.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-pushing-claims.adoc @@ -10,7 +10,7 @@ an authorization request to the token endpoint as follows: [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "claim_token=ewogICAib3JnYW5pemF0aW9uIjogWyJhY21lIl0KfQ==" \ --data "claim_token_format=urn:ietf:params:oauth:token-type:jwt" \ diff --git a/docs/documentation/authorization_services/topics/service-authorization-uma-account-my-resources.adoc b/docs/documentation/authorization_services/topics/service-authorization-uma-account-my-resources.adoc index 2f10032f43e0..7690fa2941be 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-uma-account-my-resources.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-uma-account-my-resources.adoc @@ -21,7 +21,7 @@ image:images/service/account-my-resource.png[alt="My Resources"] + This section contains a list of all resources owned by the user. Users can click on a resource for more details and share the resource with others. -When there is a permission requests awaiting approval an icon is put next to the name of the resource. +When permission requests are awaiting approval, an icon is put next to the name of the resource. These requests are connected to the parties (users) requesting access to a particular resource. Users are allowed to approve or deny these requests. You can do so by clicking the icon + diff --git a/docs/documentation/authorization_services/topics/service-authorization-uma-authz-process.adoc b/docs/documentation/authorization_services/topics/service-authorization-uma-authz-process.adoc index 7991c0852676..aeabd843edaf 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-uma-authz-process.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-uma-authz-process.adoc @@ -12,15 +12,15 @@ curl -X GET \ http://${host}:${port}/my-resource-server/resource/1bfdfe78-a4e1-4c2d-b142-fc92b75b986f ``` -The resource server sends a response back to the client with a permission `ticket` and a `as_uri` parameter with the location +The resource server sends a response back to the client with a permission `ticket` and an `as_uri` parameter with the location of a {project_name} server to where the ticket should be sent in order to obtain an RPT. .Resource server responds with a permission ticket [source,bash,subs="attributes+"] ---- HTTP/1.1 401 Unauthorized -WWW-Authenticate: UMA realm="${realm}", - as_uri="https://${host}:${port}{kc_realms_path}/${realm}", +WWW-Authenticate: UMA realm="${realm-name}", + as_uri="https://${host}:${port}{kc_realms_path}/${realm-name}", ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de" ---- @@ -34,7 +34,7 @@ to obtain the location of the token endpoint and send an authorization request. [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "ticket=${permission_ticket} diff --git a/docs/documentation/authorization_services/topics/service-authorization-uma-submiting-permission-requests.adoc b/docs/documentation/authorization_services/topics/service-authorization-uma-submiting-permission-requests.adoc index ebc1e53a5bfe..87f3f024f0b8 100644 --- a/docs/documentation/authorization_services/topics/service-authorization-uma-submiting-permission-requests.adoc +++ b/docs/documentation/authorization_services/topics/service-authorization-uma-submiting-permission-requests.adoc @@ -26,7 +26,7 @@ with an authorization request to the token endpoint: [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm}/protocol/openid-connect/token \ + http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token \ -H "Authorization: Bearer ${access_token}" \ --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ --data "ticket=${permission_ticket} \ diff --git a/docs/documentation/authorization_services/topics/service-client-api.adoc b/docs/documentation/authorization_services/topics/service-client-api.adoc deleted file mode 100644 index 7d3b1d81b9e0..000000000000 --- a/docs/documentation/authorization_services/topics/service-client-api.adoc +++ /dev/null @@ -1,158 +0,0 @@ -[[_service_client_api]] -= Authorization client java API - -Depending on your requirements, a resource server should be able to manage resources remotely or even check for permissions programmatically. -If you are using Java, you can access the {project_name} Authorization Services using the Authorization Client API. - -It is targeted for resource servers that want to access the different endpoints provided by the server such as the Token Endpoint, Resource, and Permission management endpoints. - -== Maven dependency - -```xml - - - org.keycloak - keycloak-authz-client - ${KEYCLOAK_VERSION} - - -``` - -== Configuration - -The client configuration is defined in a ``keycloak.json`` file as follows: - -[source,json,subs="attributes+"] ----- -{ - "realm": "hello-world-authz", - "auth-server-url" : "http://localhost:8080{kc_base_path}", - "resource" : "hello-world-authz-service", - "credentials": { - "secret": "secret" - } -} ----- - -* *realm* (required) -+ -The name of the realm. - -* *auth-server-url* (required) -+ -The base URL of the {project_name} server. All other {project_name} pages and REST service endpoints are derived from this. It is usually in the form https://host:port{kc_base_path}. - -* *resource* (required) -+ -The client-id of the application. Each application has a client-id that is used to identify the application. - -* *credentials* (required) -+ -Specifies the credentials of the application. This is an object notation where the key is the credential type and the value is the value of the credential type. - -The configuration file is usually located in your application's classpath, the default location from where the client is going to try to find a ```keycloak.json``` file. - -== Creating the authorization client - -Considering you have a ```keycloak.json``` file in your classpath, you can create a new ```AuthzClient``` instance as follows: - -```java - // create a new instance based on the configuration defined in a keycloak.json located in your classpath - AuthzClient authzClient = AuthzClient.create(); -``` - -== Obtaining user entitlements - -Here is an example illustrating how to obtain user entitlements: - -```java -// create a new instance based on the configuration defined in keycloak.json -AuthzClient authzClient = AuthzClient.create(); - -// create an authorization request -AuthorizationRequest request = new AuthorizationRequest(); - -// send the entitlement request to the server in order to -// obtain an RPT with all permissions granted to the user -AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request); -String rpt = response.getToken(); - -System.out.println("You got an RPT: " + rpt); - -// now you can use the RPT to access protected resources on the resource server -``` - -Here is an example illustrating how to obtain user entitlements for a set of one or more resources: - -```java -// create a new instance based on the configuration defined in keycloak.json -AuthzClient authzClient = AuthzClient.create(); - -// create an authorization request -AuthorizationRequest request = new AuthorizationRequest(); - -// add permissions to the request based on the resources and scopes you want to check access -request.addPermission("Default Resource"); - -// send the entitlement request to the server in order to -// obtain an RPT with permissions for a single resource -AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request); -String rpt = response.getToken(); - -System.out.println("You got an RPT: " + rpt); - -// now you can use the RPT to access protected resources on the resource server -``` - -== Creating a resource using the protection API - -```java -// create a new instance based on the configuration defined in keycloak.json -AuthzClient authzClient = AuthzClient.create(); - -// create a new resource representation with the information we want -ResourceRepresentation newResource = new ResourceRepresentation(); - -newResource.setName("New Resource"); -newResource.setType("urn:hello-world-authz:resources:example"); - -newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view")); - -ProtectedResource resourceClient = authzClient.protection().resource(); -ResourceRepresentation existingResource = resourceClient.findByName(newResource.getName()); - -if (existingResource != null) { - resourceClient.delete(existingResource.getId()); -} - -// create the resource on the server -ResourceRepresentation response = resourceClient.create(newResource); -String resourceId = response.getId(); - -// query the resource using its newly generated id -ResourceRepresentation resource = resourceClient.findById(resourceId); - -System.out.println(resource); -``` - -== Introspecting an RPT - -```java -// create a new instance based on the configuration defined in keycloak.json -AuthzClient authzClient = AuthzClient.create(); - -// send the authorization request to the server in order to -// obtain an RPT with all permissions granted to the user -AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(); -String rpt = response.getToken(); - -// introspect the token -TokenIntrospectionResponse requestingPartyToken = authzClient.protection().introspectRequestingPartyToken(rpt); - -System.out.println("Token status is: " + requestingPartyToken.getActive()); -System.out.println("Permissions granted by the server: "); - -for (Permission granted : requestingPartyToken.getPermissions()) { - System.out.println(granted); -} -``` diff --git a/docs/documentation/authorization_services/topics/service-protection-permission-api-papi.adoc b/docs/documentation/authorization_services/topics/service-protection-permission-api-papi.adoc index 58155d19ecc5..7b1b912fb2ea 100644 --- a/docs/documentation/authorization_services/topics/service-protection-permission-api-papi.adoc +++ b/docs/documentation/authorization_services/topics/service-protection-permission-api-papi.adoc @@ -5,14 +5,14 @@ Resource servers using the UMA protocol can use a specific endpoint to manage pe [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission ---- A <<_overview_terminology_permission_ticket, permission ticket>> is a special security token type representing a permission request. Per the UMA specification, a permission ticket is: `A correlation handle that is conveyed from an authorization server to a resource server, from a resource server to a client, and ultimately from a client back to an authorization server, to enable the authorization server to assess the correct policies to apply to a request for authorization data.` -In most cases, you won't need to deal with this endpoint directly. {project_name} provides a <<_enforcer_overview, policy enforcer>> that enables UMA for your +In most cases, you do not need to deal with this endpoint directly. {project_name} provides a <<_enforcer_overview, policy enforcer>> that enables UMA for your resource server so it can obtain a permission ticket from the authorization server, return this ticket to client application, and enforce authorization decisions based on a final requesting party token (RPT). The process of obtaining permission tickets from {project_name} is performed by resource servers and not regular client applications, @@ -38,7 +38,7 @@ To create a permission ticket, send an HTTP POST request as follows: [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '[ @@ -56,7 +56,7 @@ When creating tickets you can also push arbitrary claims and associate these cla [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '[ @@ -84,7 +84,7 @@ To grant permissions for a specific resource with id {resource_id} to a user wit [source,bash,subs="attributes+"] ---- curl -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission/ticket \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission/ticket \ -H 'Authorization: Bearer '$access_token \ -H 'Content-Type: application/json' \ -d '{ @@ -99,7 +99,7 @@ curl -X POST \ [source,bash,subs="attributes+"] ---- -curl http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission/ticket \ +curl http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission/ticket \ -H 'Authorization: Bearer '$access_token ---- @@ -119,7 +119,7 @@ You can use any of these query parameters: [source,bash,subs="attributes+"] ---- curl -X PUT \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission/ticket \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission/ticket \ -H 'Authorization: Bearer '$access_token \ -H 'Content-Type: application/json' \ -d '{ @@ -135,6 +135,6 @@ curl -X PUT \ [source,bash,subs="attributes+"] ---- -curl -X DELETE http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/permission/ticket/{ticket_id} \ +curl -X DELETE http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/permission/ticket/{ticket_id} \ -H 'Authorization: Bearer '$access_token ----- \ No newline at end of file +---- diff --git a/docs/documentation/authorization_services/topics/service-protection-policy-api.adoc b/docs/documentation/authorization_services/topics/service-protection-policy-api.adoc index 91902a05937e..2bde8c1006be 100644 --- a/docs/documentation/authorization_services/topics/service-protection-policy-api.adoc +++ b/docs/documentation/authorization_services/topics/service-protection-policy-api.adoc @@ -9,7 +9,7 @@ The Policy API is available at: [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/uma-policy/{resource_id} +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy/{resource_id} ---- This API is protected by a bearer token that must represent a consent granted by the user to the resource server to manage permissions on his behalf. The bearer token can be a regular access token obtained from the @@ -77,10 +77,6 @@ curl -X POST \ Or even using a custom policy using JavaScript: -:tech_feature_name: Upload Scripts -:tech_feature_setting: -Dkeycloak.profile.feature.upload_scripts=enabled -include::./templates/deprecated.adoc[] - [source,bash,subs="attributes+"] ---- curl -X POST \ @@ -140,28 +136,28 @@ To query the permissions associated with a resource, send an HTTP GET request as [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/uma-policy?resource={resource_id} +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy?resource={resource_id} ---- To query the permissions given its name, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/uma-policy?name=Any people manager +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy?name=Any people manager ---- To query the permissions associated with a specific scope, send an HTTP GET request as follows: [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/uma-policy?scope=read +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy?scope=read ---- To query all permissions, send an HTTP GET request as follows: [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm}/authz/protection/uma-policy +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/uma-policy ---- When querying the server for permissions use parameters `first` and `max` results to limit the result. diff --git a/docs/documentation/authorization_services/topics/service-protection-resources-api-papi.adoc b/docs/documentation/authorization_services/topics/service-protection-resources-api-papi.adoc index cfcea104ae09..4f5ae729aa06 100644 --- a/docs/documentation/authorization_services/topics/service-protection-resources-api-papi.adoc +++ b/docs/documentation/authorization_services/topics/service-protection-resources-api-papi.adoc @@ -5,7 +5,7 @@ Resource servers can manage their resources remotely using a UMA-compliant endpo [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set ---- This endpoint provides operations outlined as follows (entire path omitted for clarity): @@ -25,7 +25,7 @@ To create a resource you must send an HTTP POST request as follows: [source,bash,subs="attributes+"] ---- curl -v -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '{ @@ -47,7 +47,7 @@ specific user, you can send a request as follows: [source,bash,subs="attributes+"] ---- curl -v -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '{ @@ -67,7 +67,7 @@ To create resources and allow resource owners to manage these resources, you mus [source,bash,subs="attributes+"] ---- curl -v -X POST \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '{ @@ -84,7 +84,7 @@ To update an existing resource, send an HTTP PUT request as follows: [source,bash,subs="attributes+"] ---- curl -v -X PUT \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set/{resource_id} \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set/{resource_id} \ -H 'Authorization: Bearer '$pat \ -H 'Content-Type: application/json' \ -d '{ @@ -103,7 +103,7 @@ To delete an existing resource, send an HTTP DELETE request as follows: [source,bash,subs="attributes+"] ---- curl -v -X DELETE \ - http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set/{resource_id} \ + http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set/{resource_id} \ -H 'Authorization: Bearer '$pat ---- @@ -113,49 +113,49 @@ To query the resources by `id`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set/{resource_id} +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set/{resource_id} ---- To query resources given a `name`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?name=Alice Resource +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?name=Alice Resource ---- By default, the `name` filter will match any resource with the given pattern. To restrict the query to only return resources with an exact match, use: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?name=Alice Resource&exactName=true +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?name=Alice Resource&exactName=true ---- To query resources given an `uri`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?uri=/api/alice +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?uri=/api/alice ---- To query resources given an `owner`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?owner=alice +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?owner=alice ---- To query resources given an `type`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?type=albums +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?type=albums ---- To query resources given an `scope`, send an HTTP GET request as follows: [source,bash,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/authz/protection/resource_set?scope=read +http://${host}:${port}{kc_realms_path}/${realm-name}/authz/protection/resource_set?scope=read ---- When querying the server for permissions use parameters `first` and `max` results to limit the result. \ No newline at end of file diff --git a/docs/documentation/authorization_services/topics/service-protection-whatis-obtain-pat.adoc b/docs/documentation/authorization_services/topics/service-protection-whatis-obtain-pat.adoc index aec60533c7bf..f554dd6e536b 100644 --- a/docs/documentation/authorization_services/topics/service-protection-whatis-obtain-pat.adoc +++ b/docs/documentation/authorization_services/topics/service-protection-whatis-obtain-pat.adoc @@ -14,12 +14,13 @@ Resource servers can obtain a PAT from {project_name} like any other OAuth2 acce curl -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d 'grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}' \ - "http://localhost:8080{kc_realms_path}/${realm_name}/protocol/openid-connect/token" + "http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token" ---- The example above is using the *client_credentials* grant type to obtain a PAT from the server. As a result, the server returns a response similar to the following: -```json +[source,json] +---- { "access_token": ${PAT}, "expires_in": 300, @@ -30,7 +31,7 @@ The example above is using the *client_credentials* grant type to obtain a PAT f "not-before-policy": 0, "session_state": "ccea4a55-9aec-4024-b11c-44f6f168439e" } -``` +---- [NOTE] {project_name} can authenticate your client application in different ways. For simplicity, the *client_credentials* grant type is used here, diff --git a/docs/documentation/authorization_services/topics/service-rpt-token-introspection.adoc b/docs/documentation/authorization_services/topics/service-rpt-token-introspection.adoc index 672cceabc0c6..78f113c02274 100644 --- a/docs/documentation/authorization_services/topics/service-rpt-token-introspection.adoc +++ b/docs/documentation/authorization_services/topics/service-rpt-token-introspection.adoc @@ -14,7 +14,7 @@ The token introspection is essentially a https://datatracker.ietf.org/doc/html/r [source,subs="attributes+"] ---- -http://${host}:${port}{kc_realms_path}/${realm_name}/protocol/openid-connect/token/introspect +http://${host}:${port}{kc_realms_path}/${realm-name}/protocol/openid-connect/token/introspect ---- To introspect an RPT using this endpoint, you can send a request to the server as follows: @@ -83,4 +83,3 @@ This is essentially what the policy enforcers do. Be sure to: [role="_additional-resources"] .Additional resources * https://datatracker.ietf.org/doc/html/rfc7519[JSON web token (JWT)] -* <<_enforcer_overview, policy enforcers>> diff --git a/docs/documentation/build-auto.sh b/docs/documentation/build-auto.sh index 35a1dd55a207..308c2924b954 100755 --- a/docs/documentation/build-auto.sh +++ b/docs/documentation/build-auto.sh @@ -3,7 +3,7 @@ OPTS=$1 while true; do - CHANGED=`inotifywait -r -e modify,move,create,delete authorization_services getting_started securing_apps server_admin server_development server_installation upgrading --format %w` + CHANGED=`inotifywait -r -e modify,move,create,delete authorization_services getting_started server_admin server_development server_installation upgrading --format %w` GUIDE=`echo $CHANGED | cut -d '/' -f 1` mvn clean install -f $GUIDE $OPTS done diff --git a/docs/documentation/header-maven-plugin/pom.xml b/docs/documentation/header-maven-plugin/pom.xml index 13fc1f635761..0bd4fe40645d 100644 --- a/docs/documentation/header-maven-plugin/pom.xml +++ b/docs/documentation/header-maven-plugin/pom.xml @@ -22,24 +22,24 @@ org.apache.maven - maven-project - 2.2.1 + maven-core + ${maven.version} org.apache.maven maven-plugin-api - 3.0 + ${maven.version} org.apache.maven.plugin-tools maven-plugin-annotations - 3.1 + ${maven.plugins.version} provided org.codehaus.plexus plexus-utils - 3.0.24 + ${version.plexus.utils} @@ -48,7 +48,6 @@ org.apache.maven.plugins maven-plugin-plugin - 3.4 header true diff --git a/docs/documentation/internal_resources/contributing.adoc b/docs/documentation/internal_resources/contributing.adoc index 324e97125a22..fcfde1531f19 100644 --- a/docs/documentation/internal_resources/contributing.adoc +++ b/docs/documentation/internal_resources/contributing.adoc @@ -30,21 +30,21 @@ This method is useful for any type of contribution, but necessary for larger and You only need to perform these tasks once, when preparing to contribute. -. Fork the link:https://github.com/keycloak/keycloak-documentation[Keycloak documentation repository]. This will create your own version of the repository which you can find at `https://github.com/{yourusername}/keycloak-documentation` where `{yourusername}` is the username you created in GitHub. +. Fork the link:https://github.com/keycloak/keycloak[Keycloak repository]. This will create your own version of the repository which you can find at `https://github.com/{yourusername}/keycloak` where `{yourusername}` is the username you created in GitHub. . Install `git` on your local machine. The procedure differs by operating system as described in link:https://git-scm.com/book/en/v2/Getting-Started-Installing-Git[Installing Git]. Follow up with initial Git setup tasks, as described in link:https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup[First Time Git Setup]. . Clone from your fork to create a copy on your local machine and then navigate to the new directory by entering the following from the command line on your local machine: + [source,bash] ---- -$ git clone https://github.com/{yourusername}/keycloak-documentation -$ cd keycloak-documentation +$ git clone https://github.com/{yourusername}/keycloak +$ cd keycloak/docs/documentation ---- + . Add a git remote to your local repository to link to the upstream version of the documentation. This makes it easier to update your fork and local version of the documentation. + [source,bash] ---- -$ git remote add upstream https://github.com/keycloak/keycloak-documentation +$ git remote add upstream https://github.com/keycloak/keycloak ---- + . Check your settings. @@ -52,20 +52,20 @@ $ git remote add upstream https://github.com/keycloak/keycloak-documentation [source,bash] ---- $ git remote -v -origin https://github.com/{yourusername}/keycloak-documentation.git (fetch) -origin https://github.com/{yourusername}/keycloak-documentation.git (push) -upstream https://github.com/keycloak/keycloak-documentation (fetch) -upstream https://github.com/keycloak/keycloak-documentation (push) +origin https://github.com/{yourusername}/keycloak.git (fetch) +origin https://github.com/{yourusername}/keycloak.git (push) +upstream https://github.com/keycloak/keycloak (fetch) +upstream https://github.com/keycloak/keycloak (push) ---- + NOTE: It is possible to clone using SSH so you don't have to enter a username/password every time you push. Find instructions at link:https://help.github.com/articles/connecting-to-github-with-ssh/[Connecting to GitHub with SSH] and link:https://help.github.com/articles/which-remote-url-should-i-use/[Which Remote URL Should I Use]. When using SSH, the origin lines will appear like this: -`git@github.com:{yourusername}/keycloak-documentation.git` +`git@github.com:{yourusername}/keycloak.git` [[workflow]] === Typical Workflow for Keycloak Documentation Contributions -When contributing, follow this procedure. Enter commands from the command line on your local machine in the `keycloak-documentation` directory created earlier when cloning the repository. +When contributing, follow this procedure. Enter commands from the command line on your local machine in the `keycloak` directory created earlier when cloning the repository. . Enter `git checkout main` to checkout the main branch locally. . Enter `git fetch upstream` to download the current files from the upstream repository. @@ -75,8 +75,9 @@ When contributing, follow this procedure. Enter commands from the command line o . Make your changes . (Optional) Enter `git status` now or at any time to see what branch you are on, what files you have changed, and whether those files are staged to be committed. . Enter `git add -A` to stage your changes to the commit you are about to make. -. Enter `git commit -m 'KEYCLOAK-XXXX include meaningful information about changes'` where `KEYCLOAK-XXXX` refers to the link:https://issues.redhat.com/projects/KEYCLOAK/issues[Jira issue] being fixed in this commit (if any) and where you add a short sentence that clearly describes what the commit contains. -. Follow the steps in the link:https://github.com/keycloak/keycloak-documentation/blob/main/README.md[README] to create a test build locally and confirm that your changes look correct. Make more changes and repeat steps to here, as needed. +.. Make sure your changes only affect docs/documentation directory, or you are sure about the changes outside of that package +. Enter `git commit --signoff --message ''` where message is build as described in link:https://github.com/keycloak/keycloak/blob/main/CONTRIBUTING.md#commit-messages-and-issue-linking[general contribution guide] +. Follow the steps in the documentation link:https://github.com/keycloak/keycloak/blob/main/docs/documentation/README.md[README] to create a test build locally and confirm that your changes look correct. Make more changes and repeat steps to here, as needed. . Enter `git push origin {branchname}` to push your changes to your fork in GitHub. . Use the GitHub web interface to create a pull request. First, navigate to your branch in the web UI and click *Compare*. This will show you a diff of the changes. Examine them one more time. If necessary, make more changes locally and repeat the steps to here. When you are ready, click *Create a pull request*. Enter a title and a description with enough detail for reviewers to know what you have changed and why. Click *Submit*. . Wait. The documentation team will usually review pull requests within a few days. Often suggestions and changes are requested of you to help the contribution better fit within the style guidelines for the project or to fill in information that may have been missed. If this happens, repeat the steps from making your changes to `git push origin {branchname}`. No need to create another PR as the existing one will be updated automatically. diff --git a/docs/documentation/pom.xml b/docs/documentation/pom.xml index a29f0e5d39c2..24fe1917d0d5 100644 --- a/docs/documentation/pom.xml +++ b/docs/documentation/pom.xml @@ -28,16 +28,13 @@ 3.0.2 2.5.2 2.22.2 - - 1.8 - 1.8 + 4.0.0 header-maven-plugin api_documentation authorization_services - securing_apps server_admin server_development release_notes @@ -63,15 +60,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${version.compiler.plugin} - - ${maven.compiler.target} - ${maven.compiler.source} - - org.apache.maven.plugins maven-jar-plugin diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 6a0e60d8920c..7c5a58ebddf2 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -13,6 +13,42 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.4.0 +include::topics/26_4_0.adoc[leveloffset=2] + +== {project_name_full} 26.3.0 +include::topics/26_3_0.adoc[leveloffset=2] + +== {project_name_full} 26.2.0 +include::topics/26_2_0.adoc[leveloffset=2] + +== {project_name_full} 26.1.3 +include::topics/26_1_3.adoc[leveloffset=2] + +== {project_name_full} 26.1.1 +include::topics/26_1_1.adoc[leveloffset=2] + +== {project_name_full} 26.1.0 +include::topics/26_1_0.adoc[leveloffset=2] + +== {project_name_full} 26.0.0 +include::topics/26_0_0.adoc[leveloffset=2] + +== {project_name_full} 25.0.0 +include::topics/25_0_0.adoc[leveloffset=2] + +== {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] + == {project_name_full} 23.0.0 include::topics/23_0_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/topics/11_0_0.adoc b/docs/documentation/release_notes/topics/11_0_0.adoc index 22bfd5ab6755..48593bc9466e 100644 --- a/docs/documentation/release_notes/topics/11_0_0.adoc +++ b/docs/documentation/release_notes/topics/11_0_0.adoc @@ -26,8 +26,7 @@ please take a look at link:{upgradingguide_link_latest}[{upgradingguide_name}]. The `SameSite` value `None` for `JSESSIONID` cookie is necessary for correct behavior of the {project_name} SAML adapter. Usage of a different value is causing resetting of the container's session with each request to {project_name}, when the SAML POST binging is used. Refer to the following steps for -link:{adapterguide_link}#_saml-jboss-adapter-samesite-setting[Wildfly] and -link:{adapterguide_link}#_saml-tomcat-adapter-samesite-setting[Tomcat] to keep the correct behavior. Notice, that this +link:{securing_apps_link}[Keycloak SAML Galleon feature pack for WildFly and EAP] guide to keep the correct behavior. Notice, that this workaround should be working also with the previous versions of the adapter. == Other improvements @@ -37,4 +36,4 @@ workaround should be working also with the previous versions of the adapter. * Czech translation. Thanks to https://github.com/jakubknejzlik[Jakub Knejzlík] * Possibility to fetch additional fields from the Facebook identity provider. Thanks to https://github.com/BartoszSiemienczuk[Bartosz Siemieńczuk] * Support for AES 192 and AES 256 algorithms used for signed and encrypted ID tokens. Thanks to https://github.com/tnorimat[Takashi Norimatsu] -* Ability to specify signature algorithm in Signed JWT Client Authentication. Thanks to https://github.com/tnorimat[Takashi Norimatsu] \ No newline at end of file +* Ability to specify signature algorithm in Signed JWT Client Authentication. Thanks to https://github.com/tnorimat[Takashi Norimatsu] diff --git a/docs/documentation/release_notes/topics/17_0_0.adoc b/docs/documentation/release_notes/topics/17_0_0.adoc index 99b71242a875..a20bfe97e8bc 100644 --- a/docs/documentation/release_notes/topics/17_0_0.adoc +++ b/docs/documentation/release_notes/topics/17_0_0.adoc @@ -24,12 +24,12 @@ A lot of effort went into polishing and improving the Quarkus distribution to ma * Initial support for Cross-DC * User-defined profiles are no longer supported but using different configuration files to achieve the same goal * Quickstarts updated to use the new distribution + == Other improvements -=== Offline sessions lazy loaded +=== Offline sessions lazily loaded The offline sessions are now lazily fetched from the database by default instead of preloading during the server startup. -To change the default behavior, see link:{adminguide_link}#offline-sessions-preloading[{adminguide_name}]. === Improved User Search diff --git a/docs/documentation/release_notes/topics/19_0_0.adoc b/docs/documentation/release_notes/topics/19_0_0.adoc index bbdfcbc3dd09..2bac8c85f8a2 100644 --- a/docs/documentation/release_notes/topics/19_0_0.adoc +++ b/docs/documentation/release_notes/topics/19_0_0.adoc @@ -92,4 +92,4 @@ See also the https://kubernetes.io/docs/tasks/run-application/configure-pdb/[Kub Starting with version 19, Keycloak supports sending logs using GELF to centralized logging solutions like ELK, EFK or Graylog out of the box. -You can find the documentation and examples to get you up and running quickly in the https://www.keycloak.org/server/logging#_centralized_logging_using_gelf[logging] {section}. +You can find the documentation and examples to get you up and running quickly in the https://www.keycloak.org/server/logging[logging] {section}. diff --git a/docs/documentation/release_notes/topics/22_0_0.adoc b/docs/documentation/release_notes/topics/22_0_0.adoc index 82761e952aff..5431790c425c 100644 --- a/docs/documentation/release_notes/topics/22_0_0.adoc +++ b/docs/documentation/release_notes/topics/22_0_0.adoc @@ -90,7 +90,7 @@ We still provide two separate Keycloak Admin clients, one with Jakarta EE and th == Support for count users based on custom attributes -The User API now supports querying the number of users based on custom attributes. For that, a new `q` parameter was added to the `/{realm}/users/count` endpoint. +The User API now supports querying the number of users based on custom attributes. For that, a new `q` parameter was added to the `/{realm-name}/users/count` endpoint. The `q` parameter expects the following format `q=: :`. Where `` and `` represent the attribute name and value, respectively. diff --git a/docs/documentation/release_notes/topics/23_0_0.adoc b/docs/documentation/release_notes/topics/23_0_0.adoc index 499450fdab2e..63db11f55acb 100644 --- a/docs/documentation/release_notes/topics/23_0_0.adoc +++ b/docs/documentation/release_notes/topics/23_0_0.adoc @@ -2,73 +2,85 @@ == FAPI 2 drafts support -Keycloak has new client profiles `fapi-2-security-profile` and `fapi-2-message-signing`, which ensure Keycloak enforces compliance with -the latest FAPI 2 draft specifications when communicating with your clients. Thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution. +{project_name} has new client profiles `fapi-2-security-profile` and `fapi-2-message-signing`, which ensure {project_name} enforces compliance with +the latest FAPI 2 draft specifications when communicating with your clients. +ifeval::[{project_community}==true] +Thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution. +endif::[] == DPoP preview support -Keycloak has preview for support for OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP). Thanks to +{project_name} has preview for support for OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP). +ifeval::[{project_community}==true] +Thanks to https://github.com/tnorimat[Takashi Norimatsu] and https://github.com/dteleguin[Dmitry Telegin] for their contributions. +endif::[] == More flexibility for introspection endpoint -In previous versions, introspection endpoint automatically returned most claims, which were available in the access token. Now there is new -switch `Add to token introspection` on most of protocol mappers. This addition allows more flexibility as introspection endpoint can return different -claims than access token. This is first step towards "Lightweight access tokens" support as access tokens can omit lots of the claims, which would be still returned +In previous versions, introspection endpoint automatically returned most claims, which were available in the access token. Now most of protocol mappers include a new + `Add to token introspection` switch . This addition allows more flexibility because an introspection endpoint can return different +claims than an access token. This change is a first step towards "Lightweight access tokens" support because access tokens can omit lots of the claims, which would be still returned by the introspection endpoint. When migrating from previous versions, the introspection endpoint should return same claims, which are returned from access token, -so the behavior should be effectively the same by default after the migration. Thanks to https://github.com/skabano[Shigeyuki Kabano] for the contribution. +so the behavior should be effectively the same by default after the migration. +ifeval::[{project_community}==true] +Thanks to https://github.com/skabano[Shigeyuki Kabano] for the contribution. +endif::[] == Feature flag for OAuth 2.0 device authorization grant flow The OAuth 2.0 device authorization grant flow now includes a feature flag, so you can easily disable this feature. This feature is still enabled by default. +ifeval::[{project_community}==true] Thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. - +endif::[] = Authentication == Passkeys support -Keycloak has preview support for https://fidoalliance.org/passkeys/[Passkeys]. +{project_name} has preview support for https://fidoalliance.org/passkeys/[Passkeys]. Passkey registration and authentication are realized by the features of WebAuthn. -Therefore, users of Keycloak can do passkey registration and authentication by existing WebAuthn registration and authentication. +Therefore, users of {project_name} can do Passkey registration and authentication by existing WebAuthn registration and authentication. -Both synced passkeys and device-bound passkeys can be used for both Same-Device and Cross-Device Authentication. -However, passkeys operations success depends on the user's environment. Make sure which operations can succeed in https://passkeys.dev/device-support/[the environment]. +Both synced Passkeys and device-bound Passkeys can be used for both Same-Device and Cross-Device Authentication. +However, Passkeys operations success depends on the user's environment. Make sure which operations can succeed in https://passkeys.dev/device-support/[the environment]. +ifeval::[{project_community}==true] Thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution and thanks to https://github.com/thomasdarimont[Thomas Darimont] for the help with the ideas and testing of this feature. +endif::[] == WebAuthn improvements -WebAuthn policy now includes a new field: `Extra Origins`. It provides better interoperability with non-Web platforms (for example, native mobile applications). +WebAuthn policy includes a new field: `Extra Origins`. It provides better interoperability with non-Web platforms (for example, native mobile applications). +ifeval::[{project_community}==true] Thanks to https://github.com/akunzai[Charley Wu] for the contribution. +endif::[] == You are already logged-in -There was an infamous issue that when user had login page opened in multiple browser tabs and authenticated in one of them, -the attempt to authenticate in subsequent browser tabs opened the page `You are already logged-in`. This is improved now as -other browser tabs just automatically authenticate as well after authentication of first browser tab. There are still -corner cases when the behaviour is not 100% correct, like the scenario with expired authentication session, which is then -restarted just in one browser tab and hence other browser tabs won't follow automatically with the login. -So we still plan improvements in this area. +This release addresses an issue concerning when a user has a login page open in multiple browser tabs and authenticated in one browser tab. When the user tries to authenticate in another browser tab, a message appears: `You are already logged-in`. This is improved now as +other browser tabs automatically authenticate the user after authentication in the first tab. However, more improvements are still needed. For example, when an authentication session expires and is restarted in one browser tab, other browser tabs do not follow automatically with the login. == Password policy for specify Maximum authentication time -Keycloak supports new password policy, which allows to specify the maximum age of an authentication with which a password may be changed by user without re-authentication. -When this password policy is set to 0, the user will be required to re-authenticate to change the password in the Account Console or by other means. -You can also specify a lower or higher value than the default value of 5 minutes. Thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. - +{project_name} supports a new password policy that allows you to specify the maximum age of an authentication with which a password may be changed by a user without re-authentication. +When this password policy is set to 0, the user is required to re-authenticate to change the password in the Account Console or by other means. +You can also specify a lower or higher value than the default value of 5 minutes. +ifeval::[{project_community}==true] +Thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. +endif::[] +ifeval::[{project_community}==true] = Deployments == Preview support for multi-site active-passive deployments -Deploying Keycloak to multiple independent sites is essential for some environments to provide high availability and a speedy recovery from failures. -This release adds preview-support for active-passive deployments for Keycloak. +Deploying {project_name} to multiple independent sites is essential for some environments to provide high availability and a speedy recovery from failures. +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 high-availability guide which also includes a comprehensive blueprint to deploy a highly available Keycloak 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 @@ -76,41 +88,47 @@ To get started, use the high-availability guide which also includes a comprehens OpenID Connect adapter for WildFly and JBoss EAP, which was deprecated in previous versions, has been removed in this release. It is being replaced by the Elytron OIDC adapter,which is included in WildFly, and provides a seamless migration from -Keycloak adapters. +{project_name} adapters. == SAML WildFly and JBoss EAP The SAML adapter for WildFly and JBoss EAP is no longer distributed as a ZIP download, but rather a Galleon feature pack, making it easier and more seamless to install. -See the link:{adapterguide_link}[{adapterguide_name}] for the details. +See the link:{securing_apps_link}[{securing_apps_name}] for the details. + +endif::[] = Server distribution == Load Shedding support -Keycloak now features `http-max-queued-requests` option to allow proper rejecting of incoming requests under high load. +{project_name} now features `http-max-queued-requests` option to allow proper rejecting of incoming requests under high load. For details refer to the https://www.keycloak.org/server/configuration-production[production guide]. == RESTEasy Reactive -Keycloak has switched to RESTEasy Reactive. Applications using `quarkus-resteasy-reactive` should still benefit from a better startup time, runtime performance, and memory footprint, even though not using reactive style/semantics. SPI's that depend directly on JAX-RS API should be compatible with this change. SPI's that depend on RESTEasy Classic including `ResteasyClientBuilder` will not be compatible and will require update, this will also be true for other implementation of the JAX-RS API like Jersey. +{project_name} has switched to RESTEasy Reactive. Applications using `quarkus-resteasy-reactive` should still benefit from a better startup time, runtime performance, and memory footprint, even though not using reactive style/semantics. SPIs that depend directly on JAX-RS API should be compatible with this change. SPIs that depend on RESTEasy Classic including `ResteasyClientBuilder` will not be compatible and will require an update. This update will also be needed for other implementation of the JAX-RS API like Jersey. +ifeval::[{project_community}==true] = User profile Declarative user profile is still a preview feature in this release, but we are working hard on promoting it to a supported feature. Feedback is welcome. If you find any issues or have any improvements in mind, you are welcome to create https://github.com/keycloak/keycloak/issues/new/choose[Github issue], ideally with the label `area/user-profile`. It is also recommended to check the link:{upgradingguide_link}[{upgradingguide_name}] with the migration changes for this -release for some additional informations related to the migration. +release for some additional information related to the migration. +endif::[] = Group scalability Performance around searching of groups is improved for the use-cases with many groups and subgroups. There are improvements, which allow -paginated lookup of subgroups. Thanks to https://github.com/alice-wondered[Alice] for the contribution. - +paginated lookup of subgroups. +ifeval::[{project_community}==true] +Thanks to https://github.com/alice-wondered[Alice] for the contribution. +endif::[] = Themes @@ -120,6 +138,7 @@ Message properties files for themes are now read in UTF-8 encoding, with an auto See the migration guide for more details. +ifeval::[{project_community}==true] = Storage @@ -128,3 +147,5 @@ See the migration guide for more details. The Map Store has been an experimental feature in previous releases. Starting with this release, it is removed and users should continue to use the current JPA store. See the migration guide for details. + +endif::[] diff --git a/docs/documentation/release_notes/topics/24_0_0.adoc b/docs/documentation/release_notes/topics/24_0_0.adoc new file mode 100644 index 000000000000..6772329a3574 --- /dev/null +++ b/docs/documentation/release_notes/topics/24_0_0.adoc @@ -0,0 +1,455 @@ += Supported user profile and progressive profiling + +The user profile preview feature is promoted to be fully supported and user profile is enabled by default. + +ifeval::[{project_community}==true] +In the past months, the Keycloak team spent a huge amount of effort in polishing the user +profile feature to make it fully supported. In this release, we continued the effort. Lots of improvements, fixes and +polishing were done based on the thorough testing and feedback from our awesome community. +endif::[] + +The following are a few highlights of this feature; + +* Fine-grained control over the attributes that users and administrators can manage so that you can prevent unexpected attributes and values from being set. +* Ability to specify what user attributes are managed and should be displayed on the forms to regular users or administrators. +* Dynamic forms - Previously, the forms where users created or updated their profiles, contain four basic attributes like username, email, first name and last name. The addition of any +attributes (or removing some default attributes) required you to create a custom theme. Now custom themes may not be needed because users see exactly the requested attributes based on the requirement of the particular deployment. + +* Validations - Ability to specify validators for the user attributes including built-in validators that you can use to specify a maximum or minimum length, a specific regex, or limiting a +particular attribute to be a URL or number. + +* Annotations - Ability to specify that particular attribute should be rendered for instance as a text area, an HTML select with specified options, or calendar or many other options. You can also bind JavaScript code to a specific field to change how an attribute is rendered and customize its behavior. + +* Progressive profiling - Ability to specify that some fields are required or available on the forms just for particular values of `scope` parameter. This effectively allow progressive +profiling. You no longer need to ask the user for twenty attributes during registration; you can instead ask the user to fill in attributes incrementally according to the requirements of the individual client +applications that are used by the user. + +* Migration from previous versions - The user profile is now always enabled, but it operates as before for those who did not use this feature. You can +benefit from the user profile capabilities, but you are not required to use them. For migration instructions, see the link:{upgradingguide_link}[{upgradingguide_name}]. + +The first release of the user profile as a supported feature is just the starting point and the baseline for delivering many more capabilities around identity management. + +ifeval::[{project_community}==true] +We would like to give huge thanks to the awesome Keycloak community as lots of ideas, requirements and contributions came from the community! Special thanks to: + +* https://github.com/velias[Vlastimil Eliáš] +* https://github.com/alechenninger[Alec Henninger] +* https://github.com/thomasdarimont[Thomas Darimont] +* https://github.com/bs-matil[Markus Till] +* https://github.com/sschu[Sebastian Schuster] +* https://github.com/antikalk[Oliver] +* https://github.com/patrickjennings[Patrick Jennings] +* https://github.com/adrhine[Andrew] + +endif::[] + +For more details about user profile capabilities, see the link:{adminguide_link}#user-profile[{adminguide_name}]. + +== Breaking changes to the User Profile SPI + +In this release, changes to the User Profile SPI might impact existing implementations based on this SPI. For more details, see the +link:{upgradingguide_link}[{upgradingguide_name}]. + +== Changes to Freemarker templates to render pages based on the user profile and realm + +In this release, the following templates were updated to make it possible to dynamically render attributes based +on the user profile configuration set to a realm: + +* `login-update-profile.ftl` +* `register.ftl` +* `update-email.ftl` + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + +== New Freemarker template for the update profile page at first login through a broker + +In this release, the server renders the update profile page when the user is authenticating through a broker for the +first time using the `idp-review-user-profile.ftl` template. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + +ifeval::[{project_community}==true] += Java adapter deprecation and removal + +Back in 2022 we announced the https://www.keycloak.org/2022/02/adapter-deprecation.html[deprecation of Keycloak adapters in Keycloak 19]. +To give the community more time to adopt this https://www.keycloak.org/2023/03/adapter-deprecation-update.html[was delayed]. + +With that in mind, this will be the last major release of Keycloak to include OpenID Connect and SAML adapters. +As Jetty 9.x has not been supported since 2022 the Jetty adapter has been removed already in this release. + +The generic Authorization Client library will continue to be supported, and aims to be used in combination with any +other OAuth 2.0 or OpenID Connect libraries. + +The only adapter we will continue to deliver is the SAML adapter for latest releases of WildFly and EAP 8.x. Reasoning +for continuing to support this is down to the fact that the majority of the SAML codebase in Keycloak was a contribution +from WildFly. As part of this contribution we agreed to maintain SAML adapters for WildFly and EAP in the long run. + +== Jetty adapter removed + +Jetty 9.4 has not been supported in the community for a long time, and reached end-of-life in 2022. At the same time the +adapter has not been updated or tested with more recent versions of Jetty. For these reasons the Jetty adapter has been +removed from this release. + +endif::[] + += New Welcome Page + +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. + +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 + +We introduced version 3 of the Account Console in {project_name} 22 as a preview feature. In this release, we are making it the default version, and deprecating version 2 in the process, which will be removed in a subsequent release. + +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. + +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 + +== Using `exports` field in `package.json` + +The {project_name} JS adapter now uses the https://webpack.js.org/guides/package-exports/[`exports` field] in its `package.json`. This change improves support for more modern bundlers like Webpack 5 and Vite, but comes with some unavoidable breaking changes. See the link:{upgradingguide_link}[{upgradingguide_name}] for more details. + +== PKCE enabled by default + +The {project_name} JS adapter now sets the `pkceMethod` option to `S256` by default. This change enables Proof Key Code Exchange (https://datatracker.ietf.org/doc/html/rfc7636[PKCE]) for all applications using the adapter. If you use the adapter on a system that does not support PKCE, you can set the `pkceMethod` option to `false` to disable it. + += Changes to Password Hashing + +In this release, we adapted the password hashing defaults to match the https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2[OWASP recommendations for Password Storage]. + +As part of this change, the default password hashing provider has changed from `pbkdf2-sha256` to `pbkdf2-sha512`. +Also, the number of default hash iterations for `pbkdf2` based password hashing algorithms changed. This change means better security aligned with latest recommendations, but +it has impact on performance. It is possible to stick to the old behaviour by adding password policies `hashAlgorithm` and `hashIterations` to your realm. For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += OAuth/OIDC related improvements + +== Lightweight access tokens support + +This release contains support for Lightweight access tokens. As a result, you can have smaller access tokens for specified clients. These tokens have only a few +claims, which is why they are smaller. Note that lightweight access token is still JWT signed by the realm key by default and still contains some very basic claims. + +This release introduces an *Add to lightweight access token* flag that is available on some OIDC protocol mappers. Use this flag to specify if a particular claim should be added to a lightweight +access token. It is *OFF* by default, which means that most claims are not added. + +Also, a client policy executor exists. Use it to specify if a particular client request +should use lightweight access tokens or regular access tokens. An alternative to the executor is to use an *Always use lightweight access token* flag on client advanced +settings, which causes that client to always use lightweight access tokens. An executor can be an alternative if you need +more flexibility. For instance, you may choose to use lightweight access tokens by default but use regular tokens only for the specified *scope* parameter. + +A previous release added an *Add to token introspection* switch. You use it to add +claims that are not present in the access token into the introspection endpoint response. + +ifeval::[{project_community}==true] +Thanks to https://github.com/skabano[Shigeyuki Kabano] for the contribution and Thanks to +https://github.com/tnorimat[Takashi Norimatsu] for a help and review of this feature. +endif::[] + +== OAuth 2.1 support + +This release contains optional OAuth 2.1 support. New client policy profiles were introduced in this release, which administrators can use to make sure that clients and particular client requests comply with the OAuth 2.1 specification. A dedicated client profile exists for confidential clients and a dedicated profile for public clients. +ifeval::[{project_community}==true] +Thanks to https://github.com/tnorimat[Takashi Norimatsu] and https://github.com/skabano[Shigeyuki Kabano] for the contribution. +endif::[] + +== Scope parameter supported in the refresh token flow + +Starting with this release, the *scope* parameter in the OAuth2/OIDC endpoint for token refresh is supported. Use this parameter to request access tokens with a smaller amount +of scopes than originally granted, which means you cannot increase access token scope. This scope limitation does not affect the scope of the refreshed refresh token. This function works as +described in the OAuth2 specification. +ifeval::[{project_community}==true] +Thanks to https://github.com/cgeorgilakis[Konstantinos Georgilakis] for the contribution. +endif::[] + +== Client policy executor for secure redirect URIs + +A new client policy executor `secure-redirect-uris-enforcer` is introduced. Use it to restrict which redirect URIs can be used by the clients. For instance, +you can specify that client redirect URIs cannot have wildcards, should be just from specific domain, must be OAuth 2.1 compliant, and so on. +ifeval::[{project_community}==true] +Thanks to https://github.com/lexcao[Lex Cao] and https://github.com/tnorimat[Takashi Norimatsu] for the contribution. +endif::[] + +== Client policy executor for enforcing DPoP + +A new client policy executor `dpop-bind-enforcer` is introduced. You can use it to enforce DPoP for a particular client if `dpop` preview + is enabled. +ifeval::[{project_community}==true] +Thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution. +endif::[] + +== Supporting EdDSA + +You can create EdDSA realm keys and use them as signature algorithms for various clients. For instance, you can use these keys to sign tokens or for client authentication with signed JWT. +This feature includes identity brokering where {project_name} itself signs client assertions that are used for `private_key_jwt` authentication to third party identity providers. +ifeval::[{project_community}==true] +Thanks to +https://github.com/tnorimat[Takashi Norimatsu] and https://github.com/MuhammadZakwan[Muhammad Zakwan Bin Mohd Zahid] for the contribution. +endif::[] + +== EC Keys supported by JavaKeystore provider + +The provider `JavaKeystoreProvider` for providing realm keys now supports EC keys in addition to previously supported RSA keys. +ifeval::[{project_community}==true] +Thanks to https://github.com/wistefan[Stefan Wiedemann] for the contribution. +endif::[] + +== Option to add X509 thumbprint to JWT when using private_key_jwt authentication for identity providers + +OIDC identity providers now have the *Add X.509 Headers to the JWT* option for the situation when client authentication with JWT signed by private key is used. This option can be useful +for interoperability with some identity providers such as Azure AD, which require the thumbprint to be present on the JWT. +ifeval::[{project_community}==true] +Thanks to https://github.com/MikeTangoEcho[MT] for the contribution. +endif::[] + +== OAuth Grant Type SPI + +The {project_name} codebase includes an internal update to introduce the OAuth Grant Type SPI. This update allows additional flexibility when introducing custom grant types +supported by the {project_name} OAuth 2 token endpoint. +ifeval::[{project_community}==true] +Thanks to https://github.com/dteleguin[Dmitry Telegin] for the contribution. +endif::[] + += CORS improvements + +The CORS related {project_name} functionality was extracted into the SPI, which can allow additional flexibility. Note that `CorsSPI` is internal and may change at a future release. +ifeval::[{project_community}==true] +Thanks to https://github.com/dteleguin[Dmitry Telegin] for the contribution. +endif::[] + += Truststore improvements + +{project_name} introduces improved truststores configuration options. The {project_name} truststore is now used across the server, including outgoing connections, mTLS, and database drivers. You no longer need to configure separate truststores for individual areas. To configure the truststore, you can put your truststores files or certificates in the default `conf/truststores`, or use the new `truststore-paths` config option. For details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide]. + += Versioned Features + +Features now support versioning. To preserve backward compatibility, all existing features (including `account2` and `account3`) are marked as version 1. Newly introduced features will use versioning, which means that users can select between different implementations of desired features. + +For details refer to the https://www.keycloak.org/server/features[features guide]. + +== {project_name} CR Truststores + +You may also take advantage of the new server-side handling of truststores by using the Keycloak CR, for example: + +[source,yaml] +---- +spec: + truststores: + mystore: + secret: + name: mystore-secret + myotherstore: + secret: + name: myotherstore-secret +---- + +Currently only Secrets are supported. + +== Trust Kubernetes CA + +The cert for the Kubernetes CA is added automatically to your {project_name} Pods managed by the Operator. + += Automatic certificate management for SAML identity providers + +The SAML identity providers can now be configured to automatically download the signing certificates from the IDP entity metadata descriptor endpoint. In order to use the new feature, configure the `Metadata descriptor URL` option in the provider (the URL where the IDP metadata information with the certificates is published) and set `Use metadata descriptor URL` to `ON`. The certificates are automatically downloaded and cached in the `public-key-storage` SPI from that URL. The certificates can also be reloaded or imported from the Admin Console, using the action combo in the provider page. + +See the https://www.keycloak.org/docs/latest/server_admin/index.html#saml-v2-0-identity-providers[documentation] for more details about the new options. + += Non-blocking health check for load balancers + +A new health check endpoint available at `/lb-check` was added. +The execution is running in the event loop, which means this check is responsive also in overloaded situations when {project_name} needs to handle many requests waiting in request queue. +This behavior is useful, for example, in multi-site deployment to avoid failing over to another site that is under heavy load. +The endpoint is currently checking availability of the embedded and external Infinispan caches. Other checks may be added later. + + +This endpoint is not available by default. +To enable it, run Keyloak with the `multi-site` feature. +For more details, see https://www.keycloak.org/server/features[Enabling and disabling features]. + += Keycloak CR Optimized Field + +The Keycloak CR now includes an `startOptimized` field, which may be used to override the default assumption about whether to use the `--optimized` flag for the start command. +As a result, you can use the CR to configure build time options also when a custom {project_name} image is used. + += Enhanced reverse proxy settings + +It is now possible to separately enable parsing of either `Forwarded` or `X-Forwarded-*` headers by using the new `--proxy-headers` option. +For details, see the https://www.keycloak.org/server/reverseproxy[Reverse Proxy Guide]. +The original `--proxy` option is now deprecated and will be removed in a future release. For migration instructions, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Changes to the user representation in both Admin API and Account contexts + +In this release, we are encapsulating the root user attributes (such as `username`, `email`, `firstName`, `lastName`, and `locale`) by moving them to a base/abstract class in order to align how these attributes +are marshalled and unmarshalled when using both Admin and Account REST APIs. + +This strategy provides consistency in how attributes are managed by clients and makes sure they conform to the user profile +configuration set to a realm. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Sequential loading of offline sessions and remote sessions + +Starting with this release, the first member of a {project_name} cluster will load remote sessions sequentially instead of in parallel. +If offline session preloading is enabled, those will be loaded sequentially as well. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Performing actions on behalf of another already authenticated user is not longer possible + +In this release, you can no longer perform actions such as email verification if the user is already authenticated +and the action is bound to another user. For instance, a user can not complete the verification email flow if the email link +is bound to a different account. + += Changes to the email verification flow + +In this release, if a user tries to follow the link to verify the email and the email was previously verified, a proper message +will be shown. + +In addition to that, a new error (`EMAIL_ALREADY_VERIFIED`) event will be fired to indicate an attempt to verify an already verified email. You can +use this event to track possible attempts to hijack user accounts in case the link has leaked or to alert users if they do not recognize the action. + += Deprecated offline session preloading + +The default behavior of {project_name} is to load offline sessions on demand. +The old behavior to preload them at startup is now deprecated, as pre-loading them at startup does not scale well with a growing number of sessions, and increases {project_name} memory usage. The old behavior will be removed in a future release. + +For more details, see the +link:{upgradingguide_link}[{upgradingguide_name}]. + += Configuration option for offline session lifespan override in memory + +To reduce memory requirements, we introduced a configuration option to shorten lifespan for offline sessions imported into the Infinispan caches. Currently, the offline session lifespan override is disabled by default. + +For more details, see the +link:{adminguide_link}#_offline-access[{adminguide_name}]. + += Infinispan metrics use labels for cache manager and cache names + +When enabling metrics for {project_name}'s embedded caches, the metrics now use labels for the cache manager and the cache names. + +For more details, see the +link:{upgradingguide_link}[{upgradingguide_name}]. + += User attribute value length extension + +As of this release, {project_name} supports storing and searching by user attribute values longer than 255 characters, which was previously a limitation. + +For more details, see the +link:{upgradingguide_link}[{upgradingguide_name}]. + += Brute Force Protection changes + +There have been a couple of enhancements to the Brute Protection: + +1. When an attempt to authenticate with an OTP or Recovery Code fails due to Brute Force Protection the active Authentication Session is invalidated. Any further attempts to authenticate with that session will fail. + +2. In previous versions of {project_name}, the administrator had to choose between disabling users temporarily or permanently due to a Brute Force attack on their accounts. The administrator can now permanently disable a user after a given number of temporary lockouts. + +3. The property `failedLoginNotBefore` has been added to the `brute-force/users/{userId}` endpoint + += Authorization Policy + +In previous versions of {project_name}, when the last member of a User, Group or Client policy was deleted then that policy would also be deleted. Unfortunately this could lead to an escalation of privileges if the policy was used in an aggregate policy. To avoid privilege escalation the effect policies are no longer deleted and an administrator will need to update those policies. + += {project_name} CR cache-config-file option + +The Keycloak CR now allows for specifying the `cache-config-file` option by using the `cache` spec `configMapFile` field, for example: + +[source,yaml] +---- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc +spec: + ... + cache: + configMapFile: + name: my-configmap + key: config.xml +---- + += Keycloak CR resources options + +The Keycloak CR now allows for specifying the `resources` options for managing compute resources for the Keycloak container. +It provides the ability to request and limit resources independently for the main {project_name} deployment via the Keycloak CR, and for the realm import Job via the Realm Import CR. + +When no values are specified, the default `requests` memory is set to `1700MiB`, and the `limits` memory is set to `2GiB`. + +You can specify your custom values based on your requirements as follows: + +[source,yaml] +---- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc +spec: + ... + resources: + requests: + cpu: 1200m + memory: 896Mi + limits: + cpu: 6 + memory: 3Gi +---- + +For more details, see the +https://www.keycloak.org/operator/advanced-configuration[Operator Advanced configuration]. + += Temporary lockout log replaced with event + +There is now a new event `USER_DISABLED_BY_TEMPORARY_LOCKOUT` when a user is temporarily locked out by the brute force protector. +The log with ID `KC-SERVICES0053` has been removed as the new event offers the information in a structured form. + +For more details, see the +link:{upgradingguide_link}[{upgradingguide_name}]. + += Updates to cookies + +Cookie handling code has been refactored and improved, including a new Cookie Provider. This provides better consistency +for cookies handled by {project_name}, and the ability to introduce configuration options around cookies if needed. + += SAML User Attribute Mapper For NameID now suggests only valid NameID formats + +User Attribute Mapper For NameID allowed setting `Name ID Format` option to the following values: + +- `urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName` +- `urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName` +- `urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos` +- `urn:oasis:names:tc:SAML:2.0:nameid-format:entity` + +However, {project_name} does not support receiving `AuthnRequest` document with one of these `NameIDPolicy`, therefore these +mappers would never be used. The supported options were updated to only include the following Name ID Formats: + +- `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` +- `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` +- `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 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`. + +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 + +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::[] + += 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/release_notes/topics/25_0_0.adoc b/docs/documentation/release_notes/topics/25_0_0.adoc new file mode 100644 index 000000000000..6ed2c8cc1b03 --- /dev/null +++ b/docs/documentation/release_notes/topics/25_0_0.adoc @@ -0,0 +1,250 @@ += Account Console v2 theme removed + +The Account Console v2 theme has been removed from {project_name}. This theme was deprecated in {project_name} 24 and replaced by the Account Console v3 theme. If you are still using this theme, you should migrate to the Account Console v3 theme. + += Java 21 support + +{project_name} now supports OpenJDK 21, as we want to stick to the latest LTS OpenJDK versions. + += Java 17 support is deprecated + +OpenJDK 17 support is deprecated in {project_name}, and will be removed in a following release in favor of OpenJDK 21. + += Most of Java adapters removed + +As stated in the release notes of previous {project_name} version, the most of Java adapters are now removed from the {project_name} codebase and downloads pages. + +For OAuth 2.0/OIDC, this includes removal of the Tomcat adapter, WildFly/EAP adapter, Servlet Filter adapter, `KeycloakInstalled` desktop adapter, the `jaxrs-oauth-client` adapter, JAAS login modules, Spring adapter and SpringBoot adapters. +You can check https://www.keycloak.org/2023/03/adapter-deprecation-update.html[our older post] for the list of some alternatives. + +For SAML, this includes removal of the Tomcat adapter and Servlet filter adapter. SAML adapters are still supported with WildFly and JBoss EAP. + +The generic Authorization Client library is still supported, and we still plan to support it. It aims to be used in combination with any other OAuth 2.0 or OpenID Connect libraries. You can +check the https://github.com/keycloak/keycloak-quickstarts[quickstarts] for some examples where this authorization client library is used together with the 3rd party Java adapters like +Elytron OIDC or SpringBoot. You can check the quickstarts also for the example of SAML adapter used with WildFly. + += Upgrade to PatternFly 5 + +In {project_name} 24, the Welcome page is updated to use https://www.patternfly.org/[PatternFly 5], the latest version of the design system that underpins the user interface of {project_name}. In this release, the Admin Console and Account Console are also updated to use PatternFly 5. If you want to extend and customize the Admin Console and Account Console, review https://www.patternfly.org/get-started/upgrade/[the changes in PatternFly 5] and update your customizations accordingly. + += Argon2 password hashing + +Argon2 is now the default password hashing algorithm used by {project_name} in a non-FIPS environment. + +Argon2 was the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[2015 password hashing competition] +and is the recommended hashing algorithm by https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id[OWASP]. + +In {project_name} 24 the default hashing iterations for PBKDF2 were increased from 27.5K to 210K, resulting in a more than +10 times increase in the amount of CPU time required to generate a password hash. With Argon2 it is possible to achieve +better security, with almost the same CPU time as previous releases of {project_name}. One downside is Argon2 requires more +memory, which is a requirement to be resistant against GPU attacks. The defaults for Argon2 in {project_name} requires 7MB +per-hashing request. +To prevent excessive memory and CPU usage, the parallel computation of hashes by Argon2 is by default limited to the number of cores available to the JVM. +To support the memory intensive nature of Argon2, we have updated the default GC from ParallelGC to G1GC for a better heap utilization. + +Note that Argon2 is not compliant with FIPS 140-2. So if you are in the FIPS environment, the default algorithm will be still PBKDF2. Also note that if you are on non-FIPS environment and +you plan to migrate to the FIPS environment, consider changing the password policy to a FIPS compliant algorithm such as `pbkdf2-sha512` at the outset. Otherwise, users will not be able to log in after they switch to the FIPS environment. + += New Hostname options + +In response to the complexity and lack of intuitiveness experienced with previous hostname configuration settings, we are proud to introduce Hostname v2 options. + +We have listened to your feedback, tackled the tricky issues, and created a smoother experience for managing hostname configuration. +Be aware that even the behavior behind these options has changed and requires your attention - if you are dealing with custom hostname settings. + +Hostname v2 options are supported by default, as the old hostname options are deprecated and will be removed in the following releases. +You should migrate to them as soon as possible. + +New options are activated by default, so {project_name} will not recognize the old ones. + +For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Persistent user sessions + +Previous versions of {project_name} stored only offline user and offline client sessions in the databases. +The new feature `persistent-user-sessions` stores online user sessions and online client sessions not only in memory, but also in the database. +This will allow a user to stay logged in even if all instances of {project_name} are restarted or upgraded. + +The feature is a preview feature and disabled by default. To use it, add the following to your build command: + +---- +bin/kc.sh build --features=persistent-user-sessions ... +---- + +For more details see the https://www.keycloak.org/server/features[Enabling and disabling features] {section}. +The https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing[sizing guide] contains a new paragraph describing the updated resource requirements when this feature is enabled. + +For information on how to upgrade, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Cookies updates + +== SameSite attribute set for all cookies + +The following cookies did not use to set the `SameSite` attribute, which in recent browser versions results in them +defaulting to `SameSite=Lax`: + +* `KC_STATE_CHECKER` now sets `SameSite=Strict` +* `KC_RESTART` now sets `SameSite=None` +* `KEYCLOAK_LOCALE` now sets `SameSite=None` +* `KEYCLOAK_REMEMBER_ME` now sets `SameSite=None` + +The default value `SameSite=Lax` causes issues with POST based bindings, mostly applicable to SAML, but also used in +some OpenID Connect / OAuth 2.0 flows. + +== Removing KC_AUTH_STATE cookie + +The cookie `KC_AUTH_STATE` is removed and it is no longer set by the {project_name} server as this server no longer needs this cookie. + += Deprecated cookie methods removed + +The following APIs for setting custom cookies have been removed: + +* `ServerCookie` - replaced by `NewCookie.Builder` +* `LocaleSelectorProvider.KEYCLOAK_LOCALE` - replaced by `CookieType.LOCALE` +* `HttpCookie` - replaced by `NewCookie.Builder` +* `HttpResponse.setCookieIfAbsent(HttpCookie cookie)` - replaced by `HttpResponse.setCookieIfAbsent(NewCookie cookie)` + += Addressed 'You are already logged in' for expired authentication sessions + +The Keycloak 23 release provided improvements for when a user is authenticated in parallel in multiple browser tabs. However, this improvement did not address the case when an authentication session +expired. Now for the case when user is already logged-in in one browser tab and an authentication session expired in other browser tabs, {project_name} is able to redirect back to the client +application with an OIDC/SAML error, so the client application can immediately retry authentication, which should usually automatically log in the application because of the SSO session. For more +details, see link:{adminguide_link}#_authentication-sessions[{adminguide_name} authentication sessions]. + += Lightweight access token to be even more lightweight + +In previous releases, the support for lightweight access token was added. In this release, we managed to remove even more built-in claims from the lightweight access token. The claims are added +by protocol mappers. Some of them affect even the regular access tokens or ID tokens as they were not strictly required by the OIDC specification. + +* Claims `sub` and `auth_time` are added by protocol mappers now, which are configured by default on the new client scope `basic`, which is added automatically to all the clients. The claims are still added to the ID token and access token as before, but not to lightweight access token. +* Claim `nonce` is added only to the ID token now. It is not added to a regular access token or lightweight access token. For backwards compatibility, you can add this claim to an access token by protocol mapper, which needs to be explicitly configured. +* Claim `session_state` is not added to any token now. It is still possible to add it by protocol mapper if needed. There is still the other dedicated claim `sid` supported by the specification, which was available in previous versions as well and which has exactly the same value. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}].. + += Support for application/jwt media-type in token introspection endpoint + +You can use the HTTP Header `Accept: application/jwt` when invoking a token introspection endpoint. When enabled for a particular client, it returns a claim `jwt` from the +token introspection endpoint with the full JWT access token, which can be useful especially for the use-cases when the client calling introspection endpoint used lightweight access +token. Thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. + += Password policy for check if password contains Username + +Keycloak supports a new password policy that allows you to deny user passwords which contains the user username. + += Required actions improvements + +In the Admin Console, you can now configure some required actions in the *Required actions* tab of a particular realm. Currently, the *Update password* is the only built-in configurable required action. It supports setting *Maximum Age of Authentication*, which is the maximum time users can update their password +by the `kc_action` parameter (used for instance when updating password in the Account Console) without re-authentication. The sorting of required actions is also improved. When there are multiple required +actions during authentication, all actions are sorted together regardless of whether those are actions set during authentication (for instance by the `kc_action` parameter) or actions added to the user account manually by an administrator. +Thanks to https://github.com/thomasdarimont[Thomas Darimont] and https://github.com/danielFesenmeyer[Daniel Fesenmeyer] for the contributions. + += Passkeys improvements + +The support for Passkeys conditional UI was added. When the Passkeys preview feature is enabled, there is a dedicated authenticator available, which means you can select from a list of available passkeys accounts +and authenticate a user based on that. Thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution. + += Default client profile for SAML + +The default client profile to have secured SAML clients was added. When browsing through client policies of a realm in the Admin Console, you see a new client profile `saml-security-profile`. When it is used, there are +security best practices applied for SAML clients such as signatures are enforced, SAML Redirect binding is disabled, and wildcard redirect URLs are prohibited. + += Authenticator for override existing IDP link during first-broker-login + +There was new authenticator `Confirm override existing link` added. This authenticator allows to override linked IDP username for the {project_name} user, which was already linked to different +IDP identity before. More details in the link:{adminguide_link}#_override_existing_broker_link[{adminguide_name}]. Thanks to https://github.com/lexcao[Lex Cao] for the contribution. + += OpenID for Verifiable Credential Issuance - experimental support + +There is work in progress on the support of OpenID for Verifiable Credential Issuance (OID4VCI). Right now, this is still work in progress, but things are being gradually added. {project_name} +can act as an OID4VC Issuer with support of Pre-Authorized code flow. There is support for verifiable credentials in the JWT-VC, SD-JWT-VC and VCDM formats. Thanks to the members of the OAuth SIG +groups for the contributions and feedback and especially thanks to https://github.com/wistefan[Stefan Wiedemann], https://github.com/francis-pouatcha[Francis Pouatcha], https://github.com/tnorimat[Takashi Norimatsu] +and https://github.com/bucchi[Yutaka Obuchi]. + += Searching by user attribute no longer case insensitive + +When searching for users by user attribute, {project_name} no longer searches for user attribute names forcing lower case comparisons. The goal of this change was to speed up searches by using {project_name}'s native index on the user attribute table. If your database collation is case-insensitive, your search results will stay the same. If your database collation is case-sensitive, you might see less search results than before. + += Breaking fix in authorization client library + +For users of the `keycloak-authz-client` library, calling `AuthorizationResource.getPermissions(...)` now correctly returns a `List`. + +Previously, it would return a `List` at runtime, even though the method declaration advertised `List`. + +This fix will break code that relied on casting the List or its contents to `List`. If you have used this method in any capacity, you are likely to have done this and be affected. + += IDs are no longer set when exporting authorization settings for a client + +When exporting the authorization settings for a client, the IDs for resources, scopes, and policies are no longer set. As a +result, you can now import the settings from a client to another client. + += Management port for metrics and health endpoints + +Metrics and health checks endpoints are no longer accessible through the standard {project_name} server port. +As these endpoints should be hidden from the outside world, they can be accessed on a separate default management port `9000`. + +It allows to not expose it to the users as standard Keycloak endpoints in Kubernetes environments. +The new management interface provides a new set of options and is fully configurable. + +{project_name} Operator assumes the management interface is turned on by default. +For more details, see https://www.keycloak.org/server/management-interface[Configuring the Management Interface]. + += Syslog for remote logging + +{project_name} now supports https://en.wikipedia.org/wiki/Syslog[Syslog] protocol for remote logging. +It utilizes the protocol defined in https://datatracker.ietf.org/doc/html/rfc5424[RFC 5424]. +By default, the syslog handler is disabled, but when enabled, it sends all log events to a remote syslog server. + +For more information, see the https://www.keycloak.org/server/logging[Configuring logging] guide. + += Change to class `EnvironmentDependentProviderFactory` + +The method `EnvironmentDependentProviderFactory.isSupported()` was deprecated for several releases and has now been removed. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += All `cache` options are runtime + +It is now possible to specify the `cache`, `cache-stack`, and `cache-config-file` options during runtime. +This eliminates the need to execute the build phase and rebuild your image due to them. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += High availability guide enhanced + +The high availability guide now contains a {section} on how to configure an AWS Lambda to prevent an intended automatic failback from the Backup site to the Primary site. + += Removing deprecated methods from `AccessToken`, `IDToken`, and `JsonWebToken` classes + +In this release, we are finally removing deprecated methods from the following classes: + +* `AccessToken` +* `IDToken` +* `JsonWebToken` + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Method `getExp` added to `SingleUseObjectKeyModel` + +As a consequence of the removal of deprecated methods from `AccessToken`, `IDToken`, and `JsonWebToken`, +the `SingleUseObjectKeyModel` also changed to keep consistency with the method names related to expiration values. + +For more details, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Support for PostgreSQL 16 + +The supported and tested databases now include PostgreSQL 16. + += Introducing support for Customer Identity and Access Management (CIAM) and Multi-tenancy + +In this release, we are delivering Keycloak Organizations as a technology preview feature. + +This feature provides a realm with some core CIAM capabilities, which will serve as the baseline for more capabilities +in the future to address Business-to-Business (B2B) and Business-to-Business-to-Customers (B2B2C) use cases. + +In terms of functionality, the feature is completed. However, we still have work to do to make it fully supported in the next major release. +This remaining work is mainly about preparing the feature for production deployments with a focus on scalability. Also, depending +on the feedback we get until the next major release, we might eventually accept additional capabilities and add more value to +the feature, without compromising its roadmap. + +For more details, see link:{adminguide_link}#_managing_organizations[{adminguide_name}]. diff --git a/docs/documentation/release_notes/topics/26_0_0.adoc b/docs/documentation/release_notes/topics/26_0_0.adoc new file mode 100644 index 000000000000..44a514bc22f3 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_0_0.adoc @@ -0,0 +1,282 @@ += Organizations supported + +Starting with {project_name} 26, the Organizations feature is fully supported. + += Client libraries updates + +== Dedicated release cycle for the client libraries + +From this release, some of the {project_name} client libraries will have release cycle independent of the {project_name} server release cycle. The 26.0.0 release may be the last one +when the client libraries are released together with the {project_name} server. But from now on, the client libraries may be released at a different time than the {project_name} server. + +The client libraries are these artifacts: + +* Java admin client - Maven artifact `org.keycloak:keycloak-admin-client` +* Java authorization client - Maven artifact `org.keycloak:keycloak-authz-client` +* Java policy enforcer - Maven artifact `org.keycloak:keycloak-policy-enforcer` + +It is possible that in the future, some more libraries will be included. + +The client libraries are supported with Java 8, so it is possible to use them with the client applications deployed on the older application servers. + +== Compatibility of the client libraries with the server + +Beginning with this release, we are testing and supporting client libraries with the same server version and a few previous major server versions. + +For details about supported versions of client libraries with server versions, see the link:{upgradingclientlibs_link}[{upgradingclientlibs_name}]. + += User sessions persisted by default + +{project_name} 25 introduced the feature `persistent-user-sessions`. With this feature enabled all user sessions are persisted in the database as opposed to the previous behavior where only offline sessions were persisted. +In {project_name} 26, this feature is enabled by default. This means that all user sessions are persisted in the database by default. + +It is possible to revert this behavior to the previous state by disabling the feature. Follow the `Volatile user sessions` section in https://www.keycloak.org/server/caching[Configuring distributed caches] guide for more details. + +For information on how to upgrade, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += New default login theme + +There is now a new version (`v2`) of the `keycloak` login theme, which provides an improved look and feel, including support for switching automatically to a dark theme based on user preferences. + +The previous version (`v1`) is now deprecated, and will be removed in a future release. + +For all new realms, `keycloak.v2` will be the default login theme. Also, any existing realm that never explicitly set a login theme will be switched to `keycloak.v2`. + += Highly available multi-site deployments + +{project_name} 26 introduces significant improvements to the recommended HA multi-site architecture, most notably: + +- {project_name} deployments are now able to handle user requests simultaneously in both sites. + +- Active monitoring of the connectivity between the sites is now required to update the replication between the sites in case of a failure. + +- The loadbalancer blueprint has been updated to use the AWS Global Accelerator as this avoids prolonged fail-over times caused by DNS caching by clients. + +- Persistent user sessions are now a requirement of the architecture. Consequently, user sessions will be kept +on {project_name} or {jdgserver_name} upgrades. + +For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Admin Bootstrapping and Recovery + +In the past, regaining access to a {project_name} instance when all admin users were locked out was a challenging and complex process. Recognizing these challenges and aiming to significantly enhance the user experience, {project_name} now offers several straightforward methods to bootstrap a temporary admin account and recover lost admin access. + +It is now possible to run the `start` or `start-dev` commands with specific options to create a temporary admin account. Additionally, a new dedicated command has been introduced, which allows users to regain admin access without hassle. + +For detailed instructions and more information on this topic, refer to the link:{bootstrapadminrecovery_link}[{bootstrapadminrecovery_name}] guide. + += OpenTelemetry Tracing preview + +The underlying Quarkus support for OpenTelemetry Tracing has been exposed to {project_name} and allows obtaining application traces for better observability. +It helps to find performance bottlenecks, determine the cause of application failures, trace a request through the distributed system, and much more. +The support is in preview mode, and we would be happy to obtain any feedback. + +For more information, see the link:{tracingguide_link}[{tracingguide_name}] guide. + +ifeval::[{project_community}==true] += OpenID for Verifiable Credential Issuance + +The OpenID for Verifiable Credential Issuance (OID4VCI) is still an experimental feature in {project_name}, but it was greatly improved in this release. You will find significant development and discussions +in the https://github.com/keycloak/kc-sig-fapi[Keycloak OAuth SIG]. Anyone from the Keycloak community is welcome to join. + +Many thanks to all members of the OAuth SIG group for the participation on the development and discussions about this feature. Especially thanks to the +https://github.com/francis-pouatcha[Francis Pouatcha], https://github.com/Captain-P-Goldfish[Pascal Knüppel], https://github.com/tnorimat[Takashi Norimatsu], +https://github.com/IngridPuppet[Ingrid Kamga], https://github.com/wistefan[Stefan Wiedemann] and https://github.com/thomasdarimont[Thomas Darimont] +endif::[] + += DPoP improvements + +The DPoP (OAuth 2.0 Demonstrating Proof-of-Possession) preview feature has improvements. The DPoP is now supported for all grant types. +With previous releases, this feature was supported only for the `authorization_code` grant type. Support also exists for the DPoP token type on the UserInfo endpoint. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/Captain-P-Goldfish[Pascal Knüppel] for the contribution. +endif::[] + += Removal of GELF logging handler + +GELF support has been deprecated for a while now, and with this release it has been finally removed from {project_name}. +Other log handlers are available and fully supported to be used as a replacement of GELF, for example Syslog. For details +see the https://www.keycloak.org/server/logging[Logging guide]. + += Lightweight access tokens for Admin REST API + +Lightweight access tokens can now be used on the admin REST API. The `security-admin-console` and `admin-cli` clients are now using lightweight access tokens by default, so “Always Use Lightweight Access Token” and “Full Scope Allowed” are now enabled on these two clients. However, the behavior in the admin console should effectively remain the same. Be cautious if you have made changes to these two clients and if you are using them for other purposes. + += Keycloak JavaScript adapter now standalone + +Keycloak JavaScript adapter is now a standalone library and is therefore no longer served statically from the Keycloak server. The goal is to de-couple the library from the Keycloak server, so that it can be refactored independently, simplifying the code and making it easier to maintain in the future. Additionally, the library is now free of third-party dependencies, which makes it more lightweight and easier to use in different environments. + +For a complete breakdown of the changes consult the link:{upgradingguide_link}[{upgradingguide_name}]. + += Hostname v1 feature removed + +The deprecated hostname v1 feature was removed. This feature was deprecated in {project_name} 25 and replaced by hostname v2. If you are still using this feature, you must migrate to hostname v2. For more details, see the https://www.keycloak.org/server/hostname[Configuring the hostname (v2)] and https://www.keycloak.org/docs/latest/upgrading/#new-hostname-options[the initial migration guide]. + += Automatic redirect from root to relative path + +User is automatically redirected to the path where {project_name} is hosted when the `http-relative-path` property is specified. +It means when the relative path is set to `/auth`, and the user access `localhost:8080/`, the page is redirected to `localhost:8080/auth`. + +The same applies to the management interface when the `http-management-relative-path` or `http-relative-path` property is specified. + +It improves user experience as users no longer need to set the relative path to the URL explicitly. + += Persisting revoked access tokens across restarts + +In this release, revoked access tokens are written to the database and reloaded when the cluster is restarted by default when using the embedded caches. + +For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Client Attribute condition in Client Policies + +The condition based on the client-attribute was added into Client Policies. You can use condition to specify for the clients +with the specified client attribute having a specified value. It is possible to use either an AND or OR condition when evaluating this condition as mentioned in the documentation +for client policies. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/y-tabata[Yoshiyuki Tabata] for the contribution. +endif::[] + += Specify different log levels for log handlers + +It is possible to specify log levels for all available log handlers, such as `console`, `file`, or `syslog`. +The more fine-grained approach provides the ability to control logging over the whole application and be tailored to your needs. + +For more information, see the https://www.keycloak.org/server/logging[Logging guide]. + += Proxy option removed + +The deprecated `proxy` option was removed. This option was deprecated in {project_name} 24 and replaced by the `proxy-headers` option in combination with hostname options as needed. For more details, see https://www.keycloak.org/server/reverseproxy[using a reverse proxy] and https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[the initial migration guide]. + += Option `proxy-trusted-addresses` added + +The `proxy-trusted-addresses` can be used when the `proxy-headers` option is set to specify a allowlist of trusted proxy addresses. If the proxy address for a given request is not trusted, then the respective proxy header values will not be used. + += Option `proxy-protocol-enabled` added + +The `proxy-protocol-enabled` option controls whether the server should use the HA PROXY protocol when serving requests from behind a proxy. When set to true, the remote address returned will be the one from the actual connecting client. + += Option to reload trust and key material added + +The `https-certificates-reload-period` option can be set to define the reloading period of key store, trust store, and certificate files referenced by https-* options. Use -1 to disable reloading. Defaults to 1h (one hour). + += Options to configure cache max-count added + +The `--cache-embedded-$\{CACHE_NAME}-max-count=` can be set to define an upper bound on the number of cache entries in the specified cache. + += The `https-trust-store-*` options have been undeprecated + +Based on the community feedback, we decided to undeprecate `https-trust-store-*` options to allow better granularity in trusted certificates. + += The `java-keystore` key provider supports more algorithms and vault secrets + +The `java-keystore` key provider, which allows loading a realm key from an external java keystore file, has been modified to manage all {project_name} algorithms. Besides, the keystore and key secrets, needed to retrieve the actual key from the store, can be configured using the link:{adminguide_link}#_vault-administration[vault]. Therefore a {project_name} realm can externalize any key to the encrypted file without sensitive data stored in the database. + +For more information about this subject, see link:{adminguide_link}#realm_keys[Configuring realm keys]. + += Adding support for ECDH-ES encryption key management algorithms + +Now {project_name} allows configuring ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW as the encryption key management algorithm for clients. The Key Agreement with Elliptic Curve Diffie-Hellman Ephemeral Static (ECDH-ES) specification introduces three new header parameters for the JWT: `epk`, `apu` and `apv`. Currently {project_name} implementation only manages the compulsory `epk` while the other two (which are optional) are never added to the header. For more information about those algorithms please refer to the link:https://datatracker.ietf.org/doc/html/rfc7518#section-4.6[JSON Web Algorithms (JWA)]. + +Also, a new key provider, `ecdh-generated`, is available to generate realm keys and support for ECDH algorithms is added into the Java KeyStore provider. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/justin-tay[Justin Tay] for the contribution. +endif::[] + += Support for multiple instances of a social broker in a realm + +It is now possible to have multiple instances of the same social broker in a realm. + +Most of the time a realm does not need multiple instances of the same social broker. But due to the introduction +of the `organization` feature, it should be possible to link different instances of the same social broker +to different organizations. + +When creating a social broker, you should now provide an `Alias` and optionally a `Display name` just like any other +broker. + += New generalized event types for credentials + +There are now generalized events for updating (`UPDATE_CREDENTIAL`) and removing (`REMOVE_CREDENTIAL`) a credential. The credential type is described in the `credential_type` attribute of the events. The new event types are supported by the Email Event Listener. + +The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR` + += Customizable Footer in login Themes + +The `template.ftl` file in the `base/login` and the `keycloak.v2/login` theme now allows to customize the footer +of the login box. This can be used to show common links or include custom scripts at the end of the page. + +The new `footer.ftl` template provides a `content` macro that is rendered at the bottom of the "login box". + += Keycloak CR supports standard scheduling options + +The Keycloak CR now exposes first class properties for controlling the scheduling of your Keycloak Pods. + +For more details, see the +https://www.keycloak.org/operator/advanced-configuration[Operator Advanced Configuration]. + += KeycloakRealmImport CR supports placeholder replacement + +The KeycloakRealmImport CR now exposes `spec.placeholders` to create environment variables for placeholder replacement in the import. + +For more details, see the +https://www.keycloak.org/operator/realm-import[Operator Realm Import]. + += Configuring the LDAP Connection Pool + +In this release, the LDAP connection pool configuration relies solely on system properties. + +For more details, see link:{adminguide_link}#_ldap_connection_pool[Configuring the connection pool]. + += Infinispan marshalling changes to Infinispan Protostream + +Marshalling is the process of converting Java objects into bytes to send them across the network between {project_name} servers. +With {project_name} 26, we changed the marshalling format from JBoss Marshalling to Infinispan Protostream. + +WARNING: JBoss Marshalling and Infinispan Protostream are not compatible with each other and incorrect usage may lead to data loss. +Consequently, all caches are cleared when upgrading to this version. + +Infinispan Protostream is based on https://protobuf.dev/programming-guides/proto3/[Protocol Buffers] (proto 3), which has the advantage of backwards/forwards compatibility. + += Removal of OSGi metadata + +Since all of the Java adapters that used OSGi metadata have been removed we have stopped generating OSGi metadata for our jars. + += Group-related events no longer fired when removing a realm + +With the goal of improving the scalability of groups, they are now removed directly from the database when removing a realm. +As a consequence, group-related events like the `GroupRemovedEvent` are no longer fired when removing a realm. + +For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Identity Providers no longer available from the realm representation + +As part of the improvements around the scalability of realms and organizations when they have many identity providers, the realm representation +no longer holds the list of identity providers. However, they are still available from the realm representation +when exporting a realm. + +For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + +ifeval::[{project_community}==true] += Securing Applications documentation converted into the guide format + +The _Securing Applications and Services_ documentation was converted into the new format similar to the _Server Installation and Configuration_ documentation converted in the previous releases. +The documentation is now available under https://www.keycloak.org/guides[Keycloak Guides]. +endif::[] + += Removal of legacy cookies + +Keycloak no longer sends `_LEGACY` cookies, which where introduced as a work-around to older browsers not supporting +the `SameSite` flag on cookies. + +The `_LEGACY` cookies also served another purpose, which was to allow login from an insecure context. Although, this is +not recommended at all in production deployments of Keycloak, it is fairly frequent to access Keycloak over `http` outside +of `localhost`. As an alternative to the `_LEGACY` cookies Keycloak now doesn't set the `secure` flag and sets `SameSite=Lax` +instead of `SameSite=None` when it detects an insecure context is used. + += Property `origin` in the `UserRepresentation` is deprecated + +The `origin` property in the `UserRepresentation` is deprecated and planned to be removed in future releases. + +Instead, prefer using the `federationLink` property to obtain the provider to which a user is linked with. diff --git a/docs/documentation/release_notes/topics/26_0_6.adoc b/docs/documentation/release_notes/topics/26_0_6.adoc new file mode 100644 index 000000000000..84a512846a5a --- /dev/null +++ b/docs/documentation/release_notes/topics/26_0_6.adoc @@ -0,0 +1,6 @@ += Updates to documentation of X.509 client certificate lookup via proxy + +Potential vulnerable configurations have been identified in the X.509 client certificate lookup when using a reverse proxy. +Additional configuration steps might be required depending on your current configuration. Make sure to review the updated +link:{client_certificate_lookup_link}[reverse proxy guide] if you have configured +the client certificate lookup via a proxy header. diff --git a/docs/documentation/release_notes/topics/26_1_0.adoc b/docs/documentation/release_notes/topics/26_1_0.adoc new file mode 100644 index 000000000000..80b00b3043e5 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_1_0.adoc @@ -0,0 +1,189 @@ += Transport stack `jdbc-ping` as new default + +{project_name} now uses by default its database to discover other nodes of the same cluster, which removes the need of additional network related configurations especially for cloud providers. It is also a default that will work out-of-the-box in cloud environments. + +Previous versions of {project_name} used as a default UDP multicast to discover other nodes to form a cluster and to synchronize the replicated caches of {project_name}. +This required multicast to be available and to be configured correctly, which is usually not the case in cloud environments. + +Starting with this version, the default changes to the `jdbc-ping` configuration which uses {project_name}'s database to discover other nodes. +As this removes the need for multicast network capabilities and UDP and no longer using dynamic ports for the TCP-based failure detection, this is a simplification and a drop-in replacement for environments which used the previous default. +To enable the previous behavior, choose the transport stack `udp` which is now deprecated. + +The {project_name} Operator will continue to configure `kubernetes` as a transport stack. + +See the https://www.keycloak.org/server/caching[Configuring distributed caches] guide for more information. + += Virtual Threads enabled for Infinispan and JGroups thread pools + +Starting from this release, {project_name} automatically enables the virtual thread pool support in both the embedded Infinispan and JGroups when running on OpenJDK 21 for environments with at least 2 CPU cores available. +This removes the need to configure the JGroups thread pool, the need to align the JGroups thread pool with the HTTP worker thread pool, and reduces the overall memory footprint. + += OpenTelemetry Tracing supported + +In the previous release, the OpenTelemetry Tracing feature was preview and is *fully supported* now. +It means the `opentelemetry` feature is enabled by default. + +There were made multiple improvements to the tracing capabilities in {project_name} such as: + +* *Configuration via Keycloak CR* in {project_name} Operator +* *Custom spans* for: +** Incoming/outgoing HTTP requests including Identity Providers brokerage +** Database operations and connections +** LDAP requests +** Time-consuming operations (passwords hashing, persistent sessions operations, ...) + +For more information, see the link:{tracingguide_link}[{tracingguide_name}] guide. + += Infinispan default XML configuration location + +Previous releases ignored any change to `conf/cache-ispn.xml` if the `--cache-config-file` option was not provided. + +Starting from this release, when `--cache-config-file` is not set, the default Infinispan XML configuration file is `conf/cache-ispn.xml` as this is both the expected behavior and the implied behavior given the docs of the current and previous releases. + += Individual options for category-specific log levels + +It is now possible to set category-specific log levels as individual `log-level-category` options. + +For more details, see the https://www.keycloak.org/server/logging#_configuring_levels_as_individual_options[Logging guide]. + += OpenID for Verifiable Credential Issuance + +The OpenID for Verifiable Credential Issuance (OID4VCI) remains an experimental feature in {project_name}, but it has great improvements in this release. +This feature benefits from much polishing of the existing configuration and making the feature more dynamic and customizable. + +You will find significant development and discussions in the https://github.com/keycloak/kc-sig-fapi[Keycloak OAuth SIG]. Anyone from the Keycloak community is welcome to join. + +Many thanks to all members of the OAuth SIG group for the participation in the development and discussions about this feature. Especially thanks to +https://github.com/francis-pouatcha[Francis Pouatcha], https://github.com/IngridPuppet[Ingrid Kamga], https://github.com/Captain-P-Goldfish[Pascal Knüppel], +https://github.com/thomasdarimont[Thomas Darimont], https://github.com/Ogenbertrand[Ogen Bertrand], https://github.com/Awambeng[Awambeng Rodrick] and https://github.com/tnorimat[Takashi Norimatsu]. + += Minimum ACR Value for the client + +The option *Minimum ACR value* is added as a configuration option on the realm OIDC clients. This addition is an enhancement related to step-up authentication, which makes it possible +to enforce minimum ACR level when logging in to the particular client. + +Many thanks to https://github.com/sonOfRa[Simon Levermann] for the contribution. + += Support for prompt=create + +Support now exists for the https://openid.net/specs/openid-connect-prompt-create-1_0.html[Initiating user registration standard], which allows OIDC clients to initiate the login request with +the parameter `prompt=create` to notify {project_name} that a new user should be registered rather than an existing user authenticated. Initiating user registration was already supported in {project_name} with the use of dedicated endpoint `/realms//protocol/openid-connect/registrations`. +However, this endpoint is now deprecated in favor of the standard way as it was a proprietary solution specific to {project_name}. + +Many thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. + += Option to create certificates for generated EC keys + +A new option, *Generate certificate*, exists for EC-DSA and Ed-DSA key providers. When the generated key is created by a realm administrator, a +certificate might be generated for this key. The certificate information is available in the Admin Console and in the JWK representation of this key, which is available +from JWKS endpoint with the realm keys. + +Many thanks to https://github.com/Captain-P-Goldfish[Pascal Knüppel] for the contribution. + += Authorization Code Binding to a DPoP Key + +Support now exists for https://datatracker.ietf.org/doc/html/rfc9449#section-10[Authorization Code Binding to a DPoP Key] including support for the DPoP with Pushed Authorization Requests. + +Many thanks to https://github.com/tnorimat[Takashi Norimatsu] for the contribution. + += Maximum count and length for additional parameters sent to OIDC authentication request + +The OIDC authentication request supports a limited number of additional custom parameters of maximum length. The additional parameters can be +used for custom purposes (for example, adding the claims into the token with the use of the protocol mappers). In the previous versions, the maximum count of +the parameters was hardcoded to 5 and the maximum length of the parameters was hardcoded to 2000. Now both values are configurable. Additionally it can be possible to configure +if additional parameters cause a request to fail or if parameters are ignored. + +Many thanks to https://github.com/mschallar[Manuel Schallar] and https://github.com/patrick-primesign[Patrick Weiner] for the contribution. + += Network Policy support added to the {project_name} Operator + +NOTE: Preview feature. + +To improve the security of your Kubernetes deployment, https://kubernetes.io/docs/concepts/services-networking/network-policies/[Network Policies] can be specified in your {project_name} CR. +The {project_name} Operator accepts the ingress rules, which define from where the traffic is allowed to come from, and automatically creates the necessary Network Policies. + += LDAP users are created as enabled by default when using Microsoft Active Directory + +If you are using Microsoft AD and creating users through the administrative interfaces, the user will be created as enabled by default. + +In previous versions, it was only possible to update the user status after setting a (non-temporary) password to the user. +This behavior was not consistent with other built-in user storages as well as not consistent with other LDAP vendors supported +by the LDAP provider. + += New conditional authenticators `Condition - sub-flow executed` and `Condition - client scope` + +The *Condition - sub-flow executed* and *Condition - client scope* are new conditional authenticators in {project_name}. The condition *Condition - sub-flow executed* checks if a previous sub-flow was +executed (or not executed) successfully during the authentication flow execution. The condition *Condition - client scope* checks if a configured client scope is present as a client scope of the +client requesting authentication. For more details, see link:{adminguide_link}#conditions-in-conditional-flows[Conditions in conditional flows]. + += Defining dependencies between provider factories + +When developing extensions for {project_name}, developers can now specify dependencies between provider factories classes by implementing the method `dependsOn()` in the `ProviderFactory` interface. +See the Javadoc for a detailed description. + += Dark mode enabled for the welcome theme + +We've now enabled dark mode support for all the `keycloak` themes. This feature was previously present in the admin console, account console and login, and is now also available on the welcome page. If a user indicates their preference through an operating system setting (e.g. light or dark mode) or a user agent setting, the theme will automatically follow these preferences. + +If you are using a custom theme that extends any of the `keycloak` themes and are not yet ready to support dark mode, or have styling conflicts that prevent you from implementing dark mode, you can disable support by adding the following property to your theme: + +[source,properties] +---- +darkMode=false +---- + +Alternatively, you can disable dark mode support for the built-in Keycloak themes on a per-realm basis by turning off the *Dark mode* setting under the *Theme* tab in the realm settings. + += Metrics on password hashing + +There is a new metric available counting how many password validations were performed by {project_name}. +This allows you to better assess where CPU resources are used, and can feed into your sizing calculations. + +See https://www.keycloak.org/observability/metrics-for-troubleshooting-http[Keycloak metrics] and https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing#_measuring_the_activity_of_a_running_keycloak_instance[Concepts for sizing CPU and memory resources] for more details. + += Sign out all active sessions in admin console now effectively removes all sessions + +In previous versions, clicking on *Sign out all active sessions* in the admin console resulted in the removal of regular sessions only. Offline sessions would still be displayed despite being effectively invalidated. + +This has been changed. Now all sessions, regular and offline, are removed when signing out of all active sessions. + += Dedicated release cycle for the Node.js adapter and JavaScript adapter + +From this release onwards, the {project_name} JavaScript adapter and {project_name} Node.js adapter will have a release cycle independent of the {project_name} server release cycle. The 26.1.0 release may be the last one +where these adapters are released together with the {project_name} server, but from now on, these adapters may be released at a different time than the {project_name} server. + += Updates in quickstarts + +The {project_name} quickstarts are now using `main` as the base branch. The `latest` branch, used previously, is removed. The `main` branch depends on the +last released version of the {project_name} server, {project_name} client libraries, and adapters. As a result, contributions to the quickstarts are immediately visible to quickstart +consumers with no need to wait for the next {project_name} server release. + += Updated format of KEYCLOAK_SESSION cookie and AUTH_SESSION_ID cookie + +The format of `KEYCLOAK_SESSION` cookie was slightly updated to not contain any private data in plain text. Until now, the format of the cookie was `realmName/userId/userSessionId`. Now the cookie contains user session ID, which is hashed by SHA-256 and URL encoded. + + +The format of `AUTH_SESSION_ID` cookie was updated to include a signature of the auth session id to ensure its integrity through signature verification. The new format is `base64(auth_session_id.auth_session_id_signature)`. With this update, the old format will no longer be accepted, meaning that old auth sessions will no longer be valid. This change has no impact on user sessions. + +These changes can affect you just in case when implementing your own providers and relying on the format of internal Keycloak cookies. + += Removal of robots.txt file + +The `robots.txt` file, previously included by default, is now removed. The default `robots.txt` file blocked all crawling, which prevented the `noindex`/`nofollow` directives from being followed. The desired default behaviour is for {project_name} pages to not show up in search engine results and this is accomplished by the existing `X-Robots-Tag` header, which is set to `none` by default. The value of this header can be overridden per-realm if a different behaviour is needed. + +If you previously added a rule in your reverse proxy configuration for this, you can now remove it. + += Imported key providers check and passivate keys with an expired cetificate + +The key providers that allow to import externally generated keys (`rsa` and `java-keystore` factories) now check the validity of the associated certificate if present. Therefore a key with a certificate that is expired cannot be imported in {project_name} anymore. If the certificate expires at runtime, the key is converted into a passive key (enabled but not active). A passive key is not used for new tokens, but it is still valid for validating previous issued tokens. + +The default `generated` key providers generate a certificate valid for 10 years (the types that have or can have an associated certificate). Because of the long validity and the recommendation to rotate keys frequently, the generated providers do not perform this check. + += Admin events might include now additional details about the context when the event is fired + +In this release, admin events might hold additional details about the context when the event is fired. When upgrading you should +expect the database schema being updated to add a new column `DETAILS_JSON` to the `ADMIN_EVENT_ENTITY` table. + += OpenShift v3 identity brokering removed + +As OpenShift v3 reached end-of-life a while back, support for identity brokering with OpenShift v3 has been removed from Keycloak. diff --git a/docs/documentation/release_notes/topics/26_1_1.adoc b/docs/documentation/release_notes/topics/26_1_1.adoc new file mode 100644 index 000000000000..1bf7ece2bc41 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_1_1.adoc @@ -0,0 +1,11 @@ += New option in X.509 authenticator to abort authentication if CRL is outdated + +The X.509 authenticator has a new option `x509-cert-auth-crl-abort-if-non-updated` (*CRL abort if non updated* in the Admin Console) to abort the login if a CRL is configured to validate the certificate and the CRL is not updated in the time specified in the next update field. The new option defaults to `true` in the Admin Console. For more details about the CRL next update field, see link:https://datatracker.ietf.org/doc/html/rfc5280#section-5.1.2.5[RFC5280, Section-5.1.2.5]. + +The value `false` is maintained for compatibility with the previous behavior. Note that existing configurations will not have the new option and will act as if this option was set to `false`, but the Admin Console will add the default value `true` on edit. + += New option in Send Reset Email to force a login after reset credentials + +The `reset-credential-email` (*Send Reset Email*) is the authenticator used in the *reset credentials* flow (*forgot password* feature) for sending the email to the user with the reset credentials token link. This authenticator now has a new option `force-login` (*Force login after reset*). When this option is set to `true`, the authenticator terminates the session and forces a new login. + +For more details about this new option, see link:{adminguide_link}#enabling-forgot-password[Enable forgot password]. \ No newline at end of file diff --git a/docs/documentation/release_notes/topics/26_1_3.adoc b/docs/documentation/release_notes/topics/26_1_3.adoc new file mode 100644 index 000000000000..c428b5bb8837 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_1_3.adoc @@ -0,0 +1,5 @@ += Send Reset Email force login again for federated users after reset credentials + +In <> a new configuration option was added to the `reset-credential-email` (*Send Reset Email*) authenticator to allow changing the default behavior after the reset credentials flow. Now the option `force-login` (*Force login after reset*) is adding a third configuration value `only-federated`, which means that the force login is true for federated users and false for the internal database users. The new behavior is now the default. This way all users managed by user federation providers, whose implementation can be not so tightly integrated with {project_name}, are forced to login again after the reset credentials flow to avoid any issue. This change in behavior is due to the secure by default policy. + +For more information, see link:{adminguide_link}#enabling-forgot-password[Enable forgot password]. \ No newline at end of file diff --git a/docs/documentation/release_notes/topics/26_2_0.adoc b/docs/documentation/release_notes/topics/26_2_0.adoc new file mode 100644 index 000000000000..1dd1f133e774 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_2_0.adoc @@ -0,0 +1,173 @@ += Supported Standard Token Exchange + +In this release, we added support for the Standard token exchange! The token exchange feature was in preview for a long time, so we are glad to finally support the standard token exchange. +For now, this is limited to exchanging the Internal token to internal token compliant with the https://datatracker.ietf.org/doc/html/rfc8693[Token exchange specification]. It does not yet cover use +cases related to identity brokering or subject impersonation. We hope to support even more token exchange use cases in subsequent releases. + +For more details, see the link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard token exchange]. + +For information on how to upgrade from the legacy token exchange used in previous {project_name} versions, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Fine-grained admin permissions supported + +This release introduces support for a new version of fine-grained admin permissions. Version 2 (V2) provides enhanced flexibility and control over administrative access within realms. +With this feature, administrators can define permissions for administering users, groups, clients, and roles without relying on broad administrative roles. V2 offers the same level of access control over realm resources as the previous version, with plans to extend its capabilities in future versions. Some key points follow: + +* *Centralized Admin Console Management* - New *Permissions* section was introduced to allow management from a single place without having to navigate to different places in the Admin Console. +* *Improved manageability* - Administrators can more easily search and evaluate permissions when building a permission model for realm resources. +* *Resource-Specific and Global Permissions* – Permissions can be defined for individual resources (such as specific users or groups), or entire resource types (such as all users or all groups). +* *Explicit Operation Scoping* – Permissions are now independent, removing hidden dependencies between operations. Administrators must assign each scope explicitly, making it easier to see what is granted without needing prior knowledge of implicit relationships. +* *Per-Realm Enablement* – Fine-Grained Admin Permissions can be enabled on a per-realm basis, allowing greater control over adoption and configuration. + +For more details, see link:{adminguide_finegrained_link}[{adminguide_finegrained_name}]. + +For more information about migration, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Guides for metrics and Grafana dashboards + +In addition to the list of useful metric names link:{observablitycategory_link}[the Observability guides category] now also contains a guide on how to display these metrics in Grafana. +link:{grafanadashboards_link}[The guide] contains two dashboards. + +* Keycloak troubleshooting dashboard - showing metrics related to service level indicators and troubleshooting. +* Keycloak capacity planning dashboard - showing metrics related to estimating the load handled by Keycloak. + += Zero-configuration secure cluster communication + +For clustering multiple nodes, {project_name} uses distributed caches. +Starting with this release for all TCP-based transport stacks, the communication between the nodes is encrypted with TLS and secured with automatically generated ephemeral keys and certificates. + +This strengthens a secure-by-default setup and minimizes the configuration steps of new setups. + +For more information, check the link:https://www.keycloak.org/server/caching#_securing_transport_stacks[Securing Transport Stacks] in the distributed caches guide. + += Rolling updates for optimized and customized images + +When using an optimized or customized image, the {project_name} Operator can now perform a rolling update for a new image if the old and the new image contain the same version of {project_name}. +This is helpful when you want to roll out, for example, an updated theme or provider without downtime. + +To use the functionality in the Operator, enable the `Auto` update strategy and the {project_name} Operator will on image change briefly start up the old and the new image to determine if a rolling update without downtime is possible. +Read the section https://www.keycloak.org/operator/rolling-updates[Managing Rolling Updates] in the {project_name} Operator Advanced Configuration guide for more details on this functionality. + +The checks to determine if a rolling update is possible are also available on the {project_name} command line so you can use them in your deployment pipeline. Continue reading in the https://www.keycloak.org/server/update-compatibility[Update Compatibility Tool] guide for more information about the functionality available on the command line. + += Metrics on user activities + +Event metrics provide admins an aggregated view of the different user activities in a Keycloak instance. +For now, only metrics for user events are captured. For example, you can monitor the number of logins, login failures, or token refreshes performed. +For more information, see Monitoring user activities with event metrics. + +While this was a preview feature in 26.1, this is now fully supported in 26.2. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/bohmber[Bernd Bohmann] for the contribution. +endif::[] + +For more information, check the link:https://www.keycloak.org/observability/event-metrics[Monitoring user activities with event metrics] {section}. + += Additional query parameters in Admin Events API + +The Admin Events API now supports filtering for events based on Epoc timestamps in addition to the previous +`yyyy-MM-dd` format. This provides more fine-grained control of the window of events to retrieve. + +A `direction` query parameter was also added, allowing controlling the order of returned items as `asc` or +`desc`. In the past the events where always returned in `desc` order (most recent events first). + +Finally, the returned event representations now also include the `id`, which provides a unique identifier for +an event. + += Logs support ECS format + +All available log handlers now support *ECS* (Elastic Common Schema) JSON format. +It helps to improve {project_name}'s observability story and centralized logging. + +For more details, see the https://www.keycloak.org/server/logging[Logging guide]. + += New cache for CRLs loaded for the X.509 authenticator + +Now the Certificate Revocation Lists (CRL), that are used to validate certificates in the X.509 authenticator, are cached inside a new infinispan cache called `crl`. Caching improves the validation performance and decreases the memory consumption because just one CRL is maintained per source. + +Check the `crl-storage` section in the link:https://www.keycloak.org/server/all-provider-config[All provider configuration] {section} to know the options for the new cache provider. + += Operator creates NetworkPolicies to restrict traffic + +The {project_name} Operator now creates by default a NetworkPolicy to restrict traffic to internal ports used for {project_name}'s distributed caches. + +This strengthens a secure-by-default setup and minimizes the configuration steps of new setups. + +You can restrict the access to the management and HTTP endpoints further using the Kubernetes NetworkPolicies rule syntax. + +Read more about this in the https://www.keycloak.org/operator/advanced-configuration[Operator Advanced configuration]. + += Option to reload trust and key material for the management interface + +The `https-management-certificates-reload-period` option can be set to define the reloading period of key store, trust store, and certificate files referenced by `https-management-*` options for the management interface. +Use -1 to disable reloading. Defaults to `https-certificates-reload-period`, which defaults to 1h (one hour). + +For more information, check the link:https://www.keycloak.org/server/management-interface#_tls_support[Configuring the Management Interface] guide. + += Dynamic Authentication Flow selection using Client Policies + +Introduced the ability to dynamically select authentication flows based on conditions such as requested scopes, ACR (Authentication Context Class Reference) and others. +This can be achieved using link:{adminguide_link}#_client_policies[Client Policies] by combining the new `AuthenticationFlowSelectorExecutor` with conditions like the new `ACRCondition`. For more details, see the link:{adminguide_link}#_client-policy-auth-flow[{adminguide_name}]. + += JWT Client authentication aligned with the latest OIDC specification + +The latest version of the link:https://openid.net/specs/openid-connect-core-1_0-36.html#rfc.section.9[OpenID Connect Core Specification] tightened the rules for +audience validation in JWT client assertions for the Client Authentication methods `private_key_jwt` and `client_secret_jwt` . {project_name} now enforces by default that there is single audience +in the JWT token used for client authentication. + +For information on the changed audience validation in JWT Client authentication {project_name} versions, see the link:{upgradingguide_link}[{upgradingguide_name}]. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution. +endif::[] + += Federated credentials are available now when fetching user credentials + +Until now, querying user credentials using the User API will not return credentials managed by user storage providers and, as a consequence, +prevent fetching additional metadata associated with federated credentials like the last time a credential was updated. + +In this release, we are adding a new method `getCredentials(RealmModel, UserModel)` to the `org.keycloak.credential.CredentialInputUpdater` interface so that +user storage providers can return the credentials they manage for a specific user in a realm. By doing this, user storage providers can indicate +whether the credential is linked to it as well as provide additional metadata so that additional information can be shown when managing users through the administration console. + +For LDAP, it should be possible now to see the last time the password was updated based on the standard `pwdChangedTime` attribute or, if +using Microsoft AD, based on the `pwdLastSet` attribute. + +In order to check if a credential is local - managed by {project_name} - or federated, you can check the `federationLink` property available from both +`CredentialRepresentation` and `CredentialModel` types. If set, the `federationLink` property holds the UUID of the component model associated with a given +user storage provider. + += Token based authentication for SMTP (XOAUTH2) + +The Keycloak outgoing link:{adminguide_email_link}[SMTP mail configuration] now supports token authentication (XOAUTH2). +Many service providers (Microsoft, Google) are moving towards SMTP OAuth authentication and end the support for basic authentication. +The token is gathered using Client Credentials Grant. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/srose[Sebastian Rose] for the contribution. +endif::[] + += New client configuration for access token header type + +A new admin setting has been added: Clients -> Advanced -> Fine grain OpenID Connect configuration -> Use "at+jwt" as access token header type + +If enabled, access tokens will get header type `at+jwt` in compliance with https://datatracker.ietf.org/doc/html/rfc9068#section-2.1[rfc9068#section-2.1]. Otherwise, the access token header type will be `JWT`. + +This setting is turned off by default. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/laurids[Laurids Møller Jepsen] for the contribution. +endif::[] + +ifeval::[{project_community}==true] += OpenID for Verifiable Credential Issuance documentation + +The OpenID for Verifiable Credential Issuance (OID4VCI) remains an experimental feature in {project_name}, but it received further improvements and especially the link:{adminguide_link}#_oid4vci[The documentation], +with the steps how to try this feature. + +You will find significant development and discussions in the https://github.com/keycloak/kc-sig-fapi[Keycloak OAuth SIG]. Anyone from the Keycloak community is welcome to join and provide the feedback. + +Many thanks to all members of the OAuth SIG group for the participation in the development and discussions about this feature. Especially thanks to +https://github.com/Awambeng[Awambeng Rodrick] and https://github.com/IngridPuppet[Ingrid Kamga]. +endif::[] diff --git a/docs/documentation/release_notes/topics/26_3_0.adoc b/docs/documentation/release_notes/topics/26_3_0.adoc new file mode 100644 index 000000000000..4a725e99e73e --- /dev/null +++ b/docs/documentation/release_notes/topics/26_3_0.adoc @@ -0,0 +1,88 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + +This release delivers advancements to optimize your system and improve the experience of users, developers and administrators: + +* *Account recovery* with 2FA recovery codes, protecting users from lockout. +* Simplified experiences for application developers with *streamlined WebAuthn/Passkey registration* and *simplified account linking* to identity providers via application initiated actions. +* Broader connectivity with the ability to *broker with any OAuth 2.0 compliant authorization server*, and enhanced *trusted email verification* for OpenID Connect providers. +* *Asynchronous logging* for higher throughput and lower latency, ensuring more efficient deployments. +* For administrators, *experimental rolling updates for patch releases* mean minimized downtime and smoother upgrades. + +Read on to learn more about each new feature, and https://www.keycloak.org/docs/latest/upgrading/index.html[find additional details in the upgrading guide] if you are upgrading from a previous release of {project_name}. + += Recovering your account if you lose your 2FA credentials + +When using for example a one-time-password (OTP) generators as a second factor for authenticating users (2FA), a user can get locked out of their account when they, for example, lose their phone that contains the OTP generator. +To prepare for such a case, the recovery codes feature allows users to print a set of recovery codes as an additional second factor. +If the recovery codes are then allowed as an alternative 2FA in the login flow, they can be used instead of the OTP generated passwords. + +With this release, the recovery codes feature is promoted from preview to a supported feature. +For newly created realms, the browser flow now includes the Recovery Authentication Code Form as _Disabled_, and it can be switched to _Alternative_ by admins if they want to use this feature. + +For more information about this 2FA method, see the link:{adminguide_link}#_recovery-codes[Recovery Codes] chapter in the {adminguide_name}. + += Performance improvements to import, export and migration + +The time it takes to run imports, exports or migrations involving a large number of realms has been improved. There is no longer a cumulative performance degradation for each additional realm processed. + += Simplified registration for WebAuthn and Passkeys + +Both WebAuthn Register actions (`webauthn-register` and `webauthn-register-passwordless`) which are also used for Passkeys now support a parameter `skip_if_exists` when initiated by the application (AIA). + +This should make it more convenient to use the AIA in scenarios where a user has already set up WebAuthn or Passkeys. +The parameter allows skipping the action if the user already has a credential of that type. + +For more information, see the link:{adminguide_link}#_webauthn_aia[Registering WebAuthn credentials using AIA] chapter in the {adminguide_name}. + += Simplified linking of the user account to an identity provider + +Client-initiated linking a user account to the identity provider is now based on application-initiated action (AIA) implementation. +This functionality aligns configuring this functionality and simplifies the error handling the calling of the client application, +making it more useful for a broader audience. + +The custom protocol, which was previously used for client-initiated account linking, is now deprecated. + += Brokering with OAuth v2 compliant authorization servers + +In previous releases {project_name} already supported federation with other OpenID Connect and SAML providers, as well as with several Social Providers like GitHub and Google which are based on OAuth 2.0. + +The new OAuth 2.0 broker now closes the gap to federate with any OAuth 2.0 provider. +This then allows you to federate, for example, with Amazon or other providers. +As this is a generic provider, you will need to specify the different claims and a user info endpoint in the provider's configuration. + +For more information, see the link:{adminguide_link}#_identity_broker_oauth[OAuth v2 identity providers] chapter in the {adminguide_name}. + += Trusted email verification when brokering OpenID Connect Providers + +Until now, the OpenID Connect broker did not support the standard `email_verified` claim available from the ID Tokens issued by OpenID Connect Providers. + +Starting with this release, {project_name} supports this standard claim as defined by the https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[OpenID Connect Core Specification] for federation. + +Whenever users are federated for the first time or re-authenticating and if the *Trust email* setting is enabled, *Sync Mode* is set to `FORCE` and the provider sends the `email_verified` claim, the user account will have their email marked according to the `email_verified` claim. +If the provider does not send the claim, it defaults to the original behavior and sets the email as verified. + += Asynchronous logging for higher throughput and lower latency + +All available log handlers now support asynchronous logging capabilities. +Asynchronous logging helps deployments that require high throughput and low latency. + +For more details on this opt-in feature, see the https://www.keycloak.org/server/logging[Logging guide]. + += Rolling updates for patch releases for minimized downtime (preview) + +In the previous release, the Keycloak Operator was enhanced to support performing rolling updates of the Keycloak image if both images contain the same version. +This is useful, for example, when switching to an optimized image, changing a theme or a provider source code. + +In this release, we extended this to perform rolling update when the new image contains a future patch release from the same `major.minor` release stream as a preview feature. +This can reduce the service's downtime even further, as downtime is only needed when upgrading from a different minor or major version. + +Read more on how to enable this feature in https://www.keycloak.org/server/update-compatibility#rolling-updates-for-patch-releases[update compatibility command]. + += Passkeys integrated in the default username forms + +In this release {project_name} integrates *Passkeys* in the default authentications forms. A new switch *Enable Passkeys* is available in the configuration, *Authentication* → *Policies* → *Webauthn Passwordless Policy*, that seamlessly incorporates passkeys support to the realm. With just one click, {project_name} offers conditional and modal user interfaces in the default login forms to allow users to authenticate with a passkey. + +The *Passkeys* feature is still in preview. Follow the https://www.keycloak.org/server/features[Enabling and disabling features] {section} to enable it. + +For more information, see link:{adminguide_link}#passkeys_server_administration_guide[Passkeys section in the {adminguide_name}]. diff --git a/docs/documentation/release_notes/topics/26_4_0.adoc b/docs/documentation/release_notes/topics/26_4_0.adoc new file mode 100644 index 000000000000..b029f60620c0 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_4_0.adoc @@ -0,0 +1,22 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + +Read on to learn more about each new feature, and https://www.keycloak.org/docs/latest/upgrading/index.html[find additional details in the upgrading guide] if you are upgrading from a previous release of {project_name}. + += Supported Update Email Workflow + +The Update Email Workflow is now a supported feature. The feature provides a more secure and consistent flow to update user emails +because they will be forced to re-authenticate as well as verify their emails before any update to their account. + +For more information, see the link:{adminguide_link}#_update-email-workflow[Update Email Workflow] chapter in the {adminguide_name}. + += Option to force management interface to use HTTP. + +There's a new option `http-management-scheme` that may be set to `http` to force the management interface to use HTTP rather than inheriting the HTTPS settings of the main interface. + += Additional context information for log messages (preview) + +You can now add context information to each log message like the realm or the client that initiated the request. +This helps you to track down a warning or error message in the log to a specific caller or environment + +For more details on this opt-in feature, see the https://www.keycloak.org/server/logging[Logging guide]. diff --git a/docs/documentation/release_notes/topics/template.adoc b/docs/documentation/release_notes/topics/template.adoc new file mode 100644 index 000000000000..d7b239ac2ddf --- /dev/null +++ b/docs/documentation/release_notes/topics/template.adoc @@ -0,0 +1,4 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + += diff --git a/docs/documentation/securing_apps/.asciidoctorconfig b/docs/documentation/securing_apps/.asciidoctorconfig deleted file mode 100644 index 16c70ad6ae06..000000000000 --- a/docs/documentation/securing_apps/.asciidoctorconfig +++ /dev/null @@ -1,2 +0,0 @@ -// show images in the preview when using an IDE like IntelliJ -:imagesdir: {asciidoctorconfigdir} \ No newline at end of file diff --git a/docs/documentation/securing_apps/docinfo-footer.html b/docs/documentation/securing_apps/docinfo-footer.html deleted file mode 120000 index a39d3bd0f6de..000000000000 --- a/docs/documentation/securing_apps/docinfo-footer.html +++ /dev/null @@ -1 +0,0 @@ -../aggregation/navbar.html \ No newline at end of file diff --git a/docs/documentation/securing_apps/docinfo.html b/docs/documentation/securing_apps/docinfo.html deleted file mode 120000 index 14514f94d24b..000000000000 --- a/docs/documentation/securing_apps/docinfo.html +++ /dev/null @@ -1 +0,0 @@ -../aggregation/navbar-head.html \ No newline at end of file diff --git a/docs/documentation/securing_apps/index.adoc b/docs/documentation/securing_apps/index.adoc deleted file mode 100644 index 1540014600e0..000000000000 --- a/docs/documentation/securing_apps/index.adoc +++ /dev/null @@ -1,16 +0,0 @@ -:toc: -:toclevels: 3 -:numbered: -:linkattrs: - -include::topics/templates/document-attributes.adoc[] - -:secure_applications_and_services_guide: - -= {adapterguide_name} - -:release_header_guide: {adapterguide_name_short} -:release_header_latest_link: {adapterguide_link_latest} -include::topics/templates/release-header.adoc[] - -include::topics.adoc[] \ No newline at end of file diff --git a/docs/documentation/securing_apps/pom.xml b/docs/documentation/securing_apps/pom.xml deleted file mode 100644 index 6d8dce060ebe..000000000000 --- a/docs/documentation/securing_apps/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - - org.keycloak.documentation - documentation-parent - 999.0.0-SNAPSHOT - ../pom.xml - - - Securing Applications and Services - securing-apps - pom - - - - - org.keycloak.documentation - header-maven-plugin - - - add-file-headers - - - - - org.asciidoctor - asciidoctor-maven-plugin - - - asciidoc-to-html - - - - - maven-antrun-plugin - - - echo-output - - - - - - diff --git a/docs/documentation/securing_apps/topics.adoc b/docs/documentation/securing_apps/topics.adoc deleted file mode 100644 index 19b68ac4145a..000000000000 --- a/docs/documentation/securing_apps/topics.adoc +++ /dev/null @@ -1,49 +0,0 @@ -include::topics/overview/overview.adoc[] - -include::topics/overview/basic-steps.adoc[] - -include::topics/overview/getting-started.adoc[] - -include::topics/overview/terminology.adoc[] - -include::topics/oidc/oidc-overview.adoc[] - -include::topics/oidc/available-endpoints.adoc[] - -include::topics/oidc/supported-grant-types.adoc[] - -ifeval::[{project_community}==true] -include::topics/oidc/java/java-adapters.adoc[] -endif::[] -ifeval::[{project_product}==true] -include::topics/oidc/java/java-adapters-product.adoc[] -endif::[] - -include::topics/oidc/javascript-adapter.adoc[] - -include::topics/oidc/nodejs-adapter.adoc[] - -ifeval::[{project_community}==true] -include::topics/oidc/mod-auth-openidc.adoc[] -endif::[] - -include::topics/oidc/fapi-support.adoc[] - -include::topics/oidc/recommendations.adoc[] - -include::topics/saml/saml-overview.adoc[] -ifeval::[{project_community}==true] -include::topics/saml/java/java-adapters.adoc[] -include::topics/saml/mod-auth-mellon.adoc[] -endif::[] -ifeval::[{project_product}==true] -include::topics/saml/java/java-adapters-product.adoc[] -endif::[] - -include::topics/docker/docker-overview.adoc[] -include::topics/client-registration.adoc[] -include::topics/client-registration/client-registration-cli.adoc[] -ifeval::[{project_community}==true] -include::topics/token-exchange/token-exchange.adoc[] -endif::[] - diff --git a/docs/documentation/securing_apps/topics/docker/docker-overview.adoc b/docs/documentation/securing_apps/topics/docker/docker-overview.adoc deleted file mode 100644 index 136a640abd0f..000000000000 --- a/docs/documentation/securing_apps/topics/docker/docker-overview.adoc +++ /dev/null @@ -1,63 +0,0 @@ - -== Configuring a Docker registry to use {project_name} - -NOTE: Docker authentication is disabled by default. To enable see the https://www.keycloak.org/server/features[Enabling and disabling features] {section}. - -This section describes how you can configure a Docker registry to use {project_name} as its authentication server. - -For more information on how to set up and configure a Docker registry, see the link:https://distribution.github.io/distribution/about/configuration/[Docker Registry Configuration Guide]. - - - -=== Docker registry configuration file installation - -For users with more advanced Docker registry configurations, it is generally recommended to provide your own registry configuration file. The {project_name} Docker provider supports this mechanism via the _Registry Config File_ Format Option. Choosing this option will generate output similar to the following: - -[source,subs="attributes+"] ----- -auth: - token: - realm: http://localhost:8080{kc_realms_path}/master/protocol/docker-v2/auth - service: docker-test - issuer: http://localhost:8080{kc_realms_path}/master ----- - -This output can then be copied into any existing registry config file. See the link:https://distribution.github.io/distribution/about/configuration/[registry config file specification] for more information on how the file should be set up, or start with link:https://github.com/distribution/distribution/blob/main/cmd/registry/config-example.yml[a basic example]. - -WARNING: Don't forget to configure the `rootcertbundle` field with the location of the {project_name} realm's public key. The auth configuration will not work without this argument. - - -=== Docker registry environment variable override installation - -Often times it is appropriate to use a simple environment variable override for develop or POC Docker registries. While this approach is usually not recommended for production use, it can be helpful when one requires quick-and-dirty way to stand up a registry. Simply use the _Variable Override_ Format Option from the client details, and an output should appear like the one below: - -[source,subs="attributes+"] ----- -REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080{kc_realms_path}/master/protocol/docker-v2/auth -REGISTRY_AUTH_TOKEN_SERVICE: docker-test -REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080{kc_realms_path}/master ----- - -WARNING: Don't forget to configure the `REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE` override with the location of the {project_name} realm's public key. The auth configuration will not work without this argument. - - -=== Docker Compose YAML File - -WARNING: This installation method is meant to be an easy way to get a docker registry authenticating against a {project_name} server. It is intended for development purposes only and should never be used in a production or production-like environment. - -The zip file installation mechanism provides a quickstart for developers who want to understand how the {project_name} server can interact with the Docker registry. In order to configure: - -.Procedure - - 1. From the desired realm, create a client configuration. At this point you will not have a Docker registry - the quickstart will take care of that part. - 2. Choose the "Docker Compose YAML" option from the from _Action_ menu and select the *Download adapter config* option to download the ZIP file. - 3. Unzip the archive to the desired location, and open the directory. - 4. Start the Docker registry with `docker-compose up` - -NOTE: it is recommended that you configure the Docker registry client in a realm other than 'master', since the HTTP Basic auth flow will not present forms. - -Once the above configuration has taken place, and the keycloak server and Docker registry are running, docker authentication should be successful: - - [user ~]# docker login localhost:5000 -u $username - Password: ******* - Login Succeeded diff --git a/docs/documentation/securing_apps/topics/oidc/java/adapter-context.adoc b/docs/documentation/securing_apps/topics/oidc/java/adapter-context.adoc deleted file mode 100644 index 7d74a2fcb7ba..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/adapter-context.adoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Security Context - -The `KeycloakSecurityContext` interface is available if you need to access to the tokens directly. This could be useful if you want to retrieve additional -details from the token (such as user profile information) or you want to invoke a RESTful service that is protected by {project_name}. - -In servlet environments it is available in secured invocations as an attribute in HttpServletRequest: -[source,java] ----- -httpServletRequest - .getAttribute(KeycloakSecurityContext.class.getName()); ----- - -Or, it is available in insecured requests in the HttpSession: - -[source,java] ----- -httpServletRequest.getSession() - .getAttribute(KeycloakSecurityContext.class.getName()); ----- - diff --git a/docs/documentation/securing_apps/topics/oidc/java/adapter-deprecation-notice.adoc b/docs/documentation/securing_apps/topics/oidc/java/adapter-deprecation-notice.adoc deleted file mode 100644 index 4bce350a6516..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/adapter-deprecation-notice.adoc +++ /dev/null @@ -1,5 +0,0 @@ -[WARNING] -==== -This adapter is deprecated and will be removed in a future release of {project_name}. No further enhancements or new features -will be added to this adapter. -==== \ No newline at end of file diff --git a/docs/documentation/securing_apps/topics/oidc/java/adapter_error_handling.adoc b/docs/documentation/securing_apps/topics/oidc/java/adapter_error_handling.adoc deleted file mode 100644 index 39731ecd9106..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/adapter_error_handling.adoc +++ /dev/null @@ -1,35 +0,0 @@ - -[[_adapter_error_handling]] -==== Error handling - -{project_name} has some error handling facilities for servlet based client adapters. -When an error is encountered in authentication, {project_name} will call `HttpServletResponse.sendError()`. -You can set up an error-page within your `web.xml` file to handle the error however you want. -{project_name} can throw 400, 401, 403, and 500 errors. - -[source,xml] ----- - - 403 - /ErrorHandler - ----- - -{project_name} also sets a `HttpServletRequest` attribute that you can retrieve. -The attribute name is `org.keycloak.adapters.spi.AuthenticationError`, which should be cast to `org.keycloak.adapters.OIDCAuthenticationError`. - -For example: - -[source,java] ----- -import org.keycloak.adapters.OIDCAuthenticationError; -import org.keycloak.adapters.OIDCAuthenticationError.Reason; -... - -OIDCAuthenticationError error = (OIDCAuthenticationError) httpServletRequest - .getAttribute('org.keycloak.adapters.spi.AuthenticationError'); - -Reason reason = error.getReason(); -System.out.println(reason.name()); ----- - diff --git a/docs/documentation/securing_apps/topics/oidc/java/application-clustering.adoc b/docs/documentation/securing_apps/topics/oidc/java/application-clustering.adoc deleted file mode 100644 index bc772c9b580a..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/application-clustering.adoc +++ /dev/null @@ -1,117 +0,0 @@ -[[_applicationclustering]] -==== Application clustering - -ifeval::[{project_community}==true] -This chapter is related to supporting clustered applications deployed to JBoss EAP, WildFly and JBoss AS. -endif::[] -ifeval::[{project_product}==true] -This chapter is related to supporting clustered applications deployed to JBoss EAP. -endif::[] - -There are a few options available depending on whether your application is: - -* Stateless or stateful -* Distributable (replicated http session) or non-distributable -* Relying on sticky sessions provided by load balancer -* Hosted on same domain as {project_name} - -Dealing with clustering is not quite as simple as for a regular application. Mainly due to the fact that both the browser and the server-side application -sends requests to {project_name}, so it's not as simple as enabling sticky sessions on your load balancer. - -===== Stateless token store - -By default, the web application secured by {project_name} uses the HTTP session to store security context. This means that you either have to -enable sticky sessions or replicate the HTTP session. - -As an alternative to storing the security context in the HTTP session the adapter can be configured to store this in a cookie instead. This is useful if you want -to make your application stateless or if you don't want to store the security context in the HTTP session. - -To use the cookie store for saving the security context, edit your applications `WEB-INF/keycloak.json` and add: -[source,json] ----- -"token-store": "cookie" ----- - -NOTE: The default value for `token-store` is `session`, which stores the security context in the HTTP session. - -One limitation of using the cookie store is that the whole security context is passed in the cookie for every HTTP request. This may impact performance. - -Another small limitation is limited support for Single-Sign Out. It works without issues if you init servlet logout (HttpServletRequest.logout) from the -application itself as the adapter will delete the KEYCLOAK_ADAPTER_STATE cookie. However, back-channel logout initialized from a different application isn't -propagated by {project_name} to applications using cookie store. Hence it's recommended to use a short value for the access token timeout (for example 1 minute). - -NOTE: Some load balancers do not allow any configuration of the sticky session cookie name or contents, such as Amazon ALB. For these, it is recommended to set the `shouldAttachRoute` option to `false`. - -===== Relative URI optimization - -In deployment scenarios where {project_name} and the application is hosted on the same domain (through a reverse proxy or load balancer) it can be -convenient to use relative URI options in your client configuration. - -With relative URIs the URI is resolved as relative to the URL used to access {project_name}. - -For example if the URL to your application is `$$https://acme.org/myapp$$` and the URL to {project_name} is `\https://acme.org{kc_base_path}`, then you can use -the redirect-uri `/myapp` instead of `$$https://acme.org/myapp$$`. - -===== Admin URL configuration - -Admin URL for a particular client can be configured in the {project_name} Admin Console. -It's used by the {project_name} server to send backend requests to the application for various tasks, like logout users or push revocation policies. - -For example the way backchannel logout works is: - -. User sends logout request from one application -. The application sends logout request to {project_name} -. The {project_name} server invalidates the user session -. The {project_name} server then sends a backchannel request to application with an admin url that are associated with the session -. When an application receives the logout request it invalidates the corresponding HTTP session - -If admin URL contains `${application.session.host}` it will be replaced with the URL to the node associated with the HTTP session. - -[[_registration_app_nodes]] -===== Registration of application nodes - -The previous section describes how {project_name} can send logout request to node associated with a specific HTTP session. -However, in some cases admin may want to propagate admin tasks to all registered cluster nodes, not just one of them. -For example to push a new not before policy to the application or to log out all users from the application. - -In this case {project_name} needs to be aware of all application cluster nodes, so it can send the event to all of them. -To achieve this, we support auto-discovery mechanism: - -. When a new application node joins the cluster, it sends a registration request to the {project_name} server -. The request may be re-sent to {project_name} in configured periodic intervals -. If the {project_name} server doesn't receive a re-registration request within a specified timeout then it automatically unregisters the specific node -. The node is also unregistered in {project_name} when it sends an unregistration request, which is usually during node shutdown or application undeployment. - This may not work properly for forced shutdown when undeployment listeners are not invoked, which results in the need for automatic unregistration - -Sending startup registrations and periodic re-registration is disabled by default as it's only required for some clustered applications. - -To enable the feature edit the `WEB-INF/keycloak.json` file for your application and add: - -[source,json] ----- -"register-node-at-startup": true, -"register-node-period": 600, ----- - -This means the adapter will send the registration request on startup and re-register every 10 minutes. - -In the {project_name} Admin Console you can specify the maximum node re-registration timeout (should be larger than _register-node-period_ from -the adapter configuration). You can also manually add and remove cluster nodes in through the Admin Console, which is useful if you don't want to rely -on the automatic registration feature or if you want to remove stale application nodes in the event you're not using the automatic unregistration feature. - -[[_refresh_token_each_req]] -===== Refresh token in each request - -By default the application adapter will only refresh the access token when it's expired. However, you can also configure the adapter to refresh the token on every -request. This may have a performance impact as your application will send more requests to the {project_name} server. - -To enable the feature edit the `WEB-INF/keycloak.json` file for your application and add: - -[source,json] ----- -"always-refresh-token": true ----- - -NOTE: This may have a significant impact on performance. Only enable this feature if you can't rely on backchannel messages to propagate logout and not before - policies. Another thing to consider is that by default access tokens has a short expiration so even if logout is not propagated the token will expire within - minutes of the logout. diff --git a/docs/documentation/securing_apps/topics/oidc/java/client-authentication.adoc b/docs/documentation/securing_apps/topics/oidc/java/client-authentication.adoc deleted file mode 100644 index 496f9ae717db..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/client-authentication.adoc +++ /dev/null @@ -1,82 +0,0 @@ -[[_client_authentication_adapter]] -==== Client authentication - -When a confidential OIDC client needs to send a backchannel request (for example, to exchange code for the token, or to refresh the token) it needs to authenticate against the {project_name} server. By default, there are three ways to authenticate the client: client ID and client secret, client authentication with signed JWT, or client authentication with signed JWT using client secret. - -===== Client ID and Client Secret - -This is the traditional method described in the OAuth2 specification. The client has a secret, which needs to be known to both the adapter (application) and the {project_name} server. -You can generate the secret for a particular client in the {project_name} Admin Console, and then paste this secret into the `keycloak.json` file on the application side: - - -[source,json] ----- -"credentials": { - "secret": "19666a4f-32dd-4049-b082-684c74115f28" -} ----- - -===== Client authentication with Signed JWT - -This is based on the https://datatracker.ietf.org/doc/html/rfc7523[RFC7523] specification. It works this way: - -* The client must have the private key and certificate. For {project_name} this is available through the traditional `keystore` file, which is either available on the client application's classpath or somewhere on the file system. - -* Once the client application is started, it allows to download its public key in https://datatracker.ietf.org/doc/html/rfc7517[JWKS] format using a URL such as \http://myhost.com/myapp/k_jwks, assuming that \http://myhost.com/myapp is the base URL of your client application. This URL can be used by {project_name} (see below). - -* During authentication, the client generates a JWT token and signs it with its private key and sends it to {project_name} in -the particular backchannel request (for example, code-to-token request) in the `client_assertion` parameter. - -* {project_name} must have the public key or certificate of the client so that it can verify the signature on JWT. In {project_name} you need to configure client credentials for your client. First you need to choose `Signed JWT` as the method of authenticating your client in the tab `Credentials` in the Admin Console. -Then you can choose to either in the tab `Keys`: -** Configure the JWKS URL where {project_name} can download the client's public keys. This can be a URL such as \http://myhost.com/myapp/k_jwks (see details above). This option is the most flexible, since the client can rotate its keys anytime and {project_name} then always downloads new keys when needed without needing to change the configuration. More accurately, {project_name} downloads new keys when it sees the token signed by an unknown `kid` (Key ID). -** Upload the client's public key or certificate, either in PEM format, in JWK format, or from the keystore. With this option, the public key is hardcoded and must be changed when the client generates a new key pair. -You can even generate your own keystore from the {project_name} Admin Console if you don't have your own available. -For more details on how to set up the {project_name} Admin Console, see the {adminguide_link}[{adminguide_name}]. - -For set up on the adapter side you need to have something like this in your `keycloak.json` file: - -[source,json] ----- -"credentials": { - "jwt": { - "client-keystore-file": "classpath:keystore-client.jks", - "client-keystore-type": "JKS", - "client-keystore-password": "storepass", - "client-key-password": "keypass", - "client-key-alias": "clientkey", - "token-expiration": 10 - } -} ----- - -With this configuration, the keystore file `keystore-client.jks` must be available on classpath in your WAR. If you do not use the prefix `classpath:` -you can point to any file on the file system where the client application is running. - -ifeval::[{project_community}==true] -For inspiration, you can take a look at the examples distribution into the main demo example into the `product-portal` application. - -===== Client authentication with Signed JWT using client secret - -This is the same as Client Authentication with Signed JWT except for using the client secret instead of the private key and certificate. - -The client has a secret, which needs to be known to both the adapter (application) and the {project_name} server. You need to choose `Signed JWT with Client Secret` as the method of authenticating your client in the tab `Credentials` in the Admin Console, and then paste this secret into the `keycloak.json` file on the application side: - -[source,json] ----- -"credentials": { - "secret-jwt": { - "secret": "19666a4f-32dd-4049-b082-684c74115f28", - "algorithm": "HS512" - } -} ----- - -The "algorithm" field specifies the algorithm for Signed JWT using Client Secret. It needs to be one of the following values : HS256, HS384, and HS512. For details, please refer to https://datatracker.ietf.org/doc/html/rfc7518#section-3.2[JSON Web Algorithms (JWA)]. - -This "algorithm" field is optional so that HS256 is applied automatically if the "algorithm" field does not exist on the `keycloak.json` file. - -===== Add your own client authentication method - -You can add your own client authentication method as well. You will need to implement both client-side and server-side providers. For more details see the `Authentication SPI` section in link:{developerguide_link}[{developerguide_name}]. -endif::[] diff --git a/docs/documentation/securing_apps/topics/oidc/java/installed-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/installed-adapter.adoc deleted file mode 100644 index 0c89b0780f38..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/installed-adapter.adoc +++ /dev/null @@ -1,164 +0,0 @@ -[[_installed_adapter]] -==== CLI / Desktop Applications - -{project_name} supports securing desktop -(for example Swing, JavaFX) or CLI applications via the -`KeycloakInstalled` adapter by performing the authentication step via the system browser. - -The `KeycloakInstalled` adapter supports a `desktop` and a `manual` -variant. The desktop variant uses the system browser -to gather the user credentials. The manual variant -reads the user credentials from `STDIN`. - -===== How it works - -To authenticate a user with the `desktop` variant the `KeycloakInstalled` -adapter opens a desktop browser window where a user uses the regular {project_name} -login pages to log in when the `loginDesktop()` method is called on the `KeycloakInstalled` object. - -The login page URL is opened with redirect parameter -that points to a local `ServerSocket` listening on a free ephemeral port -on `localhost` which is started by the adapter. - -After a successful login the `KeycloakInstalled` receives the authorization code -from the incoming HTTP request and performs the authorization code flow. -Once the code to token exchange is completed the `ServerSocket` is shutdown. - -TIP: If the user already has an active {project_name} session then -the login form is not shown but the code to token exchange is continued, -which enables a smooth Web based SSO experience. - -The client eventually receives the tokens (access_token, refresh_token, -id_token) which can then be used to call backend services. - -The `KeycloakInstalled` adapter provides support for renewal of stale tokens. - -[[_installed_adapter_installation]] -===== Installing the adapter - -[source,xml,subs="attributes+"] ----- - - - - org.keycloak - keycloak-installed-adapter - {project_versionMvn} - - ----- - - -===== Client configuration - -The application needs to be configured as a `public` OpenID Connect client with -`Standard Flow Enabled` and pass:[http://localhost] as an allowed `Valid Redirect URI`. - -TIP: The `KeycloakInstalled` adapter supports the `PKCE` [RFC 7636] mechanism to provide additional protection during -code to token exchanges in the `OIDC` protocol. PKCE can be enabled with the `"enable-pkce": true` setting -in the adapter configuration. Enabling PKCE is highly recommended, to avoid code injection and code replay attacks. - -===== Usage - -The `KeycloakInstalled` adapter reads its configuration from -`META-INF/keycloak.json` on the classpath. Custom configurations -can be supplied with an `InputStream` or a `KeycloakDeployment` -through the `KeycloakInstalled` constructor. - -In the example below, the client configuration for `desktop-app` -uses the following `keycloak.json`: - -[source,json,subs="attributes+"] ----- - -{ - "realm": "desktop-app-auth", - "auth-server-url": "http://localhost:8081{kc_base_path}", - "ssl-required": "external", - "resource": "desktop-app", - "public-client": true, - "use-resource-role-mappings": true, - "enable-pkce": true -} - ----- - -the following sketch demonstrates working with the `KeycloakInstalled` adapter: -[source,java] ----- - -// reads the configuration from classpath: META-INF/keycloak.json -KeycloakInstalled keycloak = new KeycloakInstalled(); - -// opens desktop browser -keycloak.loginDesktop(); - -AccessToken token = keycloak.getToken(); -// use token to send backend request - -// ensure token is valid for at least 30 seconds -long minValidity = 30L; -String tokenString = keycloak.getTokenString(minValidity, TimeUnit.SECONDS); - - - // when you want to logout the user. -keycloak.logout(); - ----- - -TIP: The `KeycloakInstalled` class supports customization of the http responses returned by -login / logout requests via the `loginResponseWriter` and `logoutResponseWriter` attributes. - -===== Example - -The following provides an example for the configuration mentioned above. - -[source,java] ----- -import java.util.Locale; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import org.keycloak.adapters.installed.KeycloakInstalled; -import org.keycloak.representations.AccessToken; - -public class DesktopApp { - - public static void main(String[] args) throws Exception { - - KeycloakInstalled keycloak = new KeycloakInstalled(); - keycloak.setLocale(Locale.ENGLISH); - keycloak.loginDesktop(); - - AccessToken token = keycloak.getToken(); - Executors.newSingleThreadExecutor().submit(() -> { - - System.out.println("Logged in..."); - System.out.println("Token: " + token.getSubject()); - System.out.println("Username: " + token.getPreferredUsername()); - try { - System.out.println("AccessToken: " + keycloak.getTokenString()); - } catch (Exception ex) { - ex.printStackTrace(); - } - - int timeoutSeconds = 20; - System.out.printf("Logging out in...%d Seconds%n", timeoutSeconds); - try { - TimeUnit.SECONDS.sleep(timeoutSeconds); - } catch (Exception e) { - e.printStackTrace(); - } - - try { - keycloak.logout(); - } catch (Exception e) { - e.printStackTrace(); - } - - System.out.println("Exiting..."); - System.exit(0); - }); - } -} ----- diff --git a/docs/documentation/securing_apps/topics/oidc/java/jaas.adoc b/docs/documentation/securing_apps/topics/oidc/java/jaas.adoc deleted file mode 100644 index 2493818c918e..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/jaas.adoc +++ /dev/null @@ -1,37 +0,0 @@ -[[_jaas_adapter]] -==== JAAS plugin - -include::adapter-deprecation-notice.adoc[] - -It's generally not needed to use JAAS for most of the applications, especially if they are HTTP based, and you should most likely choose one of our other adapters. -However, some applications and systems may still rely on pure legacy JAAS solution. -{project_name} provides two login modules to help in these situations. - -The provided login modules are: - -org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule:: - This login module allows to authenticate with username/password from {project_name}. - It's using <<_resource_owner_password_credentials_flow,Resource Owner Password Credentials>> flow to validate if the provided - username/password is valid. It's useful for non-web based systems, which need to rely on JAAS and want to use {project_name}, but can't use the standard browser - based flows due to their non-web nature. Example of such application could be messaging or SSH. - -org.keycloak.adapters.jaas.BearerTokenLoginModule:: - This login module allows to authenticate with {project_name} access token passed to it through CallbackHandler as password. - It may be useful for example in case, when you have {project_name} access token from standard based authentication flow and your web application then - needs to talk to external non-web based system, which rely on JAAS. For example a messaging system. - -Both modules use the following configuration properties: - -keycloak-config-file:: - The location of the `keycloak.json` configuration file. The configuration file can either be located on the filesystem or on the classpath. If it's located - on the classpath you need to prefix the location with `classpath:` (for example `classpath:/path/keycloak.json`). - This is _REQUIRED._ - -`role-principal-class`:: - Configure alternative class for Role principals attached to JAAS Subject. - Default value is `org.keycloak.adapters.jaas.RolePrincipal`. Note: The class is required to have a constructor with a single `String` argument. - -`scope`:: - This option is only applicable to the `DirectAccessGrantsLoginModule`. The specified value will be used as the OAuth2 `scope` - parameter in the Resource Owner Password Credentials Grant request. - diff --git a/docs/documentation/securing_apps/topics/oidc/java/java-adapter-config.adoc b/docs/documentation/securing_apps/topics/oidc/java/java-adapter-config.adoc deleted file mode 100644 index 4fcecc97ccfd..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/java-adapter-config.adoc +++ /dev/null @@ -1,286 +0,0 @@ - -[[_java_adapter_config]] -==== Configuration - -Each Java adapter supported by {project_name} can be configured by a simple JSON file. -This is what one might look like: - -[source,json,subs="attributes+"] ----- -{ - "realm" : "demo", - "resource" : "customer-portal", - "realm-public-key" : "MIGfMA0GCSqGSIb3D...31LwIDAQAB", - "auth-server-url" : "https://localhost:8443{kc_base_path}", - "ssl-required" : "external", - "use-resource-role-mappings" : false, - "enable-cors" : true, - "cors-max-age" : 1000, - "cors-allowed-methods" : "POST, PUT, DELETE, GET", - "cors-exposed-headers" : "WWW-Authenticate, My-custom-exposed-Header", - "bearer-only" : false, - "enable-basic-auth" : false, - "expose-token" : true, - "verify-token-audience" : true, - "credentials" : { - "secret" : "234234-234234-234234" - }, - - "connection-pool-size" : 20, - "socket-timeout-millis" : 5000, - "connection-timeout-millis" : 6000, - "connection-ttl-millis" : 500, - "disable-trust-manager" : false, - "allow-any-hostname" : false, - "truststore" : "path/to/truststore.jks", - "truststore-password" : "geheim", - "client-keystore" : "path/to/client-keystore.jks", - "client-keystore-password" : "geheim", - "client-key-password" : "geheim", - "token-minimum-time-to-live" : 10, - "min-time-between-jwks-requests" : 10, - "public-key-cache-ttl" : 86400, - "redirect-rewrite-rules" : { - "^/wsmaster/api/(.*)$" : "/api/$1" - } -} ----- - -You can use `${...}` enclosure for system property replacement. For example `${jboss.server.config.dir}` would be replaced by `/path/to/{project_name}`. -Replacement of environment variables is also supported via the `env` prefix, for example `${env.MY_ENVIRONMENT_VARIABLE}`. You can also define a default value `${...:default_value}`, which is used if the system property `${jboss.server.config.dir:default_value}` or the environment variable `${env.MY_ENVIRONMENT_VARIABLE:default_value}` is absent. - -The initial config file can be obtained from the admin console. This can be done by opening the admin console, select `Clients` from the menu and clicking -on the corresponding client. Once the page for the client is opened click on the `Installation` tab and select `Keycloak OIDC JSON`. - -Here is a description of each configuration option: - -realm:: - - _REQUIRED._ - Name of the realm. - -resource:: - _REQUIRED._ The client-id of the application. Each application has a client-id that is used to identify the application. - -realm-public-key:: - _OPTIONAL_ and it's not recommended to set it. PEM format of the realm public key. You can obtain this from the Admin Console. - If not set, the adapter will download this from {project_name} and - it will always re-download it when needed (e.g. {project_name} rotates its keys). However if realm-public-key is set, then adapter - will never download new keys from {project_name}, so when {project_name} rotate its keys, adapter will break. - -auth-server-url:: - _REQUIRED._ The base URL of the {project_name} server. All other {project_name} pages and REST service endpoints are derived from this. It is usually of the form `\https://host:port{kc_base_path}`. - -ssl-required:: - _OPTIONAL_. Ensures that all communication to and from the {project_name} server is over HTTPS. - In production this should be set to `all`. - - The default value is _external_ meaning that HTTPS is required by default for external requests. - Valid values are 'all', 'external' and 'none'. - -confidential-port:: - _OPTIONAL_. The confidential port used by the {project_name} server for secure connections over SSL/TLS. - The default value is _8443_. - -use-resource-role-mappings:: - _OPTIONAL_. - If set to true, the adapter will look inside the token for application level role mappings for the user. If false, it will look at the realm level for user role mappings. - The default value is _false_. - -public-client:: - _OPTIONAL_. If set to true, the adapter will not send credentials for the client to {project_name}. - The default value is _false_. - -enable-cors:: - _OPTIONAL_. This enables CORS support. It will handle CORS preflight requests. It will also look into the access token to determine valid origins. - The default value is _false_. - -cors-max-age:: - _OPTIONAL_. - If CORS is enabled, this sets the value of the `Access-Control-Max-Age` header. - If not set, this header is not returned in CORS responses. - -cors-allowed-methods:: - _OPTIONAL_. - If CORS is enabled, this sets the value of the `Access-Control-Allow-Methods` header. - This should be a comma-separated string. - If not set, this header is not returned in CORS responses. - -cors-allowed-headers:: - _OPTIONAL_. - If CORS is enabled, this sets the value of the `Access-Control-Allow-Headers` header. - This should be a comma-separated string. - If not set, this header is not returned in CORS responses. - -cors-exposed-headers:: - _OPTIONAL_. - If CORS is enabled, this sets the value of the `Access-Control-Expose-Headers` header. - This should be a comma-separated string. - If not set, this header is not returned in CORS responses. - -bearer-only:: - _OPTIONAL_. - This should be set to _true_ for services. If enabled the adapter will not attempt to authenticate users, but only verify bearer tokens. - The default value is _false_. - -autodetect-bearer-only:: - This should be set to __true__ if your application serves both a web application and web services (for example SOAP or REST). - It allows you to redirect unauthenticated users of the web application to the {project_name} login page, - but send an HTTP `401` status code to unauthenticated SOAP or REST clients instead as they would not understand a redirect to the login page. - {project_name} auto-detects SOAP or REST clients based on typical headers like `X-Requested-With`, `SOAPAction` or `Accept`. - The default value is _false_. - -enable-basic-auth:: - _OPTIONAL_. - This tells the adapter to also support basic authentication. If this option is enabled, then _secret_ must also be provided. - The default value is _false_. - -expose-token:: - _OPTIONAL_. - If `true`, an authenticated browser client (via a JavaScript HTTP invocation) can obtain the signed access token via the URL `root/k_query_bearer_token`. - The default value is _false_. - -credentials:: - _REQUIRED_ only for clients with 'Confidential' access type. Specify the credentials of the application. This is an object notation where the key is the credential type and the value is the value of the credential type. - Currently password and jwt is supported. T - -connection-pool-size:: - _OPTIONAL_. - This config option defines how many connections to the {project_name} server should be pooled. - The default value is `20`. - -socket-timeout-millis:: - _OPTIONAL_. - Timeout for socket waiting for data after establishing the connection in milliseconds. - Maximum time of inactivity between two data packets. - A timeout value of zero is interpreted as an infinite timeout. - A negative value is interpreted as undefined (system default if applicable). - The default value is `-1`. - -connection-timeout-millis:: - Timeout for establishing the connection with the remote host in milliseconds. - A timeout value of zero is interpreted as an infinite timeout. - A negative value is interpreted as undefined (system default if applicable). - The default value is `-1`. - - -connection-ttl-millis:: - _OPTIONAL_. - Connection time-to-live for client in milliseconds. - A value less than or equal to zero is interpreted as an infinite value. - The default value is `-1`. - - -disable-trust-manager:: - _OPTIONAL_. - If the {project_name} server requires HTTPS and this config option is set to `true` you do not have to specify a truststore. - This setting should only be used during development and *never* in production as it will disable verification of SSL certificates. - The default value is `false`. - -allow-any-hostname:: - _OPTIONAL_. - If the {project_name} server requires HTTPS and this config option is set to `true` the {project_name} server's certificate is validated via the truststore, - but host name validation is not done. - This setting should only be used during development and *never* in production as it will disable verification of SSL certificates. - This setting may be useful in test environments This is _OPTIONAL_. - The default value is `false`. - -proxy-url:: - The URL for the HTTP proxy if one is used. - -truststore:: - The value is the file path to a truststore file. - If you prefix the path with `classpath:`, then the truststore will be obtained from the deployment's classpath instead. - Used for outgoing HTTPS communications to the {project_name} server. - Client making HTTPS requests need a way to verify the host of the server they are talking to. - This is what the truststore does. - The keystore contains one or more trusted host certificates or certificate authorities. - You can create this truststore by extracting the public certificate of the {project_name} server's SSL keystore. - _REQUIRED_ unless `ssl-required` is `none` or `disable-trust-manager` is `true`. - -truststore-password:: - Password for the truststore. - _REQUIRED_ if `truststore` is set and the truststore requires a password. - -client-keystore:: - _OPTIONAL_. - This is the file path to a keystore file. - This keystore contains client certificate for two-way SSL when the adapter makes HTTPS requests to the {project_name} server. - -client-keystore-password:: - _REQUIRED_ if `client-keystore` is set. - Password for the client keystore. - -client-key-password:: - _REQUIRED_ if `client-keystore` is set. - Password for the client's key. - -always-refresh-token:: - If _true_, the adapter will refresh token in every request. - Warning - when enabled this will result in a request to {project_name} for every request to your application. - -register-node-at-startup:: - If _true_, then adapter will send registration request to {project_name}. - It's _false_ by default and useful only when application is clustered. - See <<_applicationclustering,Application Clustering>> for details - -register-node-period:: - Period for re-registration adapter to {project_name}. - Useful when application is clustered. - See <<_applicationclustering,Application Clustering>> for details - -token-store:: - Possible values are _session_ and _cookie_. - Default is _session_, which means that adapter stores account info in HTTP Session. - Alternative _cookie_ means storage of info in cookie. - See <<_applicationclustering,Application Clustering>> for details - -token-cookie-path:: - When using a cookie store, this option sets the path of the cookie used to store account info. If it's a relative path, - then it is assumed that the application is running in a context root, and is interpreted relative to that context root. - If it's an absolute path, then the absolute path is used to set the cookie path. Defaults to use paths relative to the context root. - -principal-attribute:: - OpenID Connect ID Token attribute to populate the UserPrincipal name with. - If token attribute is null, defaults to `sub`. - Possible values are `sub`, `preferred_username`, `email`, `name`, `nickname`, `given_name`, `family_name`. - -turn-off-change-session-id-on-login:: - _OPTIONAL_. The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true if you want to turn this off - The default value is _false_. - -token-minimum-time-to-live:: - _OPTIONAL_. - Amount of time, in seconds, to preemptively refresh an active access token with the {project_name} server before it expires. - This is especially useful when the access token is sent to another REST client where it could expire before being evaluated. - This value should never exceed the realm's access token lifespan. - The default value is `0` seconds, so adapter will refresh access token just if it's expired. - -min-time-between-jwks-requests:: - Amount of time, in seconds, specifying minimum interval between two requests to {project_name} to retrieve new public keys. - It is 10 seconds by default. - Adapter will always try to download new public key when it recognizes token with unknown `kid` . However it won't try it more - than once per 10 seconds (by default). This is to avoid DoS when attacker sends lots of tokens with bad `kid` forcing adapter - to send lots of requests to {project_name}. - -public-key-cache-ttl:: - Amount of time, in seconds, specifying maximum interval between two requests to {project_name} to retrieve new public keys. - It is 86400 seconds (1 day) by default. - Adapter will always try to download new public key when it recognizes token with unknown `kid` . If it recognizes token with known `kid`, it will - just use the public key downloaded previously. However at least once per this configured interval (1 day by default) will be new - public key always downloaded even if the `kid` of token is already known. - -ignore-oauth-query-parameter:: - Defaults to `false`, if set to `true` will turn off processing of the `access_token` - query parameter for bearer token processing. Users will not be able to authenticate - if they only pass in an `access_token` - -redirect-rewrite-rules:: - If needed, specify the Redirect URI rewrite rule. This is an object notation where the key is the regular expression to which the Redirect URI is to be matched and the value is the replacement String. - `$` character can be used for backreferences in the replacement String. - -verify-token-audience:: - If set to `true`, then during authentication with the bearer token, the adapter will verify whether the token contains this - client name (resource) as an audience. The option is especially useful for services, which primarily serve requests authenticated - by the bearer token. This is set to `false` by default, however for improved security, it is recommended to enable this. - See link:{adminguide_link}#audience-support[Audience Support] for more details about audience 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 deleted file mode 100644 index 51f69183953d..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/java-adapters-product.adoc +++ /dev/null @@ -1,35 +0,0 @@ -=== {project_name} Java adapters - -==== Red Hat JBoss Enterprise Application Platform - -{project_name} does not include any adapters for Red Hat JBoss Enterprise Application Platform. However, there are -alternatives for existing applications deployed to Red Hat JBoss Enterprise Application Platform. - -===== 8.0 Beta - -Red Hat Enterprise Application Platform 8.0 Beta provides a native OpenID Connect client through the Elytron OIDC client -subsystem. - -For more information, see the https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/8-beta/html/using_single_sign-on_with_jboss_eap/index[Red Hat JBoss Enterprise Application Platform documentation]. - -===== 6.4 and 7.x - -Existing applications deployed to Red Hat JBoss Enterprise Application Platform 6.4 and 7.x can leverage adapters from -Red Hat Single Sign-On 7.6 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]. - - -==== Spring Boot adapter - -{project_name} does not include any adapters for Spring Boot. However, there are -alternatives for existing applications built with Spring Boot. - -Spring Security provides comprehensive support for OAuth 2 and OpenID Connect. For more information, see the -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]. - - diff --git a/docs/documentation/securing_apps/topics/oidc/java/java-adapters.adoc b/docs/documentation/securing_apps/topics/oidc/java/java-adapters.adoc deleted file mode 100644 index 5319f4fcca39..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/java-adapters.adoc +++ /dev/null @@ -1,45 +0,0 @@ -=== {project_name} Java adapters - -[WARNING] -==== -{project_name} Java Adapters are now deprecated and will be removed in a future release. No further enhancements or features will be added to the adapters until then. - -For more details about how to integrate {project_name} with Java applications, consider looking at the -{quickstartRepo_link}[Keycloak Quickstart GitHub Repository]. -==== - -{project_name} comes with a range of different adapters for Java application. Selecting the correct adapter depends on the target platform. - -All Java adapters share a set of common configuration options described in the <<_java_adapter_config,Java Adapters Config>> chapter. - -ifeval::[{project_community}==true] -include::java-adapter-config.adoc[] -endif::[] -include::jboss-adapter.adoc[] - -include::spring-boot-adapter.adoc[] - -ifeval::[{project_community}==true] -include::tomcat-adapter.adoc[] -include::jetty9-adapter.adoc[] -include::spring-security-adapter.adoc[] -endif::[] - -ifeval::[{project_community}==true] -include::servlet-filter-adapter.adoc[] -endif::[] - -ifeval::[{project_community}==true] -include::jaas.adoc[] -include::installed-adapter.adoc[] -endif::[] - -ifeval::[{project_community}==true] -include::adapter-context.adoc[] -include::adapter_error_handling.adoc[] -include::logout.adoc[] -include::params_forwarding.adoc[] -include::client-authentication.adoc[] -include::multi-tenancy.adoc[] -include::application-clustering.adoc[] -endif::[] diff --git a/docs/documentation/securing_apps/topics/oidc/java/jboss-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/jboss-adapter.adoc deleted file mode 100644 index 4e04ff590bc1..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/jboss-adapter.adoc +++ /dev/null @@ -1,15 +0,0 @@ -[[_jboss_adapter]] - -==== JBoss EAP/WildFly adapter - -{project_name} provided this adapter in the past, but it is not provided anymore. - -We recommend that you switch to the Elytron OIDC library to secure your applications. -This library has a similar configuration to the {project_name} WildFly adapters, so you can expect a smooth migration of your applications -if you used this adapter with the older {project_name} versions. - -Elytron OIDC library works with WildFly 28 or newer versions. For the older WildFly versions or for JBoss EAP 7, it is recommended to upgrade -to newer WildFly/EAP or look for some alternative OIDC client library. Otherwise, you will need to stick with the older {project_name} adapters, but those -are not maintained and officially supported. - -For more details on how to integrate {project_name} with JakartaEE applications running on latest Wildfly/EAP, take a look at the Jakarta EE quickstarts within the {quickstartRepo_link}[Keycloak Quickstart GitHub Repository]. diff --git a/docs/documentation/securing_apps/topics/oidc/java/jetty9-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/jetty9-adapter.adoc deleted file mode 100644 index 1b87c618e3e0..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/jetty9-adapter.adoc +++ /dev/null @@ -1,141 +0,0 @@ - -[[_jetty9_adapter]] -==== Jetty 9.4 adapter - -include::adapter-deprecation-notice.adoc[] - -{project_name} has a separate adapter for Jetty 9.4 that you will have to install into your Jetty installation. -You then have to provide some extra configuration in each WAR you deploy to Jetty. - -[[_jetty9_adapter_installation]] -===== Installing the adapter - -Adapters are no longer included with the appliance or war distribution. Each adapter is a separate download on the {project_name} downloads site. They are also available as a maven artifact. - -.Procedure -. Download the {project_name} Jetty 9.4 adapter ZIP archive from the link:https://www.keycloak.org/downloads[Keycloak Downloads] site. - -. Unzip the Jetty 9.4 distro into Jetty 9.4's link:https://eclipse.dev/jetty/documentation/jetty-9/index.html[base directory]. In the example below, the Jetty base is named `your-base`: -+ -[source, subs="attributes"] ----- -$ cd your-base -$ unzip keycloak-jetty94-adapter-dist-{project_version}.Final.zip ----- - -. Enable the `keycloak` module for your Jetty base: -+ -[source] ----- -$ java -jar $JETTY_HOME/start.jar --add-to-startd=keycloak ----- -+ -==== -[NOTE] -Including the adapter's jars within your WEB-INF/lib directory will not work. -==== - -[[_jetty9_per_war]] -===== Jetty 9 Securing a WAR - -Use this procedure to secure a WAR directly by adding config and editing files within your WAR package. - -.Procedure - -. Create a `WEB-INF/jetty-web.xml` file in your WAR package. This is a Jetty specific config fil. You define a {project_name} specific authenticator within it. -+ -[source] ----- - - - - - - - - - - ----- - -. Create a `keycloak.json` adapter config file within the `WEB-INF` directory of your WAR. -+ -The format of this config file is described in the <<_java_adapter_config,Java adapter configuration>> section. -+ -WARNING: The Jetty 9.4 adapter will not be able to find the `keycloak.json` file. -You will have to define all adapter settings within the `jetty-web.xml` file as described below. -Instead of using keycloak.json, you can define everything within the `jetty-web.xml`. -You'll just have to figure out how the json settings match to the `org.keycloak.representations.adapters.config.AdapterConfig` class. -+ -[source,subs="attributes+"] ----- - - - - - - - - - tomcat - customer-portal - http://localhost:8081{kc_base_path} - external - - - - secret - password - - - - - - - - - ----- - - -. Create the jetty-web.xml file in your webapps directory with the name of yourwar.xml. -Jetty should pick it up. You do not need to open your WAR to secure it with {project_name}. -In this mode, you declare keycloak.json configuration directly within the xml file. - -. Specify both a `login-config` and use standard servlet security to specify role-base constraints on your URLs. Here's an example: -+ -[source,xml] ----- - - - customer-portal - - - - Customers - /* - - - user - - - CONFIDENTIAL - - - - - BASIC - this is ignored currently - - - - admin - - - user - - ----- diff --git a/docs/documentation/securing_apps/topics/oidc/java/logout.adoc b/docs/documentation/securing_apps/topics/oidc/java/logout.adoc deleted file mode 100644 index 53a1301327c5..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/logout.adoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Logout - -[[_java_adapter_logout]] -You can log out of a web application in multiple ways. -For Jakarta EE servlet containers, you can call `HttpServletRequest.logout()`. For other browser applications, you can redirect the browser to -`\http://auth-server{kc_realms_path}/{realm-name}/protocol/openid-connect/logout`, which logs the user out if that user has an SSO session with his browser. The actual logout is done once -the user confirms the logout. You can optionally include parameters such as `id_token_hint`, `post_logout_redirect_uri`, `client_id` and others as described in the -https://openid.net/specs/openid-connect-rpinitiated-1_0.html[OpenID Connect RP-Initiated Logout]. As a result, that logout does not need to be explicitly confirmed -by the user if you include the `id_token_hint` parameter. After logout, the user will be automatically redirected to the specified `post_logout_redirect_uri` as long as it is provided. -Note that you need to include either the `client_id` or `id_token_hint` parameter in case that `post_logout_redirect_uri` is included. - -If you want to avoid logging out of an external identity provider as part of the logout process, you can supply the parameter `$$initiating_idp$$`, with the value being -the identity (alias) of the identity provider in question. This parameter is useful when the logout endpoint is invoked as part of single logout initiated by the external identity provider. -The parameter `initiating_idp` is the supported parameter of the {project_name} logout endpoint in addition to the parameters described in the RP-Initiated Logout specification. - -When using the `HttpServletRequest.logout()` option the adapter executes a back-channel POST call against the {project_name} server passing the refresh token. -If the method is executed from an unprotected page (a page that does not check for a valid token) the refresh token can be unavailable and, in that case, -the adapter skips the call. For this reason, using a protected page to execute `HttpServletRequest.logout()` is recommended so that current tokens are always -taken into account and an interaction with the {project_name} server is performed if needed. - diff --git a/docs/documentation/securing_apps/topics/oidc/java/multi-tenancy.adoc b/docs/documentation/securing_apps/topics/oidc/java/multi-tenancy.adoc deleted file mode 100644 index 8656e10c058e..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/multi-tenancy.adoc +++ /dev/null @@ -1,54 +0,0 @@ -[[_multi_tenancy]] -==== Multi Tenancy - -Multi Tenancy, in our context, means that a single target application (WAR) can be secured with multiple {project_name} realms. The realms can be located -on the same {project_name} instance or on different instances. - -In practice, this means that the application needs to have multiple `keycloak.json` adapter configuration files. - -You could have multiple instances of your WAR with different adapter configuration files deployed to different context-paths. However, this may be inconvenient -and you may also want to select the realm based on something else than context-path. - -{project_name} makes it possible to have a custom config resolver so you can choose what adapter config is used for each request. - -To achieve this first you need to create an implementation of `org.keycloak.adapters.KeycloakConfigResolver`. For example: - -[source,java] ----- -package example; - -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; - -public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver { - - @Override - public KeycloakDeployment resolve(OIDCHttpFacade.Request request) { - if (path.startsWith("alternative")) { - KeycloakDeployment deployment = cache.get(realm); - if (null == deployment) { - InputStream is = getClass().getResourceAsStream("/tenant1-keycloak.json"); - return KeycloakDeploymentBuilder.build(is); - } - } else { - InputStream is = getClass().getResourceAsStream("/default-keycloak.json"); - return KeycloakDeploymentBuilder.build(is); - } - } - -} ----- - -You also need to configure which `KeycloakConfigResolver` implementation to use with the `keycloak.config.resolver` context-param in your `web.xml`: - -[source,xml] ----- - - ... - - keycloak.config.resolver - example.PathBasedKeycloakConfigResolver - - ----- diff --git a/docs/documentation/securing_apps/topics/oidc/java/params_forwarding.adoc b/docs/documentation/securing_apps/topics/oidc/java/params_forwarding.adoc deleted file mode 100644 index e9d1ffb71719..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/params_forwarding.adoc +++ /dev/null @@ -1,46 +0,0 @@ -[[_params_forwarding]] -==== Parameters forwarding - -The {project_name} initial authorization endpoint request has support for various parameters. Most of the parameters are described in -https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint[OIDC specification]. Some parameters are added automatically by the adapter based -on the adapter configuration. However, there are also a few parameters that can be added on a per-invocation basis. When you open the secured application URI, -the particular parameter will be forwarded to the {project_name} authorization endpoint. - -For example, if you request an offline token, then you can open the secured application URI with the `scope` parameter like: - -[source] ----- -http://myappserver/mysecuredapp?scope=offline_access ----- - -and the parameter `scope=offline_access` will be automatically forwarded to the {project_name} authorization endpoint. - -The supported parameters are: - -* scope - Use a space-delimited list of scopes. A space-delimited list typically references link:{adminguide_link}#_client_scopes[Client scopes] -defined on particular client. Note that the scope `openid` will be always be added to the list of scopes by the adapter. For example, if you -enter the scope options `address phone`, then the request to {project_name} will contain the scope parameter `scope=openid address phone`. - -* prompt - {project_name} supports these settings: -** `login` - SSO will be ignored and the {project_name} login page will always be shown, even if the user is already authenticated -** `consent` - Applicable only for the clients with `Consent Required`. If it is used, the Consent page will always be displayed, -even if the user previously granted consent to this client. -** `none` - The login page will never be shown; instead the user will be redirected to the application, with an error if the user -is not yet authenticated. This setting allows you to create a filter/interceptor on the application side and show a custom error page -to the user. See more details in the specification. - -* max_age - Used only if a user is already authenticated. Specifies maximum permitted time for the authentication to persist, measured -from when the user authenticated. If user is authenticated longer than `maxAge`, the SSO is ignored and he must re-authenticate. - -* login_hint - Used to pre-fill the username/email field on the login form. - -* kc_idp_hint - Used to tell {project_name} to skip showing login page and automatically redirect to specified identity provider instead. -More info in the link:{adminguide_link}#_client_suggested_idp[Identity Provider documentation]. - -Most of the parameters are described in the https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint[OIDC specification]. -The only exception is parameter `kc_idp_hint`, which is specific to {project_name} and contains the name of the identity provider to automatically use. -For more information see the `Identity Brokering` section in the link:{adminguide_link}[{adminguide_name}]. - -WARNING: If you open the URL using the attached parameters, the adapter will not redirect you to {project_name} if you are already authenticated -in the application. For example, opening $$http://myappserver/mysecuredapp?prompt=login$$ will not automatically redirect you to -the {project_name} login page if you are already authenticated to the application `mysecuredapp` . This behavior may be changed in the future. diff --git a/docs/documentation/securing_apps/topics/oidc/java/servlet-filter-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/servlet-filter-adapter.adoc deleted file mode 100644 index f93a56ea7f48..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/servlet-filter-adapter.adoc +++ /dev/null @@ -1,173 +0,0 @@ -[[_servlet_filter_adapter]] -==== Java servlet filter adapter - -include::adapter-deprecation-notice.adoc[] - -If you are deploying your Java Servlet application on a platform where there is no {project_name} adapter you opt to use the servlet filter adapter. -This adapter works a bit differently than the other adapters. You do not define security constraints in web.xml. -Instead you define a filter mapping using the {project_name} servlet filter adapter to secure the url patterns you want to secure. - -WARNING: Backchannel logout works a bit differently than the standard adapters. -Instead of invalidating the HTTP session it marks the session id as logged out. -There's no standard way to invalidate an HTTP session based on a session id. - -[source,xml] ----- - - - application - - - Keycloak Filter - org.keycloak.adapters.servlet.KeycloakOIDCFilter - - - Keycloak Filter - /keycloak/* - /protected/* - - ----- - -In the snippet above there are two url-patterns. - _/protected/*_ are the files we want protected, while the _/keycloak/*_ url-pattern handles callbacks from the {project_name} server. - -If you need to exclude some paths beneath the configured `url-patterns` you can use the Filter init-param `keycloak.config.skipPattern` to configure -a regular expression that describes a path-pattern for which the keycloak filter should immediately delegate to the filter-chain. -By default no skipPattern is configured. - -Patterns are matched against the `requestURI` without the `context-path`. Given the context-path `/myapp` a request for `/myapp/index.html` will be matched with `/index.html` against the skip pattern. - -[source,xml] ----- - - keycloak.config.skipPattern - ^/(path1|path2|path3).* - ----- - -Note that you should configure your client in the {project_name} Admin Console with an Admin URL that points to a secured section covered by the filter's url-pattern. - -The Admin URL will make callbacks to the Admin URL to do things like backchannel logout. -So, the Admin URL in this example should be `http[s]://hostname/{context-root}/keycloak`. - -If you need to customize the session ID mapper, you can configure the fully qualified name of the class in the Filter init-param keycloak.config.idMapper. Session ID mapper is a mapper that is used to map user IDs and session IDs. By default org.keycloak.adapters.spi.InMemorySessionIdMapper is configured. - -[source,xml] ----- - - keycloak.config.idMapper - org.keycloak.adapters.spi.InMemorySessionIdMapper - ----- - -The {project_name} filter has the same configuration parameters as the other adapters except you must define them as filter init params instead of context params. - -To use this filter, include this maven artifact in your WAR poms: - -[source,xml,subs="attributes+"] ----- - - org.keycloak - keycloak-servlet-filter-adapter - {project_versionMvn} - ----- -ifeval::[{project_community}==true] -===== Using on OSGi - -The servlet filter adapter is packaged as an OSGi bundle, and thus is usable in a generic OSGi environment (R6 and above) with HTTP Service and HTTP Whiteboard. - -====== Installation - -The adapter and its dependencies are distributed as Maven artifacts, so you'll need either working Internet connection to access Maven Central, or have the artifacts cached in your local Maven repo. - -If you are using Apache Karaf, you can simply install a feature from the Keycloak feature repo: - -[source,subs="attributes+"] ----- -karaf@root()> feature:repo-add mvn:org.keycloak/keycloak-osgi-features/{project_versionMvn}/xml/features -karaf@root()> feature:install keycloak-servlet-filter-adapter ----- - -For other OSGi runtimes, please refer to the runtime documentation on how to install the adapter bundle and its dependencies. - -NOTE: If your OSGi platform is Apache Karaf with Pax Web, you should consider using https://access.redhat.com/products/red-hat-single-sign-on/[Red Hat Single Sign-On] 7.x JBoss Fuse 7 adapters instead. - -====== Configuration - -First, the adapter needs to be registered as a servlet filter with the OSGi HTTP Service. The most common ways to do this are programmatic (for example via bundle activator) and declarative (using OSGi annotations). -We recommend using the latter since it simplifies the process of dynamically registering and un-registering the filter: - -[source,java] ----- -package mypackage; - -import javax.servlet.Filter; -import org.keycloak.adapters.servlet.KeycloakOIDCFilter; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; - -@Component( - immediate = true, - service = Filter.class, - property = { - KeycloakOIDCFilter.CONFIG_FILE_PARAM + "=" + "keycloak.json", - HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN + "=" +"/*", - HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=" + "(osgi.http.whiteboard.context.name=mycontext)" - } -) -public class KeycloakFilter extends KeycloakOIDCFilter { - // -} ----- - -The above snippet uses OSGi declarative service specification to expose the filter as an OSGI service under `javax.servlet.Filter` class. -Once the class is published in the OSGi service registry, it is going to be picked up by OSGi HTTP Service implementation and used for filtering requests for the specified servlet context. This will trigger Keycloak adapter for every request that matches servlet context path + filter path. - -Since the component is put under the control of OSGi Configuration Admin Service, it's properties can be configured dynamically. -To do that, either create a `mypackage.KeycloakFilter.cfg` file under the standard config location for your OSGi runtime: -[source] - ----- -keycloak.config.file = /path/to/keycloak.json -osgi.http.whiteboard.filter.pattern = /secure/* ----- - -or use interactive console, if your runtime allows for that: - -[source] ----- -karaf@root()> config:edit mypackage.KeycloakFilter -karaf@root()> config:property-set keycloak.config.file '${karaf.etc}/keycloak.json' -karaf@root()> config:update ----- - -If you need more control, for example, providing custom `KeycloakConfigResolver` to implement <<_multi_tenancy,multi tenancy>>, you can register the filter programmatically: - -[source,java] ----- -public class Activator implements BundleActivator { - - private ServiceRegistration registration; - - public void start(BundleContext context) throws Exception { - Hashtable props = new Hashtable(); - props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, "/secure/*"); - props.put(KeycloakOIDCFilter.CONFIG_RESOLVER_PARAM, new MyConfigResolver()); - - this.registration = context.registerService(Filter.class.getName(), new KeycloakOIDCFilter(), props); - } - - public void stop(BundleContext context) throws Exception { - this.registration.unregister(); - } -} ----- - -Please refer to https://github.com/apache/felix-dev/tree/master/http#using-the-osgi-http-whiteboard[Apache Felix HTTP Service] for more info on programmatic registration. - -endif::[] diff --git a/docs/documentation/securing_apps/topics/oidc/java/spring-boot-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/spring-boot-adapter.adoc deleted file mode 100644 index fa08e852e11a..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/spring-boot-adapter.adoc +++ /dev/null @@ -1,99 +0,0 @@ -[[_spring_boot_adapter]] -==== Spring Boot adapter - -[WARNING] -==== -This adapter is deprecated and will be removed in a future release of Keycloak. No further enhancements or new features -will be added to this adapter. - -We recommend that you leverage the OAuth2/OpenID Connect support from Spring Security. - -For more details about how to integrate {project_name} with Spring Boot applications, consider looking at the -{quickstartRepo_link}[Keycloak Quickstart GitHub Repository]. -==== - - -[[_spring_boot_adapter_installation]] -===== Installing the Spring Boot adapter - -To be able to secure Spring Boot apps you must add the Keycloak Spring Boot adapter JAR to your app. -You then have to provide some extra configuration via normal Spring Boot configuration (`application.properties`). - -The Keycloak Spring Boot adapter takes advantage of Spring Boot's autoconfiguration so all you need to do is add this adapter Keycloak Spring Boot starter to your project. - -.Procedure - -. To add the starter to your project using Maven, add the following to your dependencies: -+ -[source,xml,subs="attributes+"] ----- - - org.keycloak - keycloak-spring-boot-starter - ----- - -. Add the Adapter BOM dependency: -+ -[source,xml,subs="attributes+"] ----- - - - - org.keycloak.bom - keycloak-adapter-bom - {project_versionMvn} - pom - import - - - ----- - - -Currently the following embedded containers are supported and do not require any extra dependencies if using the Starter: - -* Tomcat -* Undertow -* Jetty - -[[_spring_boot_adapter_configuration]] -===== Configuring the Spring Boot Adapter - -Use the procedure to configure your Spring Boot app to use {project_name}. - -.Procedure - -. Instead of a `keycloak.json` file, you configure the realm for the Spring Boot adapter via the normal Spring Boot configuration. For example: -+ -[source,subs="attributes+"] ----- -keycloak.realm = demorealm -keycloak.auth-server-url = http://127.0.0.1:8080{kc_base_path} -keycloak.ssl-required = external -keycloak.resource = demoapp -keycloak.credentials.secret = 11111111-1111-1111-1111-111111111111 -keycloak.use-resource-role-mappings = true ----- -+ -You can disable the Keycloak Spring Boot Adapter (for example in tests) by setting `keycloak.enabled = false`. - -. To configure a Policy Enforcer, unlike keycloak.json, use `policy-enforcer-config` instead of just `policy-enforcer`. - -. Specify the Jakarta EE security config that would normally go in the `web.xml`. -+ -The Spring Boot Adapter will set the `login-method` to `KEYCLOAK` and configure the `security-constraints` at startup time. Here's an example configuration: -+ -[source] ----- -keycloak.securityConstraints[0].authRoles[0] = admin -keycloak.securityConstraints[0].authRoles[1] = user -keycloak.securityConstraints[0].securityCollections[0].name = insecure stuff -keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /insecure - -keycloak.securityConstraints[1].authRoles[0] = admin -keycloak.securityConstraints[1].securityCollections[0].name = admin stuff -keycloak.securityConstraints[1].securityCollections[0].patterns[0] = /admin ----- - -WARNING: If you plan to deploy your Spring Application as a WAR then you should not use the Spring Boot Adapter and use the dedicated adapter for the application server or servlet container you are using. Your Spring Boot should also contain a `web.xml` file. diff --git a/docs/documentation/securing_apps/topics/oidc/java/spring-security-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/spring-security-adapter.adoc deleted file mode 100644 index 3d7552e06ba5..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/spring-security-adapter.adoc +++ /dev/null @@ -1,392 +0,0 @@ - -[[_spring_security_adapter]] -==== Spring Security adapter - -[WARNING] -==== -This adapter is deprecated and will be removed in a future release of Keycloak. No further enhancements or new features -will be added to this adapter. - -We recommend that you leverage the OAuth2/OpenID Connect support from Spring Security. - -For more details about how to integrate {project_name} with Spring Boot applications, consider looking at the -{quickstartRepo_link}[Keycloak Quickstart GitHub Repository]. -==== - -To secure an application with Spring Security and Keycloak, add this adapter as a dependency to your project. -You then have to provide some extra beans in your Spring Security configuration file and add the Keycloak security filter to your pipeline. - -Unlike the other Keycloak Adapters, you should not configure your security in web.xml. -However, keycloak.json is still required. -In order for Single Sign Out to work properly you have to define a session listener. - -.The session listener can be defined: -* in web.xml (for pure Spring Security environments): -[source,xml] ----- - - org.springframework.security.web.session.HttpSessionEventPublisher - ----- -* as a Spring bean (in Spring Boot environments using Spring Security adapter) -[source,java] ----- -@Bean -public ServletListenerRegistrationBean httpSessionEventPublisher() { - return new ServletListenerRegistrationBean(new HttpSessionEventPublisher()); -} ----- - - -===== Installing the adapter - -Add Keycloak Spring Security adapter as a dependency to your Maven POM or Gradle build. - - -[source,xml,subs="attributes+"] ----- - - org.keycloak - keycloak-spring-security-adapter - {project_versionMvn} - ----- - -===== Configuring the Spring Security Adapter - -The Keycloak Spring Security adapter takes advantage of Spring Security's flexible security configuration syntax. - -====== Java configuration - -Keycloak provides a KeycloakWebSecurityConfigurerAdapter as a convenient base class for creating a https://docs.spring.io/spring-security/site/docs/4.0.x/apidocs/org/springframework/security/config/annotation/web/WebSecurityConfigurer.html[WebSecurityConfigurer] instance. -The implementation allows customization by overriding methods. -While its use is not required, it greatly simplifies your security context configuration. - - -[source,java] ----- - - -@KeycloakConfiguration -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter -{ - /** - * Registers the KeycloakAuthenticationProvider with the authentication manager. - */ - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(keycloakAuthenticationProvider()); - } - - /** - * Defines the session authentication strategy. - */ - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new RegisterSessionAuthenticationStrategy(buildSessionRegistry()); - } - - @Bean - protected SessionRegistry buildSessionRegistry() { - return new SessionRegistryImpl(); - } - - @Override - protected void configure(HttpSecurity http) throws Exception - { - super.configure(http); - http - .authorizeRequests() - .antMatchers("/customers*").hasRole("USER") - .antMatchers("/admin*").hasRole("ADMIN") - .anyRequest().permitAll(); - } -} ----- - -You must provide a session authentication strategy bean which should be of type `RegisterSessionAuthenticationStrategy` for public or confidential applications and `NullAuthenticatedSessionStrategy` for bearer-only applications. - -Spring Security's `SessionFixationProtectionStrategy` is currently not supported because it changes the session identifier after login via Keycloak. -If the session identifier changes, universal log out will not work because Keycloak is unaware of the new session identifier. - -TIP: The `@KeycloakConfiguration` annotation is a metadata annotation that defines all annotations that are needed to integrate -{project_name} in Spring Security. If you have a complex Spring Security setup you can simply have a look at the annotations of -the `@KeycloakConfiguration` annotation and create your own custom meta annotation or just use specific Spring annotations -for the {project_name} adapter. - -====== XML configuration - -While Spring Security's XML namespace simplifies configuration, customizing the configuration can be a bit verbose. - - -[source,xml] ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ----- - -===== Multi Tenancy - -The Keycloak Spring Security adapter also supports Multi Tenancy. -Instead of injecting `AdapterDeploymentContextFactoryBean` with the path to `keycloak.json` you can inject an implementation of the `KeycloakConfigResolver` interface. -More details on how to implement the `KeycloakConfigResolver` can be found in <<_multi_tenancy,Multi Tenancy>>. - -===== Naming security roles - -Spring Security, when using role-based authentication, requires that role names start with `ROLE_`. -For example, an administrator role must be declared in Keycloak as `ROLE_ADMIN` or similar, not simply `ADMIN`. - -The class `org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider` supports an optional `org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper` which can be used to map roles coming from Keycloak to roles recognized by Spring Security. -Use, for example, `org.springframework.security.core.authority.mapping.SimpleAuthorityMapper`, which allows for case conversion and the addition of a prefix (which defaults to `ROLE_`). -The following code will convert the role names to upper case and, by default, add the `ROLE_` prefix to them: - -[source,java] ----- -@KeycloakConfiguration -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) { - auth.authenticationProvider(getKeycloakAuthenticationProvider()); - } - - private KeycloakAuthenticationProvider getKeycloakAuthenticationProvider() { - KeycloakAuthenticationProvider authenticationProvider = keycloakAuthenticationProvider(); - SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); - mapper.setConvertToUpperCase(true); - authenticationProvider.setGrantedAuthoritiesMapper(mapper); - - return authenticationProvider; - } - - ... -} - ----- - -===== Client to Client Support - -To simplify communication between clients, Keycloak provides an extension of Spring's `RestTemplate` that handles bearer token authentication for you. -To enable this feature your security configuration must add the `KeycloakRestTemplate` bean. -Note that it must be scoped as a prototype to function correctly. - -For Java configuration: -[source,java] ----- - - -@Configuration -@EnableWebSecurity -@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { - - ... - - @Autowired - public KeycloakClientRequestFactory keycloakClientRequestFactory; - - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) - public KeycloakRestTemplate keycloakRestTemplate() { - return new KeycloakRestTemplate(keycloakClientRequestFactory); - } - - ... -} ----- - -For XML configuration: -[source,xml] ----- - - - - - ----- - -Your application code can then use `KeycloakRestTemplate` any time it needs to make a call to another client. -For example: -[source,java] ----- - - - -@Service -public class RemoteProductService implements ProductService { - - @Autowired - private KeycloakRestTemplate template; - - private String endpoint; - - @Override - public List getProducts() { - ResponseEntity response = template.getForEntity(endpoint, String[].class); - return Arrays.asList(response.getBody()); - } -} ----- - -===== Spring Boot Integration - -The Spring Boot and the Spring Security adapters can be combined. - -If you are using the Keycloak Spring Boot Starter to make use of the Spring Security adapter you just need to add the Spring Security starter : - -[source,xml] ----- - - - org.springframework.boot - spring-boot-starter-security - - ----- - -====== Using Spring Boot Configuration - -By Default, the Spring Security Adapter looks for a `keycloak.json` configuration file. You can make sure it looks at the configuration provided by the Spring Boot Adapter by adding this bean: - -[source,java] ----- - -@Configuration -public class CustomKeycloakConfig { - - @Bean - public KeycloakConfigResolver keycloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); - } -} - ----- - -Do not declare the `KeycloakConfigResolver` bean in a configuration class that extends `KeycloakWebSecurityConfigurerAdapter` as this will cause a `Circular References` problem in Spring Boot starting with version 2.6.0. - -====== Avoid double bean registration - -Spring Boot attempts to eagerly register filter beans with the web application context. -Therefore, when running the Keycloak Spring Security adapter in a Spring Boot environment, it may be necessary to add ``FilterRegistrationBean``s to your security configuration to prevent the Keycloak filters from being registered twice. - -Spring Boot 2.1 also disables `spring.main.allow-bean-definition-overriding` by default. This can mean that an `BeanDefinitionOverrideException` will be encountered if a `Configuration` class extending `KeycloakWebSecurityConfigurerAdapter` registers a bean that is already detected by a `@ComponentScan`. This can be avoided by overriding the registration to use the Boot-specific `@ConditionalOnMissingBean` annotation, as with `HttpSessionManager` below. - -[source,java] ----- - - -@Configuration -@EnableWebSecurity -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter -{ - ... - - @Bean - public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( - KeycloakAuthenticationProcessingFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); - registrationBean.setEnabled(false); - return registrationBean; - } - - @Bean - public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( - KeycloakPreAuthActionsFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); - registrationBean.setEnabled(false); - return registrationBean; - } - - @Bean - public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean( - KeycloakAuthenticatedActionsFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); - registrationBean.setEnabled(false); - return registrationBean; - } - - @Bean - public FilterRegistrationBean keycloakSecurityContextRequestFilterBean( - KeycloakSecurityContextRequestFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); - registrationBean.setEnabled(false); - return registrationBean; - } - - @Bean - @Override - @ConditionalOnMissingBean(HttpSessionManager.class) - protected HttpSessionManager httpSessionManager() { - return new HttpSessionManager(); - } - ... -} ----- diff --git a/docs/documentation/securing_apps/topics/oidc/java/tomcat-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/java/tomcat-adapter.adoc deleted file mode 100644 index 5e13c8ab3857..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/java/tomcat-adapter.adoc +++ /dev/null @@ -1,88 +0,0 @@ - -[[_tomcat_adapter]] -==== Tomcat 8 and 9 adapters - -include::adapter-deprecation-notice.adoc[] - -To be able to secure WAR apps deployed on Tomcat 8, and 9, you install the Keycloak Tomcat adapter into your Tomcat installation. You then perform extra configuration to secure each WAR you deploy to Tomcat. - -[[_tomcat_adapter_installation]] -===== Installing the adapter - -Adapters are no longer included with the appliance or war distribution. -Each adapter is a separate download on the Keycloak Downloads site. -They are also available as a maven artifact. - -.Procedure - -. Download the adapter for the Tomcat version on your system from the link:https://www.keycloak.org/downloads[Keycloak Downloads] site. - -* Install on Tomcat 8 or 9: -+ -[source] ----- - -$ cd $TOMCAT_HOME/lib -$ unzip keycloak-tomcat-adapter-dist.zip ----- - -==== -[NOTE] -Including the adapter's jars within your WEB-INF/lib directory will not work. The Keycloak adapter is implemented as a Valve and valve code must reside in Tomcat's main lib/ directory. -==== - -===== Securing a WAR - -This section describes how to secure a WAR directly by adding config and editing files within your WAR package. - -.Procedure - -. Create a `META-INF/context.xml` file in your WAR package. -+ -This is a Tomcat specific config file and you must define a Keycloak specific Valve. -+ -[source] ----- - - - ----- - -. Create a `keycloak.json` adapter config file within the `WEB-INF` directory of your WAR. -+ -The format of this config file is described in the <<_java_adapter_config,Java adapter configuration>> - -. Specify both a `login-config` and use standard servlet security to specify role-base constraints on your URLs. Here's an example: -+ -[source,xml] ----- - - - customer-portal - - - - Customers - /* - - - user - - - - - BASIC - this is ignored currently - - - - admin - - - user - - ----- diff --git a/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc deleted file mode 100644 index 4fc32d9027c4..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/javascript-adapter.adoc +++ /dev/null @@ -1,523 +0,0 @@ -[[_javascript_adapter]] -=== {project_name} JavaScript adapter - -{project_name} comes with a client-side JavaScript library called `keycloak-js` that can be used to secure web applications. The adapter also comes with built-in support for Cordova applications. - -==== Installation - -The adapter is distributed in several ways, but we recommend that you install the https://www.npmjs.com/package/keycloak-js[`keycloak-js`] package from NPM: - -[source,bash] ----- -npm install keycloak-js ----- - -Alternatively, the library can be retrieved directly from the {project_name} server at `{kc_js_path}/keycloak.js` and is also distributed as a ZIP archive. We are however considering the inclusion of the adapter directly from the Keycloak server as deprecated, and this functionality might be removed in the future. - -==== {project_name} server configuration - -One important thing to consider about using client-side applications is that the client has to be a public client as there is no secure way to store client credentials in a client-side application. This consideration makes it very important to make sure the redirect URIs you have configured for the client are correct and as specific as possible. - -To use the adapter, create a client for your application in the {project_name} Admin Console. Make the client public by toggling *Client authentication* to *Off* on the *Capability config* page. - -You also need to configure `Valid Redirect URIs` and `Web Origins`. Be as specific as possible as failing to do so may result in a security vulnerability. - -==== Using the adapter - -The following example shows how to initialize the adapter. Make sure that you replace the options passed to the `Keycloak` constructor with those of the client you have configured. - -[source,javascript] ----- -import Keycloak from 'keycloak-js'; - -const keycloak = new Keycloak({ - url: 'http://keycloak-server${kc_base_path}', - realm: 'myrealm', - clientId: 'myapp' -}); - -try { - const authenticated = await keycloak.init(); - console.log(`User is ${authenticated ? 'authenticated' : 'not authenticated'}`); -} catch (error) { - console.error('Failed to initialize adapter:', error); -} ----- - -To authenticate, you call the `login` function. Two options exist to make the adapter automatically authenticate. You can pass `login-required` or `check-sso` to the `init()` function. - -* `login-required` authenticates the client if the user is logged in to {project_name} or displays the login page if the user is not logged in. -* `check-sso` only authenticates the client if the user is already logged in. If the user is not logged in, the browser is redirected back to the application and remains unauthenticated. - -You can configure a _silent_ `check-sso` option. With this feature enabled, your browser will not perform a full redirect to the {project_name} server and back to your application, but this action will be performed in a hidden iframe. Therefore, your application resources are only loaded and parsed once by the browser, namely when the application is initialized and not again after the redirect back from {project_name} to your application. This approach is particularly useful in case of SPAs (Single Page Applications). - -To enable the _silent_ `check-sso`, you provide a `silentCheckSsoRedirectUri` attribute in the init method. Make sure this URI is a valid endpoint in the application; it must be configured as a valid redirect for the client in the {project_name} Admin Console: - -[source,javascript] ----- -keycloak.init({ - onLoad: 'check-sso', - silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html` -}); ----- - -The page at the silent check-sso redirect uri is loaded in the iframe after successfully checking your authentication state and retrieving the tokens from the {project_name} server. -It has no other task than sending the received tokens to the main application and should only look like this: - -[source,html] ----- - - - - - - ----- - -Remember that this page must be served by your application at the specified location in `silentCheckSsoRedirectUri` and is _not_ part of the adapter. - -WARNING: _Silent_ `check-sso` functionality is limited in some modern browsers. Please see the <<_modern_browsers,Modern Browsers with Tracking Protection Section>>. - -To enable `login-required` set `onLoad` to `login-required` and pass to the init method: - -[source,javascript] ----- -keycloak.init({ - onLoad: 'login-required' -}); ----- - -After the user is authenticated the application can make requests to RESTful services secured by {project_name} by including the bearer token in the -`Authorization` header. For example: - -[source,javascript] ----- -async function fetchUsers() { - const response = await fetch('/api/users', { - headers: { - accept: 'application/json', - authorization: `Bearer ${keycloak.token}` - } - }); - - return response.json(); -} ----- - -One thing to keep in mind is that the access token by default has a short life expiration so you may need to refresh the access token prior to sending the request. You refresh this token by calling the `updateToken()` method. This method returns a Promise, which makes it easy to invoke the service only if the token was successfully refreshed and displays an error to the user if it was not refreshed. For example: - -[source,javascript] ----- -try { - await keycloak.updateToken(30); -} catch (error) { - console.error('Failed to refresh token:', error); -} - -const users = await fetchUsers(); ----- - -==== Session Status iframe - -By default, the adapter creates a hidden iframe that is used to detect if a Single-Sign Out has occurred. This iframe does not require any network traffic. Instead the status is retrieved by looking at a special status cookie. This feature can be disabled by setting `checkLoginIframe: false` in the options passed to the `init()` method. - -You should not rely on looking at this cookie directly. Its format can change and it's also associated with the URL of the {project_name} server, not -your application. - -WARNING: Session Status iframe functionality is limited in some modern browsers. Please see <<_modern_browsers,Modern Browsers with Tracking Protection Section>>. - -[[_javascript_implicit_flow]] -==== Implicit and hybrid flow - -By default, the adapter uses the https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authorization Code] flow. - -With this flow, the {project_name} server returns an authorization code, not an authentication token, to the application. The JavaScript adapter exchanges the `code` for an access token and a refresh token after the browser is redirected back to the application. - -{project_name} also supports the https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth[Implicit] flow where an access token is sent immediately after successful authentication with {project_name}. This flow may have better performance than the standard flow because no additional request exists to exchange the code for tokens, but it has implications when the access token expires. - -However, sending the access token in the URL fragment can be a security vulnerability. For example the token could be leaked through web server logs and or -browser history. - -To enable implicit flow, you enable the *Implicit Flow Enabled* flag for the client in the {project_name} Admin Console. You also pass the parameter `flow` with the value `implicit` to `init` method: - -[source,javascript] ----- -keycloak.init({ - flow: 'implicit' -}) ----- - -Note that only an access token is provided and no refresh token exists. This situation means that once the access token has expired, the application has to redirect to {project_name} again to obtain a new access token. - -{project_name} also supports the https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth[Hybrid] flow. - -This flow requires the client to have both the *Standard Flow* and *Implicit Flow* enabled in the Admin Console. The {project_name} server then sends both the code and tokens to your application. The access token can be used immediately while the code can be exchanged for access and refresh tokens. Similar to the implicit flow, the hybrid flow is good for performance because the access token is available immediately. -But, the token is still sent in the URL, and the security vulnerability mentioned earlier may still apply. - -One advantage in the Hybrid flow is that the refresh token is made available to the application. - -For the Hybrid flow, you need to pass the parameter `flow` with value `hybrid` to the `init` method: - -[source,javascript] ----- -keycloak.init({ - flow: 'hybrid' -}); ----- - -[#hybrid-apps-with-cordova] -==== Hybrid Apps with Cordova - -{project_name} supports hybrid mobile apps developed with https://cordova.apache.org/[Apache Cordova]. The adapter has two modes for this: `cordova` and `cordova-native`: - -The default is `cordova`, which the adapter automatically selects if no adapter type has been explicitly configured and `window.cordova` is present. When logging in, it opens an https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/[InApp Browser] that lets the user interact with {project_name} and afterwards returns to the app by redirecting to `http://localhost`. Because of this behavior, you whitelist this URL as a valid redirect-uri in the client configuration section of the Admin Console. - -While this mode is easy to set up, it also has some disadvantages: - -* The InApp-Browser is a browser embedded in the app and is not the phone's default browser. Therefore it will have different settings and stored credentials will not be available. -* 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: - -[source,javascript] ----- -keycloak.init({ - adapter: 'cordova-native' -}); ----- - -This adapter requires two additional plugins: - -* https://github.com/google/cordova-plugin-browsertab[cordova-plugin-browsertab]: allows the app to open webpages in the system's browser -* https://github.com/e-imaxina/cordova-plugin-deeplinks[cordova-plugin-deeplinks]: allow the browser to redirect back to your app by special URLs - -The technical details for linking to an app differ on each platform and special setup is needed. -Please refer to the Android and iOS sections of the https://github.com/e-imaxina/cordova-plugin-deeplinks/blob/master/README.md[deeplinks plugin documentation] for further instructions. - -Different kinds of links exist for opening apps: -* custom schemes, such as `myapp://login` or `android-app://com.example.myapp/https/example.com/login` -* https://developer.apple.com/ios/universal-links/[Universal Links (iOS)]) / https://developer.android.com/training/app-links/deep-linking[Deep Links (Android)]. -While the former are easier to set up and tend to work more reliably, the latter offer extra security because they are unique and only the owner of a domain can register them. Custom-URLs are deprecated on iOS. For best reliability, we recommend that you use universal links combined with a fallback site that uses a custom-url link. - -Furthermore, we recommend the following steps to improve compatibility with the adapter: - -* Universal Links on iOS seem to work more reliably with `response-mode` set to `query` -* To prevent Android from opening a new instance of your app on redirect add the following snippet to `config.xml`: - -[source,xml] ----- - ----- - -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 - -In some situations, you may need to run the adapter in environments that are not supported by default, such as Capacitor. To use the JavasScript client in these environments, you can pass a custom adapter. For example, a third-party library could provide such an adapter to make it possible to reliably run the adapter: - -[source,javascript] ----- -import Keycloak from 'keycloak-js'; -import KeycloakCapacitorAdapter from 'keycloak-capacitor-adapter'; - -const keycloak = new Keycloak(); - -keycloak.init({ - adapter: KeycloakCapacitorAdapter, -}); ----- - -This specific package does not exist, but it gives a pretty good example of how such an adapter could be passed into the client. - -It's also possible to make your own adapter, to do so you will have to implement the methods described in the `KeycloakAdapter` interface. For example the following TypeScript code ensures that all the methods are properly implemented: - -[source,typescript] ----- -import Keycloak, { KeycloakAdapter } from 'keycloak-js'; - -// Implement the 'KeycloakAdapter' interface so that all required methods are guaranteed to be present. -const MyCustomAdapter: KeycloakAdapter = { - login(options) { - // Write your own implementation here. - } - - // The other methods go here... -}; - -const keycloak = new Keycloak(); - -keycloak.init({ - adapter: MyCustomAdapter, -}); ----- - -Naturally you can also do this without TypeScript by omitting the type information, but ensuring implementing the interface properly will then be left entirely up to you. - -[[_modern_browsers]] -==== Modern Browsers with Tracking Protection -In the latest versions of some browsers, various cookies policies are applied to prevent tracking of the users by third parties, such as SameSite in Chrome or completely blocked third-party cookies. Those policies are likely to become more restrictive and adopted by other browsers over time. Eventually cookies in third-party contexts may become completely unsupported and blocked by the browsers. As a result, the affected adapter features might ultimately be deprecated. - -The adapter relies on third-party cookies for Session Status iframe, _silent_ `check-sso` and partially also for regular (non-silent) `check-sso`. Those features have limited functionality or are completely disabled based on how restrictive the browser is regarding cookies. The adapter tries to detect this setting and reacts accordingly. - -===== Browsers with "SameSite=Lax by Default" Policy -All features are supported if SSL / TLS connection is configured on the {project_name} side as well as on the application side. For example, Chrome is affected starting with version 84. - -===== Browsers with Blocked Third-Party Cookies -Session Status iframe is not supported and is automatically disabled if such browser behavior is detected by the adapter. This means the adapter cannot use a session cookie for Single Sign-Out detection and must rely purely on tokens. As a result, when a user logs out in another window, the application using the adapter will not be logged out until the application tries to refresh the Access Token. Therefore, consider setting the Access Token Lifespan to a relatively short time, so that the logout is detected as soon as possible. For more details, see link:{adminguide_link}#_timeouts[Session and Token Timeouts]. - -_Silent_ `check-sso` is not supported and falls back to regular (non-silent) `check-sso` by default. This behavior can be changed by setting `silentCheckSsoFallback: false` in the options passed to the `init` method. In this case, `check-sso` will be completely disabled if restrictive browser behavior is detected. - -Regular `check-sso` is affected as well. Since Session Status iframe is unsupported, an additional redirect to {project_name} has to be made when the adapter is initialized to check the user's login status. This check is different from the standard behavior when the iframe is used to tell whether the user is logged in, and the redirect is performed only when the user is logged out. - -An affected browser is for example Safari starting with version 13.1. - -==== API Reference - -===== Constructor - -[source,javascript,subs="attributes+"] ----- -new Keycloak(); -new Keycloak('http://localhost/keycloak.json'); -new Keycloak({ url: 'http://localhost{kc_base_path}', realm: 'myrealm', clientId: 'myApp' }); ----- - -===== Properties - -authenticated:: - Is `true` if the user is authenticated, `false` otherwise. - -token:: - The base64 encoded token that can be sent in the `Authorization` header in requests to services. - -tokenParsed:: - The parsed token as a JavaScript object. - -subject:: - The user id. - -idToken:: - The base64 encoded ID token. - -idTokenParsed:: - The parsed id token as a JavaScript object. - -realmAccess:: - The realm roles associated with the token. - -resourceAccess:: - The resource roles associated with the token. - -refreshToken:: - The base64 encoded refresh token that can be used to retrieve a new token. - -refreshTokenParsed:: - The parsed refresh token as a JavaScript object. - -timeSkew:: - The estimated time difference between the browser time and the {project_name} server in seconds. This value is just an estimation, but is accurate - enough when determining if a token is expired or not. - -responseMode:: - Response mode passed in init (default value is fragment). - -flow:: - Flow passed in init. - -adapter:: - Allows you to override the way that redirects and other browser-related functions will be handled by the library. - Available options: - * "default" - the library uses the browser api for redirects (this is the default) - * "cordova" - the library will try to use the InAppBrowser cordova plugin to load keycloak login/registration pages (this is used automatically when the library is working in a cordova ecosystem) - * "cordova-native" - the library tries to open the login and registration page using the phone's system browser using the BrowserTabs cordova plugin. This requires extra setup for redirecting back to the app (see <>). - * "custom" - allows you to implement a custom adapter (only for advanced use cases) - -responseType:: - Response type sent to {project_name} with login requests. This is determined based on the flow value used during initialization, but can be overridden by setting this value. - -===== Methods - -*init(options)* - -Called to initialize the adapter. - -Options is an Object, where: - -* useNonce - Adds a cryptographic nonce to verify that the authentication response matches the request (default is `true`). -* onLoad - Specifies an action to do on load. Supported values are `login-required` or `check-sso`. -* silentCheckSsoRedirectUri - Set the redirect uri for silent authentication check if onLoad is set to 'check-sso'. -* silentCheckSsoFallback - Enables fall back to regular `check-sso` when _silent_ `check-sso` is not supported by the browser (default is `true`). -* token - Set an initial value for the token. -* refreshToken - Set an initial value for the refresh token. -* idToken - Set an initial value for the id token (only together with token or refreshToken). -* scope - Set the default scope parameter to the {project_name} login endpoint. Use a space-delimited list of scopes. Those typically -reference link:{adminguide_link}#_client_scopes[Client scopes] defined on a particular client. Note that the scope `openid` will -always be added to the list of scopes by the adapter. For example, if you enter the scope options `address phone`, then the request -to {project_name} will contain the scope parameter `scope=openid address phone`. Note that the default scope specified here is overwritten if the `login()` options specify scope explicitly. -* timeSkew - Set an initial value for skew between local time and {project_name} server in seconds (only together with token or refreshToken). -* checkLoginIframe - Set to enable/disable monitoring login state (default is `true`). -* checkLoginIframeInterval - Set the interval to check login state (default is 5 seconds). -* responseMode - Set the OpenID Connect response mode send to {project_name} server at login request. Valid values are `query` or `fragment`. Default value is `fragment`, which means that after successful authentication will {project_name} redirect to JavaScript application with OpenID Connect parameters added in URL fragment. This is generally safer and recommended over `query`. -* flow - Set the OpenID Connect flow. Valid values are `standard`, `implicit` or `hybrid`. -* enableLogging - Enables logging messages from Keycloak to the console (default is `false`). -* pkceMethod - The method for Proof Key Code Exchange (https://datatracker.ietf.org/doc/html/rfc7636[PKCE]) to use. Configuring this value enables the PKCE mechanism. Available options: - - "S256" - The SHA256 based PKCE method -* acrValues - Generates the `acr_values` parameter which refers to authentication context class reference and allows clients to declare the required assurance level requirements, e.g. authentication mechanisms. See https://openid.net/specs/openid-connect-modrna-authentication-1_0.html#acr_values[Section 4. acr_values request values and level of assurance in OpenID Connect MODRNA Authentication Profile 1.0]. -* messageReceiveTimeout - Set a timeout in milliseconds for waiting for message responses from the Keycloak server. This is used, for example, when waiting for a message during 3rd party cookies check. The default value is 10000. -* locale - When onLoad is 'login-required', sets the 'ui_locales' query param in compliance with https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest[section 3.1.2.1 of the OIDC 1.0 specification]. - -Returns a promise that resolves when initialization completes. - -*login(options)* - -Redirects to login form. - -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 the details and all the possible values of the `prompt` parameter. -* 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. -* idpHint - Used to tell {project_name} to skip showing the login page and automatically redirect to the specified identity -provider instead. More info in the link:{adminguide_link}#_client_suggested_idp[Identity Provider documentation]. -* acr - Contains the information about `acr` claim, which will be sent inside `claims` parameter to the {project_name} server. Typical usage -is for step-up authentication. Example of use `{ values: ["silver", "gold"], essential: true }`. See OpenID Connect specification -and link:{adminguide_link}#_step-up-flow[Step-up authentication documentation] for more details. -* acrValues - Generates the `acr_values` parameter which refers to authentication context class reference and allows clients to declare the required assurance level requirements, e.g. authentication mechanisms. See https://openid.net/specs/openid-connect-modrna-authentication-1_0.html#acr_values[Section 4. acr_values request values and level of assurance in OpenID Connect MODRNA Authentication Profile 1.0]. -* action - If value is `register` then user is redirected to registration page, if the value is `UPDATE_PASSWORD` then the user will be redirected to the reset password page (if not authenticated will send user to login page first and redirect after authenticated), otherwise to login page. -* locale - Sets the 'ui_locales' query param in compliance with https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest[section 3.1.2.1 of the OIDC 1.0 specification]. -* cordovaOptions - Specifies the arguments that are passed to the Cordova in-app-browser (if applicable). Options `hidden` and `location` are not affected by these arguments. All available options are defined at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/. Example of use: `{ zoom: "no", hardwareback: "yes" }`; - -*createLoginUrl(options)* - -Returns the URL to login form. - -Options is an optional Object, which supports same options as the function `login` . - -*logout(options)* - -Redirects to logout. - -Options is an Object, where: - -* redirectUri - Specifies the uri to redirect to after logout. - -*createLogoutUrl(options)* - -Returns the URL to log out the user. - -Options is an Object, where: - -* redirectUri - Specifies the uri to redirect to after logout. - -*register(options)* - -Redirects to registration form. Shortcut for login with option action = 'register' - -Options are same as for the login method but 'action' is set to 'register' - -*createRegisterUrl(options)* - -Returns the url to registration page. Shortcut for createLoginUrl with option action = 'register' - -Options are same as for the createLoginUrl method but 'action' is set to 'register' - -*accountManagement()* - -Redirects to the Account Management Console. - -*createAccountUrl(options)* - -Returns the URL to the Account Management Console. - -Options is an Object, where: - -* redirectUri - Specifies the uri to redirect to when redirecting back to the application. - -*hasRealmRole(role)* - -Returns true if the token has the given realm role. - -*hasResourceRole(role, resource)* - -Returns true if the token has the given role for the resource (resource is optional, if not specified clientId is used). - -*loadUserProfile()* - -Loads the users profile. - -Returns a promise that resolves with the profile. - -For example: - -[source,javascript] ----- -try { - const profile = await keycloak.loadUserProfile(); - console.log('Retrieved user profile:', profile); -} catch (error) { - console.error('Failed to load user profile:', error); -} ----- - -*isTokenExpired(minValidity)* - -Returns true if the token has less than minValidity seconds left before it expires (minValidity is optional, if not specified 0 is used). - -*updateToken(minValidity)* - -If the token expires within minValidity seconds (minValidity is optional, if not specified 5 is used) the token is refreshed. -If -1 is passed as the minValidity, the token will be forcibly refreshed. -If the session status iframe is enabled, the session status is also checked. - -Returns a promise that resolves with a boolean indicating whether or not the token has been refreshed. - -For example: - -[source,javascript] ----- -try { - const refreshed = await keycloak.updateToken(5); - console.log(refreshed ? 'Token was refreshed' : 'Token is still valid'); -} catch (error) { - console.error('Failed to refresh the token:', error); -} ----- - -*clearToken()* - -Clear authentication state, including tokens. -This can be useful if application has detected the session was expired, for example if updating token fails. - -Invoking this results in onAuthLogout callback listener being invoked. - -===== Callback Events - -The adapter supports setting callback listeners for certain events. Keep in mind that these have to be set before the call to the `init()` method. - -For example: -[source,javascript] ----- -keycloak.onAuthSuccess = () => console.log('Authenticated!'); ----- - -The available events are: - -* *onReady(authenticated)* - Called when the adapter is initialized. -* *onAuthSuccess* - Called when a user is successfully authenticated. -* *onAuthError* - Called if there was an error during authentication. -* *onAuthRefreshSuccess* - Called when the token is refreshed. -* *onAuthRefreshError* - Called if there was an error while trying to refresh the token. -* *onAuthLogout* - Called if the user is logged out (will only be called if the session status iframe is enabled, or in Cordova mode). -* *onTokenExpired* - Called when the access token is expired. If a refresh token is available the token can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow) you can redirect to the login screen to obtain a new access token. diff --git a/docs/documentation/securing_apps/topics/oidc/mod-auth-openidc.adoc b/docs/documentation/securing_apps/topics/oidc/mod-auth-openidc.adoc deleted file mode 100644 index 54028e97f75a..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/mod-auth-openidc.adoc +++ /dev/null @@ -1,49 +0,0 @@ -[[_mod_auth_openidc]] -=== mod_auth_openidc Apache HTTPD Module - -The https://github.com/OpenIDC/mod_auth_openidc[mod_auth_openidc] is an Apache HTTP plugin for OpenID Connect. If your language/environment supports using Apache HTTPD -as a proxy, then you can use _mod_auth_openidc_ to secure your web application with OpenID Connect. Configuration of this module -is beyond the scope of this document. Please see the _mod_auth_openidc_ GitHub repo for more details on configuration. - -To configure _mod_auth_openidc_ you'll need - -* The client_id. -* The client_secret. -* The redirect_uri to your application. -* The {project_name} openid-configuration url -* _mod_auth_openidc_ specific Apache HTTPD module config. - -An example configuration would look like the following. - -[source,subs="attributes+"] ----- -LoadModule auth_openidc_module modules/mod_auth_openidc.so - -ServerName ${HOSTIP} - - - - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - #this is required by mod_auth_openidc - OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer - - OIDCProviderMetadataURL ${KC_ADDR}{kc_realms_path}/${KC_REALM}/.well-known/openid-configuration - - OIDCClientID ${CLIENT_ID} - OIDCClientSecret ${CLIENT_SECRET} - OIDCRedirectURI http://${HOSTIP}/${CLIENT_APP_NAME}/redirect_uri - - # maps the preferred_username claim to the REMOTE_USER environment variable - OIDCRemoteUserClaim preferred_username - - - AuthType openid-connect - Require valid-user - - ----- - -Further information on how to configure mod_auth_openidc can be found on the https://github.com/OpenIDC/mod_auth_openidc[mod_auth_openidc] -project page. diff --git a/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc b/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc deleted file mode 100644 index 90bad646f10c..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/nodejs-adapter.adoc +++ /dev/null @@ -1,373 +0,0 @@ -[[_nodejs_adapter]] -=== {project_name} Node.js adapter - -{project_name} provides a Node.js adapter built on top of https://github.com/senchalabs/connect[Connect] to protect server-side JavaScript apps - the goal was to be flexible enough to integrate with frameworks like https://expressjs.com/[Express.js]. - -ifeval::[{project_community}==true] -The library can be downloaded directly from https://www.npmjs.com/package/keycloak-connect[ {project_name} organization] and the source is available at -https://github.com/keycloak/keycloak-nodejs-connect[GitHub]. -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. - -==== Installation - -Assuming you've already installed https://nodejs.org[Node.js], create a folder for your application: - - mkdir myapp && cd myapp - -Use `npm init` command to create a `package.json` for your application. Now add the {project_name} connect adapter in the dependencies list: - -ifeval::[{project_community}==true] - -[source,json,subs="attributes"] ----- - "dependencies": { - "keycloak-connect": "{project_versionNpm}" - } ----- - -endif::[] - -ifeval::[{project_product}==true] - -[source,json,subs="attributes"] ----- - "dependencies": { - "keycloak-connect": "file:keycloak-connect-{project_versionNpm}.tgz" - } ----- - -endif::[] - -==== Usage -Instantiate a Keycloak class:: - -The `Keycloak` class provides a central point for configuration -and integration with your application. The simplest creation -involves no arguments. - -In the root directory of your project create a file called `server.js` and add the following code: - -[source,javascript] ----- - const session = require('express-session'); - const Keycloak = require('keycloak-connect'); - - const memoryStore = new session.MemoryStore(); - const keycloak = new Keycloak({ store: memoryStore }); ----- - -Install the `express-session` dependency: - ----- - npm install express-session ----- - -To start the `server.js` script, add the following command in the 'scripts' section of the `package.json`: - ----- - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" - }, ----- - -Now we have the ability to run our server with following command: - ----- - npm run start ----- - -By default, this will locate a file named `keycloak.json` alongside -the main executable of your application, in our case on the root folder, to initialize {project_name} specific -settings such as public key, realm name, various URLs. - -In that case a {project_name} deployment is necessary to access {project_name} admin console. - -Please visit links on how to deploy a {project_name} admin console with -https://www.keycloak.org/getting-started/getting-started-podman[Podman] or https://www.keycloak.org/getting-started/getting-started-docker[Docker] - -Now we are ready to obtain the `keycloak.json` file by visiting the {project_name} Admin Console -> clients (left sidebar) -> choose your client -> Installation -> Format Option -> Keycloak OIDC JSON -> Download - -Paste the downloaded file on the root folder of our project. - -Instantiation with this method results in all the reasonable defaults -being used. As alternative, it's also possible to provide a configuration -object, rather than the `keycloak.json` file: - -[source,javascript,subs="attributes+"] ----- - const kcConfig = { - clientId: 'myclient', - bearerOnly: true, - serverUrl: 'http://localhost:8080{kc_base_path}', - realm: 'myrealm', - realmPublicKey: 'MIIBIjANB...' - }; - - const keycloak = new Keycloak({ store: memoryStore }, kcConfig); ----- - -Applications can also redirect users to their preferred identity provider by using: -[source,javascript] ----- - const keycloak = new Keycloak({ store: memoryStore, idpHint: myIdP }, kcConfig); ----- - -Configuring a web session store:: - -If you want to use web sessions to manage -server-side state for authentication, you need to initialize the -`Keycloak(...)` with at least a `store` parameter, passing in the actual -session store that `express-session` is using. -[source,javascript] ----- - const session = require('express-session'); - const memoryStore = new session.MemoryStore(); - - // Configure session - app.use( - session({ - secret: 'mySecret', - resave: false, - saveUninitialized: true, - store: memoryStore, - }) - ); - - const keycloak = new Keycloak({ store: memoryStore }); ----- -Passing a custom scope value:: - -By default, the scope value `openid` is passed as a query parameter to {project_name}'s login URL, but you can add an additional custom value: -[source,javascript] - const keycloak = new Keycloak({ scope: 'offline_access' }); - -==== Installing middleware - -Once instantiated, install the middleware into your connect-capable app: - -In order to do so, first we have to install Express: ----- - npm install express ----- - -then require Express in our project as outlined below: - -[source,javascript] ----- - const express = require('express'); - const app = express(); ----- - - -and configure Keycloak middleware in Express, by adding at the code below: - -[source,javascript] ----- - app.use( keycloak.middleware() ); ----- - -Last but not least, let's set up our server to listen for HTTP requests on port 3000 by adding the following code to `main.js`: - -[source,javascript] ----- - app.listen(3000, function () { - console.log('App listening on port 3000'); - }); ----- - -==== Configuration for proxies - -If the application is running behind a proxy that terminates an SSL connection -Express must be configured per the link:https://expressjs.com/en/guide/behind-proxies.html[express behind proxies] guide. -Using an incorrect proxy configuration can result in invalid redirect URIs -being generated. - -Example configuration: - -[source,javascript] ----- - const app = express(); - - app.set( 'trust proxy', true ); - - app.use( keycloak.middleware() ); ----- - -==== Protecting resources - -Simple authentication:: - -To enforce that a user must be authenticated before accessing a resource, -simply use a no-argument version of `keycloak.protect()`: - -[source,javascript] ----- - app.get( '/complain', keycloak.protect(), complaintHandler ); ----- - -Role-based authorization:: - -To secure a resource with an application role for the current app: - -[source,javascript] ----- - app.get( '/special', keycloak.protect('special'), specialHandler ); ----- - -To secure a resource with an application role for a *different* app: - -[source,javascript] - app.get( '/extra-special', keycloak.protect('other-app:special'), extraSpecialHandler ); - -To secure a resource with a realm role: - -[source,javascript] - app.get( '/admin', keycloak.protect( 'realm:admin' ), adminHandler ); - -Resource-Based Authorization:: - -Resource-Based Authorization allows you to protect resources, and their specific methods/actions,**** based on a set of policies defined in Keycloak, thus externalizing authorization from your application. This is achieved by exposing a `keycloak.enforcer` method which you can use to protect resources.* - -[source,javascript] ----- - app.get('/apis/me', keycloak.enforcer('user:profile'), userProfileHandler); ----- - -The `keycloak-enforcer` method operates in two modes, depending on the value of the `response_mode` configuration option. - -[source,javascript] ----- - app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'token'}), userProfileHandler); ----- - -If `response_mode` is set to `token`, permissions are obtained from the server on behalf of the subject represented by the bearer token that was sent to your application. In this case, a new access token is issued by Keycloak with the permissions granted by the server. If the server did not respond with a token with the expected permissions, the request is denied. When using this mode, you should be able to obtain the token from the request as follows: - -[source,javascript] ----- - app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'token'}), function (req, res) { - const token = req.kauth.grant.access_token.content; - const permissions = token.authorization ? token.authorization.permissions : undefined; - - // show user profile - }); ----- - -Prefer this mode when your application is using sessions and you want to cache previous decisions from the server, as well automatically handle refresh tokens. This mode is especially useful for applications acting as a client and resource server. - -If `response_mode` is set to `permissions` (default mode), the server only returns the list of granted permissions, without issuing a new access token. In addition to not issuing a new token, this method exposes the permissions granted by the server through the `request` as follows: - -[source,javascript] ----- - app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'permissions'}), function (req, res) { - const permissions = req.permissions; - - // show user profile - }); ----- - -Regardless of the `response_mode` in use, the `keycloak.enforcer` method will first try to check the permissions within the bearer token that was sent to your application. If the bearer token already carries the expected permissions, there is no need -to interact with the server to obtain a decision. This is specially useful when your clients are capable of obtaining access tokens from the server with the expected permissions before accessing a protected resource, so they can use some capabilities provided by Keycloak Authorization Services such as incremental authorization and avoid additional requests to the server when `keycloak.enforcer` is enforcing access to the resource. - -By default, the policy enforcer will use the `client_id` defined to the application (for instance, via `keycloak.json`) to - reference a client in Keycloak that supports Keycloak Authorization Services. In this case, the client can not be public given - that it is actually a resource server. - -If your application is acting as both a public client(frontend) and resource server(backend), you can use the following configuration to reference a different -client in Keycloak with the policies that you want to enforce: - -[source,javascript] ----- - keycloak.enforcer('user:profile', {resource_server_id: 'my-apiserver'}) ----- - -It is recommended to use distinct clients in Keycloak to represent your frontend and backend. - -If the application you are protecting is enabled with Keycloak authorization services and you have defined client credentials - in `keycloak.json`, you can push additional claims to the server and make them available to your policies in order to make decisions. -For that, you can define a `claims` configuration option which expects a `function` that returns a JSON with the claims you want to push: - -[source,javascript] ----- - app.get('/protected/resource', keycloak.enforcer(['resource:view', 'resource:write'], { - claims: function(request) { - return { - "http.uri": ["/protected/resource"], - "user.agent": // get user agent from request - } - } - }), function (req, res) { - // access granted ----- - -For more details about how to configure Keycloak to protected your application resources, please take a look at the link:{authorizationguide_link}[{authorizationguide_name}]. - -Advanced authorization:: - -To secure resources based on parts of the URL itself, assuming a role exists -for each section: - -[source,javascript] ----- - function protectBySection(token, request) { - return token.hasRole( request.params.section ); - } - - app.get( '/:section/:page', keycloak.protect( protectBySection ), sectionHandler ); ----- - -Advanced Login Configuration: - -By default, all unauthorized requests will be redirected to the {project_name} login page unless your client is bearer-only. -However, a confidential or public client may host both browsable and API endpoints. To prevent redirects on unauthenticated -API requests and instead return an HTTP 401, you can override the redirectToLogin function. - -For example, this override checks if the URL contains /api/ and disables login redirects: - -[source,javascript] ----- - Keycloak.prototype.redirectToLogin = function(req) { - const apiReqMatcher = /\/api\//i; - return !apiReqMatcher.test(req.originalUrl || req.url); - }; ----- - -==== Additional URLs - -Explicit user-triggered logout:: - -By default, the middleware catches calls to `/logout` to send the user through a -{project_name}-centric logout workflow. This can be changed by specifying a `logout` -configuration parameter to the `middleware()` call: - -[source,javascript] ----- - app.use( keycloak.middleware( { logout: '/logoff' } )); ----- - -When the user-triggered logout is invoked a query parameter `redirect_url` can be passed: - -[source] ----- -https://example.com/logoff?redirect_url=https%3A%2F%2Fexample.com%3A3000%2Flogged%2Fout ----- - -This parameter is then used as the redirect url of the OIDC logout endpoint and the user will be redirected to -`\https://example.com/logged/out`. - -{project_name} Admin Callbacks:: - -Also, the middleware supports callbacks from the {project_name} console to log out a single -session or all sessions. By default, these type of admin callbacks occur relative -to the root URL of `/` but can be changed by providing an `admin` parameter -to the `middleware()` call: -[source,javascript] - app.use( keycloak.middleware( { admin: '/callbacks' } ); - -==== Complete example - -A complete example using the Node.js adapter usage can be found in {quickstartRepo_link}/tree/latest/nodejs/resource-server[Keycloak quickstarts for Node.js] diff --git a/docs/documentation/securing_apps/topics/oidc/oidc-overview.adoc b/docs/documentation/securing_apps/topics/oidc/oidc-overview.adoc deleted file mode 100644 index 1902035890ef..000000000000 --- a/docs/documentation/securing_apps/topics/oidc/oidc-overview.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[_oidc]] -== Using OpenID Connect to secure applications and services - -This section describes how you can secure applications and services with OpenID Connect using {project_name}. \ No newline at end of file diff --git a/docs/documentation/securing_apps/topics/overview/getting-started.adoc b/docs/documentation/securing_apps/topics/overview/getting-started.adoc deleted file mode 100644 index 78ae117a7455..000000000000 --- a/docs/documentation/securing_apps/topics/overview/getting-started.adoc +++ /dev/null @@ -1,64 +0,0 @@ -=== Getting Started - -The link:{quickstartRepo_link}[{quickstartRepo_name}] provides examples about how to secure applications and services -using different programming languages and frameworks. By going through their documentation and codebase, you will -understand the bare minimum changes required in your application and service in order to secure it with {project_name}. - -Also, see the following sections for recommendations for trusted and well-known client-side implementations for both OpenID -Connect and SAML protocols. - -==== OpenID Connect - -ifeval::[{project_community}==true] -===== Java -* {quickstartRepo_link}/tree/latest/jakarta/servlet-authz-client[Wildfly Elytron OIDC] -* {quickstartRepo_link}/tree/latest/spring/rest-authz-resource-server[Spring Boot] -* <<_jboss_adapter, {project_name} Wildfly Adapter>> (Deprecated) -* <<_tomcat_adapter,{project_name} Tomcat Adapter>> (Deprecated) -* <<_jetty9_adapter,{project_name} Jetty 9>> (Deprecated) -* <<_servlet_filter_adapter,{project_name} Servlet Filter>> (Deprecated) -* <<_spring_boot_adapter,{project_name} Spring Boot>> (Deprecated) -* <<_spring_security_adapter,{project_name} Spring Security>> (Deprecated) -endif::[] - -===== JavaScript (client-side) -* <<_javascript_adapter,JavaScript>> - -===== Node.js (server-side) -* <<_nodejs_adapter,Node.js>> - - -ifeval::[{project_community}==true] -===== C# -* https://github.com/dylanplecki/KeycloakOwinAuthentication[OWIN] - -===== Python -* https://pypi.org/project/oic/[oidc] - -===== Android -* https://github.com/openid/AppAuth-Android[AppAuth] - -===== iOS -* https://github.com/openid/AppAuth-iOS[AppAuth] - -===== Apache HTTP Server -* https://github.com/OpenIDC/mod_auth_openidc[mod_auth_openidc] -endif::[] - -==== SAML - -===== Java - -* <<_saml_jboss_adapter,JBoss EAP>> -ifeval::[{project_community}==true] -* <<_saml_jboss_adapter,WildFly>> -* <<_saml-tomcat-adapter,Tomcat>> -endif::[] -* <<_java-servlet-filter-adapter,Servlet filter>> -ifeval::[{project_community}==true] -* <<_jetty_saml_adapter,Jetty>> -endif::[] - -===== Apache HTTP Server - -* <<_mod_auth_mellon,mod_auth_mellon>> diff --git a/docs/documentation/securing_apps/topics/overview/overview.adoc b/docs/documentation/securing_apps/topics/overview/overview.adoc deleted file mode 100644 index 37654e21c6e0..000000000000 --- a/docs/documentation/securing_apps/topics/overview/overview.adoc +++ /dev/null @@ -1,16 +0,0 @@ -== Planning for securing applications and services - -As an OAuth2, OpenID Connect, and SAML compliant server, {project_name} can secure any application and service as long -as the technology stack they are using supports any of these protocols. For more details about the security protocols -supported by {project_name}, consider looking at link:{adminguide_link}#sso-protocols[{adminguide_name}]. - -Most of the support for some of these protocols is already available from the programming language, framework, -or reverse proxy they are using. Leveraging the support already available from the application ecosystem is a key aspect to make your -application fully compliant with security standards and best practices, so that you avoid vendor lock-in. - -For some programming languages, {project_name} provides libraries that try to fill the gap for the lack of support of -a particular security protocol or to provide a more rich and tightly coupled integration with the server. These libraries -are known by *Keycloak Client Adapters*, and they should be used as a last resort if you cannot rely on what is available -from the application ecosystem. - - diff --git a/docs/documentation/securing_apps/topics/saml/java/MigrationFromOlderVersions.adoc b/docs/documentation/securing_apps/topics/saml/java/MigrationFromOlderVersions.adoc deleted file mode 100644 index 05cbfe216f76..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/MigrationFromOlderVersions.adoc +++ /dev/null @@ -1,11 +0,0 @@ -==== Migration from older versions - -===== Migrating to 1.9.0 - -====== SAML SP Client Adapter changes - -Keycloak SAML SP Client Adapter now requires a specific endpoint, `/saml` to be registered with your IdP. -The SamlFilter must also be bound to /saml in addition to any other binding it has. -This had to be done because SAML POST binding would eat the request input stream and this would be really bad for clients that relied on it. - - diff --git a/docs/documentation/securing_apps/topics/saml/java/java-adapters-product.adoc b/docs/documentation/securing_apps/topics/saml/java/java-adapters-product.adoc deleted file mode 100644 index f865110c9a51..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/java-adapters-product.adoc +++ /dev/null @@ -1,19 +0,0 @@ - -=== {project_name} Java adapters - -{project_name} comes with a range of different adapters for Java application. Selecting the correct adapter depends on the target platform. - -==== Red Hat JBoss Enterprise Application Platform - -===== 8.0 Beta - -{project_name} provides a SAML adapter for Red Hat Enterprise Application Platform 8.0 Beta. However, the documentation -is not currently available, and will be added in the near future. - -===== 6.4 and 7.x - -Existing applications deployed to Red Hat JBoss Enterprise Application Platform 6.4 and 7.x can leverage adapters from -Red Hat Single Sign-On 7.6 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/using_saml_to_secure_applications_and_services#saml_jboss_adapter[Red Hat Single Sign-On documentation]. diff --git a/docs/documentation/securing_apps/topics/saml/java/java-adapters.adoc b/docs/documentation/securing_apps/topics/saml/java/java-adapters.adoc deleted file mode 100644 index 63de0ad1c09f..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/java-adapters.adoc +++ /dev/null @@ -1,44 +0,0 @@ - -=== {project_name} Java adapters - -{project_name} comes with a range of different adapters for Java application. Selecting the correct adapter depends on the target platform. - -include::general-config.adoc[] -include::general-config/sp_element.adoc[] -include::general-config/sp-keys.adoc[] -include::general-config/sp-keys/keystore_element.adoc[] -include::general-config/sp-keys/key_pems.adoc[] -include::general-config/sp_principalname_mapping_element.adoc[] -include::general-config/roleidentifiers_element.adoc[] -include::general-config/sp_role_mappings_provider_element.adoc[] -include::general-config/idp_element.adoc[] -include::general-config/idp_allowedclockskew_subelement.adoc[] -include::general-config/idp_singlesignonservice_subelement.adoc[] -include::general-config/idp_singlelogoutservice_subelement.adoc[] -include::general-config/idp_keys_subelement.adoc[] -include::general-config/idp_httpclient_subelement.adoc[] -include::saml-jboss-adapter.adoc[] -include::jboss-adapter/jboss_adapter_installation.adoc[] -include::jboss-adapter/jboss-adapter-samesite-setting.adoc[] -include::jboss-adapter/required_per_war_configuration.adoc[] -include::jboss-adapter/securing_wars.adoc[] -ifeval::[{project_community}==true] -include::tomcat-adapter.adoc[] -include::tomcat-adapter/tomcat_adapter_installation.adoc[] -include::tomcat-adapter/tomcat_adapter_per_war_config.adoc[] -include::tomcat-adapter/tomcat-adapter-samesite-setting.adoc[] -include::jetty-adapter.adoc[] -include::jetty-adapter/jetty9_installation.adoc[] -include::jetty-adapter/jetty9_per_war_config.adoc[] -endif::[] - -include::servlet-filter-adapter.adoc[] -include::idp-registration.adoc[] -include::logout.adoc[] -include::assertion-api.adoc[] -include::error_handling.adoc[] -include::debugging.adoc[] -include::multi-tenancy.adoc[] -ifeval::[{project_community}==true] -include::MigrationFromOlderVersions.adoc[] -endif::[] \ No newline at end of file diff --git a/docs/documentation/securing_apps/topics/saml/java/jboss-adapter/jboss_adapter_installation.adoc b/docs/documentation/securing_apps/topics/saml/java/jboss-adapter/jboss_adapter_installation.adoc deleted file mode 100644 index 7ef6b3b33be0..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/jboss-adapter/jboss_adapter_installation.adoc +++ /dev/null @@ -1,15 +0,0 @@ - -[[_saml-jboss-adapter-installation]] -==== Installing adapters from a Galleon feature pack - -For the WildFly 29 or newer, the SAML adapter is provided as a Galleon feature pack. More details about this is -in the https://docs.wildfly.org/30/WildFly_Elytron_Security.html#Keycloak_SAML_Integration[WildFly documentation]. The same option will be provided -for JBoss EAP 8 GA. - -{project_name} provided adapter ZIP download in the past, but it is not provided anymore. For the older WildFly versions, it is recommended to upgrade -to newer WildFly/EAP and use Galleon. Otherwise, you will need to stick with the older {project_name} adapters, but those are not maintained and officially supported. -For JBoss EAP 7, it is possible to stick with the Red Hat Single Sign-On 7.6 adapters, which is still supported. - -For more details on how to integrate {project_name} with JakartaEE applications running on latest Wildfly/EAP, take a look at the Jakarta EE quickstart in the {quickstartRepo_link}[Keycloak Quickstart GitHub Repository]. - -Below are the details on how to configure the SAML adapter secured by Galleon. diff --git a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter.adoc b/docs/documentation/securing_apps/topics/saml/java/jetty-adapter.adoc deleted file mode 100644 index 63a0eff26cc4..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter.adoc +++ /dev/null @@ -1,9 +0,0 @@ -[[_jetty_saml_adapter]] - -==== Jetty SAML adapters - -WARNING: The {project_name} Jetty SAML adapter is deprecated. We recommend that you use another client adapter if possible. - -To be able to secure WAR apps deployed on Jetty you must install the {project_name} Jetty 9.4 SAML adapter into your Jetty installation. You then provide some extra configuration in each WAR you deploy to Jetty. - -Use the following installation and configuration procedures. diff --git a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_installation.adoc b/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_installation.adoc deleted file mode 100644 index d3f898ba399d..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_installation.adoc +++ /dev/null @@ -1,30 +0,0 @@ -[[_jetty9_saml_adapter_installation]] - -===== Jetty 9 Installing the adapter - -{project_name} has a separate SAML adapter for Jetty 9.4. Adapters are no longer included with the appliance or war distribution. Each adapter is a separate download on the Keycloak download site. -They are also available as a maven artifact. - -.Procedure -. Download the {project_name} Jetty 9.4 adapter ZIP archive from the link:https://www.keycloak.org/downloads[Keycloak Downloads] site. - -. Unzip the Jetty 9.4 distro into Jetty 9.4's root directory. -+ -==== -[NOTE] -Including adapter's jars within your WEB-INF/lib directory will not work. -==== -+ -[source] ----- -$ cd $JETTY_HOME -$ unzip keycloak-saml-jetty94-adapter-dist.zip ----- - -. Enable the keycloak module for your jetty.base. -+ -[source] ----- -$ cd your-base -$ java -jar $JETTY_HOME/start.jar --add-to-startd=keycloak ----- diff --git a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_per_war_config.adoc b/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_per_war_config.adoc deleted file mode 100644 index 2343749c91de..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/jetty-adapter/jetty9_per_war_config.adoc +++ /dev/null @@ -1,64 +0,0 @@ - -[[_saml-jetty9-per-war]] -===== Jetty 9 WAR Configuration - -Use this procedure to secure a WAR directly. - -.Procedure -. Create a `WEB-INF/jetty-web.xml` file in your WAR package. -This is a Jetty specific config file and you must define a Keycloak specific authenticator within it. -+ -[source,xml] ----- - - - - - - - - - - ----- - -. Create a `keycloak-saml.xml` adapter config file within the `WEB-INF` directory of your WAR. -The format of this config file is described in the <<_saml-general-config,General Adapter Config>> section. - -. Specify both a `login-config` and use standard servlet security to specify role-base constraints on your URLs. Here's an example: -+ -[source,xml] ----- - - - customer-portal - - - - Customers - /* - - - user - - - CONFIDENTIAL - - - - - BASIC - this is ignored currently - - - - admin - - - user - - ----- diff --git a/docs/documentation/securing_apps/topics/saml/java/logout.adoc b/docs/documentation/securing_apps/topics/saml/java/logout.adoc deleted file mode 100644 index 15166c52238b..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/logout.adoc +++ /dev/null @@ -1,74 +0,0 @@ -==== Logout - -There are multiple ways you can log out from a web application. -For Jakarta EE servlet containers, you can call `HttpServletRequest.logout()`. For any other browser application, you can point -the browser at any url of your web application that has a security constraint and pass in a query parameter GLO, i.e. `$$http://myapp?GLO=true$$`. -This will log you out if you have an SSO session with your browser. - -[[_saml_logout_in_cluster]] -===== Logout in clustered environment - -Internally, the SAML adapter stores a mapping between the SAML session index, principal name (when known), and HTTP session ID. -This mapping can be maintained in JBoss application server family (WildFly 10/11, EAP 6/7) across cluster for distributable -applications. As a precondition, the HTTP sessions need to be distributed across cluster (i.e. application is marked with -`` tag in application's `web.xml`). - -To enable the functionality, add the following section to your `/WEB_INF/web.xml` file: - -For EAP 7, WildFly 10/11: - -[source,xml] ----- - - keycloak.sessionIdMapperUpdater.classes - org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater - ----- - -For EAP 6: - -[source,xml] ----- - - keycloak.sessionIdMapperUpdater.classes - org.keycloak.adapters.saml.jbossweb.infinispan.InfinispanSessionCacheIdMapperUpdater - ----- - -If the session cache of the deployment is named `_deployment-cache_`, the cache used for SAML mapping will be named -as `_deployment-cache_.ssoCache`. The name of the cache can be overridden by a context parameter -`keycloak.sessionIdMapperUpdater.infinispan.cacheName`. The cache container containing the cache will be the same as -the one containing the deployment session cache, but can be overridden by a context parameter -`keycloak.sessionIdMapperUpdater.infinispan.containerName`. - -By default, the configuration of the SAML mapping cache will be derived from session cache. The configuration can -be manually overridden in cache configuration section of the server just the same as other caches. - -Currently, to provide reliable service, it is recommended to use replicated cache for the SAML session cache. -Using distributed cache may lead to results where the SAML logout request would land to a node with no access -to SAML session index to HTTP session mapping which would lead to unsuccessful logout. - -[[_saml_logout_in_cross_dc]] -===== Logout in cross-site scenario - -The cross-site scenario only applies to WildFly 10 and higher, and EAP 7 and higher. - -Special handling is needed for handling sessions that span multiple data centers. Imagine the following scenario: - -1. Login requests are handled within cluster in data center 1. - -2. Admin issues logout request for a particular SAML session, the request lands in data center 2. - -The data center 2 has to log out all sessions that are present in data center 1 (and all other data centers that -share HTTP sessions). - -To cover this case, the SAML session cache described <<_saml_logout_in_cluster,above>> needs to be replicated -not only within individual clusters but across all the data centers for example -https://access.redhat.com/documentation/en-us/red_hat_data_grid/6.6/html/administration_and_configuration_guide/chap-externalize_sessions#Externalize_HTTP_Session_from_JBoss_EAP_6.x_to_JBoss_Data_Grid[via standalone Infinispan/JDG server]: - -1. A cache has to be added to the standalone Infinispan/JDG server. - -2. The cache from previous item has to be added as a remote store for the respective SAML session cache. - -Once remote store is found to be present on SAML session cache during deployment, it is watched for changes -and the local SAML session cache is updated accordingly. diff --git a/docs/documentation/securing_apps/topics/saml/java/multi-tenancy.adoc b/docs/documentation/securing_apps/topics/saml/java/multi-tenancy.adoc deleted file mode 100644 index ac72b6a4fb29..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/multi-tenancy.adoc +++ /dev/null @@ -1,72 +0,0 @@ -[[_saml_multi_tenancy]] -==== Multi Tenancy - -SAML offers Multi Tenancy, meaning that a single target application (WAR) can be secured with multiple {project_name} realms. The realms can be located on the same {project_name} instance or on different instances. - -To do this, the application must have multiple `keycloak-saml.xml` adapter configuration files. - -While you could have multiple instances of your WAR with different adapter configuration files deployed to different context-paths, this may be inconvenient and you may also want to select the realm based on something other than context-path. - -{project_name} makes it possible to have a custom config resolver, so you can choose which adapter config is used for each request. In SAML, the configuration is only interesting in the login processing; once the user is logged in, the session is authenticated and it does not matter if the `keycloak-saml.xml` returned is different. For that reason, returning the same configuration for the same session is the correct way to go. - -To achieve this, create an implementation of `org.keycloak.adapters.saml.SamlConfigResolver`. The following example uses the `Host` header to locate the proper configuration and load it and the associated elements from the applications' Java classpath: - -[source,java] ----- -package example; - -import java.io.InputStream; -import org.keycloak.adapters.saml.SamlConfigResolver; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.saml.common.exceptions.ParsingException; - -public class SamlMultiTenantResolver implements SamlConfigResolver { - - @Override - public SamlDeployment resolve(HttpFacade.Request request) { - String host = request.getHeader("Host"); - String realm = null; - if (host.contains("tenant1")) { - realm = "tenant1"; - } else if (host.contains("tenant2")) { - realm = "tenant2"; - } else { - throw new IllegalStateException("Not able to guess the keycloak-saml.xml to load"); - } - - InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak-saml.xml"); - if (is == null) { - throw new IllegalStateException("Not able to find the file /" + realm + "-keycloak-saml.xml"); - } - - ResourceLoader loader = new ResourceLoader() { - @Override - public InputStream getResourceAsStream(String path) { - return getClass().getResourceAsStream(path); - } - }; - - try { - return new DeploymentBuilder().build(is, loader); - } catch (ParsingException e) { - throw new IllegalStateException("Cannot load SAML deployment", e); - } - } -} ----- - -You must also configure which `SamlConfigResolver` implementation to use with the `keycloak.config.resolver` context-param in your `web.xml`: - -[source,xml] ----- - - ... - - keycloak.config.resolver - example.SamlMultiTenantResolver - - ----- diff --git a/docs/documentation/securing_apps/topics/saml/java/saml-jboss-adapter.adoc b/docs/documentation/securing_apps/topics/saml/java/saml-jboss-adapter.adoc deleted file mode 100644 index 4fc0ca7743f2..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/saml-jboss-adapter.adoc +++ /dev/null @@ -1,26 +0,0 @@ -[[_saml_jboss_adapter]] - -ifeval::[{project_community}==true] -==== JBoss EAP/WildFly adapter -endif::[] -ifeval::[{project_product}==true] -==== JBoss EAP adapter -endif::[] - -ifeval::[{project_community}==true] -To be able to secure WAR apps deployed on JBoss EAP or WildFly, you must install and configure the {project_name} SAML Adapter Subsystem. -endif::[] -ifeval::[{project_product}==true] -To be able to secure WAR apps deployed on JBoss EAP, you must install and configure the {project_name} SAML Adapter Subsystem. -endif::[] - -You then provide a keycloak config, `/WEB-INF/keycloak-saml.xml` file in your WAR and change the auth-method to KEYCLOAK-SAML within web.xml. - -ifeval::[{project_product}==true] -You install the adapters by using a ZIP file or an RPM. - -* <<_saml-jboss-adapter-installation, Installing adapters from a ZIP file>> -* <<_jboss7_adapter_rpm_saml, Installing JBoss EAP 7 Adapters from an RPM>> -* <<_jboss6_adapter_rpm_saml, Installing JBoss EAP 6 Adapters from an RPM>> -endif::[] - diff --git a/docs/documentation/securing_apps/topics/saml/java/saml_adapter_overview.adoc b/docs/documentation/securing_apps/topics/saml/java/saml_adapter_overview.adoc deleted file mode 100644 index 7cb067749646..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/saml_adapter_overview.adoc +++ /dev/null @@ -1,6 +0,0 @@ -= Overview - -This document describes the Keycloak SAML client adapter and how it can be configured for a variety of platforms. -The Keycloak SAML client adapter is a standalone component that provides generic SAML 2.0 support for your web applications. -There are no Keycloak server extensions built into it. -As long as the Identity Provider (IdP) being communicated with supports standard SAML, the Keycloak SAML client adapter should be able to integrate with it. diff --git a/docs/documentation/securing_apps/topics/saml/java/servlet-filter-adapter.adoc b/docs/documentation/securing_apps/topics/saml/java/servlet-filter-adapter.adoc deleted file mode 100644 index f78252954ce2..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/servlet-filter-adapter.adoc +++ /dev/null @@ -1,71 +0,0 @@ -[[_java-servlet-filter-adapter]] -==== Java Servlet filter adapter - -If you want to use SAML with a Java servlet application that doesn't have an adapter for that servlet platform, you can -opt to use the servlet filter adapter that {project_name} has. -This adapter works a little differently than the other adapters. -You still have to specify a `/WEB-INF/keycloak-saml.xml` file as defined in -the <<_saml-general-config,General Adapter Config>> section, but -you do not define security constraints in _web.xml_. -Instead you define a filter mapping using the {project_name} servlet filter adapter to secure the url patterns you want to secure. - -NOTE: Backchannel logout works a bit differently than the standard adapters. - Instead of invalidating the http session it instead marks the session ID as logged out. - There's just no way of arbitrarily invalidating an HTTP session based on a session ID. - -WARNING: Backchannel logout does not currently work when you have a clustered application that uses the SAML filter. - -[source,xml] ----- - - - customer-portal - - - Keycloak Filter - org.keycloak.adapters.saml.servlet.SamlFilter - - - Keycloak Filter - /* - - ----- - -The {project_name} filter has the same configuration parameters available as the other adapters except you must -define them as filter init params instead of context params. - -You can define multiple filter mappings if you have various different secure and unsecure url patterns. - -WARNING: You must have a filter mapping that covers `/saml`. - This mapping covers all server callbacks. - -When registering SPs with an IdP, you must register `http[s]://hostname/{context-root}/saml` as your Assert Consumer Service URL and Single Logout Service URL. - -To use this filter, include this maven artifact in your WAR poms: - -[source,xml,subs="attributes+"] ----- - - org.keycloak - keycloak-saml-servlet-filter-adapter - {project_versionMvn} - ----- - -In order to use <<_saml_multi_tenancy,Multi Tenancy>> the `keycloak.config.resolver` parameter should be passed as a filter parameter. - -[source,xml] ----- - - Keycloak Filter - org.keycloak.adapters.saml.servlet.SamlFilter - - keycloak.config.resolver - example.SamlMultiTenantResolver - - ----- diff --git a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter.adoc b/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter.adoc deleted file mode 100644 index 3df0d7d390b7..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter.adoc +++ /dev/null @@ -1,10 +0,0 @@ -[[_saml-tomcat-adapter]] - -==== Tomcat SAML adapters - -WARNING: The {project_name} Tomcat SAML adapter is deprecated. We recommend that you use another client adapter if possible. - -To be able to secure WAR apps deployed on Tomcat 8 or 9 you must install the Keycloak Tomcat SAML adapter into your Tomcat installation. -You then have to provide some extra configuration in each WAR you deploy to Tomcat. - - diff --git a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat-adapter-samesite-setting.adoc b/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat-adapter-samesite-setting.adoc deleted file mode 100644 index 8861a0d14d3f..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat-adapter-samesite-setting.adoc +++ /dev/null @@ -1,24 +0,0 @@ -[[_saml-tomcat-adapter-samesite-setting]] -===== Setting SameSite value for JSESSIONID cookie - -Browsers are planning to set the default value for the `SameSite` attribute for cookies to `Lax`. This setting means -that cookies will be sent to applications only if the request originates in the same domain. This behavior can affect -the SAML POST binding which may become non-functional. To preserve full functionality of the SAML adapter, we recommend -setting the `SameSite` value to `None` for the `JSESSIONID` cookie created by your container. Not doing so may result in -resetting the container's session with each request to {project_name}. - -NOTE: To avoid setting the `SameSite` attribute to `None`, consider switching to the REDIRECT binding -if it is acceptable, or to OIDC protocol where this workaround is not necessary. - -To set the `SameSite` value to `None` for `JSESSIONID` cookie in Tomcat add following configuration to the`context.xml` -of your application. Note, this will set the `SameSite` value to `None` for all cookies created by Tomcat container. - -[source,xml] ----- - ----- - -WARNING: It is not possible to set the `SameSite` attribute only to a subset of cookies, therefore all cookies created -for your application will have this attribute set to `None`. - -The support for this feature is available in Tomcat from versions 9.0.29 and 8.5.49. diff --git a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_installation.adoc b/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_installation.adoc deleted file mode 100644 index a5d3e949119f..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_installation.adoc +++ /dev/null @@ -1,26 +0,0 @@ - -[[_saml-tomcat-adapter-installation]] -===== Installing the adapter - -Adapters are no longer included with the appliance or war distribution. -Each adapter is a separate download on the Keycloak Downloads site. -They are also available as a maven artifact. - -.Procedure - -. Download the adapter for the Tomcat version on your system from the link:https://www.keycloak.org/downloads[Keycloak Downloads] site: - -. Install on the Tomcat version on your system: - -* Install on Tomcat 8 or 9: -+ -[source] ----- -$ cd $TOMCAT_HOME/lib -$ unzip keycloak-saml-tomcat-adapter-dist.zip ----- - -==== -[NOTE] -Including the adapter's jars within your WEB-INF/lib directory will not work. The Keycloak SAML adapter is implemented as a Valve and valve code must reside in Tomcat's main lib/ directory. -==== diff --git a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_per_war_config.adoc b/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_per_war_config.adoc deleted file mode 100644 index 3119addaf1ad..000000000000 --- a/docs/documentation/securing_apps/topics/saml/java/tomcat-adapter/tomcat_adapter_per_war_config.adoc +++ /dev/null @@ -1,57 +0,0 @@ - -===== Securing a WAR - -Use this procedure to secure a WAR directly by adding config and editing files within your WAR package. - -.Procedure - -. Create a `META-INF/context.xml` file in your WAR package. -This is a Tomcat specific config file and you must define a Keycloak specific Valve. -+ -[source,xml] ----- - - - ----- - -. Create a `keycloak-saml.xml` adapter config file within the `WEB-INF` directory of your WAR. -The format of this config file is described in the <<_saml-general-config,General Adapter Config>> section. - -. Specify both a `login-config` and use standard servlet security to specify role-base constraints on your URLs. -Here's an example: -+ -[source,xml] ----- - - - customer-portal - - - - Customers - /* - - - user - - - - - BASIC - this is ignored currently - - - - admin - - - user - - ----- - -If the `keycloak-saml.xml` does not explicitly set `assertionConsumerServiceUrl`, the SAML adapter will implicitly listen for SAML assertions at the location `/my-context-path/saml`. This has to match `Master SAML Processing URL` in the IDP realm/client settings, for example `\http://sp.domain.com/my-context-path/saml`. If not, Tomcat will probably redirect infinitely to the IDP login service, as it does not receive the SAML assertion after the user logged in. diff --git a/docs/documentation/securing_apps/topics/saml/saml-overview.adoc b/docs/documentation/securing_apps/topics/saml/saml-overview.adoc deleted file mode 100644 index 7dad2641f227..000000000000 --- a/docs/documentation/securing_apps/topics/saml/saml-overview.adoc +++ /dev/null @@ -1,3 +0,0 @@ -== Using SAML to secure applications and services - -This section describes how you can secure applications and services with SAML using either {project_name} client adapters or generic SAML provider libraries. \ No newline at end of file diff --git a/docs/documentation/securing_apps/topics/templates b/docs/documentation/securing_apps/topics/templates deleted file mode 120000 index d19126411538..000000000000 --- a/docs/documentation/securing_apps/topics/templates +++ /dev/null @@ -1 +0,0 @@ -../../topics/templates \ No newline at end of file diff --git a/docs/documentation/securing_apps/topics/token-exchange/token-exchange.adoc b/docs/documentation/securing_apps/topics/token-exchange/token-exchange.adoc deleted file mode 100644 index 66a4ee86f2d4..000000000000 --- a/docs/documentation/securing_apps/topics/token-exchange/token-exchange.adoc +++ /dev/null @@ -1,530 +0,0 @@ - -[[_token-exchange]] - -== Using token exchange - -:tech_feature_name: Token Exchange -:tech_feature_setting: -Dkeycloak.profile.feature.token_exchange=enabled -include::../templates/techpreview.adoc[] - -[NOTE] -==== -To use more than the <<_internal-token-to-internal-token-exchange,Internal Token to Internal Token Exchange>> flow, also enable the `admin-fine-grained-authz` feature. -For details, see the https://www.keycloak.org/server/features[Enabling and disabling features] {section}. -==== - -=== How token exchange works - -In {project_name}, token exchange is the process of using a set of credentials or token to obtain an entirely different token. -A client may want to invoke on a less trusted application so it may want to downgrade the current token it has. -A client may want to exchange a {project_name} token for a token stored for a linked social provider account. -You may want to trust external tokens minted by other {project_name} realms or foreign IDPs. A client may have a need -to impersonate a user. Here's a short summary of the current capabilities of {project_name} around token exchange. - -* A client can exchange an existing {project_name} token created for a specific client for a new token targeted to a different client -* A client can exchange an existing {project_name} token for an external token, i.e. a linked Facebook account -* A client can exchange an external token for a {project_name} token. -* A client can impersonate a user - -Token exchange in {project_name} is a very loose implementation of the link:https://datatracker.ietf.org/doc/html/rfc8693[OAuth Token Exchange] specification at the IETF. -We have extended it a little, ignored some of it, and loosely interpreted other parts of the specification. It is -a simple grant type invocation on a realm's OpenID Connect token endpoint. - -[source,subs="attributes+"] ----- -{kc_realms_path}/{realm}/protocol/openid-connect/token ----- - -It accepts form parameters (`application/x-www-form-urlencoded`) as input and the output depends on the type of token you requested an exchange for. -Token exchange is a client endpoint so requests must provide authentication information for the calling client. -Public clients specify their client identifier as a form parameter. Confidential clients can also use form parameters -to pass their client id and secret, Basic Auth, or however your admin has configured the client authentication flow in your -realm. - -==== Form parameters - -client_id:: - _REQUIRED MAYBE._ This parameter is required for clients using form parameters for authentication. If you are using - Basic Auth, a client JWT token, or client cert authentication, then do not specify this parameter. -client_secret:: - _REQUIRED MAYBE_. This parameter is required for clients using form parameters for authentication and using a client secret as a credential. - Do not specify this parameter if client invocations in your realm are authenticated by a different means. - -grant_type:: - _REQUIRED._ The value of the parameter must be `urn:ietf:params:oauth:grant-type:token-exchange`. -subject_token:: - _OPTIONAL._ A security token that represents the identity of the party on behalf of whom the request is being made. It is required if you are exchanging an existing token for a new one. -subject_issuer:: - _OPTIONAL._ Identifies the issuer of the `subject_token`. It can be left blank if the token comes from the current realm or if the issuer - can be determined from the `subject_token_type`. Otherwise it is required to be specified. Valid values are the alias of an `Identity Provider` configured for your realm. Or an issuer claim identifier - configured by a specific `Identity Provider`. -subject_token_type:: - _OPTIONAL._ This parameter is the type of the token passed with the `subject_token` parameter. This defaults - to `urn:ietf:params:oauth:token-type:access_token` if the `subject_token` comes from the realm and is an access token. - If it is an external token, this parameter may or may not have to be specified depending on the requirements of the - `subject_issuer`. -requested_token_type:: - _OPTIONAL._ This parameter represents the type of token the client wants to exchange for. Currently only oauth - and OpenID Connect token types are supported. The default value for this depends on whether it - is `urn:ietf:params:oauth:token-type:refresh_token` in which case you will be returned both an access token and refresh - token within the response. Other appropriate values are `urn:ietf:params:oauth:token-type:access_token` and `urn:ietf:params:oauth:token-type:id_token` -audience:: - _OPTIONAL._ This parameter specifies the target client you want the new token minted for. -requested_issuer:: - _OPTIONAL._ This parameter specifies that the client wants a token minted by an external provider. It must - be the alias of an `Identity Provider` configured within the realm. -requested_subject:: - _OPTIONAL._ This specifies a username or user id if your client wants to impersonate a different user. -scope:: - _NOT IMPLEMENTED._ This parameter represents the target set of OAuth and OpenID Connect scopes the client - is requesting. It is not implemented at this time but will be once {project_name} has better support for - scopes in general. - -NOTE: We currently only support OpenID Connect and OAuth exchanges. Support for SAML based clients and identity providers may be added in the future depending on user demand. - -==== Responses from a token exchange request - -A successful response from an exchange invocation will return the HTTP 200 response code with a content type that -depends on the `requested-token-type` and `requested_issuer` the client asks for. OAuth requested token types will return -a JSON document as described in the link:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-token-exchange-16[OAuth Token Exchange] specification. - -[source,json] ----- -{ - "access_token" : ".....", - "refresh_token" : ".....", - "expires_in" : "...." - } ----- - -Clients requesting a refresh token will get back both an access and refresh token in the response. Clients requesting only -access token type will only get an access token in the response. Expiration information may or may not be included for -clients requesting an external issuer through the `requested_issuer` parameter. - -Error responses generally fall under the 400 HTTP response code category, but other error status codes may be returned -depending on the severity of the error. Error responses may include content depending on the `requested_issuer`. -OAuth based exchanges may return a JSON document as follows: - -[source,json] ----- -{ - "error" : "...." - "error_description" : "...." -} ----- - -Additional error claims may be returned depending on the exchange type. For example, OAuth Identity Providers may include -an additional `account-link-url` claim if the user does not have a link to an identity provider. This link can be used -for a client initiated link request. - -NOTE: Token exchange setup requires knowledge of fine grain admin permissions (See the link:{adminguide_link}[{adminguide_name}] for more information). You will need to grant clients - permission to exchange. This is discussed more later in this chapter. - -The rest of this chapter discusses the setup requirements and provides examples for different exchange scenarios. -For simplicity's sake, let's call a token minted by the current realm as an _internal_ token and a token minted by -an external realm or identity provider as an _external_ token. - -[[_internal-token-to-internal-token-exchange]] -=== Internal token to internal token exchange - -With an internal token to token exchange you have an existing token minted to a specific client and you want to exchange -this token for a new one minted for a different target client. Why would you want to do this? This generally happens -when a client has a token minted for itself, and needs to make additional requests to other applications that require different -claims and permissions within the access token. Other reasons this type of exchange might be required is if you -need to perform a "permission downgrade" where your app needs to invoke on a less trusted app and you don't want -to propagate your current access token. - -[[_client_to_client_permission]] -==== Granting permission for the exchange - -Clients that want to exchange tokens for a different client need to be authorized in the Admin Console. -You need to define a `token-exchange` fine grain permission in the target client you want permission to exchange to. - -.Target Client Permission -image:images/exchange-target-client-permission-unset.png[Target Client Permission] - -.Procedure - -. Toggle *Permissions Enabled* to *On*. -+ -.Target Client Permission -image:images/exchange-target-client-permission-set.png[Target Client Exchange Permission Set] -+ -That page displays a *token-exchange* link. - -. Click that link to start defining the permission. -+ -This setup page displays. -+ -.Target Client Exchange Permission Setup -image:images/exchange-target-client-permission-setup.png[Target Client Exchange Permission Setup] - -. Click *Client details* in the breadcrumbs at the top of the screen. -. Define a policy for this permission. -. Click *Authorization* in the breadcrumbs at the top of the screen. -. Define a policy for this permission. -. Click the *Policies* tab. -. Create a *Client* Policy by clicking *Create policy* button. -+ -.Client Policy Creation -image:images/exchange-target-client-policy.png[Client Policy Creation] - -. Enter in the starting client that is the authenticated client that is requesting a token exchange. - -. After you create this policy, go back to the target client's *token-exchange* permission and add the client policy you just defined. -+ -.Apply Client Policy -image:images/exchange-target-client-exchange-apply-policy.png[Apply Client Policy] - -Your client now has permission to invoke. If you do not do this correctly, you will get a 403 Forbidden response if you -try to make an exchange. - -[[_internal_internal_making_request]] -==== Making the request - -When your client is exchanging an existing token for a token targeting another client, you use the `audience` parameter. -This parameter must be the client identifier for the target client that you configured in the Admin Console. - -[source,bash,subs="attributes+"] ----- -curl -X POST \ - -d "client_id=starting-client" \ - -d "client_secret=the client secret" \ - --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - -d "subject_token=...." \ - --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:refresh_token" \ - -d "audience=target-client" \ - http://localhost:8080{kc_realms_path}/myrealm/protocol/openid-connect/token ----- - -The `subject_token` parameter must be an access token for the target realm. If your `requested_token_type` parameter -is a refresh token type, then the response will contain both an access token, refresh token, and expiration. Here's -an example JSON response you get back from this call. - -When the `audience` parameter is not set, the value of the parameter defaults to the client making the token exchange request. - -Unlike with confidential clients, public clients are not allowed to perform token exchanges using tokens from other clients. -If you are passing a `subject_token`, the (confidential) client that was issued the token should either match the client making the request or, if issued to a different client, -the client making the request should be among the audiences set to the token. - -If you are explicitly setting a target `audience` (with a client different from the client making the request), you should also make sure that the `token-exchange` scope permission is configured for the client set to the `audience` parameter to allow -the client making the request to successfully complete the exchange. - -[source,json] ----- -{ - "access_token" : "....", - "refresh_token" : "....", - "expires_in" : 3600 -} ----- - -=== Internal token to external token exchange - -You can exchange a realm token for an external token minted by an external identity provider. This external identity provider -must be configured within the `Identity Provider` section of the Admin Console. Currently only OAuth/OpenID Connect based external -identity providers are supported, this includes all social providers. {project_name} does not perform a backchannel exchange to the external provider. So if the account -is not linked, you will not be able to get the external token. To be able to obtain an external token one of -these conditions must be met: - -* The user must have logged in with the external identity provider at least once -* The user must have linked with the external identity provider through the User Account Service -* The user account was linked through the external identity provider using link:{developerguide_link}[Client Initiated Account Linking] API. - -Finally, the external identity provider must have been configured to store tokens, or, one of the above actions must -have been performed with the same user session as the internal token you are exchanging. - -If the account is not linked, the exchange response will contain a link you can use to establish it. This is -discussed more in the <<_internal_external_making_request, Making the Request>> section. - -[[_grant_permission_external_exchange]] -==== Granting permission for the exchange - -Internal to external token exchange requests will be denied with a 403, Forbidden response until you grant permission for the calling client to exchange tokens with the external identity provider. To grant permission to the client, you go to the identity provider's configuration page to the *Permissions* tab. - -.Identity Provider Permission -image:images/exchange-idp-permission-unset.png[Identity Provider Exchange Permission] - -.Procedure - -. Toggle *Permissions Enabled* to *On*. -+ -.Identity Provider Permission -image:images/exchange-idp-permission-set.png[Identity Provider Exchange Permission Set] -+ -The page displays *token-exchange* link. - -. Click the link to start defining the permission. -+ -This setup page appears. -+ -.Identity Provider Exchange Permission Setup -image:images/exchange-idp-permission-setup.png[Identity Provider Exchange Permission Setup] - -. Click *Client details* in the breadcrumbs at the top of the screen. - -. Click *Policies* tab to create a client policy. -+ -.Client Policy Creation -image:images/exchange-idp-client-policy.png[Client Policy Creation] - -. Enter the starting client that is the authenticated client that is requesting a token exchange. - -. Return to the identity provider's *token-exchange* permission and add the client policy you just defined. -+ -.Apply Client Policy -image:images/exchange-idp-apply-policy.png[Apply Client Policy] - -Your client now has permission to invoke. If you do not do this correctly, you will get a 403 Forbidden response if you try to make an exchange. - -[[_internal_external_making_request]] -==== Making the request - -When your client is exchanging an existing internal token to an external one, you provide the `requested_issuer` parameter. The parameter must be the alias of a configured identity provider. - -[source,bash,subs="attributes+"] ----- -curl -X POST \ - -d "client_id=starting-client" \ - -d "client_secret=the client secret" \ - --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - -d "subject_token=...." \ - --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \ - -d "requested_issuer=google" \ - http://localhost:8080{kc_realms_path}/myrealm/protocol/openid-connect/token ----- - -The `subject_token` parameter must be an access token for the target realm. The `requested_token_type` parameter -must be `urn:ietf:params:oauth:token-type:access_token` or left blank. No other requested token type is supported -at this time. Here's -an example successful JSON response you get back from this call. - -[source,json] ----- -{ - "access_token" : "....", - "expires_in" : 3600 - "account-link-url" : "https://...." -} ----- - -If the external identity provider is not linked for whatever reason, you will get an HTTP 400 response code with -this JSON document: - -[source,json] ----- -{ - "error" : "....", - "error_description" : "..." - "account-link-url" : "https://...." -} ----- - -The `error` claim will be either `token_expired` or `not_linked`. The `account-link-url` claim is provided -so that the client can perform link:{developerguide_link}[Client Initiated Account Linking]. Most, if not all, -providers require linking through browser OAuth protocol. With the `account-link-url` just add a `redirect_uri` -query parameter to it and you can forward browsers to perform the link. - -=== External token to internal token exchange - -You can trust and exchange external tokens minted by external identity providers for internal tokens. This can be -used to bridge between realms or just to trust tokens from your social provider. It works similarly to an identity provider -browser login in that a new user is imported into your realm if it doesn't exist. - -NOTE: The current limitation on external token exchanges is that if the external token maps to an existing user an - exchange will not be allowed unless the existing user already has an account link to the external identity - provider. - -When the exchange is complete, a user session will be created within the realm, and you will receive an access -and or refresh token depending on the `requested_token_type` parameter value. You should note that this new -user session will remain active until it times out or until you call the logout endpoint of the realm passing this -new access token. - -These types of changes required a configured identity provider in the Admin Console. - -NOTE: SAML identity providers are not supported at this time. Twitter tokens cannot be exchanged either. - -==== Granting permission for the exchange - -Before external token exchanges can be done, you grant permission for the calling client to make the exchange. This -permission is granted in the same manner as <<_grant_permission_external_exchange, internal to external permission is granted>>. - -If you also provide an `audience` parameter whose value points to a different client other than the calling one, you -must also grant the calling client permission to exchange to the target client specific in the `audience` parameter. How -to do this is <<_client_to_client_permission, discussed earlier>> in this section. - -==== Making the request - -The `subject_token_type` must either be `urn:ietf:params:oauth:token-type:access_token` or `urn:ietf:params:oauth:token-type:jwt`. -If the type is `urn:ietf:params:oauth:token-type:access_token` you specify the `subject_issuer` parameter and it must be the -alias of the configured identity provider. If the type is `urn:ietf:params:oauth:token-type:jwt`, the provider will be matched via -the `issuer` claim within the JWT which must be the alias of the provider, or a registered issuer within the providers configuration. - -For validation, if the token is an access token, the provider's user info service will be invoked to validate the token. A successful call -will mean that the access token is valid. If the subject token is a JWT and if the provider has signature validation enabled, that will be attempted, -otherwise, it will default to also invoking on the user info service to validate the token. - -By default, the internal token minted will use the calling client to determine what's in the token using the protocol -mappers defined for the calling client. Alternatively, you can specify a different target client using the `audience` -parameter. - -[source,bash,subs="attributes+"] ----- -curl -X POST \ - -d "client_id=starting-client" \ - -d "client_secret=the client secret" \ - --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - -d "subject_token=...." \ - -d "subject_issuer=myOidcProvider" \ - --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ - -d "audience=target-client" \ - http://localhost:8080{kc_realms_path}/myrealm/protocol/openid-connect/token ----- - - -If your `requested_token_type` parameter -is a refresh token type, then the response will contain both an access token, refresh token, and expiration. Here's -an example JSON response you get back from this call. - -[source,json] ----- -{ - "access_token" : "....", - "refresh_token" : "....", - "expires_in" : 3600 -} ----- - - -=== Impersonation - -For internal and external token exchanges, the client can request on behalf of a user to impersonate a different user. -For example, you may have an admin application that needs to impersonate a user so that a support engineer can debug -a problem. - - -==== Granting permission for the exchange - -The user that the subject token represents must have permission to impersonate other users. See the -link:{adminguide_link}[{adminguide_name}] on how to enable this permission. It can be done through a role or through -fine grain admin permissions. - - -==== Making the request - -Make the request as described in other chapters except additionally specify the `requested_subject` parameter. The -value of this parameter must be a username or user id. - -[source,bash,subs="attributes+"] ----- -curl -X POST \ - -d "client_id=starting-client" \ - -d "client_secret=the client secret" \ - --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - -d "subject_token=...." \ - --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \ - -d "audience=target-client" \ - -d "requested_subject=wburke" \ - http://localhost:8080{kc_realms_path}/myrealm/protocol/openid-connect/token ----- - -=== Direct Naked Impersonation - -You can make an internal token exchange request without providing a `subject_token`. This is called a direct -naked impersonation because it places a lot of trust in a client as that client can impersonate any user in the realm. -You might need this to bridge for applications where it is impossible to obtain a subject token to exchange. For example, -you may be integrating a legacy application that performs login directly with LDAP. In that case, the legacy app -is able to authenticate users itself, but not able to obtain a token. - -WARNING: It is very risky to enable direct naked impersonation for a client. If the client's credentials are ever - stolen, that client can impersonate any user in the system. - -==== Granting permission for the exchange - -If the `audience` parameter is provided, then the calling client must have permission to exchange to the client. How -to set this up is discussed earlier in this chapter. - -Additionally, the calling client must be granted permission to impersonate users. - -.Procedure - -. Click *Users* in the menu. - -. Click the *Permissions* tab. -+ -.User Permissions -image:images/exchange-users-permission-unset.png[User Permissions] - -. Toggle *Permissions Enabled* to *On*. -+ -.Identity Provider Permission -image:images/exchange-users-permission-set.png[Users Impersonation Permission Set] -+ -The page displays an *impersonate* link. -. Click that link to start defining the permission. -+ -This setup page displays. -+ -.Users Impersonation Permission Setup -image:images/exchange-users-permission-setup.png[Users Impersonation Permission Setup] - -. Click *Client details* in the breadcrumbs at the top of the screen. -. Define a policy for this permission. -. Go to the *Policies* tab and create a client policy. -+ -.Client Policy Creation -image:images/exchange-users-client-policy.png[Client Policy Creation] - -. Enter the starting client that is the authenticated client that is requesting a token exchange. - -. Return to the users' *impersonation* permission and add the client policy you just -defined. -+ -.Apply Client Policy -image:images/exchange-users-apply-policy.png[Apply Client Policy] - -Your client now has permission to impersonate users. If you do not do this correctly, you will get a 403 Forbidden response if you -try to make this type of exchange. - -NOTE: Public clients are not allowed to do direct naked impersonations. - - -==== Making the request - -To make the request, simply specify the `requested_subject` parameter. This must be the username or user id of -a valid user. You can also specify an `audience` parameter if you wish. - -[source,bash,subs="attributes+"] ----- -curl -X POST \ - -d "client_id=starting-client" \ - -d "client_secret=the client secret" \ - --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - -d "requested_subject=wburke" \ - http://localhost:8080{kc_realms_path}/myrealm/protocol/openid-connect/token ----- - -=== Expand permission model with service accounts - -When granting clients permission to exchange, you don't necessarily manually enable those permissions for each and every client. -If the client has a service account associated with it, you can use a role to group permissions together and assign exchange permissions -by assigning a role to the client's service account. For example, you might define a `naked-exchange` role and any service account that has that -role can do a naked exchange. - -=== Exchange vulnerabilities - -When you start allowing token exchanges, there are various things you have to both be aware of and careful of. - -The first is public clients. Public clients do not have or require a client credential in order to perform an exchange. Anybody that has a valid -token will be able to __impersonate__ the public client and perform the exchanges that public client is allowed to perform. If there -are any untrustworthy clients that are managed by your realm, public clients may open up vulnerabilities in your permission models. -This is why direct naked exchanges do not allow public clients and will abort with an error if the calling client is public. - -It is possible to exchange social tokens provided by Facebook, Google, etc. for a realm token. Be careful and vigilante on what -the exchange token is allowed to do as it's not hard to create fake accounts on these social websites. Use default roles, groups, and identity provider mappers to control what attributes and roles -are assigned to the external social user. - -Direct naked exchanges are quite dangerous. You are putting a lot of trust in the calling client that it will never leak out -its client credentials. If those credentials are leaked, then the thief can impersonate anybody in your system. This is in direct -contrast to confidential clients that have existing tokens. You have two factors of authentication, the access token and the client -credentials, and you're only dealing with one user. So use direct naked exchanges sparingly. diff --git a/docs/documentation/server_admin/images/2fa-example1.png b/docs/documentation/server_admin/images/2fa-example1.png new file mode 100644 index 000000000000..6da90fb4fea8 Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example1.png differ diff --git a/docs/documentation/server_admin/images/2fa-example2-config.png b/docs/documentation/server_admin/images/2fa-example2-config.png new file mode 100644 index 000000000000..183c938dd17a Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example2-config.png differ diff --git a/docs/documentation/server_admin/images/2fa-example2.png b/docs/documentation/server_admin/images/2fa-example2.png new file mode 100644 index 000000000000..cc94b424e2a8 Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example2.png differ diff --git a/docs/documentation/server_admin/images/2fa-example3.png b/docs/documentation/server_admin/images/2fa-example3.png new file mode 100644 index 000000000000..2b17af38bf31 Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example3.png differ diff --git a/docs/documentation/server_admin/images/Create-top-level-flow.png b/docs/documentation/server_admin/images/Create-top-level-flow.png index c3bc2f0be9b3..6e605d09e68c 100644 Binary files a/docs/documentation/server_admin/images/Create-top-level-flow.png and b/docs/documentation/server_admin/images/Create-top-level-flow.png differ diff --git a/docs/documentation/server_admin/images/New-flow.png b/docs/documentation/server_admin/images/New-flow.png index 2b1ab2883595..2246fe0ad14d 100644 Binary files a/docs/documentation/server_admin/images/New-flow.png and b/docs/documentation/server_admin/images/New-flow.png differ diff --git a/docs/documentation/server_admin/images/account-console-applications.png b/docs/documentation/server_admin/images/account-console-applications.png index 8fd53a99ac8a..61f09ea82643 100644 Binary files a/docs/documentation/server_admin/images/account-console-applications.png and b/docs/documentation/server_admin/images/account-console-applications.png differ diff --git a/docs/documentation/server_admin/images/account-console-device.png b/docs/documentation/server_admin/images/account-console-device.png index d4e948e5d984..8436f653c1d2 100644 Binary files a/docs/documentation/server_admin/images/account-console-device.png and b/docs/documentation/server_admin/images/account-console-device.png differ diff --git a/docs/documentation/server_admin/images/account-console-groups.png b/docs/documentation/server_admin/images/account-console-groups.png new file mode 100644 index 000000000000..c1d5207068ad Binary files /dev/null and b/docs/documentation/server_admin/images/account-console-groups.png differ diff --git a/docs/documentation/server_admin/images/account-console-intro.png b/docs/documentation/server_admin/images/account-console-intro.png index 9cdd16852257..4761618b8a12 100644 Binary files a/docs/documentation/server_admin/images/account-console-intro.png and b/docs/documentation/server_admin/images/account-console-intro.png differ diff --git a/docs/documentation/server_admin/images/account-console-linked.png b/docs/documentation/server_admin/images/account-console-linked.png index 9046b7aa8e35..e9332258b66c 100644 Binary files a/docs/documentation/server_admin/images/account-console-linked.png and b/docs/documentation/server_admin/images/account-console-linked.png differ diff --git a/docs/documentation/server_admin/images/account-console-signing-in-webauthn-2factor.png b/docs/documentation/server_admin/images/account-console-signing-in-webauthn-2factor.png index ebafccc5115e..05cb9653f243 100644 Binary files a/docs/documentation/server_admin/images/account-console-signing-in-webauthn-2factor.png and b/docs/documentation/server_admin/images/account-console-signing-in-webauthn-2factor.png differ diff --git a/docs/documentation/server_admin/images/account-console-signing-in-webauthn-passwordless.png b/docs/documentation/server_admin/images/account-console-signing-in-webauthn-passwordless.png index 6cda9af80ee0..e05dbd43a308 100644 Binary files a/docs/documentation/server_admin/images/account-console-signing-in-webauthn-passwordless.png and b/docs/documentation/server_admin/images/account-console-signing-in-webauthn-passwordless.png differ diff --git a/docs/documentation/server_admin/images/account-console-signing-in.png b/docs/documentation/server_admin/images/account-console-signing-in.png index 13ac49170988..dbc654ee2f21 100644 Binary files a/docs/documentation/server_admin/images/account-console-signing-in.png and b/docs/documentation/server_admin/images/account-console-signing-in.png differ diff --git a/docs/documentation/server_admin/images/add-client-oidc.png b/docs/documentation/server_admin/images/add-client-oidc.png index 8495d76e0af7..b6b77f142608 100644 Binary files a/docs/documentation/server_admin/images/add-client-oidc.png and b/docs/documentation/server_admin/images/add-client-oidc.png differ diff --git a/docs/documentation/server_admin/images/add-client-saml.png b/docs/documentation/server_admin/images/add-client-saml.png index 62d903cabd8c..1883c9b5a50d 100644 Binary files a/docs/documentation/server_admin/images/add-client-saml.png and b/docs/documentation/server_admin/images/add-client-saml.png differ diff --git a/docs/documentation/server_admin/images/add-identity-provider.png b/docs/documentation/server_admin/images/add-identity-provider.png index 1c9a72b103e7..793dcc86f4d1 100644 Binary files a/docs/documentation/server_admin/images/add-identity-provider.png and b/docs/documentation/server_admin/images/add-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/add-mapper.png b/docs/documentation/server_admin/images/add-mapper.png index f199cde627fc..6c31a0c4908a 100644 Binary files a/docs/documentation/server_admin/images/add-mapper.png and b/docs/documentation/server_admin/images/add-mapper.png differ diff --git a/docs/documentation/server_admin/images/add-provider-dialog.png b/docs/documentation/server_admin/images/add-provider-dialog.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/add-provider-select.png b/docs/documentation/server_admin/images/add-provider-select.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/add-realm-menu.png b/docs/documentation/server_admin/images/add-realm-menu.png deleted file mode 100644 index 4462459bbd56..000000000000 Binary files a/docs/documentation/server_admin/images/add-realm-menu.png and /dev/null differ diff --git a/docs/documentation/server_admin/images/add-user.png b/docs/documentation/server_admin/images/add-user.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/admin-console.png b/docs/documentation/server_admin/images/admin-console.png index 69e7a1d47be4..267f7db107a1 100644 Binary files a/docs/documentation/server_admin/images/admin-console.png and b/docs/documentation/server_admin/images/admin-console.png differ diff --git a/docs/documentation/server_admin/images/audience_mapper.png b/docs/documentation/server_admin/images/audience_mapper.png index ee4b8cf67e5b..7625d7d7614a 100644 Binary files a/docs/documentation/server_admin/images/audience_mapper.png and b/docs/documentation/server_admin/images/audience_mapper.png differ diff --git a/docs/documentation/server_admin/images/audience_resolving_evaluate.png b/docs/documentation/server_admin/images/audience_resolving_evaluate.png new file mode 100644 index 000000000000..4be5084ccee9 Binary files /dev/null and b/docs/documentation/server_admin/images/audience_resolving_evaluate.png differ diff --git a/docs/documentation/server_admin/images/authentication-step-up-flow.png b/docs/documentation/server_admin/images/authentication-step-up-flow.png index a27c96884e9a..93f9380970b3 100644 Binary files a/docs/documentation/server_admin/images/authentication-step-up-flow.png and b/docs/documentation/server_admin/images/authentication-step-up-flow.png differ diff --git a/docs/documentation/server_admin/images/browser-flow.png b/docs/documentation/server_admin/images/browser-flow.png index 8ca5bf786b69..53454e4660df 100644 Binary files a/docs/documentation/server_admin/images/browser-flow.png and b/docs/documentation/server_admin/images/browser-flow.png differ diff --git a/docs/documentation/server_admin/images/brute-force-mixed.png b/docs/documentation/server_admin/images/brute-force-mixed.png new file mode 100644 index 000000000000..a3c31d37789a Binary files /dev/null and b/docs/documentation/server_admin/images/brute-force-mixed.png differ diff --git a/docs/documentation/server_admin/images/brute-force-permanently.png b/docs/documentation/server_admin/images/brute-force-permanently.png new file mode 100644 index 000000000000..f77a1018626a Binary files /dev/null and b/docs/documentation/server_admin/images/brute-force-permanently.png differ diff --git a/docs/documentation/server_admin/images/brute-force-temporarily.png b/docs/documentation/server_admin/images/brute-force-temporarily.png new file mode 100644 index 000000000000..291c1b5bcc16 Binary files /dev/null and b/docs/documentation/server_admin/images/brute-force-temporarily.png differ diff --git a/docs/documentation/server_admin/images/brute-force.png b/docs/documentation/server_admin/images/brute-force.png index 56c0c8df429c..c9e2dcd4c7c1 100644 Binary files a/docs/documentation/server_admin/images/brute-force.png and b/docs/documentation/server_admin/images/brute-force.png differ diff --git a/docs/documentation/server_admin/images/client-credentials-jwt.png b/docs/documentation/server_admin/images/client-credentials-jwt.png index da328b5fe71b..fc9148614c68 100644 Binary files a/docs/documentation/server_admin/images/client-credentials-jwt.png and b/docs/documentation/server_admin/images/client-credentials-jwt.png differ diff --git a/docs/documentation/server_admin/images/client-credentials.png b/docs/documentation/server_admin/images/client-credentials.png index 07fbf857238a..abbeea2450bb 100644 Binary files a/docs/documentation/server_admin/images/client-credentials.png and b/docs/documentation/server_admin/images/client-credentials.png differ diff --git a/docs/documentation/server_admin/images/client-oidc-keys.png b/docs/documentation/server_admin/images/client-oidc-keys.png index 4f0cd91b713a..bf9b4c7ad824 100644 Binary files a/docs/documentation/server_admin/images/client-oidc-keys.png and b/docs/documentation/server_admin/images/client-oidc-keys.png differ diff --git a/docs/documentation/server_admin/images/client-oidc-map-acr-to-loa.png b/docs/documentation/server_admin/images/client-oidc-map-acr-to-loa.png index fbce25023efd..9aa56208af4a 100644 Binary files a/docs/documentation/server_admin/images/client-oidc-map-acr-to-loa.png and b/docs/documentation/server_admin/images/client-oidc-map-acr-to-loa.png differ diff --git a/docs/documentation/server_admin/images/client-scope.png b/docs/documentation/server_admin/images/client-scope.png index 74983834b0e4..d71c323d0a6e 100644 Binary files a/docs/documentation/server_admin/images/client-scope.png and b/docs/documentation/server_admin/images/client-scope.png differ diff --git a/docs/documentation/server_admin/images/client-scopes-evaluate.png b/docs/documentation/server_admin/images/client-scopes-evaluate.png index 24495a3de315..eacbbc490620 100644 Binary files a/docs/documentation/server_admin/images/client-scopes-evaluate.png and b/docs/documentation/server_admin/images/client-scopes-evaluate.png differ diff --git a/docs/documentation/server_admin/images/client-scopes-list.png b/docs/documentation/server_admin/images/client-scopes-list.png index 58cd44011632..026d58d3a2e8 100644 Binary files a/docs/documentation/server_admin/images/client-scopes-list.png and b/docs/documentation/server_admin/images/client-scopes-list.png differ diff --git a/docs/documentation/server_admin/images/client-scopes-phone.png b/docs/documentation/server_admin/images/client-scopes-phone.png index 4a315c36ed5c..107e1fc9b3f6 100644 Binary files a/docs/documentation/server_admin/images/client-scopes-phone.png and b/docs/documentation/server_admin/images/client-scopes-phone.png differ diff --git a/docs/documentation/server_admin/images/client-sessions.png b/docs/documentation/server_admin/images/client-sessions.png index 5fbfc298f024..f156627c0ebd 100644 Binary files a/docs/documentation/server_admin/images/client-sessions.png and b/docs/documentation/server_admin/images/client-sessions.png differ diff --git a/docs/documentation/server_admin/images/client-settings-oidc.png b/docs/documentation/server_admin/images/client-settings-oidc.png index 3319850eb422..ae624a81d683 100644 Binary files a/docs/documentation/server_admin/images/client-settings-oidc.png and b/docs/documentation/server_admin/images/client-settings-oidc.png differ diff --git a/docs/documentation/server_admin/images/client-settings-saml.png b/docs/documentation/server_admin/images/client-settings-saml.png index 7309e740c5d0..e010073014e2 100644 Binary files a/docs/documentation/server_admin/images/client-settings-saml.png and b/docs/documentation/server_admin/images/client-settings-saml.png differ diff --git a/docs/documentation/server_admin/images/config-authenticator-reference.png b/docs/documentation/server_admin/images/config-authenticator-reference.png new file mode 100644 index 000000000000..347c45974e3a Binary files /dev/null and b/docs/documentation/server_admin/images/config-authenticator-reference.png differ diff --git a/docs/documentation/server_admin/images/create-oidc-client-profile.png b/docs/documentation/server_admin/images/create-oidc-client-profile.png index f84d3e4ca624..f1443d9beba9 100644 Binary files a/docs/documentation/server_admin/images/create-oidc-client-profile.png and b/docs/documentation/server_admin/images/create-oidc-client-profile.png differ diff --git a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-condition.png b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-condition.png index d894fef87406..74f8c7a5209d 100644 Binary files a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-condition.png and b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-condition.png differ diff --git a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-executor.png b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-executor.png index d99d1292a8ac..fb2d5524f732 100644 Binary files a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-executor.png and b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-executor.png differ diff --git a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-policy.png b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-policy.png index 8673a818779c..f1817b9e8139 100644 Binary files a/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-policy.png and b/docs/documentation/server_admin/images/create-oidc-client-secret-rotation-policy.png differ diff --git a/docs/documentation/server_admin/images/create-permission.png b/docs/documentation/server_admin/images/create-permission.png new file mode 100644 index 000000000000..db6ca708fc8f Binary files /dev/null and b/docs/documentation/server_admin/images/create-permission.png differ diff --git a/docs/documentation/server_admin/images/credentials.png b/docs/documentation/server_admin/images/credentials.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/default-groups.png b/docs/documentation/server_admin/images/default-groups.png index 7f7baca6975e..a4530b5b2e56 100644 Binary files a/docs/documentation/server_admin/images/default-groups.png and b/docs/documentation/server_admin/images/default-groups.png differ diff --git a/docs/documentation/server_admin/images/default-roles.png b/docs/documentation/server_admin/images/default-roles.png index 7aea18c8d6c7..716fb1e19f6f 100644 Binary files a/docs/documentation/server_admin/images/default-roles.png and b/docs/documentation/server_admin/images/default-roles.png differ diff --git a/docs/documentation/server_admin/images/domain-mode.png b/docs/documentation/server_admin/images/domain-mode.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/effective-role-mappings.png b/docs/documentation/server_admin/images/effective-role-mappings.png index b3e5d78a4c6c..67c1a0bca571 100644 Binary files a/docs/documentation/server_admin/images/effective-role-mappings.png and b/docs/documentation/server_admin/images/effective-role-mappings.png differ diff --git a/docs/documentation/server_admin/images/email-simple-example.png b/docs/documentation/server_admin/images/email-simple-example.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/fine-grain-add-view-leads.png b/docs/documentation/server_admin/images/fine-grain-add-view-leads.png index 5bca8c7df7fe..2f5c9e99b43b 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-add-view-leads.png and b/docs/documentation/server_admin/images/fine-grain-add-view-leads.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-add-view-users.png b/docs/documentation/server_admin/images/fine-grain-add-view-users.png index 2533f1c9a6bd..b763ecd0964b 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-add-view-users.png and b/docs/documentation/server_admin/images/fine-grain-add-view-users.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-assign-query-clients.png b/docs/documentation/server_admin/images/fine-grain-assign-query-clients.png index 6b94e228c1b4..c64fa5349758 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-assign-query-clients.png and b/docs/documentation/server_admin/images/fine-grain-assign-query-clients.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client-assign-user-policy.png b/docs/documentation/server_admin/images/fine-grain-client-assign-user-policy.png index d3ba30dbee88..57f445f0161c 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client-assign-user-policy.png and b/docs/documentation/server_admin/images/fine-grain-client-assign-user-policy.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client-manage-permissions.png b/docs/documentation/server_admin/images/fine-grain-client-manage-permissions.png index 822a396fef5a..6fd94a6b750d 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client-manage-permissions.png and b/docs/documentation/server_admin/images/fine-grain-client-manage-permissions.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-off.png b/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-off.png index 3c427349e9ae..3e0174b16b0e 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-off.png and b/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-off.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-on.png b/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-on.png index 5b84142d6d45..22c77a988542 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-on.png and b/docs/documentation/server_admin/images/fine-grain-client-permissions-tab-on.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client-user-policy.png b/docs/documentation/server_admin/images/fine-grain-client-user-policy.png index e828123e6610..cd2e55782362 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client-user-policy.png and b/docs/documentation/server_admin/images/fine-grain-client-user-policy.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-client.png b/docs/documentation/server_admin/images/fine-grain-client.png index dbd380a40741..f6f7b5ecd320 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-client.png and b/docs/documentation/server_admin/images/fine-grain-client.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-enable.png b/docs/documentation/server_admin/images/fine-grain-enable.png new file mode 100644 index 000000000000..4e0b705d059d Binary files /dev/null and b/docs/documentation/server_admin/images/fine-grain-enable.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-evaluation.png b/docs/documentation/server_admin/images/fine-grain-evaluation.png new file mode 100644 index 000000000000..959efa30f861 Binary files /dev/null and b/docs/documentation/server_admin/images/fine-grain-evaluation.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-map-roles-permission.png b/docs/documentation/server_admin/images/fine-grain-map-roles-permission.png index ea1ca3b84831..75b28802fe41 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-map-roles-permission.png and b/docs/documentation/server_admin/images/fine-grain-map-roles-permission.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-permissions-tab.png b/docs/documentation/server_admin/images/fine-grain-permissions-tab.png new file mode 100644 index 000000000000..1cce9b58ddfd Binary files /dev/null and b/docs/documentation/server_admin/images/fine-grain-permissions-tab.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-sales-admin-login.png b/docs/documentation/server_admin/images/fine-grain-sales-admin-login.png index 0f1815d2a8d9..b521f38d4bdb 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-sales-admin-login.png and b/docs/documentation/server_admin/images/fine-grain-sales-admin-login.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-sales-application-roles.png b/docs/documentation/server_admin/images/fine-grain-sales-application-roles.png index bf7c132bbef9..43cc8fffda3b 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-sales-application-roles.png and b/docs/documentation/server_admin/images/fine-grain-sales-application-roles.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-search.png b/docs/documentation/server_admin/images/fine-grain-search.png new file mode 100644 index 000000000000..807365c36e88 Binary files /dev/null and b/docs/documentation/server_admin/images/fine-grain-search.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-users-permissions.png b/docs/documentation/server_admin/images/fine-grain-users-permissions.png index 3a2e1f2e2d50..7aa5dc5ee298 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-users-permissions.png and b/docs/documentation/server_admin/images/fine-grain-users-permissions.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-view-leads-permissions.png b/docs/documentation/server_admin/images/fine-grain-view-leads-permissions.png index 3364f6514116..93c4f4a9e6be 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-view-leads-permissions.png and b/docs/documentation/server_admin/images/fine-grain-view-leads-permissions.png differ diff --git a/docs/documentation/server_admin/images/fine-grain-view-leads-role-tab.png b/docs/documentation/server_admin/images/fine-grain-view-leads-role-tab.png index fa0628ac33ee..b0b0890d2c6c 100644 Binary files a/docs/documentation/server_admin/images/fine-grain-view-leads-role-tab.png and b/docs/documentation/server_admin/images/fine-grain-view-leads-role-tab.png differ diff --git a/docs/documentation/server_admin/images/full-client-scope.png b/docs/documentation/server_admin/images/full-client-scope.png index 067058ca9599..d2584c747f78 100644 Binary files a/docs/documentation/server_admin/images/full-client-scope.png and b/docs/documentation/server_admin/images/full-client-scope.png differ diff --git a/docs/documentation/server_admin/images/group-membership.png b/docs/documentation/server_admin/images/group-membership.png index 663f82ec5e31..f0131089c8ea 100644 Binary files a/docs/documentation/server_admin/images/group-membership.png and b/docs/documentation/server_admin/images/group-membership.png differ diff --git a/docs/documentation/server_admin/images/group.png b/docs/documentation/server_admin/images/group.png index 73b394aa085b..09dd49b08252 100644 Binary files a/docs/documentation/server_admin/images/group.png and b/docs/documentation/server_admin/images/group.png differ diff --git a/docs/documentation/server_admin/images/groups.png b/docs/documentation/server_admin/images/groups.png index 966882a685bd..49659c5a76a3 100644 Binary files a/docs/documentation/server_admin/images/groups.png and b/docs/documentation/server_admin/images/groups.png differ diff --git a/docs/documentation/server_admin/images/groups_account_console.png b/docs/documentation/server_admin/images/groups_account_console.png deleted file mode 100644 index b8aedc6d1956..000000000000 Binary files a/docs/documentation/server_admin/images/groups_account_console.png and /dev/null differ diff --git a/docs/documentation/server_admin/images/identity-provider-mapper.png b/docs/documentation/server_admin/images/identity-provider-mapper.png index 413d9f7ab994..ab0b39a62936 100644 Binary files a/docs/documentation/server_admin/images/identity-provider-mapper.png and b/docs/documentation/server_admin/images/identity-provider-mapper.png differ diff --git a/docs/documentation/server_admin/images/identity-provider-mappers.png b/docs/documentation/server_admin/images/identity-provider-mappers.png index 9d2614d341a1..8236ee35e979 100644 Binary files a/docs/documentation/server_admin/images/identity-provider-mappers.png and b/docs/documentation/server_admin/images/identity-provider-mappers.png differ diff --git a/docs/documentation/server_admin/images/identity-providers.png b/docs/documentation/server_admin/images/identity-providers.png index af7d28ef1427..372dcc194111 100644 Binary files a/docs/documentation/server_admin/images/identity-providers.png and b/docs/documentation/server_admin/images/identity-providers.png differ diff --git a/docs/documentation/server_admin/images/identity_broker_flow.png b/docs/documentation/server_admin/images/identity_broker_flow.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/import-client-saml.png b/docs/documentation/server_admin/images/import-client-saml.png index 49f80ebb9198..86984ec0c230 100644 Binary files a/docs/documentation/server_admin/images/import-client-saml.png and b/docs/documentation/server_admin/images/import-client-saml.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..5338f5a7c9c7 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/images/kerberos-provider.png b/docs/documentation/server_admin/images/kerberos-provider.png index b352d86f3536..48d588835b4d 100644 Binary files a/docs/documentation/server_admin/images/kerberos-provider.png and b/docs/documentation/server_admin/images/kerberos-provider.png differ diff --git a/docs/documentation/server_admin/images/keycloak_logo.png b/docs/documentation/server_admin/images/keycloak_logo.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/login-page.png b/docs/documentation/server_admin/images/login-page.png index 8f28b18bddd9..1b65ac873df1 100644 Binary files a/docs/documentation/server_admin/images/login-page.png and b/docs/documentation/server_admin/images/login-page.png differ diff --git a/docs/documentation/server_admin/images/login-tab.png b/docs/documentation/server_admin/images/login-tab.png index 823643763b02..66ca105e0b3e 100644 Binary files a/docs/documentation/server_admin/images/login-tab.png and b/docs/documentation/server_admin/images/login-tab.png differ diff --git a/docs/documentation/server_admin/images/mapper-config.png b/docs/documentation/server_admin/images/mapper-config.png index 12c5e7ff3b95..4721663ae72e 100644 Binary files a/docs/documentation/server_admin/images/mapper-config.png and b/docs/documentation/server_admin/images/mapper-config.png differ diff --git a/docs/documentation/server_admin/images/mapper-oidc-client-roles.png b/docs/documentation/server_admin/images/mapper-oidc-client-roles.png new file mode 100644 index 000000000000..18039bb13a59 Binary files /dev/null and b/docs/documentation/server_admin/images/mapper-oidc-client-roles.png differ diff --git a/docs/documentation/server_admin/images/mapper-oidc-realm-roles.png b/docs/documentation/server_admin/images/mapper-oidc-realm-roles.png new file mode 100644 index 000000000000..41e16d521195 Binary files /dev/null and b/docs/documentation/server_admin/images/mapper-oidc-realm-roles.png differ diff --git a/docs/documentation/server_admin/images/mappers-oidc.png b/docs/documentation/server_admin/images/mappers-oidc.png index 7f6b9e954841..2118257952ee 100644 Binary files a/docs/documentation/server_admin/images/mappers-oidc.png and b/docs/documentation/server_admin/images/mappers-oidc.png differ diff --git a/docs/documentation/server_admin/images/oidc-add-identity-provider.png b/docs/documentation/server_admin/images/oidc-add-identity-provider.png index 27553eaf99dd..7a5719848418 100644 Binary files a/docs/documentation/server_admin/images/oidc-add-identity-provider.png and b/docs/documentation/server_admin/images/oidc-add-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/oidc-client-secret-rotation-policy.png b/docs/documentation/server_admin/images/oidc-client-secret-rotation-policy.png index a056c6f75499..1daa54b45682 100644 Binary files a/docs/documentation/server_admin/images/oidc-client-secret-rotation-policy.png and b/docs/documentation/server_admin/images/oidc-client-secret-rotation-policy.png differ diff --git a/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png b/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png new file mode 100644 index 000000000000..17bd74e222f3 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png differ diff --git a/docs/documentation/server_admin/images/organizations-add-realm-user.png b/docs/documentation/server_admin/images/organizations-add-realm-user.png new file mode 100644 index 000000000000..6192b8740008 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-add-realm-user.png differ diff --git a/docs/documentation/server_admin/images/organizations-browser-flow.png b/docs/documentation/server_admin/images/organizations-browser-flow.png new file mode 100644 index 000000000000..afcdba2ebaab Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-browser-flow.png differ diff --git a/docs/documentation/server_admin/images/organizations-create-org.png b/docs/documentation/server_admin/images/organizations-create-org.png new file mode 100644 index 000000000000..fe5c72034ef0 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-create-org.png differ diff --git a/docs/documentation/server_admin/images/organizations-delete-org.png b/docs/documentation/server_admin/images/organizations-delete-org.png new file mode 100644 index 000000000000..436b00aaf84d Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-delete-org.png differ diff --git a/docs/documentation/server_admin/images/organizations-disable-org.png b/docs/documentation/server_admin/images/organizations-disable-org.png new file mode 100644 index 000000000000..aa817a3d8ef6 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-disable-org.png differ diff --git a/docs/documentation/server_admin/images/organizations-edit-identity-provider.png b/docs/documentation/server_admin/images/organizations-edit-identity-provider.png new file mode 100644 index 000000000000..ac7016d335b0 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-edit-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/organizations-enabling-orgs.png b/docs/documentation/server_admin/images/organizations-enabling-orgs.png new file mode 100644 index 000000000000..589ea57fd46f Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-enabling-orgs.png differ diff --git a/docs/documentation/server_admin/images/organizations-first-broker-flow.png b/docs/documentation/server_admin/images/organizations-first-broker-flow.png new file mode 100644 index 000000000000..3a0a53e4a1c1 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-first-broker-flow.png differ diff --git a/docs/documentation/server_admin/images/organizations-identity-first-error.png b/docs/documentation/server_admin/images/organizations-identity-first-error.png new file mode 100644 index 000000000000..f05613b9f11d Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-identity-first-error.png differ diff --git a/docs/documentation/server_admin/images/organizations-identity-first-login.png b/docs/documentation/server_admin/images/organizations-identity-first-login.png new file mode 100644 index 000000000000..b870fa8ce997 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-identity-first-login.png differ diff --git a/docs/documentation/server_admin/images/organizations-identity-providers.png b/docs/documentation/server_admin/images/organizations-identity-providers.png new file mode 100644 index 000000000000..a1c16f4776b5 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-identity-providers.png differ diff --git a/docs/documentation/server_admin/images/organizations-invite-member.png b/docs/documentation/server_admin/images/organizations-invite-member.png new file mode 100644 index 000000000000..9725d1875ed2 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-invite-member.png differ diff --git a/docs/documentation/server_admin/images/organizations-link-identity-provider.png b/docs/documentation/server_admin/images/organizations-link-identity-provider.png new file mode 100644 index 000000000000..641cb558fb19 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-link-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/organizations-manage-attributes.png b/docs/documentation/server_admin/images/organizations-manage-attributes.png new file mode 100644 index 000000000000..25f28f5e0fa1 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-manage-attributes.png differ diff --git a/docs/documentation/server_admin/images/organizations-manage-members.png b/docs/documentation/server_admin/images/organizations-manage-members.png new file mode 100644 index 000000000000..b1088b14aa19 Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-manage-members.png differ diff --git a/docs/documentation/server_admin/images/organizations-management-screen.png b/docs/documentation/server_admin/images/organizations-management-screen.png new file mode 100644 index 000000000000..fe4fff2c851f Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-management-screen.png differ diff --git a/docs/documentation/server_admin/images/organizations-unlink-identity-provider.png b/docs/documentation/server_admin/images/organizations-unlink-identity-provider.png new file mode 100644 index 000000000000..f7ad1a80ea0c Binary files /dev/null and b/docs/documentation/server_admin/images/organizations-unlink-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/otp-policy.png b/docs/documentation/server_admin/images/otp-policy.png index b61b8a71f842..c285962542b3 100644 Binary files a/docs/documentation/server_admin/images/otp-policy.png and b/docs/documentation/server_admin/images/otp-policy.png differ diff --git a/docs/documentation/server_admin/images/passkey-conditional-ui-autofill.png b/docs/documentation/server_admin/images/passkey-conditional-ui-autofill.png new file mode 100644 index 000000000000..9657ef2c591b Binary files /dev/null and b/docs/documentation/server_admin/images/passkey-conditional-ui-autofill.png differ diff --git a/docs/documentation/server_admin/images/passkey-conditional-ui-fallback-authentication.png b/docs/documentation/server_admin/images/passkey-conditional-ui-fallback-authentication.png new file mode 100644 index 000000000000..8c71a7f865be Binary files /dev/null and b/docs/documentation/server_admin/images/passkey-conditional-ui-fallback-authentication.png differ diff --git a/docs/documentation/server_admin/images/passkey-conditional-ui-flow.png b/docs/documentation/server_admin/images/passkey-conditional-ui-flow.png new file mode 100644 index 000000000000..f5d6bb90e2cb Binary files /dev/null and b/docs/documentation/server_admin/images/passkey-conditional-ui-flow.png differ diff --git a/docs/documentation/server_admin/images/passkey-modal-ui.png b/docs/documentation/server_admin/images/passkey-modal-ui.png new file mode 100644 index 000000000000..0ea058cf73ae Binary files /dev/null and b/docs/documentation/server_admin/images/passkey-modal-ui.png differ diff --git a/docs/documentation/server_admin/images/password-policy.png b/docs/documentation/server_admin/images/password-policy.png index 8936f4f03747..c8e783266717 100644 Binary files a/docs/documentation/server_admin/images/password-policy.png and b/docs/documentation/server_admin/images/password-policy.png differ diff --git a/docs/documentation/server_admin/images/post-login-flow-client-scope-config.png b/docs/documentation/server_admin/images/post-login-flow-client-scope-config.png new file mode 100644 index 000000000000..8be6150b3440 Binary files /dev/null and b/docs/documentation/server_admin/images/post-login-flow-client-scope-config.png differ diff --git a/docs/documentation/server_admin/images/post-login-flow-client-scope.png b/docs/documentation/server_admin/images/post-login-flow-client-scope.png new file mode 100644 index 000000000000..68c0d3278c70 Binary files /dev/null and b/docs/documentation/server_admin/images/post-login-flow-client-scope.png differ diff --git a/docs/documentation/server_admin/images/post-login-flow-otp.png b/docs/documentation/server_admin/images/post-login-flow-otp.png new file mode 100644 index 000000000000..df452c4c98da Binary files /dev/null and b/docs/documentation/server_admin/images/post-login-flow-otp.png differ diff --git a/docs/documentation/server_admin/images/realm-settings.png b/docs/documentation/server_admin/images/realm-settings.png new file mode 100644 index 000000000000..9fb87faf43ba Binary files /dev/null and b/docs/documentation/server_admin/images/realm-settings.png differ diff --git a/docs/documentation/server_admin/images/recaptcha-config.png b/docs/documentation/server_admin/images/recaptcha-config.png index 3838b5e42358..3d7e7914b09b 100644 Binary files a/docs/documentation/server_admin/images/recaptcha-config.png and b/docs/documentation/server_admin/images/recaptcha-config.png differ diff --git a/docs/documentation/server_admin/images/recaptcha-enterprise-config.png b/docs/documentation/server_admin/images/recaptcha-enterprise-config.png new file mode 100644 index 000000000000..537d3406f5ca Binary files /dev/null and b/docs/documentation/server_admin/images/recaptcha-enterprise-config.png differ diff --git a/docs/documentation/server_admin/images/recovery-codes-account-console-warn.png b/docs/documentation/server_admin/images/recovery-codes-account-console-warn.png new file mode 100644 index 000000000000..31fc185b7263 Binary files /dev/null and b/docs/documentation/server_admin/images/recovery-codes-account-console-warn.png differ diff --git a/docs/documentation/server_admin/images/recovery-codes-browser-flow.png b/docs/documentation/server_admin/images/recovery-codes-browser-flow.png new file mode 100644 index 000000000000..75261b8bf630 Binary files /dev/null and b/docs/documentation/server_admin/images/recovery-codes-browser-flow.png differ diff --git a/docs/documentation/server_admin/images/recovery-codes-setup.png b/docs/documentation/server_admin/images/recovery-codes-setup.png new file mode 100644 index 000000000000..07b1a308938d Binary files /dev/null and b/docs/documentation/server_admin/images/recovery-codes-setup.png differ diff --git a/docs/documentation/server_admin/images/reset-credential-email-config.png b/docs/documentation/server_admin/images/reset-credential-email-config.png new file mode 100644 index 000000000000..be0e6e48a3c5 Binary files /dev/null and b/docs/documentation/server_admin/images/reset-credential-email-config.png differ diff --git a/docs/documentation/server_admin/images/role.png b/docs/documentation/server_admin/images/role.png deleted file mode 100644 index fdd4108c92bd..000000000000 Binary files a/docs/documentation/server_admin/images/role.png and /dev/null differ diff --git a/docs/documentation/server_admin/images/roles.png b/docs/documentation/server_admin/images/roles.png index 487c09eb3d0c..830d63c28767 100644 Binary files a/docs/documentation/server_admin/images/roles.png and b/docs/documentation/server_admin/images/roles.png differ diff --git a/docs/documentation/server_admin/images/saml-add-identity-provider.png b/docs/documentation/server_admin/images/saml-add-identity-provider.png index 895c9a4dcad3..512575c734bc 100644 Binary files a/docs/documentation/server_admin/images/saml-add-identity-provider.png and b/docs/documentation/server_admin/images/saml-add-identity-provider.png differ diff --git a/docs/documentation/server_admin/images/security-headers.png b/docs/documentation/server_admin/images/security-headers.png index d5d1abcc2047..278644259d3f 100644 Binary files a/docs/documentation/server_admin/images/security-headers.png and b/docs/documentation/server_admin/images/security-headers.png differ diff --git a/docs/documentation/server_admin/images/select-policy-type.png b/docs/documentation/server_admin/images/select-policy-type.png new file mode 100644 index 000000000000..9b43e0478024 Binary files /dev/null and b/docs/documentation/server_admin/images/select-policy-type.png differ diff --git a/docs/documentation/server_admin/images/select-resource-type.png b/docs/documentation/server_admin/images/select-resource-type.png new file mode 100644 index 000000000000..14ca9554afcd Binary files /dev/null and b/docs/documentation/server_admin/images/select-resource-type.png differ diff --git a/docs/documentation/server_admin/images/sessions.png b/docs/documentation/server_admin/images/sessions.png index dc1242aa8ba7..1b1b628f5b0d 100644 Binary files a/docs/documentation/server_admin/images/sessions.png and b/docs/documentation/server_admin/images/sessions.png differ diff --git a/docs/documentation/server_admin/images/update-server-config-dialog.png b/docs/documentation/server_admin/images/update-server-config-dialog.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/update-server-config-select.png b/docs/documentation/server_admin/images/update-server-config-select.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/user-credentials.png b/docs/documentation/server_admin/images/user-credentials.png index b6fc34422027..47acc07f82b9 100644 Binary files a/docs/documentation/server_admin/images/user-credentials.png and b/docs/documentation/server_admin/images/user-credentials.png differ diff --git a/docs/documentation/server_admin/images/user-fed-ldap.png b/docs/documentation/server_admin/images/user-fed-ldap.png new file mode 100644 index 000000000000..8b36356b9669 Binary files /dev/null and b/docs/documentation/server_admin/images/user-fed-ldap.png differ diff --git a/docs/documentation/server_admin/images/user-federation.png b/docs/documentation/server_admin/images/user-federation.png index 48a0a3ed3086..ed36fd5ecd10 100644 Binary files a/docs/documentation/server_admin/images/user-federation.png and b/docs/documentation/server_admin/images/user-federation.png differ diff --git a/docs/documentation/server_admin/images/user-groups.png b/docs/documentation/server_admin/images/user-groups.png index c397b1b3f0da..9cbeca601716 100644 Binary files a/docs/documentation/server_admin/images/user-groups.png and b/docs/documentation/server_admin/images/user-groups.png differ diff --git a/docs/documentation/server_admin/images/user-impersonate-action.png b/docs/documentation/server_admin/images/user-impersonate-action.png new file mode 100644 index 000000000000..4b692e3a31a3 Binary files /dev/null and b/docs/documentation/server_admin/images/user-impersonate-action.png differ diff --git a/docs/documentation/server_admin/images/user-profile-annotation.png b/docs/documentation/server_admin/images/user-profile-annotation.png index 39665401cc40..fe5ce1d00cb8 100644 Binary files a/docs/documentation/server_admin/images/user-profile-annotation.png and b/docs/documentation/server_admin/images/user-profile-annotation.png differ diff --git a/docs/documentation/server_admin/images/user-profile-create-attribute.png b/docs/documentation/server_admin/images/user-profile-create-attribute.png index d1b7651ab109..4c09c9cc92a0 100644 Binary files a/docs/documentation/server_admin/images/user-profile-create-attribute.png and b/docs/documentation/server_admin/images/user-profile-create-attribute.png differ diff --git a/docs/documentation/server_admin/images/user-profile-tab.png b/docs/documentation/server_admin/images/user-profile-tab.png index 3d85b5169c17..f02e59c5ba44 100644 Binary files a/docs/documentation/server_admin/images/user-profile-tab.png and b/docs/documentation/server_admin/images/user-profile-tab.png differ diff --git a/docs/documentation/server_admin/images/user-required-action.png b/docs/documentation/server_admin/images/user-required-action.png index ee3d6d5fd194..840be5b40d0c 100644 Binary files a/docs/documentation/server_admin/images/user-required-action.png and b/docs/documentation/server_admin/images/user-required-action.png differ diff --git a/docs/documentation/server_admin/images/user-role-mappings.png b/docs/documentation/server_admin/images/user-role-mappings.png index f1bb33347868..2f7b6435e93c 100644 Binary files a/docs/documentation/server_admin/images/user-role-mappings.png and b/docs/documentation/server_admin/images/user-role-mappings.png differ diff --git a/docs/documentation/server_admin/images/user-sessions.png b/docs/documentation/server_admin/images/user-sessions.png index 51b3b4477e17..b3a3ffcf4172 100644 Binary files a/docs/documentation/server_admin/images/user-sessions.png and b/docs/documentation/server_admin/images/user-sessions.png differ diff --git a/docs/documentation/server_admin/images/users.png b/docs/documentation/server_admin/images/users.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_admin/images/webauthn-browser-flow-conditional-with-OTP.png b/docs/documentation/server_admin/images/webauthn-browser-flow-conditional-with-OTP.png index f246f1e62ead..1b220d0803d9 100644 Binary files a/docs/documentation/server_admin/images/webauthn-browser-flow-conditional-with-OTP.png and b/docs/documentation/server_admin/images/webauthn-browser-flow-conditional-with-OTP.png differ diff --git a/docs/documentation/server_admin/images/webauthn-browser-flow-conditional.png b/docs/documentation/server_admin/images/webauthn-browser-flow-conditional.png index 55907bf6ca12..143809136a02 100644 Binary files a/docs/documentation/server_admin/images/webauthn-browser-flow-conditional.png and b/docs/documentation/server_admin/images/webauthn-browser-flow-conditional.png differ diff --git a/docs/documentation/server_admin/images/webauthn-browser-flow-required.png b/docs/documentation/server_admin/images/webauthn-browser-flow-required.png index 9ae672a81643..9019b5c15e5f 100644 Binary files a/docs/documentation/server_admin/images/webauthn-browser-flow-required.png and b/docs/documentation/server_admin/images/webauthn-browser-flow-required.png differ diff --git a/docs/documentation/server_admin/images/x509-browser-flow.png b/docs/documentation/server_admin/images/x509-browser-flow.png index c6eaa1c033f9..0e026a37b796 100644 Binary files a/docs/documentation/server_admin/images/x509-browser-flow.png and b/docs/documentation/server_admin/images/x509-browser-flow.png differ diff --git a/docs/documentation/server_admin/images/x509-client-auth.png b/docs/documentation/server_admin/images/x509-client-auth.png index 61896fc70096..db509da01323 100644 Binary files a/docs/documentation/server_admin/images/x509-client-auth.png and b/docs/documentation/server_admin/images/x509-client-auth.png differ diff --git a/docs/documentation/server_admin/topics.adoc b/docs/documentation/server_admin/topics.adoc index ea16cd06b175..2cbd1ccc961d 100644 --- a/docs/documentation/server_admin/topics.adoc +++ b/docs/documentation/server_admin/topics.adoc @@ -14,7 +14,6 @@ include::topics/sessions/administering.adoc[] include::topics/sessions/revocation.adoc[] include::topics/sessions/timeouts.adoc[] include::topics/sessions/offline.adoc[] -include::topics/sessions/preloading.adoc[] include::topics/sessions/transient.adoc[] include::topics/assembly-roles-groups.adoc[] include::topics/authentication.adoc[] @@ -24,9 +23,10 @@ include::topics/authentication/flows.adoc[] include::topics/authentication/kerberos.adoc[] include::topics/authentication/x509.adoc[] include::topics/authentication/webauthn.adoc[] +include::topics/authentication/passkeys.adoc[] include::topics/authentication/recovery-codes.adoc[] include::topics/authentication/conditions.adoc[] -include::topics/authentication/passkeys.adoc[] +include::topics/authentication/authentication-sessions.adoc[] include::topics/identity-broker.adoc[] include::topics/identity-broker/overview.adoc[] include::topics/identity-broker/default-provider.adoc[] @@ -45,33 +45,43 @@ include::topics/identity-broker/social/paypal.adoc[] include::topics/identity-broker/social/stack-overflow.adoc[] include::topics/identity-broker/social/twitter.adoc[] include::topics/identity-broker/oidc.adoc[] +include::topics/identity-broker/oauth2.adoc[] include::topics/identity-broker/saml.adoc[] include::topics/identity-broker/suggested.adoc[] include::topics/identity-broker/mappers.adoc[] include::topics/identity-broker/session-data.adoc[] include::topics/identity-broker/first-login-flow.adoc[] +include::topics/identity-broker/post-login-flow.adoc[] include::topics/identity-broker/tokens.adoc[] include::topics/identity-broker/logout.adoc[] include::topics/sso-protocols.adoc[] include::topics/admin-console-permissions.adoc[] include::topics/admin-console-permissions/master-realm.adoc[] include::topics/admin-console-permissions/per-realm.adoc[] +include::topics/admin-console-permissions/fine-grain-v2.adoc[] ifeval::[{project_community}==true] include::topics/admin-console-permissions/fine-grain.adoc[] endif::[] +include::topics/assembly-managing-organizations.adoc[] include::topics/assembly-managing-clients.adoc[] +ifeval::[{project_community}==true] +include::topics/oid4vci/vc-issuer-configuration.adoc[] +endif::[] include::topics/vault.adoc[] include::topics/events.adoc[] include::topics/threat.adoc[] include::topics/threat/host.adoc[] include::topics/threat/admin.adoc[] include::topics/threat/brute-force.adoc[] +include::topics/threat/password.adoc[] include::topics/threat/read-only-attributes.adoc[] +include::topics/threat/validate-user-attributes.adoc[] include::topics/threat/clickjacking.adoc[] include::topics/threat/ssl.adoc[] include::topics/threat/csrf.adoc[] include::topics/threat/redirect.adoc[] include::topics/threat/fapi-compliance.adoc[] +include::topics/threat/oauth21-compliance.adoc[] include::topics/threat/compromised-tokens.adoc[] include::topics/threat/compromised-codes.adoc[] include::topics/threat/open-redirect.adoc[] diff --git a/docs/documentation/server_admin/topics/account.adoc b/docs/documentation/server_admin/topics/account.adoc index ae9f56857db7..338c9735159e 100644 --- a/docs/documentation/server_admin/topics/account.adoc +++ b/docs/documentation/server_admin/topics/account.adoc @@ -2,28 +2,26 @@ == Account Console -{project_name} users can manage their accounts through the Account Console. Users can configure their profiles, add two-factor authentication, include identity provider accounts, and oversee device activity. +{project_name} users can manage their accounts through the Account Console. They can configure their profiles, add two-factor authentication, include identity provider accounts, and oversee device activity. [role="_additional-resources"] .Additional resources -* The Account Console can be configured in terms of appearance and language preferences. An example is adding attributes to the *Personal info* page by clicking *Personal info* link and completing and saving details. For more information, see the {developerguide_link}[{developerguide_name}]. +* The Account Console can be configured in terms of appearance and language preferences. An example is adding additional attributes to the *Personal info* page. For more information, see the {developerguide_link}[{developerguide_name}]. === Accessing the Account Console -Any user can access the Account Console. - .Procedure . Make note of the realm name and IP address for the {project_name} server where your account exists. - . In a web browser, enter a URL in this format: _server-root_{kc_realms_path}/{realm-name}/account. - . Enter your login name and password. .Account Console image:images/account-console-intro.png[Account Console] +You can also ask for additional scopes when calling the account console URL by setting the `scope` parameter in this format: _server-root_{kc_realms_path}/{realm-name}/account?scope=phone. + === Configuring ways to sign in You can sign in to this console using basic authentication (a login name and password) or two-factor authentication. For two-factor authentication, use one of the following procedures. @@ -37,21 +35,15 @@ You can sign in to this console using basic authentication (a login name and pas .Procedure . Click *Account security* in the menu. - . Click *Signing in*. - -. Click *Set up authenticator application*. +. Click *Set up Authenticator application*. + .Signing in image:images/account-console-signing-in.png[Signing in] -. Follow the directions that appear on the screen to use either - https://freeotp.github.io/[FreeOTP] or https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2[Google Authenticator] on your mobile device as your OTP generator. - +. Follow the directions that appear on the screen to use your mobile device as your OTP generator. . Scan the QR code in the screen shot into the OTP generator on your mobile device. - . Log out and log in again. - . Respond to the prompt by entering an OTP that is provided on your mobile device. ==== Two-factor authentication with WebAuthn @@ -63,21 +55,16 @@ image:images/account-console-signing-in.png[Signing in] .Procedure . Click *Account Security* in the menu. - . Click *Signing In*. - -. Click *Set up Security Key*. +. Click *Set up a Passkey*. + .Signing In -image:images/account-console-signing-in-webauthn-2factor.png[Signing In With Security Key] - -. Prepare your WebAuthn Security Key. How you prepare this key depends on the type of WebAuthn security key you use. For example, for a USB based Yubikey, you may need to put your key into the USB port on your laptop. - -. Click *Register* to register your security key. +image:images/account-console-signing-in-webauthn-2factor.png[Signing in with a Passkey] +. Prepare your Passkey. How you prepare this key depends on the type of Passkey you use. For example, for a USB based Yubikey, you may need to put your key into the USB port on your laptop. +. Click *Register* to register your Passkey. . Log out and log in again. - -. Assuming authentication flow was correctly set, a message appears asking you to authenticate with your Security Key as second factor. +. Assuming authentication flow was correctly set, a message appears asking you to authenticate with your Passkey as second factor. ==== Passwordless authentication with WebAuthn @@ -88,21 +75,16 @@ image:images/account-console-signing-in-webauthn-2factor.png[Signing In With Sec .Procedure . Click *Account Security* in the menu. - . Click *Signing In*. - -. Click *Set up Security Key* in the *Passwordless* section. +. Click *Set up a Passkey* in the *Passwordless* section. + .Signing In -image:images/account-console-signing-in-webauthn-passwordless.png[Signing In With Security Key] - -. Prepare your WebAuthn Security Key. How you prepare this key depends on the type of WebAuthn security key you use. For example, for a USB based Yubikey, you may need to put your key into the USB port on your laptop. - -. Click *Register* to register your security key. +image:images/account-console-signing-in-webauthn-passwordless.png[Signing in with a Passkey] +. Prepare your Passkey. How you prepare this key depends on the type of Passkey you use. For example, for a USB based Yubikey, you may need to put your key into the USB port on your laptop. +. Click *Register* to register your Passkey. . Log out and log in again. - -. Assuming authentication flow was correctly set, a message appears asking you to authenticate with your Security Key as second factor. You no longer need to provide your password to log in. +. Assuming authentication flow was correctly set, a message appears asking you to authenticate with your Passkey as second factor. You no longer need to provide your password to log in. === Viewing device activity @@ -124,15 +106,10 @@ You can link your account with an <<_identity_broker, identity broker>>. This op .Procedure . Log into the Admin Console. - . Click *Identity providers* in the menu. - . Select a provider and complete the fields. - . Return to the Account Console. - . Click *Account security* in the menu. - . Click *Linked accounts*. The identity provider you added appears in this page. @@ -158,4 +135,5 @@ If you select *Direct membership* checkbox, you will see only the groups you are * You need to have the *view-groups* account role for being able to view *Groups* menu. .View group memberships -image:images/groups_account_console.png[View group memberships] +.View group memberships +image:images/account-console-groups.png[View group memberships] diff --git a/docs/documentation/server_admin/topics/admin-cli.adoc b/docs/documentation/server_admin/topics/admin-cli.adoc index be1d54af719b..a698a8246357 100644 --- a/docs/documentation/server_admin/topics/admin-cli.adoc +++ b/docs/documentation/server_admin/topics/admin-cli.adoc @@ -7,29 +7,18 @@ With {project_name}, you can perform administration tasks from the command-line {project_name} packages the Admin CLI server distribution with the execution scripts in the `bin` directory. -ifeval::[{project_product}==true] -The script is called `kcadm.sh`. -endif::[] - -ifeval::[{project_product}!=true] -The Linux script is called `kcadm.sh`, and the script for Windows is called `kcadm.bat`. -endif::[] - -Add the {project_name} server directory to your `PATH` to use the client from any location on your file system. +The Linux script is called `kcadm.sh`, and the script for Windows is called `kcadm.bat`. Add the {project_name} server directory to your `PATH` to use the client from any location on your file system. For example: -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ export PATH=$PATH:$KEYCLOAK_HOME/bin $ kcadm.sh ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] @@ -37,15 +26,12 @@ ifeval::[{project_product}!=true] c:\> set PATH=%PATH%;%KEYCLOAK_HOME%\bin c:\> kcadm ---- -endif::[] [NOTE] ==== You must set the `KEYCLOAK_HOME` environment variable to the path where you extracted the {project_name} Server distribution. -ifeval::[{project_product}!=true] To avoid repetition, the rest of this document only uses Windows examples in places where the CLI differences are more than just in the `kcadm` command name. -endif::[] ==== @@ -62,10 +48,8 @@ Consult the Admin REST API documentation for details about JSON attributes for s + For example: + -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap",subs="attributes+"] ---- $ kcadm.sh config credentials --server http://localhost:8080{kc_base_path} --realm demo --user admin --client admin @@ -73,7 +57,6 @@ $ kcadm.sh create realms -s realm=demorealm -s enabled=true -o $ CID=$(kcadm.sh create clients -r demorealm -s clientId=my_client -s 'redirectUris=["http://localhost:8980/myapp/*"]' -i) $ kcadm.sh get clients/$CID/installation/providers/keycloak-oidc-keycloak-json ---- -ifeval::[{project_product}!=true] + * Windows: + @@ -85,21 +68,17 @@ c:\> kcadm create clients -r demorealm -s clientId=my_client -s "redirectUris=[\ c:\> set /p CID= kcadm get clients/%CID%/installation/providers/keycloak-oidc-keycloak-json ---- -endif::[] . In a production environment, access {project_name} by using `https:` to avoid exposing tokens. If a trusted certificate authority, included in Java's default certificate truststore, has not issued a server's certificate, prepare a `truststore.jks` file and instruct the Admin CLI to use it. + For example: + -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh config truststore --trustpass $PASSWORD ~/.keycloak/truststore.jks ---- -ifeval::[{project_product}!=true] + * Windows: + @@ -107,7 +86,10 @@ ifeval::[{project_product}!=true] ---- c:\> kcadm config truststore --trustpass %PASSWORD% %HOMEPATH%\.keycloak\truststore.jks ---- -endif::[] + +=== Sensitive Options + +Sensitive values, such as passwords, may be specified as command options. That is generally not recommended. There are also mechanisms by which you can be prompted for the sensitive value by either omitting the option or providing a value. Finally all will have a corresponding env variable that can be used instead. Check the help of the command you are running to see all possible options. === Authenticating @@ -117,7 +99,7 @@ When you log in with the Admin CLI, you specify: * A realm * A user name -Another option is to specify a clientId only, which creates a unique service account for you to use. +Another option is to specify a clientId only, which creates a unique service account for you to use. When you log in using a user name, use a password for the specified user. When you log in using a clientId, you need the client secret only, not the user password. You can also use the `Signed JWT` rather than the client secret. @@ -127,7 +109,7 @@ Two primary mechanisms are available for authentication. One mechanism uses `kca [options="nowrap",subs="attributes+"] ---- -$ kcadm.sh config credentials --server http://localhost:8080{kc_base_path} --realm master --user admin --password admin +$ kcadm.sh config credentials --server http://localhost:8080{kc_base_path} --realm master --user admin ---- This mechanism maintains an authenticated session between the `kcadm` command invocations by saving the obtained access token and its associated refresh token. It can maintain other secrets in a private configuration file. See the <<_working_with_alternative_configurations, next chapter>> for more information. @@ -137,22 +119,21 @@ The second mechanism authenticates each command invocation for the duration of t For example, when performing an operation, specify all the information required for authentication. [options="nowrap",subs="attributes+"] ---- -$ kcadm.sh get realms --no-config --server http://localhost:8080{kc_base_path} --realm master --user admin --password admin +$ kcadm.sh get realms --no-config --server http://localhost:8080{kc_base_path} --realm master --user admin ---- Run the `kcadm.sh help` command for more information on using the Admin CLI. Run the `kcadm.sh config credentials --help` command for more information about starting an authenticated session. +If you do not specify the --password option (it is generally recommended to not provide passwords as part of the command), you will be prompted for a password unless one is specified as the environment variable KC_CLI_PASSWORD. [[_working_with_alternative_configurations]] === Working with alternative configurations -By default, the Admin CLI maintains a configuration file named `kcadm.config`. {project_name} places this file in the user's home directory. +By default, the Admin CLI maintains a configuration file named `kcadm.config`. {project_name} places this file in the user's home directory. In Linux-based systems, the full pathname is `$HOME/.keycloak/kcadm.config`. -ifeval::[{project_product}!=true] In Windows, the full pathname is `%HOMEPATH%\.keycloak\kcadm.config`. -endif::[] You can use the `--config` option to point to a different file or location so you can maintain multiple authenticated sessions in parallel. @@ -208,7 +189,7 @@ SERVER_URI/admin/realms/TARGET_REALM/ENDPOINT For example: [options="nowrap",subs="attributes+"] ---- -$ kcadm.sh config credentials --server http://localhost:8080{kc_base_path} --realm master --user admin --password admin +$ kcadm.sh config credentials --server http://localhost:8080{kc_base_path} --realm master --user admin $ kcadm.sh create users -s username=testuser -s enabled=true -r demorealm ---- @@ -216,6 +197,13 @@ In this example, you start a session authenticated as the `admin` user in the `m The `create` and `update` commands send a JSON body to the server. You can use `-f FILENAME` to read a pre-made document from a file. When you can use the `-f -` option, {project_name} reads the message body from the standard input. You can specify individual attributes and their values, as seen in the `create users` example. {project_name} composes the attributes into a JSON body and sends them to the server. +[NOTE] +==== +The value in name=value pairs used in --set, -s options, are assumed to be JSON. If it cannot be parsed as valid JSON, then it will be sent to the server as a text value. + +If the value is enclosed in quotes after shell processing, but is not valid JSON, the quotes will be stripped and the rest of the value will be sent as text. This behavior is deprecated, please consider specifying your value without quotes or a valid JSON string literal with double quotes. +==== + Several methods are available in {project_name} to update a resource using the `update` command. You can determine the current state of a resource and save it to a file, edit that file, and send it to the server for an update. For example: @@ -264,10 +252,8 @@ You can send a JSON document with realm attributes directly from a file or pipe For example: -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh create realms -f - << EOF @@ -275,14 +261,12 @@ $ kcadm.sh create realms -f - << EOF EOF ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> echo { "realm": "demorealm", "enabled": true } | kcadm create realms -f - ---- -endif::[] [discrete] ==== Listing existing realms @@ -389,22 +373,18 @@ $ kcadm.sh get realms/demorealm --fields id --format csv --noquotes + For example: + -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh create components -r demorealm -s name=rsa-generated -s providerId=rsa-generated -s providerType=org.keycloak.keys.KeyProvider -s parentId=959844c1-d149-41d7-8359-6aa527fca0b0 -s 'config.priority=["101"]' -s 'config.enabled=["true"]' -s 'config.active=["true"]' -s 'config.keySize=["2048"]' ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm create components -r demorealm -s name=rsa-generated -s providerId=rsa-generated -s providerType=org.keycloak.keys.KeyProvider -s parentId=959844c1-d149-41d7-8359-6aa527fca0b0 -s "config.priority=[\"101\"]" -s "config.enabled=[\"true\"]" -s "config.active=[\"true\"]" -s "config.keySize=[\"2048\"]" ---- -endif::[] . Set the `parentId` attribute to the value of the target realm's ID. + The newly added key is now the active key, as revealed by `kcadm.sh get keys -r demorealm`. @@ -416,22 +396,18 @@ The newly added key is now the active key, as revealed by `kcadm.sh get keys -r + For example, on: + -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh create components -r demorealm -s name=java-keystore -s providerId=java-keystore -s providerType=org.keycloak.keys.KeyProvider -s parentId=959844c1-d149-41d7-8359-6aa527fca0b0 -s 'config.priority=["101"]' -s 'config.enabled=["true"]' -s 'config.active=["true"]' -s 'config.keystore=["/opt/keycloak/keystore.jks"]' -s 'config.keystorePassword=["secret"]' -s 'config.keyPassword=["secret"]' -s 'config.keyAlias=["localhost"]' ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm create components -r demorealm -s name=java-keystore -s providerId=java-keystore -s providerType=org.keycloak.keys.KeyProvider -s parentId=959844c1-d149-41d7-8359-6aa527fca0b0 -s "config.priority=[\"101\"]" -s "config.enabled=[\"true\"]" -s "config.active=[\"true\"]" -s "config.keystore=[\"/opt/keycloak/keystore.jks\"]" -s "config.keystorePassword=[\"secret\"]" -s "config.keyPassword=[\"secret\"]" -s "config.keyAlias=[\"localhost\"]" ---- -endif::[] . Ensure you change the attribute values for `keystore`, `keystorePassword`, `keyPassword`, and `alias` to match your specific keystore. . Set the `parentId` attribute to the value of the target realm's ID. @@ -449,22 +425,18 @@ $ kcadm.sh get keys -r demorealm + For example: + -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh update components/PROVIDER_ID -r demorealm -s 'config.active=["false"]' ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm update components/PROVIDER_ID -r demorealm -s "config.active=[\"false\"]" ---- -endif::[] + You can update other key attributes: @@ -499,44 +471,35 @@ You can set up a built-in event listener that receives all events and logs the e For example: -ifeval::[{project_product}!=true] * Linux: + [options="nowrap"] -+ -endif::[] ---- $ kcadm.sh update events/config -r demorealm -s 'eventsListeners=["jboss-logging"]' ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm update events/config -r demorealm -s "eventsListeners=[\"jboss-logging\"]" ---- -endif::[] For example: You can turn on storage for all available ERROR events, not including auditing events, for two days so you can retrieve the events through Admin REST. -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- -$ kcadm.sh update events/config -r demorealm -s eventsEnabled=true -s 'enabledEventTypes=["LOGIN_ERROR","REGISTER_ERROR","LOGOUT_ERROR","CODE_TO_TOKEN_ERROR","CLIENT_LOGIN_ERROR","FEDERATED_IDENTITY_LINK_ERROR","REMOVE_FEDERATED_IDENTITY_ERROR","UPDATE_EMAIL_ERROR","UPDATE_PROFILE_ERROR","UPDATE_PASSWORD_ERROR","UPDATE_TOTP_ERROR","VERIFY_EMAIL_ERROR","REMOVE_TOTP_ERROR","SEND_VERIFY_EMAIL_ERROR","SEND_RESET_PASSWORD_ERROR","SEND_IDENTITY_PROVIDER_LINK_ERROR","RESET_PASSWORD_ERROR","IDENTITY_PROVIDER_FIRST_LOGIN_ERROR","IDENTITY_PROVIDER_POST_LOGIN_ERROR","CUSTOM_REQUIRED_ACTION_ERROR","EXECUTE_ACTIONS_ERROR","CLIENT_REGISTER_ERROR","CLIENT_UPDATE_ERROR","CLIENT_DELETE_ERROR"]' -s eventsExpiration=172800 +$ kcadm.sh update events/config -r demorealm -s eventsEnabled=true -s 'enabledEventTypes=["LOGIN_ERROR","REGISTER_ERROR","LOGOUT_ERROR","CODE_TO_TOKEN_ERROR","CLIENT_LOGIN_ERROR","FEDERATED_IDENTITY_LINK_ERROR","REMOVE_FEDERATED_IDENTITY_ERROR","UPDATE_EMAIL_ERROR","UPDATE_PROFILE_ERROR","UPDATE_PASSWORD_ERROR","UPDATE_TOTP_ERROR","UPDATE_CREDENTIAL_ERROR","VERIFY_EMAIL_ERROR","REMOVE_TOTP_ERROR","REMOVE_CREDENTIAL_ERROR","SEND_VERIFY_EMAIL_ERROR","SEND_RESET_PASSWORD_ERROR","SEND_IDENTITY_PROVIDER_LINK_ERROR","RESET_PASSWORD_ERROR","IDENTITY_PROVIDER_FIRST_LOGIN_ERROR","IDENTITY_PROVIDER_POST_LOGIN_ERROR","CUSTOM_REQUIRED_ACTION_ERROR","EXECUTE_ACTIONS_ERROR","CLIENT_REGISTER_ERROR","CLIENT_UPDATE_ERROR","CLIENT_DELETE_ERROR"]' -s eventsExpiration=172800 ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- -c:\> kcadm update events/config -r demorealm -s eventsEnabled=true -s "enabledEventTypes=[\"LOGIN_ERROR\",\"REGISTER_ERROR\",\"LOGOUT_ERROR\",\"CODE_TO_TOKEN_ERROR\",\"CLIENT_LOGIN_ERROR\",\"FEDERATED_IDENTITY_LINK_ERROR\",\"REMOVE_FEDERATED_IDENTITY_ERROR\",\"UPDATE_EMAIL_ERROR\",\"UPDATE_PROFILE_ERROR\",\"UPDATE_PASSWORD_ERROR\",\"UPDATE_TOTP_ERROR\",\"VERIFY_EMAIL_ERROR\",\"REMOVE_TOTP_ERROR\",\"SEND_VERIFY_EMAIL_ERROR\",\"SEND_RESET_PASSWORD_ERROR\",\"SEND_IDENTITY_PROVIDER_LINK_ERROR\",\"RESET_PASSWORD_ERROR\",\"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR\",\"IDENTITY_PROVIDER_POST_LOGIN_ERROR\",\"CUSTOM_REQUIRED_ACTION_ERROR\",\"EXECUTE_ACTIONS_ERROR\",\"CLIENT_REGISTER_ERROR\",\"CLIENT_UPDATE_ERROR\",\"CLIENT_DELETE_ERROR\"]" -s eventsExpiration=172800 +c:\> kcadm update events/config -r demorealm -s eventsEnabled=true -s "enabledEventTypes=[\"LOGIN_ERROR\",\"REGISTER_ERROR\",\"LOGOUT_ERROR\",\"CODE_TO_TOKEN_ERROR\",\"CLIENT_LOGIN_ERROR\",\"FEDERATED_IDENTITY_LINK_ERROR\",\"REMOVE_FEDERATED_IDENTITY_ERROR\",\"UPDATE_EMAIL_ERROR\",\"UPDATE_PROFILE_ERROR\",\"UPDATE_PASSWORD_ERROR\",\"UPDATE_TOTP_ERROR\",\"UPDATE_CREDENTIAL_ERROR\",\"VERIFY_EMAIL_ERROR\",\"REMOVE_TOTP_ERROR\",\"REMOVE_CREDENTIAL_ERROR\",\"SEND_VERIFY_EMAIL_ERROR\",\"SEND_RESET_PASSWORD_ERROR\",\"SEND_IDENTITY_PROVIDER_LINK_ERROR\",\"RESET_PASSWORD_ERROR\",\"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR\",\"IDENTITY_PROVIDER_POST_LOGIN_ERROR\",\"CUSTOM_REQUIRED_ACTION_ERROR\",\"EXECUTE_ACTIONS_ERROR\",\"CLIENT_REGISTER_ERROR\",\"CLIENT_UPDATE_ERROR\",\"CLIENT_DELETE_ERROR\"]" -s eventsExpiration=172800 ---- -endif::[] You can reset stored event types to *all available event types*. Setting the value to an empty list is the same as enumerating all. [options="nowrap"] @@ -887,7 +850,7 @@ $ kcadm.sh add-roles -r demorealm --gname Group --cclientid realm-management --r Use the `remove-roles` command to remove client roles from a group. -The following example removes two roles defined on the client `realm management`, `create-client` and `view-users`, from the `Group` group. +The following example removes two roles defined on the client `realm-management`, `create-client` and `view-users`, from the `Group` group. See <<_group_operations, Group operations>> for more information. [options="nowrap"] @@ -1015,23 +978,18 @@ Use the `update` command with the same endpoint URI that you use to get a specif For example: -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh update clients/c7b8547f-e748-4333-95d0-410b76b3f4a3 -r demorealm -s enabled=false -s publicClient=true -s 'redirectUris=["http://localhost:8080/myapp/*"]' -s baseUrl=http://localhost:8080/myapp -s adminUrl=http://localhost:8080/myapp ---- - -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm update clients/c7b8547f-e748-4333-95d0-410b76b3f4a3 -r demorealm -s enabled=false -s publicClient=true -s "redirectUris=[\"http://localhost:8080/myapp/*\"]" -s baseUrl=http://localhost:8080/myapp -s adminUrl=http://localhost:8080/myapp ---- -endif::[] [discrete] ==== Deleting a client @@ -1078,15 +1036,15 @@ You can filter users by `username`, `firstName`, `lastName`, or `email`. For example: [options="nowrap"] ---- -$ kcadm.sh get users -r demorealm -q email=google.com -$ kcadm.sh get users -r demorealm -q username=testuser +$ kcadm.sh get users -r demorealm -q q=email:google.com +$ kcadm.sh get users -r demorealm -q q=username:testuser ---- [NOTE] ==== Filtering does not use exact matching. This example matches the value of the `username` attribute against the `\*testuser*` pattern. ==== -You can filter across multiple attributes by specifying multiple `-q` options. {project_name} returns users that match the condition for all the attributes only. +For clients, groups, and users you can filter across multiple attributes by specifying a more complex `q` query parameter. you may use something like -q q="field1:value1 field2:value2". {project_name} returns users that match the condition for all the attributes only. [discrete] ==== Getting a specific user @@ -1106,22 +1064,18 @@ Use the `update` command with the same endpoint URI that you use to get a specif For example: -ifeval::[{project_product}!=true] * Linux: + -endif::[] [options="nowrap"] ---- $ kcadm.sh update users/0ba7a3fd-6fd8-48cd-a60b-2e8fd82d56e2 -r demorealm -s 'requiredActions=["VERIFY_EMAIL","UPDATE_PROFILE","CONFIGURE_TOTP","UPDATE_PASSWORD"]' ---- -ifeval::[{project_product}!=true] * Windows: + [options="nowrap"] ---- c:\> kcadm update users/0ba7a3fd-6fd8-48cd-a60b-2e8fd82d56e2 -r demorealm -s "requiredActions=[\"VERIFY_EMAIL\",\"UPDATE_PROFILE\",\"CONFIGURE_TOTP\",\"UPDATE_PASSWORD\"]" ---- -endif::[] [discrete] ==== Deleting a user @@ -1244,7 +1198,7 @@ $ kcadm.sh remove-roles --uusername testuser --rolename user -r demorealm Use an `add-roles` command to add client roles to a user. -Use the following example to add two roles defined on the client `realm management`, the `create-client` role and the `view-users` role, to the user `testuser`. +Use the following example to add two roles defined on the client `realm-management`, the `create-client` role and the `view-users` role, to the user `testuser`. [options="nowrap"] ---- $ kcadm.sh add-roles -r demorealm --uusername testuser --cclientid realm-management --rolename create-client --rolename view-users @@ -1255,7 +1209,7 @@ $ kcadm.sh add-roles -r demorealm --uusername testuser --cclientid realm-managem Use a `remove-roles` command to remove client roles from a user. -Use the following example to remove two roles defined on the realm management client: +Use the following example to remove two roles defined on the realm-management client: [options="nowrap"] ---- $ kcadm.sh remove-roles -r demorealm --uusername testuser --cclientid realm-management --rolename create-client --rolename view-users @@ -1264,7 +1218,7 @@ $ kcadm.sh remove-roles -r demorealm --uusername testuser --cclientid realm-mana [discrete] ==== Listing a user's sessions -. Identify the user's ID, +. Identify the user's ID, . Use the ID to compose an endpoint URI, such as `users/ID/sessions`. . Use the `get` command to retrieve a list of the user's sessions. + @@ -1452,7 +1406,7 @@ $ kcadm.sh get-roles -r demorealm --gname Group --available Use the `get-roles` command to list assigned, available, and effective client roles for a group. -. Specify the target group by name (`--gname` option) or ID (`--gid` option), +. Specify the target group by name (`--gname` option) or ID (`--gid` option), . Specify the client by the clientId attribute (`--cclientid` option) or ID (`--id` option) to list *assigned* client roles for the user. + For example: @@ -1836,7 +1790,7 @@ $ kcadm.sh create components -r demorealm -s name=full-name-ldap-mapper -s provi . Set the realm's `passwordPolicy` attribute to an enumeration expression that includes the specific policy provider ID and optional configuration. . Use the following example to set a password policy to default values. The default values include: -* 27,500 hashing iterations +* 210,000 hashing iterations * at least one special character * at least one uppercase character * at least one digit character @@ -1850,7 +1804,7 @@ $ kcadm.sh update realms/demorealm -s 'passwordPolicy="hashIterations and specia . To use values different from defaults, pass the configuration in brackets. . Use the following example to set a password policy to: -* 25,000 hash iterations +* 300,000 hash iterations * at least two special characters * at least two uppercase characters * at least two lowercase characters @@ -1861,7 +1815,7 @@ $ kcadm.sh update realms/demorealm -s 'passwordPolicy="hashIterations and specia + [options="nowrap"] ---- -$ kcadm.sh update realms/demorealm -s 'passwordPolicy="hashIterations(25000) and specialChars(2) and upperCase(2) and lowerCase(2) and digits(2) and length(9) and notUsername and passwordHistory(4)"' +$ kcadm.sh update realms/demorealm -s 'passwordPolicy="hashIterations(300000) and specialChars(2) and upperCase(2) and lowerCase(2) and digits(2) and length(9) and notUsername and passwordHistory(4)"' ---- [discrete] @@ -1941,7 +1895,7 @@ $ kcadm get "authentication/config/dd91611a-d25c-421a-87e2-227c18421833" -r demo ==== Updating configuration for an execution . Get the execution for the flow. -. Get the flow's `authenticationConfig` attribute. +. Get the flow's `authenticationConfig` attribute. . Note the config ID from the attribute. . Run the `update` command on the `authentication/config/ID` endpoint. diff --git a/docs/documentation/server_admin/topics/admin-console-permissions.adoc b/docs/documentation/server_admin/topics/admin-console-permissions.adoc index a9f191e062d4..d24a4f2f8d0a 100644 --- a/docs/documentation/server_admin/topics/admin-console-permissions.adoc +++ b/docs/documentation/server_admin/topics/admin-console-permissions.adoc @@ -1,6 +1,6 @@ [[_admin_permissions]] -== Controlling access to the Admin Console +== Managing access to realm resources Each realm created on the {project_name} has a dedicated Admin Console from which that realm can be managed. The `master` realm is a special realm that allows admins to manage more than one realm on the system. diff --git a/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain-v2.adoc b/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain-v2.adoc new file mode 100644 index 000000000000..ae7ec36dac14 --- /dev/null +++ b/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain-v2.adoc @@ -0,0 +1,471 @@ +[[_fine_grained_permissions]] + +=== Delegating realm administration using permissions + +You can delegate realm management to other administrators, the realm administrators, using the fine-grained admin permissions +feature. +Different from the Role-Based Access Control (RBAC) Mechanism provided through the +<<_master_realm_access_control, Global and Realm specific roles>>, this feature provides a more fine-grained control over +how realm resources can be accessed and managed based on a well-defined set of operations that can be performed on them. + +By relying on a Policy-Based Access Control, server administrators can define permissions to realm resources such as users, +groups, and clients, using different policy types, or access control methods, so that a realm administrator is limited to +access a subset of realm resources and their operations. + +The feature provides an alternative to the aforementioned RBAC mechanism, but it does +not replace it. You are still able to grant administrative roles like `view-users` or `manage-clients` to delegate access +to realm administrators but doing so will skip the mechanisms provided by this feature. + +Enforcing access to realm resources only applies when managing resources through the administration console or the Admin API. + +==== Understanding the Realm Resource Types + +In a realm, you can manage different types of resources such as users, groups, clients, client scopes, roles, and so on. +As a realm administrator, you are constantly managing these resources when managing identities and how they authenticate +and are authorized to access a realm and applications. + +This feature provides the necessary mechanisms to enforce access controls when managing realm resources, limited to: + +* Users +* Groups +* Clients +* Roles + +You can manage permissions for all resources of a given resource type, such as all users in a realm, or +for a specific realm resource, such as a specific user or set of users in the realm. + +==== Understanding the scopes of access + +Each realm resource supports a well-defined set of management operations, or scopes, that can be performed on them, +such as `view`, `manage`, and resource-specific operations such as `view-members`, if you take groups as an example. + +When managing permissions, you are selecting a set of one or more scopes from a resource type to allow realm administrators +to perform specific operations on a resource type. For instance, granting a `view` scope will give access to realm administrators +to list, search, and view a realm resource. On the other hand, the `manage` scope will allow administrators to perform updates +and deletes on them. + +The scopes are completely independent of each other. If you give access to `manage` a realm resource, that does not mean the +`view` scope is granted automatically. No transitive dependency exists between scopes. Although this might impact the +overall user experience when managing permissions because you need to select individual scopes, +the benefit is that you can more easily identify the permissions that enforce access to a specific scope. + +Certain scopes from a resource type have a relationship (not a transitive dependency) to scopes in another resource type. +This relationship is mainly true when you manage a resource type that represents a group of realm resources, such as realm groups +and their members. + +===== Users Resource Type + +The *Users* realm resource type represents the users in a realm. You can manage permissions for users based on the following +set of scopes: + +[cols="30%,50%,20%"] +|=== +| *Scope* | *Description* | *Also granted by* + +| *view* | Defines if a realm administrator can view users. This scope should be set whenever you want | `view-members` + to make users available from queries. +| *manage* | Defines if a realm administrator can manage users. | `manage-members` +| *manage-group-membership* | Defines if a realm administrator can assign or unassign users to/from groups. | None +| *map-roles* | Defines if a realm administrator can assign or unassign roles to/from users. | None +| *impersonate* | Defines if a realm administrator can impersonate other users. | `impersonate-members` +|=== + +The user resource type has a strong relationship with some of the permissions you can set to groups. Most of the time, +users are members of groups and granting access to `view-members` or `manage-members` of a group should also allow +a realm administrator to `view` and `manage` members of that group. + +[NOTE] +==== +This feature does not support enforcing access to federated resource, however, this limitation is being considered +for future improvement. +==== + +===== Groups Resource Type + +The *Groups* realm resource type represents the groups in a realm. You can manage permissions for groups based on the following +set of management operations: + +[cols="30%,70%"] +|=== +| *Operation* | *Description* + +| *view* | Defines if a realm administrator can view groups. This scope should be set whenever you want + to make groups available from queries. +| *manage* | Defines if a realm administrator can manage groups. +| *view-members* | Defines if a realm administrator can view group members. + This operation applies to any child group in the group hierarchy. + This can be prevented by explicitly denying permission for specific subgroups. +| *manage-members* | Defines if a realm administrator can manage group members. + This operation applies to any child group in the group hierarchy. + This can be prevented by explicitly denying permission for specific subgroups. +| *impersonate-members* | Defines if a realm administrator can impersonate group members. + This operation applies to any child group in the group hierarchy. + This can be prevented by explicitly denying permission for specific subgroups. +| *manage-membership* | Defines if a realm administrator can add or remove members from groups. +|=== + +===== Clients Resource Type + +The *Clients* realm resource type represents the clients in a realm. You can manage permissions for clients based on the following +set of management operations: + +[cols="30%,70%"] +|=== +| *Operation* | *Description* + +| *view* | Defines if a realm administrator can view clients. This scope should be set whenever you want + to make clients available from queries. +| *manage* | Defines if a realm administrator can manage clients. +| *map-roles* | Defines if a realm administrator can assign any role defined by a client to a user. +| *map-roles-composite* | Defines if a realm administrator can assign any role defined by a client as a composite to + another role. +| *map-roles-client-scope* | Define if a realm administrator can assign any role defined by a client to a client scope. +|=== + +The *map-roles* operation does not grant the ability to manage users or assign roles arbitrarily. The administrator must also +have user role mapping permissions on the user. + +===== Roles Resource Type + +The *Roles* realm resource type represents the roles in a realm. You can manage permissions for roles based on the following set of management operations: + +[cols="30%,70%"] +|=== +| *Operation* | *Description* + +| *map-role* | Defines if a realm administrator can assign a role (or multiple roles) to a user. +| *map-role-composite* | Defines if a realm administrator can assign a role (or multiple roles) as a composite to another role. +| *map-role-client-scope* | Defines if a realm administrator can apply a role (or multiple roles) to a client scope. +|=== + +The *map-roles* operation does not grant the ability to manage users or assign roles arbitrarily. The administrator must also +have user role mapping permissions on the user. + +If there is a client resource type permission for the *map-roles*, *map-roles-composite*, or *map-roles-client-scope* scopes, +it will take precedence over any role resource type permission if the role is a client role. + +==== Enabling admin permissions to a realm + +To enable fine-grained admin permissions in a realm, follow these steps: + +* Log in to the Admin Console. +* Click *Realm settings*. +* Enable *Admin Permissions* and click *Save*. + +image:images/fine-grain-enable.png[Fine grain enable] + +Once enabled, a *Permissions* section appears in the left-side menu of the administration console. + +image:images/fine-grain-permissions-tab.png[Fine grain permissions tab] + +From this section, you can manage the permissions for realm resources. + +[[_managing-permissions]] +==== Managing Permissions + +The *Permissions* tab provides an overview of all active permissions within a realm. From here, administrators can create, +update, delete, or search for permissions. You can also pre-evaluate the permissions you have created to check +if they are enforcing access to realm resources as expected. +For more details, see link:#_managing-permissions[Evaluating Permissions]. + +To create a permission, click on the `Create permission` button and select the resource type you want to protect. + +image:images/select-resource-type.png[Selecting a resource type to protect] + +Once you select the resource type, you can now define how access should be enforced for a set of one or more resources of the selected type: + +image:images/create-permission.png[Creating a permission] + +When managing a permission you can define the following settings: + +* *Name*: A unique name for the permission. The name should also not conflict with any policy name +* *Description*: An optional description to better describe what the permission is about +* *Authorization scopes*: A set of one or more scopes representing the operations you want to protect for the selected resource type. +An administrator must have explicit permission assigned for each operation to perform the corresponding action. For example, +assigning only *manage* without *view* will prevent the user from being visible. +* *Enforce access to*: Defines if the permission should enforce access to all resources of the selected type or to specific resources in a realm. +* *Policies*: Defines a set of one or more policies that should be evaluated to grant or deny access to the selected resource(s). + +After creating the permission, it will automatically take effect when enforcing access to (all) resources and scopes you selected. +Keep that fact in mind when creating and updating permissions in production. + +===== Defining permissions for viewing realm resources + +This feature relies on a partial evaluation mechanism to partially evaluate the permissions that a realm administrator has +when listing and viewing realm resources. This mechanism will pre-fetch all the permissions set for view-related scopes where the realm administrator +is referenced either directly or indirectly. + +Permissions that grant access to `view` a realm resource of a certain type must use one of the following policies to +make them available from queries: + +* `User` +* `Group` +* `Role` + +By using any of the policies above, {project_name} can pre-calculate the set of resources that a realm administration can view +by looking for a direct (if using a user policy) or indirect (if using a role or group policy) reference to the realm administrator. +Therefore, the partial evaluation mechanism involves decorating queries with access controls that will run at the database level. This capability is mainly important to +properly allow paginating resources as well as avoid an additional overhead on the server-side when evaluating permissions for each +realm resource returned by queries. + +Partial evaluation and filtering occurs only if the feature is enabled to a realm, and if the user is not granted +with view-related administrative roles like `view-users` or `view-clients`. For instance, it will not happen for regular server administrators granted +with the `admin` role. + +When querying resources, the partial evaluation mechanism works as follows: + +* Resolve all the permissions for a certain resource type that reference the realm administrator +* Pre-evaluate each permission to check if the realm administrator does or does not have access to the resources associated with the permission +* Decorate database queries based on the resources granted or denied + +As a result, the result set of a query will hold only the realm resources where realm administrators have access to any of the view-related scopes. + +===== Searching Permissions + +The Admin Console provides several ways to search for permissions, supporting the following capabilities: + +* Search for permissions that contain a specific string in their *Name* +* Search for permissions of a specific resource type, such as *Users* +* Search for permissions of a specific resource type that apply to a particular resource (such as *Users* resource type for user `myadmin`). +* Search for permissions of a specific resource type with a given scope (such as *Users* resource type permissions with the *manage* scope). +* Search for permissions of a specific resource type that apply to a particular resource and have a specific scope (such as *Users* resource +type permissions with the *manage* scope for user `myadmin`). + +.Fine grained permissions search +image:images/fine-grain-search.png[Fine grained permissions search] + +These capabilities allow server administrators to perform queries on their universe of permissions and identify which ones +are enforcing access to a set of one or more realm resources and their scopes. Combined with the evaluation tool on the +*Evaluation* tab, they provide a key management tool for managing permissions in a realm. See <<_evaluating-permissions, Evaluating Permissions>> +for more details. + +==== Managing Policies + +The *Policies* tab allows administrators to define conditions using different access control methods to determine whether +a permission should be granted to an administrator attempting to perform operations on a realm resource. When managing permissions, +you must associate at least a single policy to grant or deny access to a realm resource. + +Policies are basically conditions that will evaluate to either a `GRANT` or a `DENY`. Their outcome will decide whether +a permission should be granted or denied. + +A permission is only granted if all its associated policies evaluate to a `GRANT`. Otherwise, the permission is denied +and a realm administrator will not be able to access the protected resource. + +{project_name} provides a set of built-in policies that you can choose from: + +image:images/select-policy-type.png[Selecting a policy type] + +Once you have a well-defined and stable permission model for your realm, less need exists to create policies. You can instead reuse existing policies to create more permissions. + +For more details about each policy type, see link:{authorizationguide_link}#_policy_overview[Managing policies]. + +[[_evaluating-permissions]] +==== Evaluating Permissions + +The *Evaluation* tab provides a testing environment where administrators can verify that permissions are enforcing access +as expected. The administrator can see what permissions are involved when enforcing access to a particular resource and what the outcome is. + +You need to provide a set of fields in order to run an evaluation: + +* `User`, the realm administrator or the subject trying to access a resource +* `Resource Type`, the resource type you want to evaluate +* `Resource Selector`, depending on the selected `Resource Type` you will be prompted to select a specific realm resource like a user, group, or client. +* `Authorization scope`, the scope or the operation you want to evaluate. If not provided, the evaluation will happen for all the scopes of the selected resource type. + +.Fine grained permissions evaluation tab +image:images/fine-grain-evaluation.png[Fine grained permissions evaluation tab] + +By clicking the `Evaluate` button, the server will evaluate all the permissions associated with the selected resource and scopes +just like if the selected `User` were trying to access the resource when using the administration console or the Admin API. + +For instance, in the example above you can see that the user `myadmin` can *manage* user `user-1` because a `Allow managing all realm users` permission +voted to a `PERMIT`, therefore granting access to the `manage` scope. However, all the other scopes were denied. + +Combined with the searching capabilities from the *Permissions* tab, you can perform troubleshooting to identify any permission that +is not behaving as expected. + +When evaluating permissions, the following rules apply: + +* The outcome from resource-specific permissions have precedence over broader permissions that give access to all resources of a certain type +* If no permissions exist for a specific resource, access will be granted based on the permission that grants access to all resources of a certain type +* The outcome from different permissions that enforce access to a specific resource will only grant access if they all permit access to the resource + +[[_resolving-conflicting-permissions]] +===== Resolving conflicting permissions + +Permissions can have multiple policies associated with them. As the authorization model evolves, it is common for some policies within a permission or +even different permissions related to a specific resource to conflict. + +The evaluation outcome will be "denied" whenever any permission is evaluated to "DENY." If there are multiple permissions related to the same resource, +all of them must grant access in order for the outcome to be "granted." + +IMPORTANT: Fine-grained admin permissions allow you to set up permissions for individual resources or for the resource type itself (such as all users, +all groups, and so on.). If a permission or permissions related to a specific resource exist, the "all-resource" permission is *NOT* taken into account +during evaluation. If no specific permission exists, the fallback is to the "all-resource" permission. This approach helps address scenarios like +allowing members of the `realm-admins` group to manage members of realm groups, but preventing them from managing members of the `realm-admins` group +themselves. + +[[_realm_access_control]] +==== Accessing a Realm administration console as a Realm Administrator + +Realm administrators can access a dedicated realm-specific administration console that allows them to manage resources within their assigned realm. +This console is separate from the main {project_name} Admin Console, which is typically used by server administrators. + +For more details on dedicated realm administration consoles and available roles, refer to: <<_per_realm_admin_permissions, Dedicated admin consoles>>. + +To access the administration console, a realm administrator must have at least one of the following roles assigned, depending on the resources they +need to administer: + +- *query-users* – Required to query realm users. +- *query-groups* – Required to query realm groups. +- *query-clients* – Required to query realm clients. + +By granting any of these roles to a realm user, they will be able to access the administration console, but only for the +areas that correspond to roles granted. For instance, if you assign the `query-users` role, the realm administrator +will only have access to the `Users` section in the administration console. If an administrator is responsible for +multiple resource types (such as both users and groups), they must have all the corresponding "query-*" roles assigned. + +These roles enable basic access to query resources but do not grant permission to view or modify them. To grant or deny access +to realm resources you need to set up the permissions for any of the operations available from each resource type. +For more details, see link:#_managing-permissions[Managing Permissions]. + +===== Roles and Permission relationship + +Fine grained permissions are used to grant additional permissions. You cannot override the default behavior of the built-in admin roles. +If a realm administrator is assigned one or more admin roles, it prevents the permissions from being evaluated. This means that +if a respective admin role is assigned to a realm administrator, permission evaluation will be bypassed, and access will be granted. + +[cols="30%,70%"] +|=== +| *Admin Role* | *Description* + +| *query-users* | A realm administrator can see the *Users* section in administration console and can search for users in the realm. + It does not grant the ability to *view* users. +| *query-groups* | A realm administrator can see the *Groups* section in administration console and can search for groups in the realm. + It does not grant the ability to *view* groups. +| *query-clients* | A realm administrator can see the *Clients* section in administration console and can search for clients in the realm. + It does not grant the ability to *view* clients. +| *view-users* | A realm administrator can *view* all users and groups in the realm. +| *manage-users* | A realm administrator can *view*, *map-roles*, *manage-group-membership* and *manage* all users in the realm, + as well as *view*, *manage-membership* and *manage* groups in the realm. +| *impersonation* | A realm administrator can *impersonate* all users in the realm. +| *view-clients* | A realm administrator can *view* all clients in the realm. +| *manage-clients* | A realm administrator can *view* and *manage* all clients and client scopes in the realm. +|=== + +==== Understanding some common use cases + +Consider a situation where an administrator wants to allow a group of administrators to manage all users in the realm except those that +belong to the administrators group. This example includes a `test` realm and a `test-admins` group. + +===== Allowing to manage users by group of administrators + +Create user permission permission, allowing to view and manage all users in the realm for members of the `test-admins` group: + +* Navigate to the *Permissions* tab in the administration console. +* Click *Create permission* and choose *Users* resource type. +* Fill in the name, such as `Disallow managing test-admins`. +* Choose *view* and *manage* authorization scopes, keep checked *All Users*. +* Create a condition, which needs to be met to get an access by clicking *Create new policy*. +* Fill in the name `Allow test-admins`, select *Group* as *Policy type*. +* Click *Add groups* button and select `test-admins` group, click *Save*. +* Click *Save* on *Create permission* page. + +===== Allowing to manage users by group of admins but not group members + +Let's exlude the members of the group itself, so that `test-admins` cannot manage other admins. + +* Create new permission by clicking *Create permission*. +* This time choose *Groups* resource type. +* Fill in the name, such as `Disallow managing test-admins`. +* Choose *manage-members* authorization scope. +* Select *Specific Groups* and choose `test-admins` group. +* *Create new policy* of type *Group*. +* Fill the name `Disallow test-admins` and select `test-admins` group. +* Switch to *Negative Logic* for the policy, *Save* the policy +* *Save* the permission + +===== Allowing to impersonate users for members of a group with a specific role assigned + +- Create a "User Permission" for specific users (or all users) you want to allow impersonation. +- Create a "Group Policy" allowing access to members of `test-admins`. +- Create a "Role Policy" allowing access to users assigned the `impersonation-admin` role. +- Assign both policies to the permission. + +===== Blacklisting specific users from being impersonated + +- Create a *User Permission* for the specific users you want to prevent from being impersonated. +- Create any policy that evaluates to deny (such as a user policy with no users selected). +- Assign the policy to the permission to effectively block impersonation for the selected users. + +===== Allowing to view users but not managing them for admins with a defined role assigned + +- Create a "User Permission" with the *view* scope for all users. +- Create a "Role Policy" allowing access to users with specific role assigned. +- Do _not_ assign the `manage` scope to prevent modification of user details. + +===== Allowing to manage users and role assignment for members of a group + +- Create a "User Permission" with the *manage*, *map-roles* scopes for all users. +- Create a "Group Policy" allowing access to members of `test-admins`. + +===== Allowing to view and manage members of a group but not members of its subgroups + +- Create a "Group Permission" with the *view-members* and *manage-members* scopes for specific group `mygroup`. +- Assign a "Group Policy" targeting `test-admins` to it. +- Create another "Group Permission" with the *view-members* and *manage-members* scopes for specific group, select all subgroups of the `mygroup`. +- Create negative "Group Policy" for `test-admins` and assign it to the "subgroups" permission. + +===== Allowing to impersonate members of a specific group + +- Create a "Group Permission" with the *impersonate-members* for specific group `mygroup`. +- Assign a "Group Policy" targeting `mygroup-helpdesk` to it. + +==== Performance considerations + +When enabling the feature to a realm, there is an additional overhead when realm administrators are managing any of the +supported resource types. This is mainly true when performing these operations: + +* Listing and searching +* Updating or deleting + +The feature introduces additional checks whenever you are listing or managing realm resources in order to enforce access +based on the permissions you have defined. This is mainly true when querying realm resources due to the additional overhead +to partially evaluate the permissions for a realm administrator to filter and paginate the results. + +Fewer permissions referencing a realm administrator user and most of the resources they can access is better. For instance, +if you want to delegate access to a realm administrator to manage users, it is better to have those users as members of a group. By doing that, +you are improving not only the performance when evaluating permissions but also creating a permission model that is easier to manage. + +The main impact of access enforcement is when querying realm resources. If a realm administrator is, for instance, referenced +in thousands of permissions through a user, role, or group policy, the partial evaluation mechanism that happens when querying +realm resources will query all those permissions from the database. A more concise and optimized model will help to fetch fewer +permissions but the enough to grant or deny access to realm resources. + +For instance, granting access to a realm administrator to view and manage users in a realm is better done with a group permission +than create individual permissions for each individual user in a realm. As well as make sure the policies associated with a +permission referencing a realm administrator either by a direct reference (user policy), +or indirect (role or group policy) reference, do not span multiple (thousands of) permissions, regardless of the resource type. + +As an example, suppose you have three users in a realm, and you want to allow `bob`, a realm administrator, to `view` and `manage` them. +A non-optimal permission model would create three different permissions, for each user, where a user policy grants access to `bob`. Instead, +you can have a single group permission, or even a single user permission, that groups those three users while still granting access to `bob` +using the same user policy. + +The same is true if you want to give access to more realm administrators to those three users. Instead of creating individual policies, +you can consider using a group or role policy instead. The permission model is use-case-specific, but these recommendations are important +to provide not only better manageability but also improve the overall performance of the server when managing realm resources. + +In terms of server configuration, depending on the size of your realm and the number of permissions and policies you have, you might consider +changing the cache configuration to increase the size of the following caches: + +* `realms` +* `users` +* `authorization` + +Consider looking at the server metrics for these caches to find the best value when sizing your deployment. + +When filtering resources, the partial evaluation mechanism will eventually rely on `IN` clauses in SQL statements +to filter the results. Depending on your database, you might have limitations on the number of parameters for the `IN` clause. +That is the case for old versions of the Oracle database, which has a hard limit to 1000 parameters. To avoid such problems, +keep in mind the considerations above about the number of permissions that grants or deny access to a single realm administrator. diff --git a/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain.adoc b/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain.adoc index 5e67a1e6de78..0d79a1e2cb91 100644 --- a/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain.adoc +++ b/docs/documentation/server_admin/topics/admin-console-permissions/fine-grain.adoc @@ -1,11 +1,9 @@ -[[_fine_grain_permissions]] -=== Fine grain admin permissions +=== Fine grained admin permissions V1 -:tech_feature_name: Fine Grain Admin Permissions -:tech_feature_setting: -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled -:tech_feature_id: admin-fine-grained-authz -include::../templates/techpreview.adoc[] +IMPORTANT: fine-grained admin permissions V1 have been replaced by a <<_fine_grained_permissions, new version>>. +Version 1 of the feature is still marked as preview and is available, but it may be deprecated and removed +in future. To enable it, start the server with `--features=admin-fine-grained-authz:v1`. Sometimes roles like `manage-realm` or `manage-users` are too coarse grain and you want to create restricted admin accounts that have more fine grain permissions. {project_name} allows you to define @@ -69,11 +67,10 @@ image:images/fine-grain-client-manage-permissions.png[Fine grain client manage p When first initialized the `manage` permission does not have any policies associated with it. You will need to create one by going to the policy tab. To get there fast, click on -the `Authorization` link shown in the above image. Then click on the policies tab. +the `Client details` link shown in the above image. Then click on the policies tab. -There's a pull down menu on this page called `Create policy`. There's a multitude of policies -you can define. You can define a policy that is associated with a role or a group or even define -rules in JavaScript. For this simple example, we're going to create a `User Policy`. +On this page, look for the `Create client policy` button, which you can use to define many policies. You can define a policy that is associated with a role or a group or even define +rules in JavaScript. For this simple example, we are going to create a `User Policy`. .User policy image:images/fine-grain-client-user-policy.png[Fine grain client user policy] @@ -84,15 +81,13 @@ This policy will match a hard-coded user in the user database. In this case, it .Assign user policy image:images/fine-grain-client-assign-user-policy.png[Fine grain client assign user policy] -The `sales-admin` user can now has permission to manage the `sales-application` client. +The `sales-admin` user now has permission to manage the `sales-application` client. -There's one more thing we have to do. Go to the `Role Mappings` tab and assign the `query-clients` -role to the `sales-admin`. +There is one more thing we have to do. Go to `Users`, select the `sales-admin` user, then go to the `Role Mappings` tab and assign the `query-clients` role to the user. .Assign query-clients image:images/fine-grain-assign-query-clients.png[Fine grain assign query clients] - Why do you have to do this? This role tells the Admin Console what menu items to render when the `sales-admin` visits the Admin Console. The `query-clients` role tells the Admin Console that it should render client menus for the `sales-admin` user. @@ -173,7 +168,7 @@ users you'll see that each user detail page is read only, except for the `Role M Going to this tab you'll find that there are no `Available` roles for the admin to map to the user except when we browse the `sales-application` roles. -.Add viewleads +.Assign viewLeads image:images/fine-grain-add-view-leads.png[Fine grain add view leads] We've only specified that the `sales-admin` can map the `viewLeads` role. @@ -302,4 +297,3 @@ manage-membership:: Policies that decide if an admin can change the membership of the group. Add or remove members from the group. - diff --git a/docs/documentation/server_admin/topics/admin-console-permissions/master-realm.adoc b/docs/documentation/server_admin/topics/admin-console-permissions/master-realm.adoc index 3c0b626a8850..4f2374df4f79 100644 --- a/docs/documentation/server_admin/topics/admin-console-permissions/master-realm.adoc +++ b/docs/documentation/server_admin/topics/admin-console-permissions/master-realm.adoc @@ -1,9 +1,10 @@ +[[_master_realm_access_control]] === Master realm access control The `master` realm in {project_name} is a special realm and treated differently than other realms. Users in the {project_name} `master` realm can be granted permission to manage zero or more realms that are deployed on the {project_name} server. -When a realm is created, {project_name} automatically creates various roles that grant fine-grain permissions to access that new realm. +When a realm is created, {project_name} automatically creates various roles that grant permissions to access that new realm. Access to The Admin Console and Admin REST endpoints can be controlled by mapping these roles to users in the `master` realm. It's possible to create multiple superusers, as well as users that can only manage specific realms. @@ -27,18 +28,24 @@ level of access to manage an individual realm. The roles available are: -* view-realm -* view-users -* view-clients -* view-events -* manage-realm -* manage-users * create-client +* impersonation +* manage-authorization * manage-clients -* manage-events -* view-identity-providers +* manage-events * manage-identity-providers -* impersonation +* manage-realm +* manage-users +* query-clients +* query-groups +* query-realms +* query-users +* view-authorization +* view-clients +* view-events +* view-identity-providers +* view-realm +* view-users Assign the roles you want to your users and they will only be able to use that specific part of the administration console. diff --git a/docs/documentation/server_admin/topics/admin-console-permissions/per-realm.adoc b/docs/documentation/server_admin/topics/admin-console-permissions/per-realm.adoc index 80a4f5d39998..78131ff55106 100644 --- a/docs/documentation/server_admin/topics/admin-console-permissions/per-realm.adoc +++ b/docs/documentation/server_admin/topics/admin-console-permissions/per-realm.adoc @@ -8,18 +8,25 @@ Users within that realm can be granted realm management permissions by assigning Each realm has a built-in client called `realm-management`. You can view this client by going to the `Clients` left menu item of your realm. This client defines client-level roles that specify permissions that can be granted to manage the realm. -* view-realm -* view-users -* view-clients -* view-events -* manage-realm -* manage-users * create-client +* impersonation +* manage-authorization * manage-clients * manage-events -* view-identity-providers * manage-identity-providers -* impersonation +* manage-realm +* manage-users +* query-clients +* query-groups +* query-realms +* query-users +* realm-admin +* view-authorization +* view-clients +* view-events +* view-identity-providers +* view-realm +* view-users Assign the roles you want to your users and they will only be able to use that specific part of the administration console. diff --git a/docs/documentation/server_admin/topics/admin-console.adoc b/docs/documentation/server_admin/topics/admin-console.adoc index 544437a7c7c9..3f69782237cb 100644 --- a/docs/documentation/server_admin/topics/admin-console.adoc +++ b/docs/documentation/server_admin/topics/admin-console.adoc @@ -1,4 +1,5 @@ +[#_configuring-realms] == Configuring realms Once you have an administrative account for the Admin Console, you can configure realms. A realm is a space where you manage objects, including users, applications, roles, and groups. A user belongs to and logs into a realm. One {project_name} deployment can define, store, and manage as many realms as there is space for in the database. @@ -15,4 +16,4 @@ include::login-settings/forgot-password.adoc[leveloffset=2] include::login-settings/remember-me.adoc[leveloffset=2] include::login-settings/acr-to-loa-mapping.adoc[leveloffset=2] include::login-settings/update-email-workflow.adoc[leveloffset=2] -include::realms/keys.adoc[] \ No newline at end of file +include::realms/keys.adoc[] diff --git a/docs/documentation/server_admin/topics/assembly-creating-first-admin.adoc b/docs/documentation/server_admin/topics/assembly-creating-first-admin.adoc index 1cb5a2861960..64c91311985c 100644 --- a/docs/documentation/server_admin/topics/assembly-creating-first-admin.adoc +++ b/docs/documentation/server_admin/topics/assembly-creating-first-admin.adoc @@ -19,13 +19,13 @@ image:images/initial-welcome-page.png[Welcome page] === Creating the account remotely -If you cannot access the server from a `localhost` address or just want to start {project_name} from the command line, use the `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` environment variables to create an initial admin account. +If you cannot access the server from a `localhost` address or just want to start {project_name} from the command line, use the `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` environment variables to create an initial admin account. For example: [source,bash] ---- -export KEYCLOAK_ADMIN= -export KEYCLOAK_ADMIN_PASSWORD= +export KC_BOOTSTRAP_ADMIN_USERNAME= +export KC_BOOTSTRAP_ADMIN_PASSWORD= bin/kc.[sh|bat] start ---- diff --git a/docs/documentation/server_admin/topics/assembly-managing-clients.adoc b/docs/documentation/server_admin/topics/assembly-managing-clients.adoc index 1196e9fe4ca0..1fef8fa02740 100644 --- a/docs/documentation/server_admin/topics/assembly-managing-clients.adoc +++ b/docs/documentation/server_admin/topics/assembly-managing-clients.adoc @@ -5,7 +5,7 @@ [role="_abstract"] Clients are entities that can request authentication of a user. Clients come in two forms. The first type of client is an application that wants -to participate in single-sign-on. These clients just want {project_name} to provide security for them. The other type +to participate in single sign-on. These clients just want {project_name} to provide security for them. The other type of client is one that is requesting an access token so that it can invoke other services on behalf of the authenticated user. This section discusses various aspects around configuring clients and various ways to do it. diff --git a/docs/documentation/server_admin/topics/assembly-managing-organizations.adoc b/docs/documentation/server_admin/topics/assembly-managing-organizations.adoc new file mode 100644 index 000000000000..76bc66dc2c6e --- /dev/null +++ b/docs/documentation/server_admin/topics/assembly-managing-organizations.adoc @@ -0,0 +1,12 @@ +[id="assembly-managing-organizations_{context}"] +[[_managing_organizations]] +== Managing organizations + +include::organizations/intro.adoc[leveloffset=+2] +include::organizations/managing-organization.adoc[leveloffset=+2] +include::organizations/managing-attributes.adoc[leveloffset=+2] +include::organizations/managing-members.adoc[leveloffset=+2] +include::organizations/managing-identity-providers.adoc[leveloffset=+2] +include::organizations/authenticating-members.adoc[leveloffset=+2] +include::organizations/mapping-organization-claims.adoc[leveloffset=+2] + diff --git a/docs/documentation/server_admin/topics/assembly-managing-users.adoc b/docs/documentation/server_admin/topics/assembly-managing-users.adoc index 303afb1b91dc..93aed18be921 100644 --- a/docs/documentation/server_admin/topics/assembly-managing-users.adoc +++ b/docs/documentation/server_admin/topics/assembly-managing-users.adoc @@ -5,12 +5,12 @@ From the Admin Console, you have a wide range of actions you can perform to manage users. include::users/proc-creating-user.adoc[leveloffset=+2] +include::users/user-profile.adoc[leveloffset=+2] include::users/ref-user-credentials.adoc[leveloffset=+2] include::users/proc-setting-password-user.adoc[leveloffset=+3] include::users/proc-creating-otp.adoc[leveloffset=+3] -include::users/proc-configuring-user-attributes.adoc[leveloffset=+2] include::users/con-user-registration.adoc[leveloffset=+2] include::users/proc-enabling-user-registration.adoc[leveloffset=3] include::users/proc-registering-new-user.adoc[leveloffset=3] @@ -21,6 +21,8 @@ include::users/proc-setting-required-actions.adoc[leveloffset=+3] include::users/proc-setting-default-required-actions.adoc[leveloffset=+3] include::users/proc-enabling-terms-conditions.adoc[leveloffset=+3] +include::users/con-aia.adoc[leveloffset=+2] + include::users/proc-searching-user.adoc[leveloffset=+2] include::users/proc-deleting-user.adoc[leveloffset=+2] @@ -29,6 +31,5 @@ include::users/proc-allow-user-to-delete-account.adoc[leveloffset=+2] include::users/con-user-impersonation.adoc[leveloffset=+2] include::users/proc-enabling-recaptcha.adoc[leveloffset=+2] -include::users/user-profile.adoc[leveloffset=+2] include::users/ref-personal-data-collected.adoc[leveloffset=+2] diff --git a/docs/documentation/server_admin/topics/authentication/authentication-sessions.adoc b/docs/documentation/server_admin/topics/authentication/authentication-sessions.adoc new file mode 100644 index 000000000000..2dcc4d8dc176 --- /dev/null +++ b/docs/documentation/server_admin/topics/authentication/authentication-sessions.adoc @@ -0,0 +1,25 @@ + +[[_authentication-sessions]] +=== Authentication sessions + +When a login page is opened for the first time in a web browser, {project_name} creates an object called authentication session that stores some useful information about the request. +Whenever a new login page is opened from a different tab in the same browser, {project_name} creates a new record called authentication sub-session that is stored within the authentication session. +Authentication requests can come from any type of clients such as the Admin CLI. In that case, a new authentication session is also created with one authentication sub-session. +Please note that authentication sessions can be created also in other ways than using a browser flow. + +The authentication session usually expires after 30 minutes by default. The exact time is specified by the *Login timeout* switch in the *Sessions* tab of the admin console when <<_configuring-realms,configuring realms>>. + +==== Authentication in more browser tabs + +As described in the previous section, a situation can involve a user who is trying to authenticate to the {project_name} server from multiple tabs of a single browser. However, when that user authenticates in one browser tab, +the other browser tabs will automatically restart the authentication. This authentication occurs due to the small javascript available on the {project_name} login pages. The restart will typically +authenticate the user in other browser tabs and redirect to clients because there is an SSO session now due to the fact that the user just successfully authenticated in first browser tab. +Some rare exceptions exist when a user is not automatically authenticated in other browser tabs, such as for instance when using an OIDC parameter _prompt=login_ or <<_step-up-flow, step-up authentication>> requesting a stronger +authentication factor than the currently authenticated factor. + +In some rare cases, it can happen that after authentication in the first browser tab, other browser tabs are not able to restart authentication because the authentication session is already +expired. In this case, the particular browser tab will redirect the error about the expired authentication session back to the client in a protocol specific way. For more details, see the corresponding sections +of *OIDC documentation* in the link:{securing_apps_link}[securing apps] section. When the client application receives such an error, it can immediately resubmit the OIDC/SAML authentication request to {project_name} as +this should usually automatically authenticate the user due to the existing SSO session as described earlier. As a result, the end user is authenticated automatically in all browser tabs. +The *Keycloak JavaScript adapter* in the link:{securing_apps_link}[securing apps] section, and <<_identity_broker, {project_name} Identity provider>> +support to handle this error automatically and retry the authentication to the {project_name} server in such a case. diff --git a/docs/documentation/server_admin/topics/authentication/conditions.adoc b/docs/documentation/server_admin/topics/authentication/conditions.adoc index f559c8744982..9090c8ae96fc 100644 --- a/docs/documentation/server_admin/topics/authentication/conditions.adoc +++ b/docs/documentation/server_admin/topics/authentication/conditions.adoc @@ -1,3 +1,4 @@ +[#conditions-in-conditional-flows] === Conditions in conditional flows As was mentioned in <<_execution-requirements, Execution requirements>>, _Condition_ executions can be only contained in _Conditional_ subflow. @@ -26,7 +27,7 @@ The Execution requirements section includes an example of the OTP form. `Condition - User Attribute`:: This checks if the user has set up the required attribute: optionally, the check can also evaluate the group attributes. There is a possibility to negate output, which means the user should not have the attribute. -The xref:proc-configuring-user-attributes_{context}[User Attributes] section shows how to add a custom attribute. +The link:#user-profile[User Attributes] section shows how to add a custom attribute. You can provide these fields: Alias::: @@ -45,6 +46,25 @@ Negate output::: You can negate the output. In other words, the attribute should not be present. +`Condition - sub-flow executed`:: +The condition checks if a previous sub-flow was successfully executed (or not executed) in the authentication process. Therefore, the flow can trigger other steps based on a previous sub-flow termination. These configuration fields exist: + +Flow name::: +The sub-flow name to check if it was executed or not executed. Required. + +Check result::: +When the condition evaluates to true. If `executed` returns true when the configured sub-flow was executed with output success, false otherwise. If `not-executed` returns false when the sub-flow was executed with output success, true otherwise (the negation of the previous option). + +`Condition - client scope`:: +The condition to evaluate if a configured client scope is present as a client scope of the client requesting authentication. These configuration fields exist: + +Client scope name::: +The name of the client scope, which should be present as a client scope of the client, which is requesting authentication. If requested client scope is default client scope of the client requesting login, the condition will be evaluated to true. If requested client scope is optional client scope of the client requesting login, condition will be evaluated to true if client scope is sent by the client in the login request (for example, by the `scope` parameter in case of OIDC/OAuth2 client login). Required. + +Negate output::: +Apply a NOT to the check result. When this is true, then the condition will evaluate to true just if configured client scope is not present. + + ==== Explicitly deny/allow access in conditional flows You can allow or deny access to resources in a conditional flow. @@ -85,4 +105,44 @@ The last thing is defining the property with an error message in the login theme [source] ---- deny-role1 = You do not have required role! ----- \ No newline at end of file +---- + +[#twofa-conditional-workflow-examples] +==== 2FA conditional workflow examples + +The section presents some examples of conditional workflows that integrates 2nd Factor Authentication (2FA) in different ways. The examples copy the default `browser` flow and modify the configuration inside the `forms` sub-flow. + +===== Conditional 2FA sub-flow + +The default `browser` flow uses a `Conditional 2FA` sub-flow that already gives 2nd factor Authentication (2FA) with OTP Form (One Time Password). It also provides WebAuthn and Recovery Codes but they are disabled by default. Consistent with this approach, different 2FA methods can be integrated with the `Condition - User Configured`. + +.2FA all alternative +image:images/2fa-example1.png[2FA all alternative] + +The `forms` sub-flow contains another `2FA` conditional sub-flow with `Condition - user configured`. Three 2FA steps (OTP, Webauthn and Recovery Codes) are allowed as alternative steps. The user will be able to choose one of the three options, if they are configured for the user. As the sub-flow is conditional, the authentication process will complete successfully if no 2FA credential is configured. + +This configuration provides the same behavior as when you configure with the default *browser* flow with both _Disabled_ steps are configured to _Alternative_. + +===== Conditional 2FA sub-flow and deny access + +The second example continues the previous one. After the `2FA` sub-flow, another flow `Deny access if no 2FA` is used to check if the previous `2FA` was not executed. In that case (the user has no 2FA credential configured) the access is denied. + +.2FA all alternative and deny access +image:images/2fa-example2.png[2FA all alternative and deny access] + +The `Condition - sub-flow executed` is configured to detect if the `2FA` sub-flow was not executed previously. + +.Configuration for the sub-flow executed +image:images/2fa-example2-config.png[Configuration for the sub-flow executed] + +The step `Deny access` denies the authentication if not executed. + +[[_conditional-2fa-otp-default]] +===== Conditional 2FA sub-flow with OTP default + +The last example is very similar to the previous one. Instead of denying the access, step `OTP Form` is configured as required. + +.2FA all alternative with OTP default +image:images/2fa-example3.png[2FA all alternative with OTP default] + +With this flow, if the user has none of the 2FA methods configured, the OTP setup will be enforced to continue the login. diff --git a/docs/documentation/server_admin/topics/authentication/flows.adoc b/docs/documentation/server_admin/topics/authentication/flows.adoc index ad76c6ab4513..7dcfe808020b 100644 --- a/docs/documentation/server_admin/topics/authentication/flows.adoc +++ b/docs/documentation/server_admin/topics/authentication/flows.adoc @@ -16,7 +16,7 @@ An _authentication flow_ is a container of authentications, screens, and actions image:images/browser-flow.png[Browser Flow] ===== Auth type -The name of the authentication or the action to execute. If an authentication is indented, it is in a sub-flow. It may or may not be executed, depending on the behavior of its parent. +The name of the authentication or the action to execute. If an authentication is indented, it is in a sub-flow. It may or may not be executed, depending on the behavior of its parent. . Cookie + @@ -36,36 +36,34 @@ Since this sub-flow is marked as _alternative_, it will not be executed if the * The first execution is the *Username Password Form*, an authentication type that renders the username and password page. It is marked as _required_, so the user must enter a valid username and password. -The second execution is the *Browser - Conditional OTP* sub-flow. This sub-flow is _conditional_ and executes depending on the result of the *Condition - User Configured* execution. If the result is true, {project_name} loads the executions for this sub-flow and processes them. +The second execution is the *Browser - Conditional 2FA* sub-flow. This sub-flow is _conditional_ and executes depending on the result of the *Condition - User Configured* execution. If the result is true, {project_name} loads the executions for this sub-flow and processes them. -The next execution is the *Condition - User Configured* authentication. This authentication checks if {project_name} has configured other executions in the flow for the user. The *Browser - Conditional OTP* sub-flow executes only when the user has a configured OTP credential. +The next execution is the *Condition - User Configured* authentication. This authentication checks if {project_name} has configured other executions in the flow for the user. The *Browser - Conditional 2FA* sub-flow executes only when the user has a configured OTP credential. -The final execution is the *OTP Form*. {project_name} marks this execution as _required_ but it runs only when the user has an OTP credential set up because of the setup in the _conditional_ sub-flow. If not, the user does not see an OTP form. +The final execution is the *OTP Form*. {project_name} marks this execution as _alternative_, but it runs only when the user has an OTP credential set up because of the setup in the _conditional_ sub-flow. If the OTP credential is not set up, the user does not see an OTP form. -===== Requirement -A set of radio buttons that control the execution of an action executes. +The default *browser* flow contains two more executions inside the *Browser - Conditional 2FA*, *WebAuthn Authenticator* and *Recovery Authentication Code Form*. These executions are _Disabled_ by default and they are the other 2FA methods that can be added to the flow. Change the requirement from _Disabled_ to _Alternative_ to make them available if the respective credential has been configured for the user. If the user has configured all alternative credential types, the credential with the highest priority is displayed by default. However, the *Try Another Way* option will appear so that the user has the alternative methods to log in. [[_execution-requirements]] -====== Required +===== Requirement +A drop-down menu that controls the execution of an action. +Required:: All _Required_ elements in the flow must be successfully sequentially executed. The flow terminates if a required element fails. -====== Alternative - +Alternative:: Only a single element must successfully execute for the flow to evaluate as successful. Because the _Required_ flow elements are sufficient to mark a flow as successful, any _Alternative_ flow element within a flow containing _Required_ flow elements will not execute. -====== Disabled - +Disabled:: The element does not count to mark a flow as successful. -====== Conditional - -This requirement type is only set on sub-flows. - -* A _Conditional_ sub-flow contains executions. These executions must evaluate to logical statements. -* If all executions evaluate as _true_, the _Conditional_ sub-flow acts as _Required_. +Conditional:: +This requirement type is only set on sub-flows. ++ +* A _Conditional_ sub-flow contains executions. These executions must evaluate to logical statements. +* If all executions evaluate as _true_, the _Conditional_ sub-flow acts as _Required_. * If any executions evaluate as _false_, the _Conditional_ sub-flow acts as _Disabled_. -* If you do not set an execution, the _Conditional_ sub-flow acts as _Disabled_. +* If you do not set an execution, the _Conditional_ sub-flow acts as _Disabled_. * If a flow contains executions and the flow is not set to _Conditional_, {project_name} does not evaluate the executions, and the executions are considered functionally _Disabled_. ==== Creating flows @@ -111,8 +109,14 @@ Executions have a wide variety of actions, from sending a reset email to validat .Adding an authentication execution image:images/Create-authentication-execution.png[Adding an Authentication Execution] +Authentication executions can optionally have a reference value configured. This can be utilized by the _Authentication Method Reference (AMR)_ protocol mapper to populate the _amr_ claim in OIDC access and ID tokens (for more information on the +AMR claim, see https://www.rfc-editor.org/rfc/rfc8176.html[RFC-8176]). When the _Authentication Method Reference (AMR)_ protocol mapper is configured for a client, it will populate the _amr_ claim with the reference value for any authenticator execution the user successfully completes during the authentication flow. + +.Adding an authenticator reference value +image:images/config-authenticator-reference.png[Configuring an Authenticator Reference Value] + Two types of executions exist, _automatic executions_ and _interactive executions_. _Automatic executions_ are similar to the *Cookie* execution and will automatically -perform their action in the flow. _Interactive executions_ halt the flow to get input. Executions executing successfully set their status to _success_. For a flow to complete, it needs at least one execution with a status of _success_. +perform their action in the flow. _Interactive executions_ halt the flow to get input. Executions executing successfully set their status to _success_. For a flow to complete, it needs at least one execution with a status of _success_. You can add sub-flows to top-level flows with the *Add sub-flow* button. The *Add sub-flow* button displays the *Create Execution Flow* page. This page is similar to the *Create Top Level Form* page. The difference is that the *Flow Type* can be *basic* (default) or *form*. The *form* type constructs a sub-flow that generates a form for the user, similar to the built-in *Registration* flow. Sub-flows success depends on how their executions evaluate, including their contained sub-flows. See the <<_execution-requirements, execution requirements section>> for an in-depth explanation of how sub-flows work. @@ -213,7 +217,7 @@ After entering the username, the flow works as follows: If users have WebAuthn passwordless credentials recorded, they can use these credentials to log in directly. This is the password-less login. The user can also select *Password with OTP* because the `WebAuthn Passwordless` execution and the `Password with OTP` flow are set to *Alternative*. If they are set to *Required*, the user has to enter WebAuthn, password, and OTP. -If the user selects the *Try another way* link with `WebAuthn passwordless` authentication, the user can choose between `Password` and `Security Key` (WebAuthn passwordless). When selecting the password, the user will need to continue and log in with the assigned OTP. If the user has no WebAuthn credentials, the user must enter the password and then the OTP. If the user has no OTP credential, they will be asked to record one. +If the user selects the *Try another way* link with `WebAuthn passwordless` authentication, the user can choose between `Password` and `Passkey` (WebAuthn passwordless). When selecting the password, the user will need to continue and log in with the assigned OTP. If the user has no WebAuthn credentials, the user must enter the password and then the OTP. If the user has no OTP credential, they will be asked to record one. [NOTE] ==== @@ -229,6 +233,17 @@ Creating an advanced flow such as this can have side effects. For example, if yo * In the *Action* menu, select *Bind flow* and select *Reset credentials flow* from the dropdown and click *Save* ==== +[[_client-policy-auth-flow]] +==== Using Client Policies to Select an Authentication Flow +<<_client_policies, Client Policies>> can be used to dynamically select an Authentication Flow based on specific conditions, such as requesting a particular scope or an ACR (Authentication Context Class Reference) using the `AuthenticationFlowSelectorExecutor` in combination with the condition you prefer. + +The `AuthenticationFlowSelectorExecutor` allows you to select an appropriate authentication flow and set the level of authentication to be applied once the selected flow is completed. + +A possible configuration involves using the `ACRCondition` in combination with the `AuthenticationFlowSelectorExecutor`. This setup enables you to select an authentication flow based on the requested ACR and have the ACR value included in the token using <<_mapping-acr-to-loa-realm,ACR to LoA Mapping>>. + +For more details, see <<_client_policies, Client Policies>>. + + [[_step-up-flow]] ==== Creating a browser login flow with step-up mechanism @@ -265,7 +280,7 @@ Now you configure the flow for the first authentication level. . Enter `Level 1` as an alias. . Enter `1` for the Level of Authentication (LoA). . Set Max Age to *36000*. This value is in seconds and it is equivalent to 10 hours, which is the default `SSO Session Max` timeout set in the realm. - As a result, when a user authenticates with this level, subsequent SSO logins can re-use this level and the user does not need to authenticate + As a result, when a user authenticates with this level, subsequent SSO logins can reuse this level and the user does not need to authenticate with this level until the end of the user session, which is 10 hours by default. . Click *Save* + @@ -337,7 +352,7 @@ claims= { ---- The {project_name} javascript adapter has support for easy construct of this JSON and sending it in the login request. -See link:{adapterguide_link_js_adapter}[Javascript adapter documentation] for more details. +See *Keycloak JavaScript adapter* in the link:{securing_apps_link}[securing apps] section for more details. You can also use simpler parameter `acr_values` instead of `claims` parameter to request particular levels as non-essential. This is mentioned in the OIDC specification. @@ -354,7 +369,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 @@ -375,8 +390,20 @@ condition found in the authentication flow, such as the Username/Password in the and that level expired, the user is not required to re-authenticate, but `acr` in the token will have the value 0. This result is considered as authentication based solely on `long-lived browser cookie` as mentioned in the section 2 of OIDC Core 1.0 specification. +NOTE: During the first authentication of the user, the first configured subflow with the *Conditional - Level Of Authentication* is always executed (regardless of the requested level) as +the user does not yet have any level. Therefore, we recommend that the first level subflow contains the minimal required authenticators for user authentication. In addition, ensure that the subflows with different values of *Conditional - Level Of Authentication* are ordered starting with the lowest as shown +in the example above. For example, if you configure a subflow with level 2 and then add another subflow with level 1, the level 2 subflow will be always asked during the first authentication, which may +not be the desired behavior. + NOTE: A conflict situation may arise when an admin specifies several flows, sets different LoA levels to each, and assigns the flows to different clients. However, the rule is always the same: if a user has a certain level, it needs only have that level to connect to a client. It's up to the admin to make sure that the LoA is coherent. +NOTE: Step-up authentication with Level of Authentication conditions is intended for use cases where each level +requires all authentication methods from the preceding levels. +For instance, level X must always include all authentication methods required by level X-1. +For use cases where a specific level, such as level 3, requires a different authentication method from the previous levels, +it may be more appropriate to use mapping of ACR to a specific flow. +For more details, see <<_client-policy-auth-flow, Using Client Policies to Select an Authentication Flow>>. + *Example scenario* . Max Age is configured as 300 seconds for level 1 condition. @@ -397,6 +424,26 @@ Note when the login request initiates a request with the `claims` parameter requ one of the specified levels. If it is not able to return one of the specified levels (For example if the requested level is unknown or bigger than configured conditions in the authentication flow), then {project_name} will throw an error. +[[_registration-rc-client-flows]] +==== Registration or Reset credentials requested by client + +Usually when the user is redirected to the {project_name} from client application, the `browser` flow is triggered. This flow may allow the user to <> in case +that realm registration is enabled and the user clicks `Register` on the login screen. Also, if <> is enabled for the realm, the user can +click `Forget password` on the login screen, which triggers the `Reset credentials` flow where users can reset credentials after email address confirmation. + +Sometimes it can be useful for the client application to directly redirect the user to the *Registration* screen or to the *Reset credentials* flow. The resulting action will match the action of when the +user clicks *Register* or *Forget password* on the normal login screen. Automatic redirect to the registration or reset-credentials screen can be done as follows: + +* When the client wants the user to be redirected directly to the registration, the OIDC client should add parameter `prompt=create` to the login request. As a deprecated alternative, clients can replace the very last +snippet from the OIDC login URL path (`/auth`) with `/registrations` . So the full URL might be similar to the following: `https://keycloak.example.com/realms/your_realm/protocol/openid-connect/registrations`. +The `prompt=create` is recommended as it is https://openid.net/specs/openid-connect-prompt-create-1_0.html[a specification standard]. + +* When the client wants a user to be redirected directly to the `Reset credentials` flow, the OIDC client should replace the very last snippet from the OIDC login URL path (`/auth`) with `/forgot-credentials` . + +WARNING: The preceding steps are the only supported method for a client to directly request a registration or reset-credentials flow. For security +purposes, it is not supported and recommended for client applications to bypass OIDC/SAML flows and directly redirect to other {project_name} endpoints (such as for instance endpoints under +`/realms/realm_name/login-actions` or `/realms/realm_name/broker`). + [[_user_session_limits]] === User session limits @@ -437,7 +484,7 @@ with password and OTP: image:images/authentication-user-session-limits-browser.png[Authentication User Session Limits Browser Flow] -Regarding `Post Broker login flow`, you can add the `User Session Limits` as the only authenticator in the authentication flow as long as you have no other authenticators that you trigger after authentication with your identity provider. However, make sure that this flow is configured as `Post Broker Flow` at your identity providers. This requirement exists needed so that +Regarding `Post Broker login flow`, you can add the `User Session Limits` as the only authenticator in the authentication flow as long as you have no other authenticators that you trigger after authentication with your identity provider. However, make sure that this flow is configured as `Post Broker Flow` at your identity providers. This requirement exists needed so that the authentication with Identity providers also participates in the session limits. NOTE: Currently, the administrator is responsible for maintaining consistency between the different configurations. So make sure that all your flows use same the configuration diff --git a/docs/documentation/server_admin/topics/authentication/kerberos.adoc b/docs/documentation/server_admin/topics/authentication/kerberos.adoc index 0f01e71a504c..56a3a4114592 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: @@ -62,7 +62,7 @@ Install a Kerberos client on your machine. .Procedure . Install a Kerberos client. If your machine runs Fedora, Ubuntu, or RHEL, install the link:https://www.freeipa.org/page/Downloads[freeipa-client] package, containing a Kerberos client and other utilities. -. Configure the Kerberos client (on Linux, the configuration settings are in the link:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html[/etc/krb5.conf] file ). +. Configure the Kerberos client (on Linux, the configuration settings are in the link:https://web.mit.edu/kerberos/krb5-1.21/doc/admin/conf_files/krb5_conf.html[/etc/krb5.conf] file ). + Add your Kerberos realm to the configuration and configure the HTTP domains your server runs on. + @@ -118,7 +118,7 @@ User profile information, such as first name, last name, and email, are not prov ==== Setup and configuration of client machines -Client machines must have a Kerberos client and set up the `krb5.conf` as described <<_server_setup, above>>. The client machines must also enable SPNEGO login support in their browser. See link:https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system-level_authentication_guide/configuring_applications_for_sso[configuring Firefox for Kerberos] if you are using the Firefox browser. +Client machines must have a Kerberos client and set up the `krb5.conf` as described <<_server_setup, above>>. The client machines must also enable SPNEGO login support in their browser. See link:https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/system-level_authentication_guide/configuring_applications_for_sso[configuring Firefox for Kerberos] if you are using the Firefox browser. The `.mydomain.org` URI must be in the `network.negotiate-auth.trusted-uris` configuration option. @@ -139,7 +139,7 @@ endif::[] ==== Credential delegation -Kerberos supports the credential delegation. Applications may need access to the Kerberos ticket so they can re-use it to interact with other services secured by Kerberos. Because the {project_name} server processed the SPNEGO protocol, you must propagate the GSS credential to your application within the OpenID Connect token claim or a SAML assertion attribute. {project_name} transmits this to your application from the {project_name} server. To insert this claim into the token or assertion, each application must enable the built-in protocol mapper `gss delegation credential`. This mapper is available in the *Mappers* tab of the application's client page. See <<_protocol-mappers, Protocol Mappers>> chapter for more details. +Kerberos supports the credential delegation. Applications may need access to the Kerberos ticket so they can reuse it to interact with other services secured by Kerberos. Because the {project_name} server processed the SPNEGO protocol, you must propagate the GSS credential to your application within the OpenID Connect token claim or a SAML assertion attribute. {project_name} transmits this to your application from the {project_name} server. To insert this claim into the token or assertion, each application must enable the built-in protocol mapper `gss delegation credential`. This mapper is available in the *Mappers* tab of the application's client page. See <<_protocol-mappers, Protocol Mappers>> chapter for more details. Applications must deserialize the claim it receives from {project_name} before using it to make GSS calls against other services. When you deserialize the credential from the access token to the GSSCredential object, create the GSSContext with this credential passed to the `GSSManager.createContext` method. For example: @@ -161,10 +161,6 @@ GSSContext context = gssManager.createContext(serviceName, krb5Oid, deserializedGssCredential, GSSContext.DEFAULT_LIFETIME); ---- -ifeval::[{project_community}==true] -Examples of this code exist in `examples/kerberos` in the {project_name} example distribution or demo distribution download. You can also check the example sources directly https://github.com/keycloak/keycloak/tree/main/examples/kerberos[here]. -endif::[] - [NOTE] ==== Configure `forwardable` Kerberos tickets in `krb5.conf` file and add support for delegated credentials to your browser. @@ -172,7 +168,7 @@ Configure `forwardable` Kerberos tickets in `krb5.conf` file and add support for [WARNING] ==== -Credential delegation has security implications, so use it only if necessary and only with HTTPS. See https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system-level_authentication_guide/configuring_applications_for_sso[this article] for more details and an example. +Credential delegation has security implications, so use it only if necessary and only with HTTPS. See https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/system-level_authentication_guide/configuring_applications_for_sso[this article] for more details and an example. ==== ==== Cross-realm trust diff --git a/docs/documentation/server_admin/topics/authentication/otp-policies.adoc b/docs/documentation/server_admin/topics/authentication/otp-policies.adoc index e2b7981377d8..20bf272d56d4 100644 --- a/docs/documentation/server_admin/topics/authentication/otp-policies.adoc +++ b/docs/documentation/server_admin/topics/authentication/otp-policies.adoc @@ -24,7 +24,7 @@ With Counter-Based One Time Passwords (HOTP), {project_name} uses a shared count TOTP is more secure than HOTP because the matchable OTP is valid for a short window of time, while the OTP for HOTP is valid for an indeterminate amount of time. HOTP is more user-friendly than TOTP because no time limit exists to enter the OTP. -HOTP requires a database update every time the server increments the counter. This update is a performance drain on the authentication server during heavy load. To increase efficiency, TOTP does not remember passwords used, so there is no need to perform database updates. The drawback is that it is possible to re-use TOTPs in the valid time interval. +HOTP requires a database update every time the server increments the counter. This update is a performance drain on the authentication server during heavy load. To increase efficiency, TOTP does not remember passwords used, so there is no need to perform database updates. The drawback is that it is possible to reuse TOTPs in the valid time interval. ==== TOTP configuration options @@ -34,7 +34,7 @@ The default algorithm is SHA1. The other, more secure options are SHA256 and SHA ===== Number of digits -The length of the OTP. Short OTP's are user-friendly, easier to type, and easier to remember. Longer OTP's are more secure than shorter OTP's. +The length of the OTP. Short OTPs are user-friendly, easier to type, and easier to remember. Longer OTPs are more secure than shorter OTPs. ===== Look around window diff --git a/docs/documentation/server_admin/topics/authentication/passkeys.adoc b/docs/documentation/server_admin/topics/authentication/passkeys.adoc index c8e79c9da0ed..b4fd2d125814 100644 --- a/docs/documentation/server_admin/topics/authentication/passkeys.adoc +++ b/docs/documentation/server_admin/topics/authentication/passkeys.adoc @@ -1,13 +1,60 @@ + [id="passkeys_{context}"] === Passkeys {project_name} provides preview support for https://fidoalliance.org/passkeys/[Passkeys]. {project_name} works as a Passkeys Relying Party (RP). -Passkey registration and authentication are realized by the features of xref:webauthn_{context}[WebAuthn]. -Therefore, users of {project_name} can do passkey registration and authentication by existing xref:webauthn_{context}[WebAuthn registraton and authentication]. +Passkey registration and authentication are performed using the same features of xref:webauthn_{context}[WebAuthn]. More specifically *Passkeys* are related to xref:_webauthn_loginless[LoginLess WebAuthn] as they try to avoid any password during login. +Therefore, users of {project_name} can do Passkey registration and authentication by existing xref:_enabling_organization_[WebAuthn registration and authentication], using the *passwordless* variants. The *Passkeys* feature have been integrated seamlessly in the default authentication forms, so, when activated, both conditional UI and modal UI are available in the forms in which the username input is displayed. + +*Passkeys* have been added to the following authenticator implementations: + +. *Username Password Form*: The username and password form used by default in {project_name}. +. *Username Form*: The form in which the username is displayed alone and is typically followed by the password form. This authenticator is used when the username and password fields want to be presented to the user in two different steps. Authenticating using *Passkeys* in the *Username Form* skips the next *Password Form* execution. The *Password Form* implementation checks if the user was already authenticated using a passwordless WebAuthn credential and, if that is the case, no password is requested. +. *Organization Identity - First Login*: The organization form that is used when the <<_enabling_organization_, organizations>> feature is enabled for the realm. Using *Passkeys* in this step avoids the subsequent execution of the username and password form in the same way than in the previous form. + +:tech_feature_name: Passkeys +:tech_feature_id: passkeys +include::../templates/techpreview.adoc[] [NOTE] ==== -Both synced passkeys and device-bound passkeys can be used for both Same-Device and Cross-Device Authentication (CDA). -However, passkeys operations success depends on the user's environment. Make sure which operations can succeed in https://passkeys.dev/device-support/[the environment]. +Both synced Passkeys and device-bound Passkeys can be used for both Same-Device and Cross-Device Authentication (CDA). +However, Passkeys operations success depends on the user's environment. Make sure which operations can succeed in https://passkeys.dev/device-support/[the environment]. ==== + +[[_passkeys-conditional-ui]] +==== Passkey Authentication with Conditional UI or autofill + +The Conditional User Interface (UI) or autofill is a feature related to passkeys in which the username input (the field in which the username to login is typed) is tagged with a `webauthn` autofill detail token (for example using the attribute `autocomplete="username webauthn"`). When the user clicks in such an input field, the user agent (browser) can render a list of discovered credentials for the user to select from, and perhaps also give the user the option to _try another way_. If the user selects one of the presented passkeys, {project_name} initiates the WebAuthn authentication with that key and avoids any password typing. + +Compared with xref:_webauthn_loginless[LoginLess WebAuthn], the authentication improves the user's experience of authentication. + +.Passkey Authentication with Conditional UI Autofill using Chrome browser +image:images/passkey-conditional-ui-autofill.png[Passkey Authentication with Conditional UI Autofill using Chrome browser] + +[NOTE] +==== +This authentication uses the https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI/[WebAuthn Conditional UI]. +Therefore, this authentication success depends on the user's environment. +If the environment does not support WebAuthn Conditional UI, the user should use the direct modal UI or username and password login. +==== + +==== Passkeys Authentication with Modal UI + +Nevertheless, because conditional UI can sometimes not show all the credentials to the user, the modal UI can always be initiated using the button *Sign in with Passkey*. The Modal User Interface (UI) ensures all passkeys are usable, including the ones stored in hardware tokens or on other devices that cannot be enumerated without user interaction. + +.Passkey Authentication with Modal UI using Chrome browser +image:images/passkey-modal-ui.png[Passkey Authentication with Modal UI using Chrome browser] + +==== Setup + +Set up Passkey Authentication for the default forms as follows: + +. (If not already done) Check the required action for *WebAuthn Register Passwordless* is enabled. Use the steps described in <<_webauthn-register, Enable WebAuthn Authenticator Registration>>, but using *WebAuthn Register Passwordless* instead of *WebAuthn Register*. + +. Configure the *WebAuthn Passwordless Policy* in the same way that is explained in xref:_webauthn_loginless[LoginLess WebAuthn]. Perform the configuration in the Admin Console, `Authentication` section, in the tab `Policies` → `WebAuthn Passwordless Policy`. Set *User Verification Requirement* to *required* and *Require discoverable credential* to *Yes* when you configure the policy for passwordless scenario. ++ +NOTE: Storage capacity is usually very limited on hardware passkeys meaning that you cannot store many discoverable credentials on your passkey. However, this limitation may be mitigated for instance if you use an Android phone backed by a Google account as a passkey device or an iPhone backed by Bitwarden. ++ +. In the same policy tab *WebAuthn Passwordless Policy* activate the *Enable Passkeys* option at the bottom. This switch is the one that really enables passkeys (both conditional and modal UI) in the default username forms. \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/authentication/password-policies.adoc b/docs/documentation/server_admin/topics/authentication/password-policies.adoc index c6fe1aecef96..314d6b040b4f 100644 --- a/docs/documentation/server_admin/topics/authentication/password-policies.adoc +++ b/docs/documentation/server_admin/topics/authentication/password-policies.adoc @@ -12,7 +12,7 @@ When {project_name} creates a realm, it does not associate password policies wit . Enter a value that applies to the policy chosen. . Click *Save*. + -Password policy +.Password policy image:images/password-policy.png[Password Policy] After saving the policy, {project_name} enforces the policy for new users. @@ -26,7 +26,37 @@ The new policy will not be effective for existing users. Therefore, make sure th ===== HashAlgorithm -Passwords are not stored in cleartext. Before storage or validation, {project_name} hashes passwords using standard hashing algorithms. PBKDF2 is the only built-in and default algorithm available. See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm. +Passwords are not stored in cleartext. Before storage or validation, {project_name} hashes passwords using standard hashing algorithms. + +Supported password hashing algorithms are shown in the following table. + +[%autowidth,cols="m,"] +|=== +|Hashing algorithm |Description + +| argon2 +| Argon2 (default for non-FIPS deployments) + +| pbkdf2-sha512 +| PBKDF2 with SHA512 (default for FIPS deployments) + +| pbkdf2-sha256 +| PBKDF2 with SHA256 + +| pbkdf2 +| PBKDF2 with SHA1 (deprecated) + +|=== + +It is highly recommended to use Argon2 when possible as it has significantly less CPU requirements compared to PBKDF2, while +at the same time being more secure. + +The default password hashing algorithm for the server can be configured with `+--spi-password-hashing--provider-default=+`. + +To prevent excessive memory and CPU usage, the parallel computation of hashes by Argon2 is by default limited to the number of cores available to the JVM. +To configure the Argon2 hashing provider, use its provider options. + +See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm. [NOTE] ==== @@ -34,13 +64,31 @@ If you change the hashing algorithm, password hashes in storage will not change ==== ===== Hashing iterations -Specifies the number of times {project_name} hashes passwords before storage or verification. The default value is 27,500. +Specifies the number of times {project_name} hashes passwords before storage or verification. The default value is -1, +which uses the default hashing iterations for the selected hashing algorithm as listed in the following table. + +[%autowidth,cols="m,>"] +|=== +|Hashing algorithm |Default hash iterations + +| argon2 +| 5 + +| pbkdf2-sha512 +| 210,000 -{project_name} hashes passwords to ensure that hostile actors with access to the password database cannot read passwords through reverse engineering. +| pbkdf2-sha256 +| 600,000 + +| pbkdf2 +| 1,300,000 + +|=== [NOTE] ==== -A high hashing iteration value can impact performance as it requires higher CPU power. +In most cases the hashing iterations should not be changed from the recommended default values. Lower values for +iterations provide insufficient security, while higher values result in higher CPU power requirements. ==== ===== Digits @@ -70,7 +118,7 @@ The password cannot be the same as the email address of the user. ===== Regular expression Password must match one or more defined Java regular expression patterns. -See https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html[Java's regular expression documentation] for the syntax of those expressions. +See https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html[Java's regular expression documentation] for the syntax of those expressions. ===== Expire password @@ -80,24 +128,33 @@ The number of days the password is valid. When the number of days has expired, t Password cannot be already used by the user. {project_name} stores a history of used passwords. The number of old passwords stored is configurable in {project_name}. +===== Not recently used (In Days) + +Password cannot be reused within the configured time period (in days). +If the new password was last set within this period, the user will be forced to provide a different one. + ===== Password blacklist Password must not be in a blacklist file. * Blacklist files are UTF-8 plain-text files with Unix line endings. Every line represents a blacklisted password. -* {project_name} compares passwords in a case-insensitive manner. All passwords in the blacklist must be lowercase. +* {project_name} compares passwords in a case-insensitive manner. * The value of the blacklist file must be the name of the blacklist file, for example, `100k_passwords.txt`. * Blacklist files resolve against `+${kc.home.dir}/data/password-blacklists/+` by default. Customize this path using: ** The `keycloak.password.blacklists.path` system property. -** The `blacklistsPath` property of the `passwordBlacklist` policy SPI configuration. To configure the blacklist folder using the CLI, use `--spi-password-policy-password-blacklist-blacklists-path=/path/to/blacklistsFolder`. +** The `blacklistsPath` property of the `passwordBlacklist` policy SPI configuration. To configure the blacklist folder using the CLI, use `+--spi-password-policy--password-blacklist--blacklists-path=/path/to/blacklistsFolder+`. .A note about False Positives The current implementation uses a BloomFilter for fast and memory efficient containment checks, such as whether a given password is contained in a blacklist, with the possibility for false positives. * By default a false positive probability of `0.01%` is used. -* To change the false positive probability by CLI configuration, use `--spi-password-policy-password-blacklist-false-positive-probability=0.00001`. +* To change the false positive probability by CLI configuration, use `+--spi-password-policy--password-blacklist--false-positive-probability=0.00001+`. +[[maximum-authentication-age]] ===== Maximum Authentication Age Specifies the maximum age of a user authentication in seconds with which the user can update a password without re-authentication. A value of `0` indicates that the user has to always re-authenticate with their current password before they can update the password. +See <> for some additional details about this policy. +NOTE: The Maximum Authentication Age is configurable also when configuring the required action *Update Password* in the *Required Actions* tab in the Admin Console. The better choice is to use the +required action for the configuration because the _Maximum Authentication Age_ password policy might be deprecated/removed in the future. diff --git a/docs/documentation/server_admin/topics/authentication/recovery-codes.adoc b/docs/documentation/server_admin/topics/authentication/recovery-codes.adoc index 639eaa5d6bd7..60a9cb50ea7c 100644 --- a/docs/documentation/server_admin/topics/authentication/recovery-codes.adoc +++ b/docs/documentation/server_admin/topics/authentication/recovery-codes.adoc @@ -1,21 +1,53 @@ [[_recovery-codes]] -=== Recovery Codes (RecoveryCodes) +=== Recovery Codes -You can configure Recovery codes for two-factor authentication by adding 'Recovery Authentication Code Form' as a two-factor authenticator to your authentication flow. For an example of configuring this authenticator, see xref:webauthn_{context}[WebAuthn]. +The Recovery Codes are a number of sequential one-time passwords (currently 12) auto-generated by {project_name}. The codes can be used as a 2nd Factor Authentication (2FA) by adding the `Recovery Authentication Code Form` authenticator to your authentication flow. When configured in the flow, {project_name} asks the user for the next generated code in order. When the current code is introduced by the user, it is removed and the next code will be required for the next login. -ifeval::[{project_product}==true] -:tech_feature_name: RecoveryCodes -:tech_feature_setting: -Dkeycloak.profile.feature.recovery_codes=enabled -:tech_feature_id: recovery-codes -include::../templates/techpreview.adoc[] -endif::[] +Due to its nature, the Recovery Codes work normally as a backup for another 2FA methods. They can complement the `OTP Form` or the `WebAuthn Authenticator` to give a backing way to log inside {project_name}, for example, if the software or hardware device used for the previous 2FA methods is broken or unavailable. -ifeval::[{project_community}==true] +==== Check Recovery Codes required action is enabled -[IMPORTANT] -==== -Please note that Recovery Codes support is in development. Use this feature experimentally. -==== +Check the Recovery Codes action is enabled in {project_name}: -endif::[] \ No newline at end of file +. Click *Authentication* in the menu. +. Click the *Required Actions* tab. +. Ensure the *Recovery Authentication Codes* switch *Enabled* is set to *On*. + +Toggle the *Default Action* switch to *On* if you want all the new users to register their Recovery Codes credentials in the first login. + +==== Configure the Recovery Codes required action + +From the *Required Actions* tab of the admin console, you have the option to configure the *Recovery Authentication Codes* required action. So far, there is a configuration option +*Warning Threshold* available. When user has smaller amount of remaining recovery codes on his account than the value configured here, account console will show warning to the user, which will +recommend him to setup new set of recovery codes. The warning displayed to the user may look similar to this: + +.Recovery Codes Account console warning +image:images/recovery-codes-account-console-warn.png[Recovery Codes Account console warning] + +==== Adding Recovery Codes to the browser flow + +The following procedure adds the `Recovery Authentication Code Form` as an alternative way of login in the default *Browser* flow. + +. Click *Authentication* in the realm menu. +. Click the *Browser* flow. +. Locate the execution *Recovery Authentication Code Form* inside the *Browser - Conditional 2FA* sub-flow. +. Change the _requirement_ from _Disabled_ to _Alternative_ for that execution. ++ +.Recovery Codes Browser flow +image:images/recovery-codes-browser-flow.png[Recovery Codes Browser flow] ++ +With this configuration, both 2FA authenticators (`OTP Form` and `Recovery Authentication Code Form`) are alternate ways to log into {project_name}. If the user has configured both credential types, the credential with the highest priority will be displayed by default, but the *Try Another Way* option will appear so that the user has the alternative methods to log in. + +You can see more examples of 2FA configurations in <>. + +==== Creating the Recovery Codes credential + +Once the Recovery Codes required action is enabled and the credential type is managed in the flow, users can request to create their own codes. The action is just another <> that can be used in {project_name} (directly called by the user by using the Account Console or assigned by an administrator by using the Admin Console). + +The required action, when executed, generates the list of codes and presents it to the user. The action offers to print, download, or copy the list of codes to help the user to store them is a safe place. In order to complete the setup, the checkbox *I have saved these codes somewhere safe* should be previously checked. + +.Recovery Authentication Codes setup page +image:images/recovery-codes-setup.png[Recovery Authentication Codes setup page] + +The Recovery Codes can be re-created at any moment. diff --git a/docs/documentation/server_admin/topics/authentication/webauthn.adoc b/docs/documentation/server_admin/topics/authentication/webauthn.adoc index babb1c87b7ae..e7d5ec8c92f3 100644 --- a/docs/documentation/server_admin/topics/authentication/webauthn.adoc +++ b/docs/documentation/server_admin/topics/authentication/webauthn.adoc @@ -9,76 +9,55 @@ WebAuthn's operations success depends on the user's WebAuthn supporting authenticator, browser, and platform. Make sure your authenticator, browser, and platform support the WebAuthn specification. ==== +[NOTE] +==== +WebAuthn's specification uses a `user.id` to map a public key credential to a specific user account in the Relying Party. This user ID handle is an opaque byte sequence with a maximum size of 64 bytes. {project_name} passes the internal database ID to the registration, which in common users is an UUID of 36 characters. But, if the user is from a external user federation provider, the internal {project_name} ID is an link:{developerguide_link}#storage-ids[storage ID] in the form `f::` that can exceed the 64 byte limitation. Please take this into account and use short IDs for the federation provider component and for the users coming from that provider when combining the Storage SPI and WebAuthn. +==== + ==== Setup The setup procedure of WebAuthn support for 2FA is the following: [[_webauthn-register]] -===== Enable WebAuthn authenticator registration +===== Check WebAuthn authenticator registration is enabled . Click *Authentication* in the menu. . Click the *Required Actions* tab. -. Toggle the *Webauthn Register* switch to *ON*. +. Check action *Webauthn Register* switch is set to *ON*. Toggle the *Default Action* switch to *ON* if you want all new users to be required to register their WebAuthn credentials. [[_webauthn-authenticator-setup]] -==== Adding WebAuthn authentication to a browser flow +==== Enable WebAuthn authentication in the default browser flow . Click *Authentication* in the menu. . Click the *Browser* flow. -. Select *Duplicate* from the "Action list" to make a copy of the built-in *Browser* flow. -. Enter "WebAuthn Browser" as the name of the copy. -. Click *Duplicate*. -. Click the name to go to the details -. Click the trash can icon 🗑️ of the "WebAuthn Browser Browser - Conditional OTP" and click *Delete*. - -If you require WebAuthn for all users: - -. Click *+* menu of the *WebAuthn Browser Forms*. -. Click *Add step*. -. Click *WebAuthn Authenticator*. -. Click *Add*. -. Select *Required* for the *WebAuthn Authenticator* authentication type to set its requirement to required. -+ -image:images/webauthn-browser-flow-required.png[Webauthn browser flow required] +. Locate the execution *WebAuthn Authenticator* inside the *Browser - Conditional 2FA* sub-flow. +. Change the _requirement_ from _Disabled_ to _Alternative_ for that execution. + -. Click the *Action* menu at the top of the screen. -. Select *Bind flow* from the drop-down list. -. Select *Browser* from the drop-down list. -. Click *Save*. +.WebAuthn browser flow conditional with OTP +image:images/webauthn-browser-flow-conditional-with-OTP.png[WebAuthn browser flow conditional with OTP] -[NOTE] -==== -If a user does not have WebAuthn credentials, the user must register WebAuthn credentials. -==== +With this configuration, the users can choose between using WebAuthn and OTP for the second factor. As the sub-flow is _conditional_, they are only asked to present a 2FA credential (OTP or WebAuthn) if they have already registered one of the respective credential types. If a user has configured both credential types, the credential with the highest priority will be displayed by default. However, the *Try Another Way* option will appear so that the user has the alternative methods to log in. -Users can log in with WebAuthn if they have a WebAuthn credential registered only. So instead of adding the *WebAuthn Authenticator* execution, you can: +If you want to substitute OTP for WebAuthn and maintain it as conditional: -.Procedure -. Click *+* menu of the *WebAuthn Browser Forms* row. -. Click *Add sub-flow*. -. Enter "Conditional 2FA" for the _name_ field. -. Select *Conditional* for the *Conditional 2FA* to set its requirement to conditional. -. On the *Conditional 2FA* row, click the plus sign + and select *Add condition*. -. Click *Add condition*. -. Select *Condition - User Configured*. -. Click *Add*. -. Select *Required* for the *Condition - User Configured* to set its requirement to required. -. Drag and drop *WebAuthn Authenticator* into the *Conditional 2FA* flow -. Select *Alternative* for the *WebAuthn Authenticator* to set its requirement to alternative. +. Change _requirement_ in *OTP Form* to _Disabled_. +. Change _requirement_ in *WebAuthn Authenticator* to _Alternative_. + +.Webauthn browser flow conditional image:images/webauthn-browser-flow-conditional.png[Webauthn browser flow conditional] -The user can choose between using WebAuthn and OTP for the second factor: +If you require WebAuthn for all users and enforce them to configure the credential if not configured: -.Procedure -. On the *Conditional 2FA* row, click the plus sign + and select *Add step*. -. Select *OTP Form* from the list. -. Click *Add*. -. Select *Alternative* for the *OTP Form* to set its requirement to alternative. +. Change _requirement_ in *Browser - Conditional 2FA* to _Required_. +. Change _requirement_ in *OTP Form* to _Disabled_. +. Change _requirement_ in *WebAuthn Authenticator* to _Required_. + -image:images/webauthn-browser-flow-conditional-with-OTP.png[WebAuthn browser flow conditional with OTP] +.Webauthn browser flow required +image:images/webauthn-browser-flow-required.png[Webauthn browser flow required] + +You can see more examples of 2FA configurations in <>. ==== Authenticate with WebAuthn authenticator @@ -119,7 +98,7 @@ The configurable items and their description are as follows: |The readable server name as a WebAuthn Relying Party. This item is mandatory and applies to the registration of the WebAuthn authenticator. The default setting is "keycloak". For more details, see https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity[WebAuthn Specification]. |Signature Algorithms -|The algorithms telling the WebAuthn authenticator which signature algorithms to use for the https://www.w3.org/TR/webauthn/#iface-pkcredential[Public Key Credential]. {project_name} uses the Public Key Credential to sign and verify https://www.w3.org/TR/webauthn/#authentication-assertion[Authentication Assertions]. If no algorithms exist, the default https://datatracker.ietf.org/doc/html/rfc8152#section-8.1[ES256] is adapted. ES256 is an optional configuration item applying to the registration of WebAuthn authenticators. For more details, see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters[WebAuthn Specification]. +|The algorithms telling the WebAuthn authenticator which signature algorithms to use for the https://www.w3.org/TR/webauthn/#iface-pkcredential[Public Key Credential]. {project_name} uses the Public Key Credential to sign and verify https://www.w3.org/TR/webauthn/#authentication-assertion[Authentication Assertions]. If no algorithms exist, the default https://datatracker.ietf.org/doc/html/rfc8152#section-8.1[ES256] and https://datatracker.ietf.org/doc/html/rfc7518#section-3.1[RS256] is adapted. ES256 and RS256 are an optional configuration item applying to the registration of WebAuthn authenticators. For more details, see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters[WebAuthn Specification]. |Relying Party ID |The ID of a WebAuthn Relying Party that determines the scope of https://www.w3.org/TR/webauthn/#public-key-credential[Public Key Credentials]. The ID must be the origin's effective domain. This ID is an optional configuration item applied to the registration of WebAuthn authenticators. If this entry is blank, {project_name} adapts the host part of {project_name}'s base URL. For more details, see https://www.w3.org/TR/webauthn/[WebAuthn Specification]. @@ -130,8 +109,8 @@ The configurable items and their description are as follows: |Authenticator Attachment |The acceptable attachment pattern of a WebAuthn authenticator for the WebAuthn Client. This pattern is an optional configuration item applying to the registration of the WebAuthn authenticator. For more details, see https://www.w3.org/TR/webauthn/#enumdef-authenticatorattachment[WebAuthn Specification]. -|Require Resident Key -|The option requiring that the WebAuthn authenticator generates the Public Key Credential as https://www.w3.org/TR/webauthn/[Client-side-resident Public Key Credential Source]. This option applies to the registration of the WebAuthn authenticator. If left blank, its behavior is the same as selecting "No". For more details, see https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-requireresidentkey[WebAuthn Specification]. +|Require Discoverable Credential +|The option requiring that the WebAuthn authenticator generates the Public Key Credential as https://www.w3.org/TR/webauthn-3/[Client-side discoverable Credential]. This option applies to the registration of the WebAuthn authenticator. If left blank, its behavior is the same as selecting "No". For more details, see https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-requireresidentkey[WebAuthn Specification]. |User Verification Requirement |The option requiring that the WebAuthn authenticator confirms the verification of a user. This is an optional configuration item applying to the registration of a WebAuthn authenticator and the authentication of a user by a WebAuthn authenticator. If no option exists, its behavior is the same as selecting "preferred". For more details, see https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-userverification[WebAuthn Specification for registering a WebAuthn authenticator] and https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-userverification[WebAuthn Specification for authenticating the user by a WebAuthn authenticator]. @@ -162,7 +141,7 @@ The appropriate method to register a WebAuthn authenticator depends on whether t ===== New user -If the *WebAuthn Register* required action is *Default Action* in a realm, new users must set up the WebAuthn security key after their first login. +If the *WebAuthn Register* required action is *Default Action* in a realm, new users must set up the Passkey after their first login. .Procedure @@ -186,12 +165,19 @@ If `WebAuthn Authenticator` is set up as required as shown in the first example, After successful registration, the user's browser asks the user to enter the text of their WebAuthn authenticator's label. +[[_webauthn_aia]] +==== Registering WebAuthn credentials using AIA + +WebAuthn credentials can also be registered for a user using <>. The actions *Webauthn Register* (`kc_action=webauthn-register`) and *Webauthn Register Passwordless* (`kc_action=webauthn-register-passwordless`) are available for the applications if enabled in the <>. + +Both required actions allow a parameter *skip_if_exists* that allows to skip the AIA execution if the user already has a credential of that type. The `kc_action_status` will be *success* if skipped. For example, adding the option to the common WebAuthn register action is just using the following query parameter `kc_action=webauthn-register:skip_if_exists`. + [[_webauthn_passwordless]] ==== Passwordless WebAuthn together with Two-Factor {project_name} uses WebAuthn for two-factor authentication, but you can use WebAuthn as the first-factor authentication. In this case, users with `passwordless` WebAuthn credentials can authenticate to {project_name} without a password. {project_name} can use WebAuthn as both the passwordless and two-factor authentication mechanism in the context of a realm and a single authentication flow. -An administrator typically requires that Security Keys registered by users for the WebAuthn passwordless authentication meet different requirements. For example, the security keys may require users to authenticate to the security key using a PIN, or the security key attests with a stronger certificate authority. +An administrator typically requires that Passkeys registered by users for the WebAuthn passwordless authentication meet different requirements. For example, the Passkeys may require users to authenticate to the Passkey using a PIN, or the Passkey attests with a stronger certificate authority. Because of this, {project_name} permits administrators to configure a separate `WebAuthn Passwordless Policy`. There is a required `Webauthn Register Passwordless` action of type and separate authenticator of type `WebAuthn Passwordless Authenticator`. @@ -201,7 +187,7 @@ Set up WebAuthn passwordless support as follows: . (if not already present) Register a new required action for WebAuthn passwordless support. Use the steps described in <<_webauthn-register, Enable WebAuthn Authenticator Registration>>. Register the `Webauthn Register Passwordless` action. -. Configure the policy. You can use the steps and configuration options described in <<_webauthn-policy, Managing Policy>>. Perform the configuration in the Admin Console in the tab *WebAuthn Passwordless Policy*. Typically the requirements for the security key will be stronger than for the two-factor policy. For example, you can set the *User Verification Requirement* to *Required* when you configure the passwordless policy. +. Configure the policy. You can use the steps and configuration options described in <<_webauthn-policy, Managing Policy>>. Perform the configuration in the Admin Console in the tab *WebAuthn Passwordless Policy*. Typically the requirements for the Passkey will be stronger than for the two-factor policy. For example, you can set the *User Verification Requirement* to *Required* when you configure the passwordless policy. . Configure the authentication flow. Use the *WebAuthn Browser* flow described in <<_webauthn-authenticator-setup, Adding WebAuthn Authentication to a Browser Flow>>. Configure the flow as follows: + @@ -225,7 +211,7 @@ You can now add *WebAuthn Register Passwordless* as the required action to a use {project_name} uses WebAuthn for two-factor authentication, but you can use WebAuthn as the first-factor authentication. In this case, users with `passwordless` WebAuthn credentials can authenticate to {project_name} without submitting a login or a password. {project_name} can use WebAuthn as both the loginless/passwordless and two-factor authentication mechanism in the context of a realm. -An administrator typically requires that Security Keys registered by users for the WebAuthn loginless authentication meet different requirements. Loginless authentication requires users to authenticate to the security key (for example by using a PIN code or a fingerprint) and that the cryptographic keys associated with the loginless credential are stored physically on the security key. Not all security keys meet that kind of requirements. Check with your security key vendor if your device supports 'user verification' and 'resident key'. See <<_webauthn-supported-keys, Supported Security Keys>>. +An administrator typically requires that Passkeys registered by users for the WebAuthn loginless authentication meet different requirements. Loginless authentication requires users to authenticate to the Passkey (for example by using a PIN code or a fingerprint) and that the cryptographic keys associated with the loginless credential are stored physically on the Passkey. Not all Passkeys meet that kind of requirement. Check with your Passkey vendor if your device supports 'user verification' and 'discoverable credential'. See <<_webauthn-supported-keys, Supported Passkeys>>. {project_name} permits administrators to configure the `WebAuthn Passwordless Policy` in a way that allows loginless authentication. Note that loginless authentication can only be configured with `WebAuthn Passwordless Policy` and with `WebAuthn Passwordless` credentials. WebAuthn loginless authentication and WebAuthn passwordless authentication can be configured on the same realm but will share the same policy `WebAuthn Passwordless Policy`. @@ -234,9 +220,9 @@ An administrator typically requires that Security Keys registered by users for t Set up WebAuthn Loginless support as follows: -. (if not already present) Register a new required action for WebAuthn passwordless support. Use the steps described in <<_webauthn-register, Enable WebAuthn Authenticator Registration>>. Register the `Webauthn Register Passwordless` action. +. (If not already done) Check the required action for *WebAuthn Register Passwordless* is enabled. Use the steps described in <<_webauthn-register, Enable WebAuthn Authenticator Registration>>, but using *WebAuthn Register Passwordless* instead of *WebAuthn Register*. -. Configure the `WebAuthn Passwordless Policy`. Perform the configuration in the Admin Console, `Authentication` section, in the tab `Policies` -> `WebAuthn Passwordless Policy`. You have to set *User Verification Requirement* to *required* and *Require Resident Key* to *Yes* when you configure the policy for loginless scenario. Note that since there isn't a dedicated Loginless policy it won't be possible to mix authentication scenarios with user verification=no/resident key=no and loginless scenarios (user verification=yes/resident key=yes). Storage capacity is usually very limited on security keys meaning that you won't be able to store many resident keys on your security key. +. Configure the `WebAuthn Passwordless Policy`. Perform the configuration in the Admin Console, `Authentication` section, in the tab `Policies` -> `WebAuthn Passwordless Policy`. You have to set *User Verification Requirement* to *required* and *Require Discoverable Credential* to *Yes* when you configure the policy for loginless scenario. Storage capacity is usually very limited on Passkeys meaning that you won't be able to store many discoverable credentials on your Passkey. . Configure the authentication flow. Create a new authentication flow, add the "WebAuthn Passwordless" execution and set the Requirement setting of the execution to *Required* @@ -245,26 +231,26 @@ The final configuration of the flow looks similar to this: .LoginLess flow image:images/webauthn-loginless-flow.png[LoginLess flow] -You can now add the required action `WebAuthn Register Passwordless` to a user, already known to {project_name}, to test this. The user with the required action configured will have to authenticate (with a username/password for example) and will then be prompted to register a security key to be used for loginless authentication. +You can now add the required action `WebAuthn Register Passwordless` to a user, already known to {project_name}, to test this. The user with the required action configured will have to authenticate (with a username/password for example) and will then be prompted to register a Passkey to be used for loginless authentication. ===== Vendor specific remarks ====== Compatibility check list -Loginless authentication with {project_name} requires the security key to meet the following features +Loginless authentication with {project_name} requires the Passkey to meet the following features ** FIDO2 compliance: not to be confused with FIDO/U2F -** User verification: the ability for the security key to authenticate the user (prevents someone finding your security key to be able to authenticate loginless and passwordless) -** Resident key: the ability for the security key to store the login and the cryptographic keys associated with the client application +** User verification: the ability for the Passkey to authenticate the user (prevents someone finding your Passkey to be able to authenticate loginless and passwordless) +** Discoverable Credential: the ability for the Passkey to store the login and the cryptographic keys associated with the client application ====== Windows Hello -To use Windows Hello based credentials to authenticate against {project_name}, configure the *Signature Algorithms* setting of the `WebAuthn Passwordless Policy` to include the *RS256* value. Note that some browsers don't allow access to platform security key (like Windows Hello) inside private windows. +To use Windows Hello based credentials to authenticate against {project_name}, configure the *Signature Algorithms* setting of the `WebAuthn Passwordless Policy` to include the *RS256* value. Note that some browsers don't allow access to platform Passkey (like Windows Hello) inside private windows. [[_webauthn-supported-keys]] -====== Supported security keys +====== Supported Passkeys -The following security keys have been successfully tested for loginless authentication with {project_name}: +The following Passkeys have been successfully tested for loginless authentication with {project_name}: * Windows Hello (Windows 10 21H1/21H2) * Yubico Yubikey 5 NFC diff --git a/docs/documentation/server_admin/topics/authentication/x509.adoc b/docs/documentation/server_admin/topics/authentication/x509.adoc index 387b81799e6d..f2a4ae0da14b 100644 --- a/docs/documentation/server_admin/topics/authentication/x509.adoc +++ b/docs/documentation/server_admin/topics/authentication/x509.adoc @@ -77,7 +77,7 @@ The certificate identity mapping can map the extracted user identity to an exist . Click *Authentication* in the menu. . Click the *Browser* flow. -. From the *Action* list, select *Duplicate*. +. From the *Action* list, select *Duplicate*. . Enter a name for the copy. . Click *Duplicate*. . Click *Add step*. @@ -101,6 +101,7 @@ image:images/x509-browser-flow.png[X509 Browser Flow] .X509 browser flow bindings image:images/x509-browser-flow-bindings.png[X509 Browser Flow Bindings] +[[_x509-config]] ==== Configuring X.509 client certificate authentication .X509 configuration @@ -133,6 +134,9 @@ Use CDP to check the certificate revocation status. Most PKI authorities include *CRL file path*:: The path to a file containing a CRL list. The value must be a path to a valid file if the *CRL Checking Enabled* option is enabled. +*CRL abort if non updated*:: +A CRL conforming to link:https://datatracker.ietf.org/doc/html/rfc5280#section-5.1.2.5[RFC5280] contains a next update field that indicates the date by which the next CRL will be issued. When that time is passed, the CRL is considered outdated and it should be refreshed. If this option is `true`, the authentication will fail if the CRL is outdated (recommended). If the option is set to `false`, the outdated CRL is still used to validate the user certificates. + *OCSP Checking Enabled*:: Checks the certificate revocation status by using Online Certificate Status Protocol. @@ -186,3 +190,37 @@ image:images/x509-directgrant-execution.png[X509 Direct Grant Execution] + .X509 direct grant flow bindings image:images/x509-directgrant-flow-bindings.png[X509 Direct Grant Flow Bindings] + +===== Example using CURL + +The following example shows how to obtain an access token for a user in the realm `test` with the direct grant flow. The example is using +*OAuth2 Resource Owner Password Credentials Grant* in the link:{securing_apps_link}[securing apps] section and the confidential client `resource-owner`: + +[source,bash,subs="attributes+"] +---- +curl \ + -d "client_id=resource-owner" \ + -d "client_secret=40cc097b-2a57-4c17-b36a-8fdf3fc2d578" \ + -d "grant_type=password" \ + --cacert /tmp/truststore.pem \ + --cert /tmp/keystore.pem:kssecret \ + "https://localhost:8543/realms/test/protocol/openid-connect/token" +---- + +The file `/tmp/truststore.pem` points to the file with the truststore containing the certificate of the {project_name} server. The file `/tmp/keystore.pem` contains +the private key and certificates corresponding to the {project_name} user, which would be successfully authenticated by this request. It is dependent on the configuration of the authenticator on how +exactly is the content from the certificate mapped to the {project_name} user as described in <<_x509-config, the configuration section>>. The `kssecret` might be the password of this keystore file. + +According to your environment, it might be needed to use more options to CURL commands like for instance: + +* Option `--insecure` if you are using self-signed certificates +* Option `--capath` to include the whole directory containing the certificate authority path +* Options `--cert-type` or `--key-type` in case you want to use different files than `PEM` + +Please consult the documentation of the `curl` tool for the details if needed. If you are using other tools than `curl`, +consult the documentation of your tool. However, the setup would be similar. A need exists to include keystore and truststore as well as client credentials in case you are using a confidential +client. + +NOTE: If it is possible, it is preferred to use <<_service_accounts, Service accounts>> together with the MTLS client authentication (client authenticator `X509 Certificate`) rather than using +the Direct grant with X.509 authentication as direct grant may require sharing of the user certificate with client applications. When using service account, the tokens are obtained on behalf +of the client itself, which in general is better and more secure practice. diff --git a/docs/documentation/server_admin/topics/clients/assembly-client-oidc.adoc b/docs/documentation/server_admin/topics/clients/assembly-client-oidc.adoc index 223ef48ac583..de58e9920853 100644 --- a/docs/documentation/server_admin/topics/clients/assembly-client-oidc.adoc +++ b/docs/documentation/server_admin/topics/clients/assembly-client-oidc.adoc @@ -16,5 +16,6 @@ include::oidc/proc-secret-rotation.adoc[leveloffset=+1] include::oidc/proc-using-a-service-account.adoc[leveloffset=+1] +include::oidc/con-token-role-mappings.adoc[leveloffset=+1] include::oidc/con-audience.adoc[leveloffset=+1] diff --git a/docs/documentation/server_admin/topics/clients/client-policies.adoc b/docs/documentation/server_admin/topics/clients/client-policies.adoc index 21e2ae4be14b..f5274b854056 100644 --- a/docs/documentation/server_admin/topics/clients/client-policies.adoc +++ b/docs/documentation/server_admin/topics/clients/client-policies.adoc @@ -6,7 +6,7 @@ To make it easy to secure client applications, it is beneficial to realize the f * Setting policies on what configuration a client can have * Validation of client configurations -* Conformance to a required security standards and profiles such as Financial-grade API (FAPI) +* Conformance to a required security standards and profiles such as Financial-grade API (FAPI) and OAuth 2.1 To realize these points in a unified way, _Client Policies_ concept is introduced. @@ -16,7 +16,7 @@ Client Policies realize the following points mentioned as follows. Setting policies on what configuration a client can have:: Configuration settings on the client can be enforced by client policies during client creation/update, but also during OpenID Connect requests to {project_name} server, which are related to particular client. - {project_name} supports similar thing also through the Client Registration Policies described in the link:{adapterguide_link}#_client_registration_policies[{adapterguide_name}]. + {project_name} supports similar thing also through the *Client Registration Policies* described in the *Client registration service* in the link:{securing_apps_link}[Securing applications and Services guide]. However, Client Registration Policies can only cover OIDC Dynamic Client Registration. Client Policies cover not only what Client Registration Policies can do, but other client registration and configuration ways. The current plans are for Client Registration to be replaced by Client Policies. @@ -28,16 +28,17 @@ Validation of client configurations:: Client Policies can do these validation of client configurations mentioned just above and they can also be used to autoconfigure some client configuration switches to meet the advanced security requirements. In the future, individual client configuration settings may be replaced by Client Policies directly performing required validations. -Conformance to a required security standards and profiles such as FAPI:: - The _Global client profiles_ are client profiles pre-configured in {project_name} by default. They are pre-configured to be compliant with standard security profiles like link:{adapterguide_link}#_fapi-support[FAPI], +Conformance to a required security standards and profiles such as FAPI and OAuth 2.1:: + The _Global client profiles_ are client profiles pre-configured in {project_name} by default. They are pre-configured to be compliant with standard security profiles like *FAPI* and *OAuth 2.1* in the link:{securing_apps_link}[securing apps] section, which makes it easy for the administrator to secure their client application to be compliant with the particular security profile. At this moment, {project_name} has global - profiles for the support of FAPI specifications. The administrator will just need to configure the client policies to specify which clients should - be compliant with the FAPI. The administrator can configure client profiles and client policies, so that {project_name} clients can be easily made compliant with various other + profiles for the support of FAPI and OAuth 2.1 specifications. The administrator will just need to configure the client policies to specify which clients should + be compliant with the FAPI and OAuth 2.1. The administrator can configure client profiles and client policies, so that {project_name} clients can be easily made compliant with various other security profiles like SPA, Native App, Open Banking and so on. == Protocol -The client policy concept is independent of any specific protocol. However, {project_name} currently supports it only just for the link:{adapterguide_link}#_oidc[OpenID Connect (OIDC) protocol]. +The client policy concept is independent of any specific protocol. {project_name} currently supports especially client profiles for the link:{adminguide_link}#con-oidc_server_administration_guide[OpenID Connect (OIDC) protocol], but there is +also a client profile available for the link:{adminguide_link}#_saml[SAML protocol]. == Architecture @@ -61,7 +62,7 @@ The way of creating/updating a client:: So for example when creating a client, a condition can be configured to evaluate to true when this client is created by OIDC Dynamic Client Registration without initial access token (Anonymous Dynamic Client Registration). So this condition can be used for example to ensure that all clients registered through OIDC Dynamic Client Registration -are FAPI compliant. +are FAPI or OAuth 2.1 compliant. Author of a client (Checked by presence to the particular role or group):: On OpenID Connect dynamic client registration, an author of a client is the end user who was authenticated to get an access token for generating a new client, not Service @@ -76,14 +77,30 @@ Client Scope:: OIDC authorization requests with scope `fapi-example-scope` need to be FAPI compliant. Client Role:: - Applies for clients with the client role of the specified name + Applies for clients with the client role of the specified name. Typically you can create a client role of specified name to requested clients and use it as a "marker role" to make + sure that specified client policy will be applied for requested clients. + +NOTE: A use-case often exists for requiring the application of a particular client policy for the specified clients such as `my-client-1` and `my-client-2`. The best way to achieve this result +is to use a *Client Role* condition in your policy and then a create client role of specified name to requested clients. This client role can be used as a "marker role" used solely +for marking that particular client policy for particular clients. Client Domain Name, Host or IP Address:: Applied for specific domain names of client. Or for the cases when the administrator registers/updates client from particular Host or IP Address. +Client Attribute:: + Applies to clients with the client attribute of the specified name and value. If you specify multiple client attributes, they will be evaluated using AND conditions. + If you want to evaluate using OR conditions, set this condition multiple times. + Any Client:: This condition always evaluates to true. It can be used for example to ensure that all clients in the particular realm are FAPI compliant. +ACR Condition:: + Applied when an ACR value requested in the authentication request matches the value configured in the condition. For example, it can be used to select an authentication flow based on the requested ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>> and the https://openid.net/specs/openid-connect-core-1_0.html#acrSemantics[official OIDC specification]. + +Grant Type:: + Evaluates to true when a specific grant type is used. For example, it can be used in combination with Client Scope to block a token exchange request when a specific client scope is requested. + + === Executor An executor specifies what action is executed on a client to which a policy is adopted. The executor executes one or several specified actions. For example, @@ -105,12 +122,12 @@ on the OIDC authorization request). Events are: * Sending a token revocation request * Sending a token introspection request * Sending a userinfo request -* Sending a logout request with a refresh token +* Sending a logout request with a refresh token (note that logout with refresh token is proprietary {project_name} functionality unsupported by any specification. It is rather recommended to rely on the <<_oidc-logout,official OIDC logout>>). On each event, an executor can work in multiple phases. For example, on creating/updating a client, the executor can modify the client configuration by autoconfigure specific client settings. After that, the executor validates this configuration in validation phase. -One of several purposes for this executor is to realize the security requirements of client conformance profiles like FAPI. To do so, the following executors are needed: +One of several purposes for this executor is to realize the security requirements of client conformance profiles like FAPI and OAuth 2.1. To do so, the following executors are needed: * Enforce secure <<_client-credentials,Client Authentication method>> is used for the client * Enforce <<_mtls-client-certificate-bound-tokens,Holder-of-key tokens>> are used @@ -127,13 +144,20 @@ One of several purposes for this executor is to realize the security requirement * Enforce checking if a client is the one to which an intent was issued in a use case where an intent is issued before starting an authorization code flow to get an access token like UK OpenBanking * Enforce prohibiting implicit and hybrid flow * Enforce checking if a PAR request includes necessary parameters included by an authorization request +* Enforce <<_dpop-bound-tokens,DPoP-binding tokens>> is used (available when `dpop` feature is enabled) +* Enforce <<_using_lightweight_access_token, using lightweight access token>> +* Enforce that <<_refresh_token_rotation,refresh token rotation>> is skipped and there is no refresh token returned from the refresh token response +* Enforce a valid redirect URI that the OAuth 2.1 specification requires +* Enforce SAML Redirect binding cannot be used or SAML requests and assertions are signed + +Another available executor is the `auth-flow-enforce`, which can be used to enforce an authentication flow during an authentication request. For instance, it can be used to select a flow based on certain conditions, such as a specific scope or an ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>>. [[_client_policy_profile]] === Profile -A profile consists of several executors, which can realize a security profile like FAPI. Profile can be configured by the Admin REST API (Admin Console) together with its executors. -Three _global profiles_ exist and they are configured in {project_name} by default with pre-configured executors compliant with the FAPI 1 Baseline, FAPI 1 Advanced, FAPI CIBA and FAPI 2 specifications. -More details exist in the FAPI section of the link:{adapterguide_link}#_fapi-support[{adapterguide_name}]. +A profile consists of several executors, which can realize a security profile like FAPI and OAuth 2.1. Profile can be configured by the Admin REST API (Admin Console) together with its executors. +Three _global profiles_ exist and they are configured in {project_name} by default with pre-configured executors compliant with the FAPI 1 Baseline, FAPI 1 Advanced, FAPI CIBA, FAPI 2 and OAuth 2.1 specifications. +More details exist in the *FAPI* and *OAuth 2.1* in the link:{securing_apps_link}[securing apps] section. [[_client_policy_policy]] === Policy @@ -154,7 +178,7 @@ There is JSON Editor available in the Admin Console, which simplifies the creati == Backward Compatibility -Client Policies can replace Client Registration Policies described in the link:{adapterguide_link}#_client_registration_policies[{adapterguide_name}]. +Client Policies can replace Client Registration Policies described in the *Client registration service* from link:{securing_apps_link}[{securing_apps_name}]. However, Client Registration Policies also still co-exist. This means that for example during a Dynamic Client Registration request to create/update a client, both client policies and client registration policies are applied. diff --git a/docs/documentation/server_admin/topics/clients/con-client-scopes.adoc b/docs/documentation/server_admin/topics/clients/con-client-scopes.adoc index ca25d92445cb..f10082544ccf 100644 --- a/docs/documentation/server_admin/topics/clients/con-client-scopes.adoc +++ b/docs/documentation/server_admin/topics/clients/con-client-scopes.adoc @@ -10,7 +10,7 @@ Client scopes also support the OAuth 2 *scope* parameter. Client applications us include::proc-creating-client-scopes.adoc[] - +[[_client_scopes_protocol]] == Protocol When you create a client scope, choose the *Protocol*. Clients linked in the same scope must have the same protocol. @@ -21,7 +21,7 @@ Each realm has a set of pre-defined built-in client scopes in the menu. ** *roles* + This scope is not defined in the OpenID Connect specification and is not added automatically to the *scope* claim in the access token. This scope has mappers, which are used to add the roles of the user to the access token and -add audiences for clients that have at least one client role. These mappers are described in more detail in the <<_audience_resolve, Audience section>>. +add audiences for clients that have at least one client role. These mappers are described in more detail in the <<_oidc_token_role_mappings, Token Role mappings section>> and <<_audience_resolve, Audience section>>. + ** *web-origins* + @@ -29,7 +29,7 @@ This scope is also not defined in the OpenID Connect specification and not added + ** *microprofile-jwt* + -This scope handles claims defined in the https://wiki.eclipse.org/MicroProfile/JWT_Auth[MicroProfile/JWT Auth Specification]. This scope defines a user property mapper for the *upn* claim and a realm role mapper for the *groups* claim. These mappers can be changed so different properties can be used to create the MicroProfile/JWT specific claims. +This scope handles claims defined in the https://github.com/eclipse/microprofile/wiki/JWT_Auth[MicroProfile/JWT Auth Specification]. This scope defines a user property mapper for the *upn* claim and a realm role mapper for the *groups* claim. These mappers can be changed so different properties can be used to create the MicroProfile/JWT specific claims. + ** *offline_access* + @@ -60,9 +60,20 @@ Display On Consent Screen:: Consent Screen Text:: The text displayed on the consent screen when this client scope is added to a client when consent required defaults to the name of client scope. The value for this text can be customised by specifying a substitution variable with *${var-name}* strings. The customised value is configured within the property files in your theme. See the link:{developerguide_link}[{developerguide_name}] for more information on customisation. +== Include in token scope + +There is the *Include in token scope* switch on the client scope. If on, the name of this client scope will be added to the access token property scope, and to the Token Response and Token Introspection Endpoint +response claim `scope`. If off, this client scope will be omitted from the token and from the Token Introspection Endpoint response. As mentioned above, some built-in client scopes have this switch disabled, which means +that they are not included in the `scope` claim even if they are applied for the particular request. + [[_client_scopes_linking]] == Link client scope with the client -Linking between a client scope and a client is configured in the *Client Scopes* tab of the client. Two ways of linking between client scope and client are available. +Linking between a client scope and a client is configured in the *Client Scopes* tab of the client. Here is how it looks for the client application `myclient`: + +.Client scopes linking to client +image:images/client-scopes-default.png[] + +There are two ways of linking between the client scope and the client. Default Client Scopes:: This setting is applicable to the OpenID Connect and SAML clients. Default client scopes are applied when issuing OpenID Connect tokens or SAML assertions for a client. The client will inherit Protocol Mappers and Role Scope Mappings that are defined on the client scope. For the OpenID Connect Protocol, the Mappers and Role Scope Mappings are always applied, regardless of the value used for the scope parameter in the OpenID Connect authorization request. @@ -80,10 +91,27 @@ scope=openid phone The scope parameter contains the string, with the scope values divided by spaces. The value *openid* is the meta-value used for all OpenID Connect requests. The token will contain mappers and role scope mappings from the default client scopes *profile* and *email* as well as *phone*, an optional client scope requested by the scope parameter. +[[_client_scopes_dedicated]] +=== Dedicated client scope + +There is a special client scope, which is linked to every client. It is a dedicated client scope, which is always shown as the first client scope when you click on the tab *Client scopes* of the particular client. +For example, for client `myclient`, the client scope is shown as `myclient-dedicated`. This client scope represents the protocol mappers and role scope mappings, which are linked directly to the client itself. + +It is not possible to unlink the dedicated client scope from a client. Also, it is not possible to link this dedicated client scope to a different client. In other words, the dedicated client scope is useful +just for protocol mappers and role scope mappings, which are specific to a single client. In case you want to share the same protocol mapper configuration among multiple clients, it is usually useful to create +a client scope in the realm tab *Client scopes* and then link this shared client scope to every client that should apply this shared configuration. + +In the tab *Scope* of the dedicated client scope, you can define role scope mappings applicable to this client. You can also see the switch *Full scope allowed* in this tab. +The details about this switch are described in <<_role_scope_mappings, this section>> and in <<_oidc_token_role_mappings,this section>>. + +NOTE: In the admin REST API and in the internal {project_name} storage, the dedicated client scope does not exist as its protocol mappers and role scope mappings are internally linked to the client itself. The. +dedicated client scope is in fact just an abstraction for the admin console UI. + [[_client_scopes_evaluate]] == Evaluating Client Scopes include::proc-evaluating-client-scopes.adoc[] +[[client-scopes-permissions]] == Client scopes permissions When issuing tokens to a user, the client scope applies only if the user is permitted to use it. @@ -96,16 +124,14 @@ If a user is not permitted to use the client scope, no protocol mappers or role include::proc-updating-default-scopes.adoc[] == Scopes explained + +The term _scope_ has multiple meanings within {project_name} and across the OAuth/OIDC specifications. Below is a clarification of the different _scopes_ used in {project_name}: + Client scope:: Client scopes are entities in {project_name} that are configured at the realm level and can be linked to clients. Client scopes are referenced by their name when a request is sent to the {project_name} authorization endpoint with a corresponding value of the *scope* parameter. See the <<_client_scopes_linking, client scopes linking>> section for more details. Role scope mapping:: This is available under the *Scope* tab of a client or client scope. Use *Role scope mapping* to limit the roles that can be used in the access tokens. See the <<_role_scope_mappings, Role Scope Mappings section>> for more details. -ifeval::[{project_community}==true] - Authorization scopes:: The *Authorization Scope* covers the actions that can be performed in the application. See the link:{authorizationguide_link}[Authorization Services Guide] for more details. - -endif::[] - diff --git a/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc b/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc index a5ae151f0707..d091afd98e84 100644 --- a/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc +++ b/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc @@ -4,7 +4,7 @@ = OIDC token and SAML assertion mappings [role="_abstract"] -Applications receiving ID tokens, access tokens, or SAML assertions may require different roles and user metadata. +Applications receiving ID tokens, access tokens, or SAML assertions may require different roles and user metadata. You can use {project_name} to: @@ -17,7 +17,7 @@ You perform these actions in the *Mappers* tab in the Admin Console. .Mappers tab image:images/mappers-oidc.png[] -New clients do not have built-in mappers but they can inherit some mappers from client scopes. See the <<_client_scopes, client scopes section>> for more details. +New clients do not have built-in mappers, but they can inherit some mappers from client scopes. See the <<_client_scopes, client scopes section>> for more details. Protocol mappers map items (such as an email address, for example) to a specific claim in the identity and access token. The function of a mapper should be self-explanatory from its name. You add pre-configured mappers by clicking *Add Builtin*. @@ -33,6 +33,7 @@ You can use most OIDC mappers to control where the claim gets placed. You opt to include::proc-creating-mappers.adoc[] +[[_protocol-mappers_priority]] == Priority order Mapper implementations have _priority order_. _Priority order_ is not the configuration property of the mapper. It is the property of the concrete implementation of the mapper. @@ -66,3 +67,28 @@ Service account sessions provide the following details: Use the *Script Mapper* to map claims to tokens by running user-defined JavaScript code. For more details about deploying scripts to the server, see link:{developerguide_jsproviders_link}[{developerguide_jsproviders_name}]. When scripts deploy, you should be able to select the deployed scripts from the list of available mappers. + +== Pairwise subject identifier mapper + +Subject claim _sub_ is mapped by default by *Subject (sub)* protocol mapper in the default client scope *basic*. + +To use a pairwise subject identifier by using a protocol mapper such as *Pairwise subject identifier*, you can remove the *Subject (sub)* protocol mapper from the *basic* client scope. +However it is not strictly needed as the *Subject (sub)* protocol mapper is executed before the *Pairwise subject identifier* mapper and hence the pairwise value will override the value added +by the Subject mapper. This is due to the <<_protocol-mappers_priority, priority>> of the Subject mapper. So the only advantage of removing the built-in *Subject (sub)* mapper might be to +save a little bit of performance by avoiding the use of the protocol mapper, which may not have any effect. + +[[_using_lightweight_access_token]] +== Using lightweight access token +The access token in {project_name} contains sensitive information, including Personal Identifiable Information (PII). +Therefore, if the resource server does not want to disclose this type of information to third party entities such as clients, {project_name} supports lightweight access tokens that remove PII from access tokens. +Further, when the resource server acquires the PII removed from the access token, it can acquire the PII by sending the access token to {project_name}'s token introspection endpoint. + +Information that cannot be removed from a lightweight access token:: + Protocol mappers can controls which information is put onto an access token and the lightweight access token use the protocol mappers. Therefore, the following information cannot be removed from the lightweight access. + + `exp`, `iat`, `jti`, `iss`, `typ`, `azp`, `sid`, `scope`, `cnf` + +Using a lightweight access token in {project_name}:: + By applying `use-lightweight-access-token` executor of <<_client_policies, client policies>> to a client, the client can receive a lightweight access token instead of an access token. The lightweight access token contains a claim controlled by a protocol mapper where its setting `Add to lightweight access token`(default OFF) is turned ON. Also, by turning ON its setting `Add to token introspection` of the protocol mapper, the client can obtain the claim by sending the access token to {project_name}'s token introspection endpoint. + +Introspection endpoint:: + In some cases, it might be useful to trigger the token introspection endpoint with the HTTP header `Accept: application/jwt` instead of `Accept: application/json`, which can be useful especially for lightweight access tokens. See the details of *Token Introspection endpoint* in the link:{securing_apps_link}[securing apps] section. diff --git a/docs/documentation/server_admin/topics/clients/oidc/con-advanced-settings.adoc b/docs/documentation/server_admin/topics/clients/oidc/con-advanced-settings.adoc index b70fafcee179..fd3892c9511e 100644 --- a/docs/documentation/server_admin/topics/clients/oidc/con-advanced-settings.adoc +++ b/docs/documentation/server_admin/topics/clients/oidc/con-advanced-settings.adoc @@ -4,7 +4,7 @@ After completing the fields on the *Settings* tab, you can use the other tabs to perform advanced configuration. ifeval::[{project_community}==true] -For example, you can use the *Permissions* and *Roles* tabs to configure fine-grained authentication for administrators. See <<_fine_grain_permissions, Fine grain admin permissions>>. Also, see the remaining sections in this chapter for other capabilities. +For example, you can use the *Roles* or *Client scopes* tabs to configure client roles defined for the client or manage client scopes for the client. Also, see the remaining sections in this chapter for other capabilities. endif::[] == Advanced tab @@ -64,7 +64,7 @@ The procedure to select the algorithm is: . Open *Fine Grain OpenID Connect Configuration*. . Select the algorithm from *ID Token Encryption Content Encryption Algorithm* pulldown menu. -== Open ID Connect Compatibility Modes +== OpenID Connect Compatibility Modes This section exists for backward compatibility. Click the question mark icons for details on each field. @@ -94,7 +94,7 @@ See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-08#section-3[Mut [NOTE] ==== -Currently, {project_name} client adapters do not support holder-of-key token verification. {project_name} adapters treat access and refresh tokens as bearer tokens. +{project_name} client adapters do not support holder-of-key token verification. {project_name} adapters treat access and refresh tokens as bearer tokens. ==== [[_dpop-bound-tokens]] @@ -104,7 +104,7 @@ DPoP binds an access token and a refresh token together with the public part of This type of token is a holder-of-key token. Unlike bearer tokens, the recipient of a holder-of-key token can verify if the sender of the token is legitimate. -If the client switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is on, the workflow is: +If the client switch `Require Demonstrating Proof of Possession (DPoP) header in token requests` is on, the workflow is: . A token request is sent to the token endpoint in an authorization code flow or hybrid flow. . {project_name} requests a DPoP proof. @@ -113,7 +113,7 @@ If the client switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is on, the wor If verification fails, {project_name} rejects the token. -If the switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is off, the client can still send `DPoP` proof in the token request. In that case, {project_name} will verify DPoP proof +If the switch `Require Demonstrating Proof of Possession (DPoP) header in token requests` is off, the client can still send `DPoP` proof in the token request. In that case, {project_name} will verify DPoP proof and will add the thumbprint to the token. But if the switch is off, DPoP binding is not enforced by the {project_name} server for this client. It is recommended to have this switch on if you want to make sure that particular client always uses DPoP binding. @@ -127,11 +127,10 @@ See https://datatracker.ietf.org/doc/html/rfc9449[OAuth 2.0 Demonstrating Proof [NOTE] ==== -Currently, {project_name} client adapters do not support DPoP holder-of-key token verification. {project_name} adapters treat access and refresh tokens as bearer tokens. +{project_name} client adapters do not support DPoP holder-of-key token verification. {project_name} adapters treat access and refresh tokens as bearer tokens. ==== :tech_feature_name: DPoP -:tech_feature_setting: -Dkeycloak.profile.feature.dpop=enabled :tech_feature_id: dpop include::../../templates/techpreview.adoc[] @@ -187,8 +186,11 @@ a `claims` parameter that has an `acr` claim attached. See https://openid.net/sp WARNING: Note that default ACR values are used as the default level, however it cannot be reliably used to enforce login with the particular level. For example, assume that you configure the `Default ACR Values` to level 2. Then by default, users will be required to authenticate with level 2. -However when the user explicitly attaches the parameter into login request such as `acr_values=1`, then the level 1 will be used. As a result, if the client +However, when the user explicitly attaches the parameter into login request such as `acr_values=1`, then the level 1 will be used. As a result, if the client really requires level 2, the client is encouraged to check the presence of the `acr` claim inside ID Token and double-check that it contains the requested level 2. +To actually enforce the usage of a certain ACR on the {project_name} side, use the `Minimum ACR Value` setting. +This allows administrators to enforce ACRs even on applications that are not able to validate the requested `acr` claim inside the token. + image:images/client-oidc-map-acr-to-loa.png[alt="ACR to LoA mapping"] diff --git a/docs/documentation/server_admin/topics/clients/oidc/con-audience.adoc b/docs/documentation/server_admin/topics/clients/oidc/con-audience.adoc index 0980b65e7ca7..40799ee6dfbf 100644 --- a/docs/documentation/server_admin/topics/clients/oidc/con-audience.adoc +++ b/docs/documentation/server_admin/topics/clients/oidc/con-audience.adoc @@ -2,78 +2,140 @@ = Audience support [role="_abstract"] Typically, the environment where {project_name} is deployed consists of a set of _confidential_ or _public_ client applications that use {project_name} for authentication. +These clients are _frontend clients_, which may directly redirect user to {project_name} to request browser authentication. The particular client would then receive set of tokens after successful authentication. -_Services_ (_Resource Servers_ in the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-08#section-4.2[OAuth 2 specification]) are also available that serve requests from client applications and provide resources to these applications. These services require an _Access token_ (Bearer token) to be sent to them to authenticate a request. This token is obtained by the frontend application upon login to {project_name}. +_Services_ (_Resource Servers_ in the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-08#section-4.2[OAuth 2 specification]) are also available that serve requests from client applications and provide resources to these applications. +These services require an _Access token_ (Bearer token) to be sent to them from _frontend application_ or from other service to authenticate a request. -In the environment where trust among services is low, you may encounter this scenario: +The care must be taken to make sure that access tokens have limited privileges and the particular access token cannot be misused by the service to access other third-party services. +In the environment where trust among services is low, you may encounter this example scenario: -. A frontend client application requires authentication against {project_name}. +. A frontend client application `frontend-client` requires authentication against {project_name}. -. {project_name} authenticates a user. +. {project_name} authenticates a user. -. {project_name} issues a token to the application. +. {project_name} issues a token to the application `frontend-client`. -. The application uses the token to invoke an untrusted service. +. The `frontend-client` application uses the token to invoke a service `service1`. -. The untrusted service returns the response to the application. However, it keeps the applications token. +. The `service1` service returns the response to the application. But assume that this service will try to misuse the token and keep it for the further use. -. The untrusted service then invokes a trusted service using the applications token. This results in broken security as the untrusted service misuses the token to access other services on behalf of the client application. +. The `service1` then invokes another service `service2` using the applications token, which was previously sent to it. The `service2` does not check that token was not supposed to be +used to invoke it and it will serve the request and return successful response. This results in broken security as the `service1` misused the token to access other services on behalf of the client application `frontend-client`. -This scenario is unlikely in environments with a high level of trust between services but not in environments where trust is low. In some environments, this workflow may be correct as the untrusted service may have to retrieve data from a trusted service to return data to the original client application. +This scenario is unlikely in environments with a high level of trust between services but not in environments where trust is low. -An unlimited audience is useful when a high level of trust exists between services. Otherwise, the audience should be limited. You can limit the audience and, at the same time, allow untrusted services to retrieve data from trusted services. In this case, ensure that the untrusted service and the trusted service are added as audiences to the token. +To prevent any misuse of the access token, the access token can contain the claim `aud`, which represents the audience. The claim `aud` should typically represent client ids of all services where the token +is supposed to be used. In the environments with low trust among services, it is recommended to: -To prevent any misuse of the access token, limit the audience on the token and configure your services to verify the audience on the token. The flow will change as follows: +* Limit the audience on the token to make sure that access tokens contain just limited amount of audiences. -. A frontend application authenticates against {project_name}. +* Configure your services to verify the audience on the token. -. {project_name} authenticates a user. +To prevent `service1` from the example above to misuse the token, the secure variant of the flow may instead look like this: -. {project_name} issues a token to the application. The application knows that it will need to invoke an untrusted service so it places *scope=* in the authentication request sent to {project_name} (see <<_client_scopes, Client Scopes section>> for more details about the _scope_ parameter). +. A frontend application `frontend-client` authenticates against {project_name}. + +. {project_name} authenticates a user. + +. {project_name} issues a token to the `frontend-client` application. The `frontend-client` knows that it will need to invoke `service1` so it places `scope=service1-scope` in the authentication request sent to {project_name}. +The scope `service1-scope` is a <<_client_scopes,Client scope>>, which may need to be created by administrator. In the <<_audience_setup,sections below>> there are some options how to setup such a client scope. +The token claim will look like: + -The token issued to the application contains a reference to the untrusted service in its audience (*"audience": [ "" ]*) which declares that the client uses this access token to invoke the untrusted service. +[source,json] +---- +"aud": "service1" +---- + -.The untrusted service serves the request to the client application but also keeps the token. +This declares that the client can use this access token to invoke the `service1`. -. The untrusted service invokes a trusted service with the token. Invocation is not successful because the trusted service checks the audience on the token and find that its audience is only for the untrusted service. This behavior is expected and security is not broken. +. The `frontend-client` application uses the token to invoke a service `service1`. -If the client wants to invoke the trusted service later, it must obtain another token by reissuing the SSO login with *scope=*. The returned token will then contain the trusted service as an audience: +. The `service1` serves the request to the client application `frontend-application`. But assume that this service will try to misuse the token and keep it for the further use. +. The `service1` will then try to invoke a `service2` with the token. Invocation is not successful because the `service2` service checks the audience on the token and find that its audience is only for the `service1`. Hence `service2` will reject the request and will return an error to `service1`. This behavior is expected and security is not broken. + +== Ability for the service to call another service + +In some environments, it may be desired that the `service1` may have to retrieve additional data from a `service2` to return data to the original client application `frontend-client`. In order to make this +possible to work, there are few possibilities: + +* Make sure that initial access token issued to `frontend-client` will contain both `service1` and `service2` as audiences. Assuming that there are proper client scopes set, the `frontend-client` can possibly use +the `scope=service1-scope service2-scope` as a value of the `scope` parameter. The issued token would then contain the `aud` claim like: ++ [source,json] ---- -"audience": [ "" ] +"aud": [ "service1", "service2" ] ---- -Use this value to invoke the **. ++ +Such access token can be used to invoke both `service1` or `service2`. Hence `service1` will be able to successfully use such token to invoke `service2` to retrieve additional data. +* The previous approach with both services in the token audience allows that `service1` is allowed to invoke `service2`. However it means that `frontend-client` can also directly use his access token to invoke `service2`. +This may not be desired in some cases. You may want `service1` to be able to invoke `service2`, but at the same time, you do not want `frontend-client` to be able to directly invoke `service2`. The solution +to such scenario might be the use of the link:{securing_apps_token_exchange_link}[Token exchange]. In that case, the initial token would still have only `service1` as an audience. +However once the token is sent to `service1`, the `service1` may send Token exchange request to exchange the token for another token, which would have `service2` as an audience. Please see +the link:{securing_apps_token_exchange_link}[{securing_apps_token_exchange_name}] for the details on how to use it. + +[[_audience_setup]] == Setup When setting up audience checking: -* Ensure that services are configured to check audience on the access token sent to them by adding the flag *_verify-token-audience_* in the adapter configuration. See link:{adapterguide_link_latest}#_java_adapter_config[Adapter configuration] for details. +* Ensure that services are configured to check audience on the access token sent to them. This may be done in a way specific to your client OIDC adapter, which you are using to secure your OIDC client application. -* Ensure that access tokens issued by {project_name} contain all necessary audiences. Audiences can be added using the client roles as described in the <<_audience_resolve, next section>> or hardcoded. See <<_audience_hardcoded, Hardcoded audience>>. +* Ensure that access tokens issued by {project_name} contain all necessary audiences. ++ +Audiences can be added to the token by two ways: ++ +** Using the client roles as described in the <<_audience_resolve, Audience resolve section>>. ++ +** Hardcoded audience as described in the <<_audience_hardcoded, Hardcoded audience section>>. [[_audience_resolve]] -== Automatically add audience +== Automatically add audience based on client roles -An _Audience Resolve_ protocol mapper is defined in the default client scope _roles_. The mapper checks for clients that have at least one client role available for the current token. The client ID of each client is then added as an audience, which is useful +An _Audience Resolve_ protocol mapper is defined in the default client scope _roles_. The mapper checks for clients that have at least one client role available for the current token. The client ID of each such client is then added as an audience, which is useful if your service clients rely on client roles. Service client could be usually a client without any flows enabled, which may not have any tokens issued directly to itself. It represents an OAuth 2 _Resource Server_. -For example, for a service client and a confidential client, -you can use the access token issued for the confidential client to invoke the service client REST service. The service client will be automatically added as an audience to the access token issued for the confidential client if the following are true: +The <<_oidc_token_role_mappings,Token role mappings section>> contains the details about how are client roles added into the token. Please also see the example below. -* The service client has any client roles defined on itself. +=== Example - token role mappings and audience claim -* Target user has at least one of those client roles assigned. +Here are the example steps how to use the client roles to make `aud` claim added to the token: -* Confidential client has the role scope mappings for the assigned role. +. Create a <> `service1`. It may be possible to disable *Standard flow* or any other flows for this client +as it is a service client, which may never directly authenticate by itself. The possible exception might be *Standard Token Exchange* switch if needed as described above. -[NOTE] -==== -If you want to ensure that the audience is not added automatically, do not configure role scope mappings directly on the confidential client. Instead, you can create a dedicated client scope that contains the role scope mappings for the client roles of your dedicated client scope. +. Go to *Roles* tab of that client and create client role `service1-role`. -Assuming that the client scope is added as an optional client scope to the confidential client, the client roles and the audience will be added to the token if explicitly requested by the *scope=* parameter. -==== +. Create user `john` in the same realm and assign him the client role `service1-role` of client `service1` created in the previous step. +<> contains some details on how to do it. + +. Create client scope named `service1-scope`. It can be marked with *Include in token scope* as *ON*. See <<_client_scopes,this section>> for the details on how to create and set new client scope. + +. Go to the tab *Scope* of the `service1-scope` and add the role `service1-role` of the client `service1` to the <<_role_scope_mappings,Role scope mappings>> of this client scope + +. Create another client `frontend-client` in the realm. + +. Click to the tab *Client scopes* of this client and select the first dedicated client scope `frontend-client-dedicated` and then go to the tab *Scope* and disable *Full scope allowed* switch + +. Go back to the tab *Client scopes* of this client and click *Add client scope* and link the `service1-scope` as *Optional*. See <<_client_scopes_linking, Client Scopes Linking section>> for more details. + +. Click the sub-tab *Evaluate* in the *Client scopes* as described in <<_client_scopes_evaluate,this section>>. When filling user `john` and the subtab *Generated access token*, it can be seen that +there is not any `aud` claim as there are not any client roles in the generated example token. However when adding also the scope `service1-scope` to the *Scope* field, it can be seen that there is client +role `service1-role` as it is in *Role scope mappings* of the `service1-scope` and also in the role mappings of the user `john`. Due to that the `aud` claim will also contain `service1`. + +.Audience resolve example +image:images/audience_resolving_evaluate.png[] + +If you want the `service1` audience to be always applied for the tokens issued to the `frontend-client` client (without using the parameter `scope=service1-scope`), it can be fine to instead do any of these: + +* Assign the `service1-scope` as *Default* client scope rather than *Optional* + +* Add the role scope mapping of the `service1-role` directly to the <<_client_scopes_dedicated,Dedicated client scope>> of the client. In this case, you will not need the `service1-scope` at all. + +Note that since this approach is based on client roles, it also requires that user himself (user `john` in the example above) is a member of some client role of the client `service1`. Otherwise if there +are not any client roles assigned, the audience `service1` will not be included. If you want audience to be included regardless of client roles, see the <<_audience_hardcoded,Hardcoded audience>> section instead. [NOTE] ==== @@ -91,29 +153,31 @@ You can use any custom value, for example a URL, if you want to use a different You can add the protocol mapper directly to the frontend client. If the protocol mapper is added directly, the audience will always be added as well. -For more control over the protocol mapper, you can create the protocol mapper on the dedicated client scope, which will be called for example *good-service*. +For more control over the protocol mapper, you can create the protocol mapper on the dedicated client scope, which will be called for example *service2*. -.Audience protocol mapper -image:images/audience_mapper.png[] +Here the example steps for the hardcoded audience -* From the <<_client_installation, Client details tab>> of the *good-service* client, you can generate the adapter configuration and confirm that _verify-token-audience_ is set to *true*. This action forces the adapter to verify the audience if you use this configuration. +. Create a client `service2` -* You need to ensure that the confidential client is able to request *good-service* as an audience in its tokens. -+ -On the confidential client: -+ -. Click the _Client Scopes_ tab. -. Assign *good-service* as an optional (or default) client scope. -+ -See <<_client_scopes_linking, Client Scopes Linking section>> for more details. +. Create a client scope `service2-scope`. -* You can optionally <<_client_scopes_evaluate, Evaluate Client Scopes>> and generate an example access token. *good-service* will be added to the audience of the generated access token if *good-service* is included in the _scope_ parameter, when you assigned it as an optional client scope. +. In the tab *Mappers* of that client scope, select *Configure a new mapper* and select *Audience* -* In your confidential client application, ensure that the _scope_ parameter is used. The value *good-service* must be included when you want to issue the token for accessing *good-service*. +. Select *Included Client Audience* as a `service2` and save the mapper + -See: +.Audience protocol mapper +image:images/audience_mapper.png[] + -** link:{adapterguide_link}#_params_forwarding[parameters forwarding section] if your application uses the servlet adapter. -** link:{adapterguide_link}#_javascript_adapter[javascript adapter section] if your application uses the javascript adapter. +. Link the newly created client scope with some client. For example it can be linked as *Optional* client scope to the client `frontend-client` created in the <<_audience_resolve,previous example>>. + +. You can optionally <<_client_scopes_evaluate, Evaluate Client Scopes>> for the client where the client scope was linked (For example `frontend-client`) and generate an example access token. +The audience `service2` will be added to the audience of the generated access token if `service2-scope` is included in the _scope_ parameter, when you assigned it as an optional client scope. + +In your confidential client application, ensure that the _scope_ parameter is used. The value like _scope=service2-scope_ must be included when you want to issue the token for accessing `service2`. + +See in the link:{securing_apps_base_link}/javascript-adapter[{project_name} JavaScript adapter] section if your application uses the javascript adapter for how to send the _scope_ parameter with the desired value. + +If you prefer to not include `scope` parameter in your requests, you can instead link the `service2-scope` as a *Default* client scope or use the client dedicated scope where you configure this mapper. +This is useful if you want to always apply the audience for all the authentication request of OIDC client `frontend-client`. -NOTE: Both the _Audience_ and _Audience Resolve_ protocol mappers add the audiences to the access token only, by default. The ID Token typically contains only a single audience, the client ID for which the token was issued, a requirement of the OpenID Connect specification. However, the access token does not necessarily have the client ID, which was the token issued for, unless the audience mappers added it. +NOTE: Both the _Audience_ and _Audience Resolve_ protocol mappers add the audiences to the access token only, by default. The ID Token typically contains only a single audience, the client ID for which the token was issued, a requirement of the OpenID Connect specification. However, the access token does not necessarily have the client ID, which was the token issued for, unless the _Audience_ mapper added it. 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..571c8bdf3291 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. + @@ -34,11 +38,11 @@ This option handles link:https://fetch.spec.whatwg.org/[Cross-Origin Resource Sh If browser JavaScript attempts an AJAX HTTP request to a server whose domain is different from the one that the JavaScript code came from, the request must use CORS. The server must handle CORS requests, otherwise the browser will not display or allow the request to be processed. This protocol protects against XSS, CSRF, and other JavaScript-based attacks. + -Domain URLs listed here are embedded within the access token sent to the client application. The client application uses this information to decide whether to allow a CORS request to be invoked on it. Only {project_name} client adapters support this feature. See link:{adapterguide_link}[{adapterguide_name}] for more information. +Domain URLs listed here are embedded within the access token sent to the client application. The client application uses this information to decide whether to allow a CORS request to be invoked on it. Only {project_name} client adapters support this feature. See link:{securing_apps_link}[{securing_apps_name}] for more information. [[_admin-url]] Admin URL:: Callback endpoint for a client. The server uses this URL to make callbacks like pushing revocation policies, performing backchannel logout, and other administrative operations. For {project_name} servlet adapters, this URL can be the root URL of the servlet application. -For more information, see link:{adapterguide_link}[{adapterguide_name}]. +For more information, see link:{securing_apps_link}[{securing_apps_name}]. == Capability Config [[_access-type]] @@ -62,6 +66,8 @@ For client-side clients that perform browser logins. As it is not possible to en *Service account roles*:: If enabled, this client can authenticate to {project_name} and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of `Client Credentials Grant` for this client. +*Standard Token Exchange*:: If enabled, this client can use the link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard token exchange]. + *Auth 2.0 Device Authorization Grant*:: If enabled, this client can use the OIDC xref:con-oidc-auth-flows_server_administration_guide[Device Authorization Grant]. *OIDC CIBA Grant*:: If enabled, this client can use the OIDC xref:con-oidc-auth-flows_{context}[Client Initiated Backchannel Authentication Grant]. @@ -90,10 +96,12 @@ There will be also one item on the consent screen about this client itself. [[_front-channel-logout]] *Front channel logout*:: If *Front Channel Logout* is enabled, the application should be able to log out users through the front channel as per link:https://openid.net/specs/openid-connect-frontchannel-1_0.html[OpenID Connect Front-Channel Logout] specification. If enabled, you should also provide the `Front-Channel Logout URL`. -*Front-channel logout URL*:: URL that will be used by {project_name} to send logout requests to clients through the front-channel. +*Front-channel logout URL*:: URL that will be used by {project_name} to send logout requests to clients through the front-channel. If not provided, it defaults to the Home URL. This option is applicable just if `Front channel logout` option is ON. + +*Front-channel logout session required*:: Specifies whether a sid (session ID) and iss (issuer) parameters are included in the Logout request when the Front-channel Logout URL is used. [[_back-channel-logout-url]] -*Backchannel logout URL*:: URL that will cause the client to log itself out when a logout request is sent to this realm (via end_session_endpoint). If omitted, no logout requests are sent to the client. +*Backchannel logout URL*:: URL that will cause the client to log itself out when a logout request is sent to this realm (via end_session_endpoint). The logout is done by sending logout token as specified in the OIDC Backchannel logout specification. If omitted, the logout request might be sent to the specified `Admin URL` (if configured) in the format specific to {project_name} adapters. If even `Admin URL` is not configured, no logout request will be sent to the client. This option is applicable just if `Front channel logout` option is OFF. *Backchannel logout session required*:: Specifies whether a session ID Claim is included in the Logout Token when the *Backchannel Logout URL* is used. diff --git a/docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc b/docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc index 0a8eb1a1af23..251a4b18e00d 100644 --- a/docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc +++ b/docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc @@ -17,7 +17,9 @@ This choice is the default setting. The secret is automatically generated. Click .Signed JWT image:images/client-credentials-jwt.png[Signed JWT] -*Signed JWT* is "Signed Json Web Token". +*Signed JWT* is "Signed JSON Web Token". + +In this authenticator you can enforce the *Signature algorithm* used by the client (any algorithm is valid by default) and the *Max expiration* allowed for the JWT token (tokens received after this period will not be accepted because they are too old, note that tokens should be issued right before the authentication, 60 seconds by default). When choosing this credential type you will have to also generate a private key and certificate for the client in the tab `Keys`. The private key will be used to sign the JWT, while the certificate is used by the server to verify the signature. @@ -63,6 +65,8 @@ If you select this option, you can use a JWT signed by client secret instead of The client secret will be used to sign the JWT by the client. +Like in the *Signed JWT* authenticator you can configure the *Signature algorithm* and the *Max expiration* for the JWT token. + *X509 Certificate* {project_name} will validate if the client uses proper X509 certificate during the TLS Handshake. diff --git a/docs/documentation/server_admin/topics/clients/oidc/con-token-role-mappings.adoc b/docs/documentation/server_admin/topics/clients/oidc/con-token-role-mappings.adoc new file mode 100644 index 000000000000..994539cdfb26 --- /dev/null +++ b/docs/documentation/server_admin/topics/clients/oidc/con-token-role-mappings.adoc @@ -0,0 +1,80 @@ +[[_oidc_token_role_mappings]] += Role mappings in the token + +When a user authenticates, there are some roles that are added to the access token. By default, the <> are added to the access +token into the `realm_access` claim. The <> are added by default to the `resource_access` claim. + +The roles added to the token are an intersection of: + +* Roles, that are <<_oidc_token_role_mappings_user_roles,assigned to the user>>. + +* <<_role_scope_mappings,Role scope mappings>> of the roles that the client is permitted to access + +[[_oidc_token_role_mappings_user_roles]] +== Roles assigned to the user + +Roles assigned to the user can be defined in the Role mappings as described in <>. Few details: + +* In case that a user is a member of some <>, then all the roles of these groups are also applied. + +* In case that a role is a <<_composite-roles,composite role>>, the child roles of the composite role are also applied. In the token, the list of the roles is expanded and would contain all the roles. + +* In case that the authenticated user is not a normal user, but a <<_service_accounts,Service account>>, which represents a client, then the service account roles are used. The service account roles are defined +in the tab *Service accounts roles* of the particular client. + +== Role protocol mappers + +Similarly to other claims, the roles are added to the access token issued for the client by the dedicated <<_protocol-mappers,Protocol mappers>>. There is a <<_client_scopes_protocol,Built-in client scope *roles*>> +defined in the realm. Since it is a <>, it is defined by default as a <<_client_scopes_linking,Default client scope>> for every realm client. +You can see this client scope in the admin console by looking at the tab *Client scopes* and then looking for the *roles* client scope. This client scope contains these protocol mappers by default: + +* The protocol mapper *realm roles* - This protocol mapper is used to add the realm roles to the token claim. By default, the configuration looks like this: + +.Realm roles mapper +image:images/mapper-oidc-realm-roles.png[] + +* The protocol mapper *client roles* - This protocol mapper is used to add the client roles to the token claim. By default, the configuration looks like this: + +.Client roles mapper +image:images/mapper-oidc-client-roles.png[] + +* The protocol mapper *audience resolve* - This protocol mapper is used to fill the `aud` claim in the access token based on the applied client roles. The details about this mapper are in the <<_audience_resolve,Audience resolve section>>. + +As you can see in the configuration of realm roles and client roles mappers, it is possible to configure: + +* If roles are added just to the access token or also to other tokens, like for example the ID token. By default, roles are added to the access token and to the introspection endpoint. + +* What are the claims where the roles would be added. By default, the realm roles are added to the `realm_access` claim. So, for example, the claim in the JWT token containing 2 realm roles `role1` and `role2` will look similar to this: ++ +[source,json] +---- +"realm_access": { + "roles": [ "role1", "role2" ] +} +---- ++ +The client roles are added to the `resource_access` token claim by default. This claim will look like this in the token, which contains +client roles `manage-account` and `manage-account-links` of client `account` and client role `target-client1-role` of the client `target-client1`: ++ +[source,json] +---- +"resource_access": { + "target-client1": { + "roles": [ "target-client1-role" ] + }, + "account": { + "roles": [ "manage-account", "manage-account-links" ] + } +} +---- + +By adjusting the configuration option *Token claim name* of the role protocol mappers, it is possible to specify that these roles will be added to the token in the configured claim. + +If you want to update the role claims just for one specific client (For example, client `foo` expects the realm roles in the claim `my-realm-roles` instead of the claim `realm_access`), then it is +possible to remove the default client scope *roles* from your client and instead configure the realm/client protocol mapper in the <<_client_scopes_dedicated,dedicated client scope>> of your client. + +== Example + +The <<_audience_resolve,Audience documentation>> contains a more detailed example, which covers some details about the role mappings and about the audience (Claim `aud`) added to the token. Also, it can be +useful to try the <<_client_scopes_evaluate,Client scopes evaluation>> to see what are the effective scopes, protocol mappers and role scope mappings used when issuing the token for the particular client +and how the JWT tokens would look like for the particular combination of user, client, and applied client scopes. diff --git a/docs/documentation/server_admin/topics/clients/oidc/proc-using-a-service-account.adoc b/docs/documentation/server_admin/topics/clients/oidc/proc-using-a-service-account.adoc index f78124a02316..0e3dc03225b3 100644 --- a/docs/documentation/server_admin/topics/clients/oidc/proc-using-a-service-account.adoc +++ b/docs/documentation/server_admin/topics/clients/oidc/proc-using-a-service-account.adoc @@ -8,16 +8,16 @@ Each OIDC client has a built-in _service account_. Use this _service account_ to .Prerequisites .Procedure -. Click *Clients* in the menu. +. Click *Clients* in the menu. . Select your client. . Click the *Settings* tab. . Toggle <<_access-type, Client authentication>> to *On*. -. Select *Service accounts roles*. +. Select *Service accounts roles* checkbox to make sure it is enabled. . Click *Save*. . Configure your <<_client-credentials, client credentials>>. -. Click the *Scope* tab. -. Verify that you have roles or toggle *Full Scope Allowed* to *ON*. -. Click the *Service Account Roles* tab +. Click the *Client Scopes* tab, select the dedicated client scope (usually first client scope in the list, more details <<_client_scopes_dedicated,in this section>>) and select *Scope* tab of the client scope. +. Verify that you have roles or toggle *Full Scope Allowed* to *ON*. Note that this switch is useful only for the development purposes and in the production, it is recommended to disable this switch and properly configure role scopes. The details about this switch are described in <<_role_scope_mappings, this section>> and in <<_oidc_token_role_mappings,this section>>. +. Click the *Service Account Roles* tab of your client . Configure the roles available to this service account for your client. Roles from access tokens are the intersection of: @@ -43,6 +43,16 @@ For example, the POST invocation to retrieve a service account can look like thi grant_type=client_credentials ---- +Note that the value of `cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=` used in the `Authorization` header is Base64 encoded value of clientId and clientSecret +in the format prescribed by the `Authorization: Basic` header. In this example, the client ID is `product-sa-client` and the client secret was `password` and hence the value was obtained for example +by this command in the Unix platform: +[source,bash] +---- +echo 'product-sa-client:password' | base64 +---- +Instead of using the header `Authorization: Basic`, it is also possible to send the credentials as parameters `client_id` and `client_secret` of the POST request. For other client credentials methods, +the format of the parameters would be different as described above. + The response would be similar to this https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3[Access Token Response] from the OAuth 2.0 specification. [source] @@ -54,9 +64,10 @@ Cache-Control: no-store Pragma: no-cache { - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"bearer", - "expires_in":60 + "access_token":"eyJhbGciOiJSUzI1NiIs...", + "token_type":"Bearer", + "expires_in":60, + "scope": "email profile" } ---- diff --git a/docs/documentation/server_admin/topics/clients/oidc/service-accounts.adoc b/docs/documentation/server_admin/topics/clients/oidc/service-accounts.adoc deleted file mode 100644 index 4d1c5e87bfda..000000000000 --- a/docs/documentation/server_admin/topics/clients/oidc/service-accounts.adoc +++ /dev/null @@ -1,57 +0,0 @@ -[[_service_accounts]] - -==== Service Accounts - -Each OIDC client has a built-in _service account_ which allows it to obtain an access token. -This is covered in the OAuth 2.0 specification under <<_client_credentials_grant,Client Credentials Grant>>. -To use this feature you must set the <<_access-type, Access Type>> of your client to `confidential`. When you do this, -the `Service Accounts Enabled` switch is displayed. You need to toggle this switch to ON. Also make sure that you have -configured your <<_client-credentials, client credentials>>. - -To use it you must have registered a valid `confidential` Client and you need to check the switch `Service Accounts Enabled` in {project_name} admin console for this client. -In tab `Service Account Roles` you can configure the roles available to the service account retrieved on behalf of this client. -Remember that you must have the roles available in Role Scope Mappings (tab `Scope`) of this client as well, unless you -have `Full Scope Allowed` on. As in a normal login, roles from access token are the intersection of: - -* Role scope mappings of particular client combined with the role scope mappings inherited from linked client scopes -* Service account roles - -The REST URL to invoke on is `{kc_realms_path}/{realm-name}/protocol/openid-connect/token`. -Invoking on this URL is a POST request and requires you to post the client credentials. -By default, client credentials are represented by clientId and clientSecret of the client in `Authorization: Basic` header, but you can also authenticate the client with a signed JWT assertion or any other custom mechanism for client authentication. -You also need to use the parameter `grant_type=client_credentials` as per the OAuth2 specification. - -For example the POST invocation to retrieve a service account can look like this: - -[source] ----- - - POST {kc_realms_path}/demo/protocol/openid-connect/token - Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ= - Content-Type: application/x-www-form-urlencoded - - grant_type=client_credentials ----- -The response would be this https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3[standard JSON document] from the OAuth 2.0 specification. - -[source] ----- - -HTTP/1.1 200 OK -Content-Type: application/json;charset=UTF-8 -Cache-Control: no-store -Pragma: no-cache - -{ - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"bearer", - "expires_in":60 -} ----- - -There is the only access token returned by default. There is no refresh token returned and there is also no user session created -on the {project_name} side upon successful authentication by default. Due to the lack of a refresh token, there is a need to re-authenticate when access token expires, -however this does not mean any additional overhead on {project_name} server side due the fact that sessions are not created by default. - -Due to this, there is no need for logout, however issued access tokens can be revoked by sending request to the OAuth2 Revocation Endpoint described -in the <<_oidc-endpoints, OpenID Connect Endpoints>> section. diff --git a/docs/documentation/server_admin/topics/clients/proc-evaluating-client-scopes.adoc b/docs/documentation/server_admin/topics/clients/proc-evaluating-client-scopes.adoc index dfa23d0b757b..47fa358c438d 100644 --- a/docs/documentation/server_admin/topics/clients/proc-evaluating-client-scopes.adoc +++ b/docs/documentation/server_admin/topics/clients/proc-evaluating-client-scopes.adoc @@ -5,17 +5,20 @@ The *Mappers* tab contains the protocol mappers and the *Scope* tab contains the .Procedure . Click the *Client Scopes* tab for the client. -. Open the sub-tab *Evaluate*. -. Select the optional client scopes that you want to apply. +. Open the sub-tab *Evaluate*. +. Select the optional client scopes that you want to apply. This will also show you the value of the *scope* parameter. This parameter needs to be sent from the application to the {project_name} OpenID Connect authorization endpoint. -.Evaluating client scopes -image:images/client-scopes-evaluate.png[] - [NOTE] ==== -To send a custom value for a *scope* parameter from your application, see the link:{adapterguide_link_latest}#_params_forwarding[parameters forwarding section], for servlet adapters or the link:{adapterguide_link_latest}#_javascript_adapter[javascript adapter section], for javascript adapters. +If your application uses the {securing_apps_base_link}/javascript-adapter[{project_name} JavaScript adapter], see its section to learn how to send the *scope* parameter with the desired value. ==== -All examples are generated for the particular user and issued for the particular client, with the specified value of the *scope* parameter. The examples include all of the claims and role mappings used. \ No newline at end of file +You can also simulate how the access token, ID token, or UserInfo response issued to this client looks for a particular selected user and for a specific value of the `audience` parameter. Note +that the `audience` parameter is currently only supported for the token exchange grant. It is recommended to leave it empty when simulating any other grant. + +.Evaluating client scopes +image:images/client-scopes-evaluate.png[] + +All examples are generated for the particular user and issued for the particular client, with the specified value of the *scope* parameter. The examples include all of the claims and role mappings used. diff --git a/docs/documentation/server_admin/topics/clients/proc-updating-default-scopes.adoc b/docs/documentation/server_admin/topics/clients/proc-updating-default-scopes.adoc index d3cd7a919808..58f62926e7f6 100644 --- a/docs/documentation/server_admin/topics/clients/proc-updating-default-scopes.adoc +++ b/docs/documentation/server_admin/topics/clients/proc-updating-default-scopes.adoc @@ -3,15 +3,7 @@ [role="_abstract"] Use *Realm Default Client Scopes* to define sets of client scopes that are automatically linked to newly created clients. -.Procedure -. Click the *Client Scopes* tab for the client. -ifeval::[{project_product}==true] -. Click *Default Client Scopes*. -endif::[] - -From here, select the client scopes that you want to add as *Default Client Scopes* to newly created clients and *Optional Client Scopes*. - -.Default client scopes -image:images/client-scopes-default.png[] +To see the realm default client scopes, click the *Client Scopes* tab on the left side of the admin console. In the *Assigned type* column, you can specify whether a particular client scope should be added as +a *Default Client Scope* or an *Optional Client Scope* to newly created clients. See <<_client_scopes_linking, this section>> for details on what _default_ and _optional_ client scopes are. When a client is created, you can unlink the default client scopes, if needed. This is similar to removing <<_default_roles, Default Roles>>. diff --git a/docs/documentation/server_admin/topics/clients/saml/idp-initiated-login.adoc b/docs/documentation/server_admin/topics/clients/saml/idp-initiated-login.adoc index 136620e4d0c9..4c4d9865e02a 100644 --- a/docs/documentation/server_admin/topics/clients/saml/idp-initiated-login.adoc +++ b/docs/documentation/server_admin/topics/clients/saml/idp-initiated-login.adoc @@ -4,7 +4,7 @@ IDP Initiated Login is a feature that allows you to set up an endpoint on the {project_name} server that will log you into a specific application/client. In the *Settings* tab for your client, you need to specify the *IDP Initiated SSO URL Name*. This is a simple string with no whitespace in it. -After this you can reference your client at the following URL: `root{kc_realms_path}/{realm}/protocol/saml/clients/{url-name}` +After this you can reference your client at the following URL: `root{kc_realms_path}/{realm-name}/protocol/saml/clients/{url-name}` The IDP initiated login implementation prefers _POST_ over _REDIRECT_ binding (check <<_saml, saml bindings>> for more information). Therefore the final binding and SP URL are selected in the following way: @@ -17,7 +17,7 @@ of the client settings) _POST_ binding is used through that URL. If your client requires a special relay state, you can also configure this on the *Settings* tab in the *IDP Initiated SSO Relay State* field. Alternatively, browsers can specify the relay state in a *RelayState* query parameter, i.e. -`root{kc_realms_path}/{realm}/protocol/saml/clients/{url-name}?RelayState=thestate`. +`root{kc_realms_path}/{realm-name}/protocol/saml/clients/{url-name}?RelayState=thestate`. When using <<_identity_broker,identity brokering>>, it is possible to set up an IDP Initiated Login for a client from an external IDP. The actual client is set up for IDP Initiated Login at broker IDP as described above. The external IDP has diff --git a/docs/documentation/server_admin/topics/clients/saml/proc-creating-saml-client.adoc b/docs/documentation/server_admin/topics/clients/saml/proc-creating-saml-client.adoc index 0c6df3145665..f20c229e1b0b 100644 --- a/docs/documentation/server_admin/topics/clients/saml/proc-creating-saml-client.adoc +++ b/docs/documentation/server_admin/topics/clients/saml/proc-creating-saml-client.adoc @@ -49,7 +49,7 @@ the name, set up a replacement string value. For example, a string value such as Wildcards values are allowed only at the end of a URL. For example, http://host.com/*$$. This field is used when the exact SAML endpoints are not registered and {project_name} pulls the Assertion Consumer URL from a request. -*IDP-Initiated SSO URL name*:: URL fragment name to reference client when you want to do IDP Initiated SSO. Leaving this empty will disable IDP Initiated SSO. The URL you will reference from your browser will be: _server-root_/realms/{realm}/protocol/saml/clients/{client-url-name} +*IDP-Initiated SSO URL name*:: URL fragment name to reference client when you want to do IDP Initiated SSO. Leaving this empty will disable IDP Initiated SSO. The URL you will reference from your browser will be: _server-root_/realms/{realm-name}/protocol/saml/clients/{client-url-name} *IDP Initiated SSO Relay State*:: Relay state you want to send with SAML request when you want to do IDP Initiated SSO. diff --git a/docs/documentation/server_admin/topics/clients/saml/proc-using-an-entity-descriptor.adoc b/docs/documentation/server_admin/topics/clients/saml/proc-using-an-entity-descriptor.adoc index 728ea3edde7d..6b577313c1a9 100644 --- a/docs/documentation/server_admin/topics/clients/saml/proc-using-an-entity-descriptor.adoc +++ b/docs/documentation/server_admin/topics/clients/saml/proc-using-an-entity-descriptor.adoc @@ -18,6 +18,6 @@ Some SAML client adapters, such as _mod-auth-mellon_, need the XML Entity Descri [source, subs="attributes"] ---- -root{kc_realms_path}/{realm}/protocol/saml/descriptor +root{kc_realms_path}/{realm-name}/protocol/saml/descriptor ---- where _realm_ is the realm of your client. diff --git a/docs/documentation/server_admin/topics/events/admin.adoc b/docs/documentation/server_admin/topics/events/admin.adoc index 73fc84912d86..e2295c6a3548 100644 --- a/docs/documentation/server_admin/topics/events/admin.adoc +++ b/docs/documentation/server_admin/topics/events/admin.adoc @@ -1,4 +1,4 @@ - + === Auditing admin events You can record all actions that are performed by an administrator in the Admin Console. The Admin Console performs administrative actions by invoking the {project_name} REST interface and {project_name} audits these REST invocations. You can view the resulting events in the Admin Console. @@ -35,9 +35,9 @@ You can now view admin events. .Admin events image:images/admin-events.png[Admin events] -When the `Include Representation` switch is ON, it can lead to storing a lot of information in the database. You can set a maximum length of the representation by using the `--spi-events-store-jpa-max-field-length` argument. This setting is useful if you want to adhere to the underlying storage limitation. For example: +When the `Include Representation` switch is ON, it can lead to storing a lot of information in the database. You can set a maximum length of the representation by using the `+--spi-events-store--jpa--max-field-length+` argument. This setting is useful if you want to adhere to the underlying storage limitation. For example: [source,bash] ---- -kc.[sh|bat] --spi-events-store-jpa-max-field-length=2500 ----- \ No newline at end of file +kc.[sh|bat] --spi-events-store--jpa--max-field-length=2500 +---- diff --git a/docs/documentation/server_admin/topics/events/login.adoc b/docs/documentation/server_admin/topics/events/login.adoc index 899bd797e9d7..7b40a02a04c8 100644 --- a/docs/documentation/server_admin/topics/events/login.adoc +++ b/docs/documentation/server_admin/topics/events/login.adoc @@ -64,6 +64,45 @@ image:images/search-user-event.png[Search user event] |=== +*Brute force protection:* + +[cols="2",options="header"] +|=== +|Event |Description +|User disabled by permanent lockout +|Brute force protection disabled the user account permanently due to too many login failures. + +|User disabled by temporary lockout +|Brute force protection disabled the user account temporarily due to too many login failures. + +|=== + +*Identity Brokering:* + +[cols="2",options="header"] +|=== +|Event |Description +|Federated identity link override +|An existing Federated identity link was overridden + +|Federated identity link override error +|Error occurred when trying to override an existing Federated identity link + +|=== + +*OAuth:* + +[cols="2",options="header"] +|=== +|Event |Description +|OAuth2 extension grant +|OAuth2 grant was executed + +|OAuth2 extension grant error +|Error occurred during OAuth2 grant execution + +|=== + *Account events:* [cols="2",options="header"] @@ -84,15 +123,21 @@ image:images/search-user-event.png[Search user event] |Send Password Reset |{project_name} sends a password reset email. -|Update Password +|Update Password (deprecated) |The password for an account changes. -|Update TOTP +|Update Credential +|The password or (time-based) one-time Password (OTP/TOTP) settings for an account changes. + +|Update TOTP (deprecated) |The Time-based One-time Password (TOTP) settings for an account changes. -|Remove TOTP +|Remove TOTP (deprecated) |{project_name} removes TOTP from an account. +|Remove Credential +|{project_name} removes a credential from an account. + |Send Verify Email |{project_name} sends an email verification email. @@ -103,6 +148,7 @@ image:images/search-user-event.png[Search user event] Each event has a corresponding error event. +[[event-listener]] ==== Event listener Event listeners listen for events and perform actions based on that event. {project_name} includes two built-in listeners, the Logging Event Listener and Email Event Listener. @@ -138,19 +184,35 @@ To change the log level used by the Logging Event listener, add the following: [source,bash] ---- -bin/kc.[sh|bat] start --spi-events-listener-jboss-logging-success-level=info --spi-events-listener-jboss-logging-error-level=error +bin/kc.[sh|bat] start --spi-events-listener-jboss-logging-success-level=info --spi-events-listener--jboss-logging--error-level=error ---- The valid values for log levels are `debug`, `info`, `warn`, `error`, and `fatal`. ===== The Email Event Listener -The Email Event Listener sends an email to the user's account when an event occurs and supports the following events: +The Email Event Listener sends a message to the user's email address when an event occurs and supports the following events: * Login Error. * Update Password. * Update Time-based One-time Password (TOTP). -* Remove Time-based One-time Password (TOTP). +* Remove One-time Password (OTP). +* Update Credential. +* Remove Credential. + +Below are the optional events you can configure: + +* User disabled by permanent lockout. +* User disabled by temporary lockout. + +The following conditions need to be met for an email to be sent: + +* User has an email address. +* User's email address is marked as verified. + +.Prerequisites + +* Realm's email settings configured. .Procedure @@ -164,10 +226,15 @@ To enable the Email Listener: .Event listeners image:images/event-listeners.png[Event listeners] -You can exclude events by using the `--spi-events-listener-email-exclude-events` argument. For example: +You can exclude events by using the `+--spi-events-listener--email--exclude-events+` argument. For example: [source,bash] ---- -kc.[sh|bat] --spi-events-listener-email-exclude-events=UPDATE_TOTP,REMOVE_TOTP +kc.[sh|bat] --spi-events-listener--email--exclude-events=UPDATE_CREDENTIAL,REMOVE_CREDENTIAL ---- +To enable optional events, use the following command: +[source,bash] +---- +kc.[sh|bat] --spi-events-listener--email--include-events=USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR,USER_DISABLED_BY_PERMANENT_LOCKOUT +---- diff --git a/docs/documentation/server_admin/topics/identity-broker/configuration.adoc b/docs/documentation/server_admin/topics/identity-broker/configuration.adoc index d86e9a433ee8..fea974e1b62e 100644 --- a/docs/documentation/server_admin/topics/identity-broker/configuration.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/configuration.adoc @@ -22,7 +22,7 @@ When you configure an identity provider, the identity provider appears on the {p image:images/identity-provider-login-page.png[] Social:: - Social providers enable social authentication in your realm. With {project_name}, users can log in to your application using a social network account. Supported providers include Twitter, Facebook, Google, LinkedIn, Instagram, Microsoft, PayPal, Openshift v3, GitHub, GitLab, Bitbucket, and Stack Overflow. + Social providers enable social authentication in your realm. With {project_name}, users can log in to your application using a social network account. Supported providers include Twitter, Facebook, Google, LinkedIn, Instagram, Microsoft, PayPal, Openshift v4, GitHub, GitLab, Bitbucket, and Stack Overflow. Protocol-based:: Protocol-based providers rely on specific protocols to authenticate and authorize users. Using these providers, you can connect to any identity provider compliant with a specific protocol. {project_name} provides support for SAML v2.0 and OpenID Connect v1.0 protocols. You can configure and broker any identity provider based on these open standards. @@ -54,6 +54,10 @@ Although each type of identity provider has its configuration options, all share |Trust Email |When *ON*, {project_name} trusts email addresses from the identity provider. If the realm requires email validation, users that log in from this identity provider do not need to perform the email verification process. +If the target identity provider supports email verification and advertises this information when returning the user profile information, the email of the federated user will be (un)marked as verified. +For instance, an OpenID Connect Provider returning a `email_verified` claim in their ID Tokens. +Note that this setting will set the email as verified when the user is federated for the first time and on subsequent logins +through the broker if the sync mode is set to `FORCE`. |GUI Order |The sort order of the available identity providers on the login page. @@ -76,4 +80,7 @@ Although each type of identity provider has its configuration options, all share |Sync Mode |Strategy to update user information from the identity provider through mappers. When choosing *legacy*, {project_name} used the current behavior. *Import* does not update user data and *force* updates user data when possible. See <<_mappers, Identity Provider Mappers>> for more information. + +|Case-sensitive username +|If enabled, the original username from the identity provider is kept as is when federating users. Otherwise, the username from the identity provider is lower-cased and might not match the original value if it is case-sensitive. This setting only affects the username associated with the federated identity as usernames in the server are always in lower-case. |=== diff --git a/docs/documentation/server_admin/topics/identity-broker/default-provider.adoc b/docs/documentation/server_admin/topics/identity-broker/default-provider.adoc index 53f5e2359aa0..652e938b987e 100644 --- a/docs/documentation/server_admin/topics/identity-broker/default-provider.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/default-provider.adoc @@ -14,3 +14,6 @@ If {project_name} does not find the configured default identity provider, the login form is displayed. This authenticator is responsible for processing the `kc_idp_hint` query parameter. See the <<_client_suggested_idp, client suggested identity provider>> section for more information. + +NOTE: The authenticator will redirect to the identity provider and authentication is delegated to the identity provider. The `browser` authentication flow will not continue after the login with the identity provider +is successfully finished. If you want to perform additional steps after the identity provider login (for example 2-factor authentication), it may be needed to configure <<_identity_broker_post_login_flow, Post login flow>>. diff --git a/docs/documentation/server_admin/topics/identity-broker/first-login-flow.adoc b/docs/documentation/server_admin/topics/identity-broker/first-login-flow.adoc index c72077075a8d..e7619f74824f 100644 --- a/docs/documentation/server_admin/topics/identity-broker/first-login-flow.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/first-login-flow.adoc @@ -70,6 +70,7 @@ This authenticator sets an existing user to the authentication context without v [NOTE] ==== This setup is the simplest setup available, but it is possible to use other authenticators. For example: + * You can add the Review Profile authenticator to the beginning of the flow if you want end users to confirm their profile information. * You can add authentication mechanisms to this flow, forcing a user to verify their credentials. Adding authentication mechanisms requires a complex flow. For example, you can set the "Automatically Set Existing User" and "Password Form" as "Required" in an "Alternative" sub-flow. ==== @@ -115,3 +116,20 @@ You could set the also set `Sync Mode` to `force` if you want to update the user NOTE: This flow can be used if you want to delegate the identity to other identity providers (such as GitHub, Facebook ...) but you want to manage which users that can log in. With this configuration, {project_name} is unable to determine which internal account corresponds to the external identity. The *Verify Existing Account By Re-authentication* authenticator asks the provider for the username and password. + +[[_override_existing_broker_link]] +==== Override existing broker link +When an another account needs to be linked to the same {project_name} account within the same identity provider, you can configure the following authenticator. + +Confirm Override Existing Link:: +This authenticator will detect the existing broker link for the user and display a confirmation page to confirm overriding the existing broker link. Set the authenticator requirement to REQUIRED. + +A typical use of this authenticator is a scenario such as the following: + +* For example, consider a {project_name} user `john` with the email `john@gmail.com`. That user is linked to the identity provider `google` with the `google` username `john@gmail.com` . +* Then for instance {project_name} user `john` creates new Google account with email `john-new@gmail.com` +* Then during login to {project_name}, the user authenticated to the identity provider `google` with a new username such as `john-new@gmail.com`, which is not linked to any {project_name} account yet (as {project_name} account `john` is still linked with the `google` user `john@gmail.com`) and hence the first-broker-login flow is triggered. +* During first-broker-login, the {project_name} user `john` is authenticated somehow (either by default first-broker-login re-authentication or for instance by authenticator like `Detect existing broker user`) +* Now with this authenticator in the authentication flow, it is possible to override the IDP link to the `google` identity provider of {project_name} user `john` with the new `google` link to `google` user `john-new@gmail.com` after user `john` confirms this. + +When creating authentication flows with this authenticator, make sure to add this authenticator once other authenticators that are already established the {project_name} user by other means (either by re-authentication or after `Detect existing broker user` as mentioned above. diff --git a/docs/documentation/server_admin/topics/identity-broker/logout.adoc b/docs/documentation/server_admin/topics/identity-broker/logout.adoc index b563fc9e7427..a630f6805776 100644 --- a/docs/documentation/server_admin/topics/identity-broker/logout.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/logout.adoc @@ -1,4 +1,4 @@ === Identity broker logout -When logging out, {project_name} sends a request to the external identity provider that is used to log in initially and logs the user out of this identity provider. You can skip this behavior and avoid logging out of the external identity provider. See link:{adapterguide_logout_link}[adapter logout documentation] for more information. +When logging out, {project_name} sends a request to the external identity provider that is used to log in initially and logs the user out of this identity provider. diff --git a/docs/documentation/server_admin/topics/identity-broker/oauth2.adoc b/docs/documentation/server_admin/topics/identity-broker/oauth2.adoc new file mode 100644 index 000000000000..05b63dc24322 --- /dev/null +++ b/docs/documentation/server_admin/topics/identity-broker/oauth2.adoc @@ -0,0 +1,90 @@ + +[[_identity_broker_oauth]] +=== OAuth v2 identity providers + +{project_name} brokers identity providers based on the OAuth v2 protocol. These identity providers (IDPs) must support the xref:con-oidc-auth-flows_{context}[Authorization Code Flow] defined in the specification to authenticate users and authorize access. + + +.Procedure +. Click *Identity Providers* in the menu. +. From the `Add provider` list, select `OAuth v2`. ++ +. Enter your initial configuration options. See <<_general-idp-config, General IDP Configuration>> for more information about configuration options. ++ +.OAuth2 settings +|=== +|Configuration|Description + +|Authorization URL +|The authorization URL endpoint. + +|Token URL +|The token URL endpoint. + +|User Info URL +|An endpoint from where information about the user will be fetched from. When invoking this endpoint, {project_name} will send +the request with the access token issued by the identity provider as a bearer token. As a result, it expects the response to be a +JSON document with the claims that should be used to obtain user profile information like ID, username, email, and first and last names. + +|Client Authentication +|Defines the Client Authentication method {project_name} uses with the Authorization Code Flow. In the case of JWT signed with a private key, {project_name} uses the realm private key. In the other cases, define a client secret. See the https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[Client Authentication specifications] for more information. + +|Client ID +|A realm acting as an OIDC client to the external IDP. The realm must have an OIDC client ID if you use the Authorization Code Flow to interact with the external IDP. + +|Client Secret +|Client secret from an external <<_vault-administration,vault>>. This secret is necessary if you are using the Authorization Code Flow. + +|Client Assertion Signature Algorithm +|Signature algorithm to create JWT assertion as client authentication. +In the case of JWT signed with private key or Client secret as jwt, it is required. If no algorithm is specified, the following algorithm is adapted. `RS256` is adapted in the case of JWT signed with private key. `HS256` is adapted in the case of Client secret as jwt. + +|Client Assertion Audience +|The audience to use for the client assertion. The default value is the IDP's token endpoint URL. + +|Default Scopes +|A space separated list of scopes {project_name} sends with the authentication request. + +|Prompt +|The prompt parameter in the OIDC specification. Through this parameter, you can force re-authentication and other options. See the specification for more details. + +|Accepts prompt=none forward from client +|Specifies if the IDP accepts forwarded authentication requests containing the `prompt=none` query parameter. If a realm receives an auth request with `prompt=none`, the realm checks if the user is currently authenticated and returns a `login_required` error if the user has not logged in. When {project_name} determines a default IDP for the auth request (using the `kc_idp_hint` query parameter or having a default IDP for the realm), you can forward the auth request with `prompt=none` to the default IDP. The default IDP checks the authentication of the user there. Because not all IDPs support requests with `prompt=none`, {project_name} uses this switch to indicate that the default IDP supports the parameter before redirecting the authentication request. + +If the user is unauthenticated in the IDP, the client still receives a `login_required` error. If the user is authentic in the IDP, the client can still receive an `interaction_required` error if {project_name} must display authentication pages that require user interaction. This authentication includes required actions (for example, password change), consent screens, and screens set to display by the `first broker login` flow or `post broker login` flow. + +|Requires short state parameter +|This switch needs to be enabled if identity provider does not support long value of the `state` parameter sent in the initial OAuth2 authorization request (EG. more than 100 characters). In this case, {project_name} will try to make shorter `state` parameter and may omit some client data to be sent in the initial request. This may result in the limited functionality in some very corner case scenarios (EG. in case that IDP redirects to {project_name} with the error in the OAuth2 authorization response, {project_name} might need to display error page instead of being able to redirect to the client in case that login session is expired). + +|=== + +After the user authenticates to the identity provider and is redirected back to {project_name}, the broker will fetch the user profile information from the endpoint defined in the `User Info URL` setting. For that, +{project_name} will invoke that endpoint using the access token issued by the identity provider as a bearer token. Even though the OAuth2 standard supports access tokens using a JWT format, this broker assumes access tokens are opaque and that user profile information should be obtained from a separate endpoint. + +In order to map the claims from the JSON document returned by the user profile endpoint, you might want to set the following settings so that they are mapped to user attributes when federating the user: + +.User profile claims +|=== +|Configuration|Description + +|ID Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's unique identifier. If not provided, defaults to `sub`. + +|Username Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's username. If not provided, defaults to `preferred_username`. + +|Email Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's email. If not provided, defaults to `email`. + +|Name Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's full name. If not provided, defaults to `name`. + +|Given name Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's given name. If not provided, defaults to `given_name`. + +|Family name Claim +|The name of the claim from the JSON document returned by the user profile endpoint representing the user's family name. If not provided, defaults to `family_name`. + +|=== + +You can import all this configuration data by providing a URL or file that points to the Authorization Server Metadata. If you connect to a {project_name} external IDP, you can import the IDP settings from `{kc_realms_path}/{realm-name}/.well-known/openid-configuration`. This link is a JSON document describing metadata about the IDP. diff --git a/docs/documentation/server_admin/topics/identity-broker/oidc.adoc b/docs/documentation/server_admin/topics/identity-broker/oidc.adoc index d152e5912bad..439741dc5849 100644 --- a/docs/documentation/server_admin/topics/identity-broker/oidc.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/oidc.adoc @@ -63,6 +63,9 @@ In the case of JWT signed with private key or Client secret as jwt, it is requir If the user is unauthenticated in the IDP, the client still receives a `login_required` error. If the user is authentic in the IDP, the client can still receive an `interaction_required` error if {project_name} must display authentication pages that require user interaction. This authentication includes required actions (for example, password change), consent screens, and screens set to display by the `first broker login` flow or `post broker login` flow. +|Requires short state parameter +|This switch needs to be enabled if identity provider does not support long value of the `state` parameter sent in the initial OIDC authentication request (EG. more than 100 characters). In this case, {project_name} will try to make shorter `state` parameter and may omit some client data to be sent in the initial request. This may result in the limited functionality in some very corner case scenarios (EG. in case that IDP redirects to {project_name} with the error in the OIDC authentication response, {project_name} might need to display error page instead of being able to redirect to the client in case that login session is expired). + |Validate Signatures |Specifies if {project_name} verifies signatures on the external ID Token signed by this IDP. If *ON*, {project_name} must know the public key of the external OIDC IDP. For performance purposes, {project_name} caches the public key of the external OIDC identity provider. @@ -82,4 +85,4 @@ If the user is unauthenticated in the IDP, the client still receives a `login_re You can import all this configuration data by providing a URL or file that points to OpenID Provider Metadata. If you connect to a {project_name} external IDP, you can import the IDP settings from `{kc_realms_path}/{realm-name}/.well-known/openid-configuration`. This link is a JSON document describing metadata about the IDP. -If you want to use https://datatracker.ietf.org/doc/html/rfc7516[Json Web Encryption (JWE)] ID Tokens or UserInfo responses in the provider, the IDP needs to know the public key to use with {project_name}. The provider uses the <> defined for the different encryption algorithms to decrypt the tokens. {project_name} provides a standard xref:con-server-oidc-uri-endpoints_{context}[JWKS endpoint] which the IDP can use for downloading the keys automatically. \ No newline at end of file +If you want to use https://datatracker.ietf.org/doc/html/rfc7516[Json Web Encryption (JWE)] ID Tokens or UserInfo responses in the provider, the IDP needs to know the public key to use with {project_name}. The provider uses the <> defined for the different encryption algorithms to decrypt the tokens. {project_name} provides a standard xref:con-server-oidc-uri-endpoints_{context}[JWKS endpoint] which the IDP can use for downloading the keys automatically. diff --git a/docs/documentation/server_admin/topics/identity-broker/post-login-flow.adoc b/docs/documentation/server_admin/topics/identity-broker/post-login-flow.adoc new file mode 100644 index 000000000000..b0a5ba3d6a4e --- /dev/null +++ b/docs/documentation/server_admin/topics/identity-broker/post-login-flow.adoc @@ -0,0 +1,44 @@ +[[_identity_broker_post_login_flow]] + +=== Post login flow + +Post login flow is useful for the situations when you want to trigger some additional authentication actions after every login with the particular identity provider. +For example, you may want to trigger 2-factor authentication after every login of {project_name} to `Facebook` because `Facebook` does not provide 2-factor authentication during its login. + +Once you setup the authentication flow with the needed steps, set it as `Post login flow` when configuring the identity provider. + +==== Post login flow examples + +===== Requesting 2-factor authentication after identity provider login + +The easiest way is to enforce authentication with one particular 2-factor method. For example, when requesting OTP, the flow can look like this with only a single authenticator configured. +This type of flow asks the user to configure the OTP during the first login with the identity provide when the user does not have OTP set on the account. + +.2FA post login flow with OTP +image:images/post-login-flow-otp.png[Post login OTP] + +The more complex setup can include multiple 2-factor authentication methods configured as `ALTERNATIVE`. In this case, make sure that the user is requested to setup one of +the methods if that user does not yet have any 2-factor authentication configured on the account. This could be done as follows: + +* Make sure that one of the 2-factor methods is configured as `REQUIRED` in the <<_identity_broker_first_login, First login flow>>. This method can works if you expect all your users to be registered by +the identity provider login. + +* Wrap the 2-factor methods as `ALTERNATIVE` into a conditional subflow such as one called `2FA` and create another conditional subflow such as one called `OTP if no 2FA`, +which will be triggered only if the previous subflow was not executed and will ask user to add one of the 2-factor methods (for example, OTP). The example of a similar flow configuration is provided +in the <<_conditional-2fa-otp-default, Conditions section of the Authentication flows chapter>>. + +==== Requesting additional authentication steps for the dedicated clients + +In some cases, a client or group of clients may need to perform some additional steps after identity provider login. +The following is an example of a flow that prescribes that when the client scope `foo` is requested, the user is required to authenticate with the OTP after identity provider login. + +.2FA post login flow with client scope and OTP +image:images/post-login-flow-client-scope.png[Post login with client scope and OTP] + +This is an example of configuring the `Condition - client scope` for requesting the specified client scope. + +.2FA post login flow client scope configuration +image:images/post-login-flow-client-scope-config.png[Post login flow client scope configuration] + +The requested clients need to have this client scope set on them either +as default or as optional. In the latter case, the flow is executed only if the client scope is requested by the client (for example, by the `scope` parameter in the case of OIDC/OAuth2 client logins). diff --git a/docs/documentation/server_admin/topics/identity-broker/saml.adoc b/docs/documentation/server_admin/topics/identity-broker/saml.adoc index d7bd9cf8d299..e4b4c9fa09ed 100644 --- a/docs/documentation/server_admin/topics/identity-broker/saml.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/saml.adoc @@ -25,6 +25,9 @@ image:images/saml-add-identity-provider.png[Add Identity Provider] |Single Sign-On Service URL |The SAML endpoint that starts the authentication process. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there. +|Artifact service URL +|The SAML artifact resolution endpoint. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there. + |Single Logout Service URL |The SAML logout endpoint. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there. @@ -46,6 +49,9 @@ image:images/saml-add-identity-provider.png[Add Identity Provider] |HTTP-POST Binding Response |Controls the SAML binding in response to any SAML requests sent by an external IDP. When *OFF*, {project_name} uses Redirect Binding. +|ARTIFACT Binding Response +|Controls the SAML binding in response to any SAML requests sent by an external IDP. When *OFF*, {project_name} evaluates the HTTP-POST Binding Response configuration. + |HTTP-POST Binding for AuthnRequest |Controls the SAML binding when requesting authentication from an external IDP. When *OFF*, {project_name} uses Redirect Binding. @@ -84,8 +90,16 @@ itself. |Validate Signature |When *ON*, the realm expects SAML requests and responses from the external IDP to be digitally signed. -|Validating X509 Certificate -|The public certificate {project_name} uses to validate the signatures of SAML requests and responses from the external IDP. +|Metadata descriptor URL +|External URL where Identity Provider publishes the `IDPSSODescriptor` metadata. This URL is used to download the Identity Provider certificates when the `Reload keys` or `Import keys` actions are clicked. + +|Use metadata descriptor URL +|When *ON*, the certificates to validate signatures are automatically downloaded from the `Metadata descriptor URL` and cached in {project_name}. The SAML provider can validate signatures in two different ways. If a specific certificate is requested (usually in `POST` binding) and it is not in the cache, certificates are automatically refreshed from the URL. If all certificates are requested to validate the signature (`REDIRECT` binding) the refresh is only done after a max cache time (see https://www.keycloak.org/server/all-provider-config[public-key-storage] spi in the all provider config guide for more information about how the cache works). The cache can also be manually updated using the action `Reload Keys` in the identity provider page. + +When the option is *OFF*, the certificates in `Validating X509 Certificates` are used to validate signatures. + +|Validating X509 Certificates +|The public certificates {project_name} uses to validate the signatures of SAML requests and responses from the external IDP when `Use metadata descriptor URL` is *OFF*. Multiple certificates can be entered separated by comma (`,`). The certificates can be re-imported from the `Metadata descriptor URL` clicking the `Import Keys` action in the identity provider page. The action downloads the current certificates in the metadata endpoint and assigns them to the config in this same option. You need to click `Save` to definitely store the re-imported certificates. |Sign Service Provider Metadata |When *ON*, {project_name} uses the realm's key pair to sign the <<_identity_broker_saml_sp_descriptor, SAML Service Provider Metadata descriptor>>. diff --git a/docs/documentation/server_admin/topics/identity-broker/social/gitlab.adoc b/docs/documentation/server_admin/topics/identity-broker/social/gitlab.adoc index 25374cf65c12..57bb0fe3bbc9 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/gitlab.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/gitlab.adoc @@ -9,7 +9,7 @@ image:images/gitlab-add-identity-provider.png[Add Identity Provider] + . Copy the value of *Redirect URI* to your clipboard. -. In a separate browser tab, https://docs.gitlab.com/ee/integration/oauth_provider.html[add a new GitLab application]. +. In a separate browser tab, https://docs.gitlab.com/integration/oauth_provider/[add a new GitLab application]. .. Use the *Redirect URI* in your clipboard as the *Redirect URI*. .. Note the *Application ID* and *Secret* when you save the application. . In {project_name}, paste the value of the `Application ID` into the *Client ID* field. diff --git a/docs/documentation/server_admin/topics/identity-broker/social/instagram.adoc b/docs/documentation/server_admin/topics/identity-broker/social/instagram.adoc index 598efa96a69b..ab21a32cb94a 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/instagram.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/instagram.adoc @@ -1,6 +1,9 @@ ==== Instagram +IMPORTANT: The Instagram Identity Broker is deprecated for removal. Prefer using the Facebook Identity Broker instead. +To enable it, start the server with `--features=instagram-broker`. + .Procedure . Click *Identity Providers* in the menu. . From the *Add provider* list, select *Instagram*. @@ -25,7 +28,7 @@ image:images/meta-select-app-type.png[Select an app type] .Create an app image:images/meta-create-app.png[Create an app] + -.. Fill in all required fields. +.. Fill in all required fields. .. Click *Create app*. Meta then brings you to the dashboard. .. In the navigation panel, select *App settings* - *Basic*. .. Select *+ Add Platform* at the bottom of the page. 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..37114423f737 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/openshift.adoc @@ -1,48 +1,23 @@ +==== OpenShift 4 -==== OpenShift 3 +.Prerequisites +. 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. -. From the *Add provider* list, select *Openshift v3*. -+ -.Add identity provider -image:images/openshift-add-identity-provider.png[Add Identity Provider] -+ -. Copy the value of *Redirect URI* to your clipboard. -. Register your client using the `oc` command-line tool. +. Locate the Openshift 4 instance's API URL by using this command: + -[source,subs="attributes+"] +[source,bash,subs=+attributes] ---- -$ oc create -f <(echo ' -kind: OAuthClient -apiVersion: v1 -metadata: - name: kc-client <1> -secret: "..." <2> -redirectURIs: - - "http://www.example.com/" <3> -grantMethod: prompt <4> -') +oc cluster-info ---- - -<1> The `name` of your OAuth client. Passed as `client_id` request parameter when making requests to `__/oauth/authorize` and `__/oauth/token`. -<2> The `secret` {project_name} uses for the `client_secret` request parameter. -<3> The `redirect_uri` parameter specified in requests to `__/oauth/authorize` and `__/oauth/token` must be equal to (or prefixed by) one of the URIs in `redirectURIs`. You can obtain this from the *Redirect URI* field in the Identity Provider screen -<4> The `grantMethod` {project_name} uses to determine the action when this client requests tokens but has not been granted access by the user. +. Look for the URL in a line that has this format: + -. In {project_name}, paste the value of the *Client ID* into the *Client ID* field. -. In {project_name}, paste the value of the *Client Secret* into the *Client Secret* field. - -. Click *Add*. - -==== 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}. - -.Procedure -. Click *Identity Providers* in the menu. +[source,bash,subs=+attributes] +---- +Kubernetes master is running at https://api.:6443 +---- +. In the Admin Console, click *Identity Providers* in the menu. . From the *Add provider* list, select *Openshift v4*. . Enter the *Client ID* and *Client Secret* and in the *Base URL* field, enter the API URL of your OpenShift 4 instance. Additionally, you can copy the *Redirect URI* to your clipboard. + diff --git a/docs/documentation/server_admin/topics/identity-broker/social/paypal.adoc b/docs/documentation/server_admin/topics/identity-broker/social/paypal.adoc index de9f8b22183e..d20a5066bb94 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/paypal.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/paypal.adoc @@ -14,7 +14,7 @@ image:images/paypal-add-identity-provider.png[Add Identity Provider] .. Note the *Client ID* and *Client Secret*. Click the *Show* link to view the secret. .. Ensure *Log in with PayPal* is checked. .. Under Log in with PayPal click on *Advanced Settings*. -.. Set the value of the *Return URL* field to the value of *Redirect URI* from {project_name}. Note that the URL can not contain `localhost`. If you want to use {project_name} locally, replace the `localhost` in the *Return URL* by `127.0.0.1` and then access {project_name} using `127.0.0.1` in the browser intead of `localhost`. +.. Set the value of the *Return URL* field to the value of *Redirect URI* from {project_name}. Note that the URL can not contain `localhost`. If you want to use {project_name} locally, replace the `localhost` in the *Return URL* by `127.0.0.1` and then access {project_name} using `127.0.0.1` in the browser instead of `localhost`. .. Ensure *Full Name* and *Email* fields are checked. .. Click *Save* and then *Save Changes*. . In {project_name}, paste the value of the `Client ID` into the *Client ID* field. diff --git a/docs/documentation/server_admin/topics/identity-broker/social/stack-overflow.adoc b/docs/documentation/server_admin/topics/identity-broker/social/stack-overflow.adoc index 063cba7942fb..707aed95aa89 100644 --- a/docs/documentation/server_admin/topics/identity-broker/social/stack-overflow.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/social/stack-overflow.adoc @@ -1,7 +1,7 @@ [[_stackoverflow]] -==== Stack overflow +==== Stack Overflow .Procedure . Click *Identity Providers* in the menu. diff --git a/docs/documentation/server_admin/topics/identity-broker/suggested.adoc b/docs/documentation/server_admin/topics/identity-broker/suggested.adoc index 3c48e308f844..74bf35226645 100644 --- a/docs/documentation/server_admin/topics/identity-broker/suggested.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/suggested.adoc @@ -16,13 +16,17 @@ Host: localhost:8080 In this case, your realm must have an identity provider with a `facebook` alias. If this provider does not exist, the login form is displayed. -If you are using the `keycloak.js` adapter, you can also achieve the same behavior as follows: +If you are using the JavaScript adapter, you can also achieve the same behavior as follows: [source,javascript] ---- -const keycloak = new Keycloak('keycloak.json'); +const keycloak = new Keycloak({ + url: "http://keycloak-server", + realm: "my-realm", + clientId: "my-app" +); -keycloak.createLoginUrl({ +await keycloak.createLoginUrl({ idpHint: 'facebook' }); ---- diff --git a/docs/documentation/server_admin/topics/identity-broker/tokens.adoc b/docs/documentation/server_admin/topics/identity-broker/tokens.adoc index aac71ddbe5bf..02ece581ded3 100644 --- a/docs/documentation/server_admin/topics/identity-broker/tokens.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/tokens.adoc @@ -7,7 +7,7 @@ Application code can retrieve these tokens and responses to import extra user in [source,subs="attributes+"] ---- -GET {kc_realms_path}/{realm}/broker/{provider_alias}/token HTTP/1.1 +GET {kc_realms_path}/{realm-name}/broker/{provider_alias}/token HTTP/1.1 Host: localhost:8080 Authorization: Bearer ---- diff --git a/docs/documentation/server_admin/topics/login-settings/acr-to-loa-mapping.adoc b/docs/documentation/server_admin/topics/login-settings/acr-to-loa-mapping.adoc index 826f93558be8..c25056671ccd 100644 --- a/docs/documentation/server_admin/topics/login-settings/acr-to-loa-mapping.adoc +++ b/docs/documentation/server_admin/topics/login-settings/acr-to-loa-mapping.adoc @@ -1,7 +1,7 @@ [[_mapping-acr-to-loa-realm]] == ACR to Level of Authentication (LoA) Mapping -In the login settings of a realm, you can define which `Authentication Context Class Reference (ACR)` value is mapped to which `Level of Authentication (LoA)`. The ACR can be any value, whereas the LoA must be numeric. +In the general settings of a realm, you can define which `Authentication Context Class Reference (ACR)` value is mapped to which `Level of Authentication (LoA)`. The ACR can be any value, whereas the LoA must be numeric. The acr claim can be requested in the `claims` or `acr_values` parameter sent in the OIDC request and it is also included in the access token and ID token. The mapped number is used in the authentication flow conditions. Mapping can be also specified at the client level in case that particular client needs to use different values than realm. However, a best practice is to stick to realm mappings. 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 ab7171ca62cd..5b37e6e9feca 100644 --- a/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc +++ b/docs/documentation/server_admin/topics/login-settings/forgot-password.adoc @@ -1,4 +1,5 @@ +[[enabling-forgot-password]] == Enabling forgot password If you enable `Forgot password`, users can reset their login credentials if they forget their passwords or lose their OTP generator. @@ -17,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. + @@ -26,7 +27,7 @@ image:images/forgot-password-page.png[Forgot Password Page] The text sent in the email is configurable. See link:{developerguide_link}[{developerguide_name}] for more information. -When users click the email link, {project_name} asks them to update their password, and if they have set up an OTP generator, {project_name} asks them to reconfigure the OTP generator. Depending on security requirements of your organization, you may not want users to reset their OTP generator through email. +When users click the email link, {project_name} asks them to update their password, and if they have set up an OTP generator, {project_name} asks them to reconfigure the OTP generator. For security reasons, the flow forces federated users to login again after the reset credentials and keeps internal database users logged in if the same authentication session (same browser) is used. Depending on the security requirements of your organization, you can change the default behavior. To change this behavior, perform these steps: @@ -38,7 +39,12 @@ To change this behavior, perform these steps: .Reset credentials flow image:images/reset-credentials-flow.png[Reset Credentials Flow] + -If you do not want to reset the OTP, set the `Reset OTP` requirement to *Disabled*. +If you do not want to reset the OTP, set the `Reset - Conditional OTP` sub-flow requirement to *Disabled*. ++ +.Send Reset Email Configuration +image:images/reset-credential-email-config.png[Send Reset Email Configuration] ++ +If you want to change default behavior for the force login option, click the *Send Reset Email* settings icon in the flow, define an *Alias*, and select the best *Force login after reset* option for you (`true`, always force re-authentication, `false`, keep the user logged in if the same browser was used, `only-federated`, default value that forces login again only for federated users). . Click *Authentication* in the menu. . Click the *Required actions* tab. . Ensure *Update Password* is enabled. diff --git a/docs/documentation/server_admin/topics/login-settings/update-email-workflow.adoc b/docs/documentation/server_admin/topics/login-settings/update-email-workflow.adoc index 2939fa1cb49e..68254c64fc6c 100644 --- a/docs/documentation/server_admin/topics/login-settings/update-email-workflow.adoc +++ b/docs/documentation/server_admin/topics/login-settings/update-email-workflow.adoc @@ -2,29 +2,49 @@ == Update Email Workflow (UpdateEmail) -With this workflow, users will have to use an UPDATE_EMAIL action to change their own email address. +With this workflow, users will have to use an `UPDATE_EMAIL` action to change their own email address. -The action is associated with a single email input form. If the realm has email verification disabled, this action will allow to update the email without verification. If the realm has email verification enabled, the action will send an email update action token to the new email address without changing the account email. Only the action token triggering will complete the email update. +This action provides a more secure and consistent flow to update user emails because they will be forced to re-authenticate +as well as verify their emails before any update to their account. -Applications are able to send their users to the email update form by leveraging UPDATE_EMAIL as an AIA (Application Initiated Action). +Applications are able to send their users to the email update form by leveraging UPDATE_EMAIL as an <>. -ifeval::[{project_product}==true] -:tech_feature_name: UpdateEmail -:tech_feature_setting: -Dkeycloak.profile.feature.update_email=enabled -:tech_feature_id: update-email -include::../templates/techpreview.adoc[] -endif::[] +To enable `Update Email` capability for a realm, go to the `Authentication` menu in the administration console and click on `Required actions` tab. +Switch the toggle for `Update Email` required action to `enabled`. -ifeval::[{project_community}==true] +=== Verifying Emails -[IMPORTANT] -==== -Please note that Update Email Workflow support is in development. Use this feature experimentally. -==== +If the realm has email verification disabled, this action will allow to update the email without verification. -endif::[] +If the realm has email verification enabled, the action will send an email with a link to the new email address without changing the account email. +Only after following the link and confirming the email, the email will be updated. + +Under certain circumstances, you do not want to enable email verification at the realm level but only when users are updating their emails. +For that, you can set the `Force Email Verification` setting on the `Update Email` required action to force users to verify their emails +even though email verification is eventually disabled at the realm level. By default, email verification is not enabled. + +In case the user is updating the email during the authentication flow (e.g.: when running the `UPDATE_PROFILE` required action), +the user will be forced to verify the email if any of the `Verify Email` or the `Force Email Verification` settings are enabled. +In case the `Verify Email` is enabled at the realm level, the `VERIFY_EMAIL` required action will be automatically added to the user account. +Otherwise, if only the `Force Email Verification` is enabled the `UPDATE_EMAIL` required +action will be added instead. + +If a user has `Email Verified` set, and both `Verify Email` and `Force Email Verification` are disabled, `Email Verified` +resets after the user updates email. + +=== Updating the user email + +When the `Update Email` required action is enabled, the user can update their emails by: + +* Self-registering to a realm if this capability is enabled to realm +* Accessing the account console and clicking the `Update email` link when at the `Personal info` section +* Updating the profile during the authentication flow (e.g.: when running the `UPDATE_PROFILE` required action) if the email is not yet set. +If an existing user does have an email set when updating the profile during the authentication flow, the email attribute will not be available. +* Administrators when updating the user account through the administration console + +=== Update Email and User Profile + +If the email attribute is set as required in the user profile configuration, the requirement is kept in the `Update Email` workflow, +meaning a user won't be able to clear his/her email in update email page. The opposite is true, if the email attribute is set as optional +in the user profile configuration. -[NOTE] -==== -If you enable this feature and you are migrating from a previous version, enable the *Update Email* required action in your realms. Otherwise, users cannot update their email addresses. -==== diff --git a/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc b/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc new file mode 100644 index 000000000000..de285d885ea4 --- /dev/null +++ b/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc @@ -0,0 +1,474 @@ +[[_oid4vci]] +== Configuring {project_name} as a Verifiable Credential Issuer + +[IMPORTANT] +==== +This is an experimental feature and should not be used in production. Backward compatibility is not guaranteed, and future updates may introduce breaking changes. +==== + +{project_name} provides experimental support for https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html[OpenID for Verifiable Credential Issuance]. + +=== Introduction + +This chapter provides step-by-step instructions for configuring {project_name} as a Verifiable Credential Issuer using the OpenID for Verifiable Credential Issuance (OID4VCI) protocol. It outlines the process for setting up a {project_name} instance to securely issue and manage Verifiable Credentials (VCs), supporting decentralized identity solutions. + +=== What are Verifiable Credentials (VCs)? + +Verifiable Credentials (VCs) are cryptographically signed, tamper-evident data structures that represent claims about an entity, such as a person, organization, or device. They are foundational to decentralized identity systems, allowing secure and privacy-preserving identity verification without reliance on centralized authorities. VCs support advanced features like selective disclosure and zero-knowledge proofs, enhancing user privacy and security. + +=== What is OID4VCI? + +OpenID for Verifiable Credential Issuance (OID4VCI) is an extension of the OpenID Connect (OIDC) protocol. It defines a standardized, interoperable framework for credential issuers to deliver VCs to holders, who can then present them to verifiers. OID4VCI leverages {project_name}'s existing authentication and authorization capabilities to streamline VC issuance. + +=== Scope of This Chapter + +This chapter covers the following technical configurations: + +- Creating a dedicated realm for VC issuance. +- Setting up a test user for credential testing. +- Configuring custom cryptographic keys for signing and encrypting VCs. +- Defining realm attributes to specify VC metadata. +- Establishing client scopes and mappers to include user attributes in VCs. +- Registering a client to handle VC requests. +- Verifying the configuration using the issuer metadata endpoint. + +=== Prerequisites + +Ensure the following requirements are met before configuring {project_name} as a Verifiable Credential Issuer: + +=== {project_name} Instance + +A running {project_name} server with the OID4VCI feature enabled. + +To enable the feature, add the following flag to the startup command: + +[source,bash] +---- +--features=oid4vc-vci +---- + +Verify activation by checking the server logs for the `OID4VC_VCI` initialization message. + +=== Configuring Credential Issuance in Keycloak + +In {project_name}, Verifiable Credentials are managed through *ClientScopes*, with each ClientScope representing a single Verifiable Credential type. To enable the issuance of a credential, the corresponding ClientScope must be assigned to an OpenID Connect client - ideally as *optional*. + +During the OAuth2 authorization process, the credential-specific scope can be requested by including the ClientScope's name in the `scope` parameter of the authorization request. Once the user has successfully authenticated, the resulting Access Token *MUST* include the requested ClientScope in its `scope` claim. To ensure this, make sure the ClientScope option *Include in token scope* is enabled. + +With this Access Token, the Verifiable Credential can be issued at the Credential Endpoint. + +=== Authentication + +An access token is required to authenticate API requests. + +Refer to the following {project_name} documentation sections for detailed steps on: + +- <> +- <<_oidc-auth-flows-direct, Obtaining an Access Token>> + +=== Configuration Steps + +Follow these steps to configure {project_name} as a Verifiable Credential Issuer. Each section is detailed with procedures, explanations, and examples where applicable. + +=== Creating a Realm + +A realm in {project_name} is a logical container that manages users, clients, roles, and authentication flows. +For Verifiable Credential (VC) issuance, create a dedicated realm to ensure isolation and maintain a clear separation of functionality. + +[NOTE] +==== +For detailed instructions on creating a realm, refer to the {project_name} documentation: +<>. +==== + +=== Creating a User Account + +A test user is required to simulate credential issuance and verify the setup. + +[NOTE] +==== +For step-by-step instructions on creating a user, refer to the {project_name} documentation: +<>. +==== + +Ensure that the user has a valid username, email, and password. If the password should not be reset upon first login, disable the "Temporary" toggle during password configuration. + +=== Key Management Configuration + +{project_name} uses cryptographic keys for signing and encrypting Verifiable Credentials (VCs). To ensure secure and standards-compliant issuance, configure **ECDSA (ES256) for signing**, **RSA (RS256) for signing**, and **RSA-OAEP for encryption** using a keystore. + +[NOTE] +==== +For a detailed guide on configuring realm keys, refer to the {project_name} documentation: +<>. +==== + +==== Configuring Key Providers + +To enable cryptographic operations for VC issuance: + +- **ECDSA (ES256) Key**: Used for signing VCs with the ES256 algorithm. +- **RSA (RS256) Key**: Alternative signing mechanism using RS256. +- **RSA-OAEP Key**: Used for encrypting sensitive data in VCs. + +Each key must be registered as a **java-keystore provider** within the **Realm Settings** > **Keys** section, ensuring: +- The keystore file is correctly specified and securely stored. +- The appropriate algorithm (ES256, RS256, or RSA-OAEP) is selected. +- The key is active, enabled, and configured with the correct usage (signing or encryption). +- Priority values are set to define precedence among keys. + +[WARNING] +==== +Ensure the keystore file is **securely stored** and accessible to the {project_name} server. Use **strong passwords** to protect both the keystore and the private keys. +==== + +=== Registering Realm Attributes + +Realm attributes define metadata for Verifiable Credentials (VCs), such as **expiration times, supported formats, and scope definitions**. These attributes allow {project_name} to issue VCs with predefined settings. + +Since the **{project_name} Admin Console does not support direct attribute creation**, use the **{project_name} Admin REST API** to configure these attributes. + +==== Define Realm Attributes + +Create a JSON file (e.g., `realm-attributes.json`) with the following content: + +[source,json] +---- +{ + "realm": "oid4vc-vci", + "enabled": true, + "attributes": { + "preAuthorizedCodeLifespanS": 120 + } +} +---- + +==== Attribute Breakdown + +The attributes section contains issuer-specific metadata: +- **preAuthorizedCodeLifespanS** – Defines how long pre-authorized codes remain valid (in seconds). + +==== Import Realm Attributes + +Use the following `curl` command to import the attributes into {project_name}: + +[source,bash] +---- +curl -X PUT "https://localhost:8443/admin/realms/oid4vc-vci" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d @realm-attributes.json +---- + +[NOTE] +==== +- Replace `$ACCESS_TOKEN` with a valid **{project_name} Admin API access token**. +- **Avoid using `-k` in production**; instead, configure a **trusted TLS certificate**. +==== + +=== Create Client Scopes with Mappers + +Client scopes define **which user attributes** are included in Verifiable Credentials (VCs). Therefore, they are considered the Verifiable Credential configuration itself. These scopes use **protocol mappers** to map specific claims into VCs and the protocol mappers will also contain the corresponding metadata for claims that is displayed at the Credential Issuer Metadata Endpoint. + +You can create the ClientScopes using the {project_name} web Administration Console, but the web Administration Console does not yet support adding metadata configuration. For metadata configuration, you will need to use the Admin REST API. + +==== Define a Client Scope with a Mapper + +Create a JSON file (e.g., `client-scopes.json`) with the following content: + +[source,json] +---- +{ + "name": "vc-scope-mapping", + "protocol": "oid4vc", + "attributes": { + "include.in.token.scope": "true", + "vc.issuer_did": "did:web:vc.example.com", + "vc.credential_configuration_id": "my-credential-configuration-id", + "vc.credential_identifier": "my-credential-identifier", + "vc.format": "jwt_vc", + "vc.expiry_in_seconds": 31536000, + "vc.verifiable_credential_type": "my-vct", + "vc.supported_credential_types": "credential-type-1,credential-type-2", + "vc.credential_contexts": "context-1,context-2", + "vc.proof_signing_alg_values_supported": "ES256", + "vc.cryptographic_binding_methods_supported": "jwk", + "vc.signing_key_id": "key-id-123456", + "vc.display": "[{\"name\": \"IdentityCredential\", \"logo\": {\"uri\": \"https://university.example.edu/public/logo.png\", \"alt_text\": \"a square logo of a university\"}, \"locale\": \"en-US\", \"background_color\": \"#12107c\", \"text_color\": \"#FFFFFF\"}]", + "vc.sd_jwt.number_of_decoys": "2", + "vc.credential_build_config.sd_jwt.visible_claims": "iat,jti,nbf,exp,given_name", + "vc.credential_build_config.hash_algorithm": "SHA-256", + "vc.credential_build_config.token_jws_type": "JWS", + "vc.include_in_metadata": "true" + }, + "protocolMappers": [ + { + "name": "academic_title-mapper-bsk", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-static-claim-mapper", + "config": { + "claim.name": "academic_title", + "staticValue": "N/A" + } + }, + { + "name": "givenName", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-user-attribute-mapper", + "config": { + "claim.name": "given_name", + "userAttribute": "firstName", + "vc.mandatory": "false", + "vc.display": "[{\"name\": \"الاسم الشخصي\", \"locale\": \"ar-SA\"}, {\"name\": \"Vorname\", \"locale\": \"de-DE\"}, {\"name\": \"Given Name\", \"locale\": \"en-US\"}, {\"name\": \"Nombre\", \"locale\": \"es-ES\"}, {\"name\": \"نام\", \"locale\": \"fa-IR\"}, {\"name\": \"Etunimi\", \"locale\": \"fi-FI\"}, {\"name\": \"Prénom\", \"locale\": \"fr-FR\"}, {\"name\": \"पहचानी गई नाम\", \"locale\": \"hi-IN\"}, {\"name\": \"Nome\", \"locale\": \"it-IT\"}, {\"name\": \"名\", \"locale\": \"ja-JP\"}, {\"name\": \"Овог нэр\", \"locale\": \"mn-MN\"}, {\"name\": \"Voornaam\", \"locale\": \"nl-NL\"}, {\"name\": \"Nome Próprio\", \"locale\": \"pt-PT\"}, {\"name\": \"Förnamn\", \"locale\": \"sv-SE\"}, {\"name\": \"مسلمان نام\", \"locale\": \"ur-PK\"}]" + } + } + ] +} +---- + +[NOTE] +==== +This is a **sample configuration**. +You can define **additional protocol mappers** to support different claim mappings, such as: + +- Dynamic attribute values instead of static ones. +- Mapping multiple attributes per credential type. +- Alternative supported credential types. +==== + +From the example above: + +- It is important to set `include.in.token.scope=true`, see <>. +- Most of the named attributes above are optional. See below: <>. +- You can determine the appropriate `protocolMapper` names by first creating them through the Web Administration Console and then retrieving their definitions via the Admin REST API. + +==== Attribute Breakdown - ClientScope [[client-scope-attribute-breakdown]] + +[cols="1,1,2", options="header"] +|=== +| Property +| Required +| Description / Default + +| `name` +| required +| Name of the client scope. + +| `protocol` +| required +| Protocol used by the client scope. Use `oid4vc` for OpenID for Verifiable Credential Issuance, which is an OAuth2 extension (like `openid-connect`). + +| `include.in.token.scope` +| required +| [[include.in.token.scope]] This value MUST be `true`. It ensures that the scope’s name is included in the `scope` claim of the issued Access Token. + +| `protocolMappers` +| optional +| Defines how claims are mapped into the credential and how metadata is exposed via the issuer’s metadata endpoint. + +| `vc.issuer_did` +| optional +| The Decentralized Identifier (DID) of the issuer. + +_Default_: `$\{name}` + +| `vc.credential_configuration_id` +| optional +| The credentials configuration ID. + +_Default_: `$\{name}+` + +| `vc.credential_identifier` +| optional +| The credentials identifier. + +_Default_: `$\{name}+` + +| `vc.format` +| optional +| Defines the VC format (e.g., `jwt_vc`). + +_Default_: `vc+sd-jwt` + +| `vc.verifiable_credential_type` +| optional +| The Verifiable Credential Type (VCT). + +_Default_: `$\{name}+` + +| `vc.supported_credential_types` +| optional +| The type values of the Verifiable Credential Type. + +_Default_: `$\{name}+` + +| `vc.credential_contexts` +| optional +| The context values of the Verifiable Credential Type. + +_Default_: `$\{name}+` + +| `vc.proof_signing_alg_values_supported` +| optional +| Supported signature algorithms for this credential. + +_Default_: All present keys supporting JWS algorithms in the realm. + +| `vc.cryptographic_binding_methods_supported` +| optional +| Supported cryptographic methods (if applicable). + +_Default_: `jwk` + +| `vc.signing_key_id` +| optional +| The ID of the key to sign this credential. + +_Default_: _none_ + +| `vc.display` +| optional +| Display information shown in the user's wallet about the issued credential. + +_Default_: _none_ + +| `vc.sd_jwt.number_of_decoys` +| optional +| Used only with format `vc+sd-jwt`. Number of decoy hashes in the SD-JWT. + +_Default_: `10` + +| `vc.credential_build_config.sd_jwt.visible_claims` +| optional +| Used only with format `vc+sd-jwt`. Claims always disclosed in the SD-JWT body. + +_Default_: `id,iat,nbf,exp,jti` + +| `vc.credential_build_config.hash_algorithm` +| optional +| Hash algorithm used before signing the credential. + +_Default_: `SHA-256` + +| `vc.credential_build_config.token_jws_type` +| optional +| JWT type written into the `typ` header of the token. + +_Default_: `JWS` + +| `vc.expiry_in_s` +| optional +| Credential expiration time in seconds. + +_Default_: `31536000` (one year) + +| `vc.include_in_metadata` +| optional +| If this claim should be listed in the credentials metadata. + +_Default_: `true` but depends on the mapper-type. Claims like `jti`, `nbf`, `exp`, etc. are set to `false` by default. +|=== + +==== Attribute Breakdown - ProtocolMappers + +- **name** – Mapper identifier. +- **protocol** – Must be `oid4vc` for Verifiable Credentials. +- **protocolMapper** – Specifies the claim mapping strategy (e.g., `oid4vc-static-claim-mapper`). +- **config**: contains the protocol-mappers specific attributes. + +Most claims are dependent on the `protocolMapper`-value, but there are also commonly used claims available for all ProtocolMappers: + +[cols="1,1,2", options="header"] +|=== +| Property +| Required +| Description / Default + +| `claim.name` +| required +| The name of the attribute that will be added into the Verifiable Credential. + +_Default_: _none_ + +| `userAttribute` +| required +| The name of the users-attribute that will be used to map the value into the `claim.name` of the Verifiable Credential. + +_Default_: _none_ + +| `vc.mandatory` +| optional +| If the credential must be issued with this claim. + +_Default_: `false` + +| `vc.display` +| optional +| Metadata information that is displayed at the credential-issuer metadata-endpoint. + +_Default_: _none_ +|=== + +==== Import the Client Scope + +Use the following `curl` command to import the client scope into {project_name}: + +[source,bash] +---- +curl -X POST "https://localhost:8443/admin/realms/oid4vc-vci/client-scopes" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d @client-scopes.json +---- + +[NOTE] +==== +- Replace `$ACCESS_TOKEN` with a valid **{project_name} Admin API access token**. +- **Avoid using `-k` in production**; instead, configure a **trusted TLS certificate**. +- If updating an existing scope, use `PUT` instead of `POST`. +==== + +=== Create the Client + +Set up a client to handle Verifiable Credential (VC) requests and assign the necessary scopes. +The client does not differ from regular OpenID Connect clients — with one exception: it must have the appropriate **optional ClientScopes** assigned that define the Verifiable Credentials it is allowed to issue. + +. Create a JSON file (e.g., `oid4vc-rest-api-client.json`) with the following content: ++ +[source,json] +---- +{ + "clientId": "oid4vc-rest-api", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["http://localhost:8080/*"], + "directAccessGrantsEnabled": true, + "defaultClientScopes": ["profile"], + "optionalClientScopes": ["vc-scope-mapping"], + "attributes": { + "client.secret.creation.time": "1719785014", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "login_theme": "keycloak", + "post.logout.redirect.uris": "http://localhost:8080" + } +} +---- ++ +- **clientId**: Unique identifier for the client. +- **optionalClientScopes**: Links the `vc-scope-mapping` scope for VC requests. + +. Import the client using the following `curl` command: ++ +[source,bash] +---- +curl -k -X POST "https://localhost:8443/admin/realms/oid4vc-vci/clients" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d @oid4vc-rest-api-client.json +---- + +=== Verify the Configuration + +Validate the setup by accessing the **issuer metadata endpoint**: + +. Open a browser or use a tool like `curl` to visit: ++ +[source,bash] +---- +https://localhost:8443/realms/oid4vc-vci/.well-known/openid-credential-issuer +---- + +A successful response returns a JSON object containing details such as: +- **Supported claims** +- **Credential formats** +- **Issuer metadata** + +=== Conclusion + +You have successfully configured **{project_name} as a Verifiable Credential Issuer** using the **OID4VCI protocol**. +This setup leverages {project_name}'s robust **identity management capabilities** to issue secure, **standards-compliant VCs**. + +For a **complete reference implementation**, see the sample project: +https://github.com/adorsys/{project_name}-ssi-deployment/tree/main[{project_name} SSI Deployment^]. diff --git a/docs/documentation/server_admin/topics/organizations/authenticating-members.adoc b/docs/documentation/server_admin/topics/organizations/authenticating-members.adoc new file mode 100644 index 000000000000..80caa3edaaf6 --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/authenticating-members.adoc @@ -0,0 +1,123 @@ +[id="authenticating-members_{context}"] + += Authenticating members +[role="_abstract"] +When you enable organizations for a realm, user authentication is changed. If the user is recognized to be +authenticating in the context of an organization, the authentication flow changes on a per-organization basis. + +When a realm is created, the authentication flows are automatically updated to enable specific steps to authenticate and onboard organization members. The authentication flows updated are: + +* *browser* + +* *first broker login* + +The main change to the *browser* flow is that it defaults to an identity-first login so that users are identified before prompting for their credentials. +Concerning the first broker login flow, the main change is automatically adding the users as organization members once they authenticate through the identity provider associated with an organization and successfully complete the flow. + +The choice to use an identity-first login or not depends on the existence of an organization in a realm. +If no organizations exist, the user follows the usual steps to authenticate using the username and password, +or any other step configured in the browser flow. Otherwise, the user is asked first for a username or email to continue authenticating to a realm. + +The identity-first login main goal is to identify the user: + +* Is the user an existing or a new user? +* Is the user a member of any organization within a realm? +* If an organization member, is the user linked to any identity provider associated with the organization? + +Depending on the outcome when identifying the user, the authentication flow changes to either proceed with authentication +by asking for the user's credentials or eventually redirect the user automatically to authenticate within the organization security +boundaries through an identity provider. + +== Understanding the identity-first login + +In addition to identifying the user once the username is provided, the identity-first login is also responsible for: + +* Matching an email domain to an organization. +* Deciding if the authentication flow should continue or not if an account already exists for the username provided +* Deciding how the user should be authenticated depending on how the domains and the identity providers are configured to an organization +* Seamlessly authenticating users through an identity provider associated with an organization if the email domain matches the domain set to the identity provider + +The identity-first login provides the same capabilities that are provided by the usual login page with the username and +password fields. Users can still self-register by clicking the register link or choose any identity or social +broker that is not linked to an organization in that realm. + +.Identity-first login page +image:images/organizations-identity-first-login.png[alt="Identity-first login page"] + +In the case of a user that does not exist, if that user tries to authenticate using an email domain that matches an organization domain, +the identity-first login page appears again with a message that the username provided is not valid. +At this point, no need exists to ask the user for credentials. + +.Identity-first when user does not exist +image:images/organizations-identity-first-error.png[alt="Identity-first login error"] + +Several options exist to register the user allowing that user to authenticate to the realm and join an organization. + +If the realm has the self-registration setting enabled, the user can click the *Register* link at the identity-first login page +and create an account at the realm. +After that, the administrator can send an invitation link to the user or manually add the user as a member of an organization. +For more details, see <<_managing_members_,Managing members>>. + +If the organization has an identity provider without a domain and the *Hide on login page* setting is *OFF*, users can also click +the identity provider link at the identity-first login page to automatically create an account and join an organization +once they authenticate through the identity provider. +For more details, see <<_managing_identity_provider_,Managing identity providers>>. + +In a similar situation to the previous section, the organization may have an identity provider set with one of the organization domains. +In this situation, the user is redirected to the identity provider if that user's email matches a specific domain from the organization. +Once the flow completes, an account is created and the user joins the organization. + +== Configuring existing authentication flows + +As previously mentioned for new realms, authentication flows are automatically updated with the necessary steps +to support organizations and authenticate their members. For existing realms, in addition to enabling organizations to the +realm, you also need to manually update your existing (custom) authenticating flows. + +Change the *browser* flow by following these steps: + +.Procedure +. Duplicate the current flow bound to the *Browser flow* binding type to avoid breaking the flow you are currently using +. Click *Add sub-flow* and give it a name such as *My Organization* +. Move the newly added *My Organization* sub-flow to execute right after the *Identity Provider Redirector* execution step. +The main point here is that the sub-flow should happen before any other sub-flow or execution step that authenticates the +user using whatever credentials you support in your realm. Once added, change the *Requirement* to *Alternative*. +. Click *Add sub-flow* in the *My Organization* sub-flow and give it a name such as *My Organization - Conditional*. Once added, change the *Requirement* to *Conditional*. +. Click *Add condition* in the *My Organization - Conditional* sub-flow and select *Condition - user configured*. Once added, change the *Requirement* to *Required*. +. Click *Add step* in the *My Organization - Conditional* sub-flow and select the *Organization Identity-First Login +* execution step. Once added, change the *Requirement* to *Alternative*. +. Bind the authentication flow to the *Browser* binding type. + +.Organizations browser flow +image:images/organizations-browser-flow.png[alt="Organizations browser flow"] + +Once you enable the <<_enabling_organization_,Organizations>> setting to the realm and create +at least a single organization, you should be able to see the identity-first login page and start using organizations +in your realm. + +Change the *first broker login* flow by following these steps: + +.Procedure +. Duplicate the current flow bound to the *First broker login flow* bind type to avoid breaking the flow you are currently using +. Click *Add sub-flow* and give it a name such as `Organization Member - Conditional`. Once added, change the *Requirement* to *Conditional*. +. Click *Add condition* in the *Organization Member - Conditional* sub-flow and select *Condition - user configured*. Once added, change the *Requirement* to *Required*. +. Click *Add step* in the *Organization Member - Conditional* sub-flow and select the *Organization Member Onboard* execution step. Once added, change the *Requirement* to *Required*. +. Bind the authentication flow to the *First broker login* binding type. + +.Organizations first broker flow +image:images/organizations-first-broker-flow.png[alt="Organizations first broker flow"] + +You should now be able to authenticate using any identity provider associated with an organization +and have the user joining the organization as a member as soon as they complete the first browser login flow. + +== Configuring how users authenticate + +If the flow supports organizations, you can configure some of the steps to change how users authenticate to the realm. + +For example, some use cases will require users to authenticate to a realm only if they are a member of any or a specific organization in the realm. + +To enable this behavior, you need to enable the `Requires user membership` setting on the `Organization Identity-First Login` execution step by clicking on its settings. + +If enabled, and after the user provides the username or email in the identity-first login page, the server will +try to resolve a organization where the user is a member by looking at any existing membership or based on the semantics of the <<_mapping_organization_claims_,organization>> scope, +if requested by the client. If not a member of an organization, an error page will be shown. + diff --git a/docs/documentation/server_admin/topics/organizations/intro.adoc b/docs/documentation/server_admin/topics/organizations/intro.adoc new file mode 100644 index 000000000000..6a360f970c1e --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/intro.adoc @@ -0,0 +1,21 @@ +[role="_abstract"] +When integrating with a third party like a customer or business partner, you might want to manage their identities +separately from others and build a unified and secure experience throughout your business ecosystem when they interact +with a realm. + +In a realm, an *organization* represents these third parties so that a realm or an organization administrator can manage +the entire lifecycle of its members and how they authenticate and authorize to a realm, on a per-organization basis. + +The organization is the entry point to start using the IAM capabilities of {project_name} to also address Business-to-Business (B2B) use cases. +It enables multi-tenancy within a realm so that users can have access to protected resources from a realm but with a more restricted +and controlled context, that context being the organization to which they belong. + +{project_name} Organizations is a feature that enables support for organizations in {project_name}. For now, it provides +some of the core capabilities needed to manage organizations such as: + +* Manage members +* Onboard organization members using invitation links +* Onboard organization members by federating their identities through identity brokering +* Identity-first login and organization-specific steps when authenticating in the scope of an organization +* Propagate organization-specific claims to applications through tokens for authorization purposes + diff --git a/docs/documentation/server_admin/topics/organizations/managing-attributes.adoc b/docs/documentation/server_admin/topics/organizations/managing-attributes.adoc new file mode 100644 index 000000000000..8a8d4be20157 --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/managing-attributes.adoc @@ -0,0 +1,15 @@ +[id="managing-organization-attributes_{context}"] + +[[_managing_attributes_]] += Managing attributes +[role="_abstract"] + +An administrator can store additional metadata about an organization using attributes. An organization attribute is a key/value pair that can hold multiple string values. + +For that, click the *Attributes* tab and set any attribute you want by providing a key and a value. + +To provide multiple values for the same attribute, and key, just add another attribute with the same key but with a different value. + +.Managing organization attributes +image:images/organizations-manage-attributes.png[alt="Managing organization attributes"] + diff --git a/docs/documentation/server_admin/topics/organizations/managing-identity-providers.adoc b/docs/documentation/server_admin/topics/organizations/managing-identity-providers.adoc new file mode 100644 index 000000000000..103417681284 --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/managing-identity-providers.adoc @@ -0,0 +1,85 @@ +[id="managing-organization-identity-providers_{context}"] + +[[_managing_identity_provider_]] += Managing identity providers +[role="_abstract"] + +An organization might have its own identity provider as the single source of truth for their identities. In this case, +you want to configure the organization to authenticate users using the organization's identity provider, federate their +identities, and finally add them as a member of the organization. + +An organization can have one or more identity providers associated with it so that they can authenticate their users from +different sources and enforce different constraints on each of them. + +Before you can link an identity provider to an organization, you create an organization at the realm level from the *Identity Providers* +section in the menu. You can link any of the built-in social and identity providers available in the realm to an organization. + +== Linking an identity provider to an organization + +An identity provider can be linked to an organization from the *Identity providers* tab. If identity providers already exist, you see a list of them and options to search, edit, or unlink from the organization. + +.Organization identity providers +image:images/organizations-identity-providers.png[alt="Organization identity providers"] + +.Procedure + +. Click *Link identity provider* +. Select an *Identity provider* +. Set the appropriate settings +. Click *Save* + +.Linking identity provider +image:images/organizations-link-identity-provider.png[alt="Linking identity provider"] + +An identity provider has the following settings: + +Identity provider:: +The identity provider you want to link to the organization. An identity provider can only be linked to a single organization. + +Domain:: +The domain from the organization that you want to link with the identity provider. + +Hide on login page:: +If this identity provider should be hidden in login pages when the user is authenticating in the scope of the organization. + +Redirect when email domain matches:: +If members should be automatically redirected to the identity provider when their email domain matches the domain set to the identity provider. If the domain is set to `Any`, members whose email domain matches *any* of the organization domains will be redirected to the identity provider. + +If the org is linked with multiple identity providers, the organization authenticator prioritizes the provider that matches the email domain of the user for automatic redirection. If none is found, it tries to locate one whose domain is set to `Any`. + +Once linked to an organization, the identity provider can be managed just like any other in a realm by accessing the *Identity Providers* section in the menu. However, the options herein described are only available when managing the identity provider in the scope of an organization. The only exception is the + *Hide on login page* option that is present here for convenience. + +== Editing a linked identity provider + +You can edit any of the organization-related settings of a linked identity provider at any time. + +.Procedure + +. In the menu, click *Organizations* and go to the *Identity providers* tab. +. Locate the *identity provider* in the list. ++ +You can use the search option for this step. +. Click the action button (three dots) at the end of the line. +. Click *Edit*. +. Make the necessary changes. +. Click *Save*. + +.Editing linked identity provider +image:images/organizations-edit-identity-provider.png[alt="Editing linked identity provider"] + +== Unlinking an identity provider from an organization + +When an identity provider is unlinked from an organization, it remains available as a realm-level provider that is no longer ssociated with an organization. To delete the unlinked provider, use the *Identity Providers* section in the menu. + +.Procedure + +. In the menu, click *Organizations* and go to the *Identity providers* tab. +. Locate the *identity provider* in the list. ++ +You can use the search capabilities for this step. +. Click the action button (three dots) at the end of the line. +. Click *Unlink provider*. + +.Unlinking identity provider +image:images/organizations-unlink-identity-provider.png[alt="Unlinking identity provider"] diff --git a/docs/documentation/server_admin/topics/organizations/managing-members.adoc b/docs/documentation/server_admin/topics/organizations/managing-members.adoc new file mode 100644 index 000000000000..8fbfea10833a --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/managing-members.adoc @@ -0,0 +1,129 @@ +[id="managing-organization-members_{context}"] + +[[_managing_members_]] += Managing members +[role="_abstract"] + +An organization member is basically a realm user but with a link to a single organization. They are logically separated +from other users in a realm so that you know exactly which users belong to an organization. + +There are different ways to add, or onboard, a member to an organization: + +* Adding an existing realm user as a member +* Through an identity provider associated with an organization +* Sending an invitation to create a new account +* Sending an invitation to an existing user to join an organization + +Once a member of an organization, that user's account can be managed just like any regular account in a realm by accessing the *Users* section in the menu. + +However, you can narrow the users to only those associated with an organization by accessing the *Members* tab when managing an organization. In this tab, you have a list of all the organization members and actions to add new members and to edit and remove existing ones. + +.Managing organization members +image:images/organizations-manage-members.png[alt="Managing organization members"] + +[[_managed_unmanaged_members_]] +== Managed and unmanaged members + +When managing members, consider how their relationship with an organization affects the lifecycle of their accounts. +Members can join an organization through different flows and each flow indicates the strength of the link between their accounts and the organization. + +There are two types of members: + +* *Managed* +* *Unmanaged* + +Managed members are those managed by the organization, and they cannot exist outside of their organization. For instance, consider +an account created through an identity provider associated with an organization. That account does not belong to a realm as it was federated from the organization. +In this case, the single source of truth for the identity is the organization and its lifecycle is controlled +by the organization. +If you remove the organization or the member, the account is also removed from the realm. + +On the other hand, unmanaged members are those that can exist without the organization. For instance, when adding an existing +realm user to an organization, the account belongs to the realm first and foremost and eventually linked to an organization. In this case, +removing an organization or a member will not remove the account from the realm; the realm is +the single-source of truth for the identity. + +== Adding an existing realm user as a member + +An existing realm user can join an organization by selecting that user from a list and adding the user to the organization. + +.Procedure + +. Click *Add member*. +. Click *Add realm user*. +. Select one or more users and click *Add* to add them to the organization. + +.Adding a realm user +image:images/organizations-add-realm-user.png[alt="Adding a realm user"] + +Once a user is a member of the organization, that user is able to authenticate to the realm just like a regular user and using +any credential supported by the realm. + +== Inviting users + +An administrator can email users to join an organization. + +.Procedure + +. Click *Add member*. +. Click *Invite member*. +. Provide an email address +. Click *Send* + +.Inviting members +image:images/organizations-invite-member.png[alt="Inviting members"] + +Optionally, you can also provide a value for the *First name* and *Last name* fields for a more personalized email +message using a greeting message with the first and last names of the person receiving the email. + +An invitation is basically an email sent with a link that the person should click to perform the necessary steps to join +an organization. These steps depend on whether the person already has an account in the realm or if a new account should +be created before joining the organization. + +If the email maps to an existing user in a realm, the steps the user will follow are basically about confirming the +intention to join the organization. + +On the other hand, if no user is associated with the given email address, the steps +will involve creating a new account through the realm's self-registration flow. In this case, the user is forced +to provide the same email address used to send the invitation. + +[[_onboard_member_identity_provider_]] +== Onboarding members using an Identity Provider + +An organization might have its own identity provider as the single source of truth for their identities. In this case, +users federated from the identity provider are automatically added as a member of the organization. + +When users join an organization through an identity provider associated with an organization, they are automatically marked +as managed members. In this case, they will go through the broker login flows configured in the realm and join the organization +automatically once they successfully authenticate. + +Onboarding new members through an identity provider can be done by either automatically redirecting the user to an organization's +identity provider or by selecting the identity provider when at the login page. + +In both cases, once the user provides the email, {project_name} will try to match an organization based on the email domain. In case +the email domain matches the organization, and an identity provider is associated with the same domain and the *Redirect when email domain matches* +setting is enabled, the user is automatically redirected to the identity provider. Once the user authenticates at the identity provider +and completes the first broker login flow, the user is automatically added as an organization member. + +On the other hand, if *Redirect when email domain matches* is not enabled, but the identity provider is configured not to +*Hide on login page*, the user can select the identity provider and then be redirected to the identity provider to continue +the onboarding process. + +For more details, see <<_managing_identity_provider_,Managing Identity Providers>>. + +== Removing a member + +You can remove a member from an organization. + +From the action menu next to the member you want to remove, click *Remove*. + +When removing a member from an organization, remember that the user may or may not be removed from a realm depending on if +that user is managed or unmanaged member, respectively. + +For more details, see <<_managed_unmanaged_members_,Managed and unmanaged members>>. + +== Support for federated members + +Users coming from federated providers can also be added as members of an organization. The only exceptions are the users from LDAP providers with *import mode disabled*. Organization members are added to an internal group that is not synchronized with external providers, so even if the LDAP provider has a group mapper with mode LDAP_ONLY it won't be possible for the non-imported users to be added as members of an organization because that membership won't be synced with the LDAP server. + +In other words, LDAP users that are not imported can't join an organization because the membership is not stored in the local DB nor in the LDAP server. So if you want to have LDAP users joining organizations, ensure that the import mode of the LDAP provider is enabled. diff --git a/docs/documentation/server_admin/topics/organizations/managing-organization.adoc b/docs/documentation/server_admin/topics/organizations/managing-organization.adoc new file mode 100644 index 000000000000..ec9fa6f80a65 --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/managing-organization.adoc @@ -0,0 +1,95 @@ +[id="managing-organization_{context}"] + +[[_enabling_organization_]] += Enabling organizations in {project_name} + +To use organizations, you have to enable the feature for the current realm. + +.Procedure + +. Click *Realm Settings* in the menu. + +. Toggle *Organizations* to *On*. + +. Click *Save* + +.Enabling Organizations +image:images/organizations-enabling-orgs.png[alt="Enabling Organizations"] + +Once the feature is enabled, you are able to manage organizations through the *Organizations* section available from the menu. + += Managing an organization +[role="_abstract"] + +From the *Organizations* section, you can manage all the organizations in your realm. + +.Managing organizations +image:images/organizations-management-screen.png[alt="Managing organizations"] + +== Creating an organization + +.Procedure + +. Click *Create Organization*. + +.Creating organization +image:images/organizations-create-org.png[alt="Creating organization"] + +An organization has the following settings: + +Name:: +A user-friendly name for the organization. The name is unique within a realm. + +Alias:: +An alias for this organization, used to reference the organization internally. The alias is unique within a realm and must be URL-friendly, so characters not usually allowed in URLs will not be allowed in the alias. If not set, {project_name} will attempt to use the name as the alias. If the name is not URL-friendly, you will get an error and will be asked to specify an alias. Once defined, the alias cannot be changed afterwards. + +Redirect URL:: +After completing registration or accepting an invitation to the organization sent via email, the user is automatically redirected to the specified redirect url. If left empty, the user will be redirected to the account console by default. + +Domains:: +A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations within a realm. + +Description:: +A free-text field to describe the organization. + +Once you create an organization, you can manage the additional settings that are described in the following sections: + +* <<_managing_attributes_,Manage attributes>> +* <<_managing_members_,Manage members>> +* <<_managing_identity_provider_,Manage identity providers>> + +== Understanding organization domains + +When managing an organization, the domain associated with an organization plays an important role in how +organization members authenticate to a realm and how their profiles are validated. + +One of the key roles of a domain is to help to identify the organizations where a user is a member. By looking at their email address, {project_name} will match a corresponding organization using the same domain and eventually change the authentication flow based on the organization requirements. + +The domain also allows organizations to enforce that users are not allowed to use a domain in their emails +other than those associated with an organization. This restriction is especially useful when users, and their identities, are federated from identity providers associated with an organization and you want to force a specific email domain for their email addresses. + +== Disabling an organization + +To disable an organization, toggle *Enabled* to *Off*. + +.Disabling organization +image:images/organizations-disable-org.png[alt="Disabling organization"] + +When an organization is disabled, you can still manage it through the management interfaces, but the organization members cannot authenticate to the realm, including authenticating through the identity providers associated with the organization as they are also automatically disabled. + +However, the unmanaged members of an organization are still able to authenticate to the realm as they are also realm users, but tokens will not hold metadata about their relationship with an organization that is disabled. + +For more details about managed and unmanaged users, see <<_managed_unmanaged_members_,Managed and unmanaged members>> section. + +== Deleting an organization + +To delete an organization, click the *Delete* action for the corresponding organization in the listing page or when editing an organization. + +.Deleting organization +image:images/organizations-delete-org.png[alt="Deleting organization"] + +When removing an organization, all data associated with it will be deleted, including any managed member. + +Unmanaged users and identity providers remain in the realm, but they are no longer linked to the organization. + +For more details about managed and unmanaged users, see <<_managed_unmanaged_members_,Managed and unmanaged members>>. diff --git a/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc b/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc new file mode 100644 index 000000000000..0e4f9fa0fe18 --- /dev/null +++ b/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc @@ -0,0 +1,43 @@ +[id="mapping-organization-claims_{context}"] + +[[_mapping_organization_claims_]] += Mapping organization claims +[role="_abstract"] +To map organization-specific claims into tokens, a client needs to request the *organization* scope when sending +authorization requests to the server. When authenticating in the context of an organization, clients can request the `organization` scope to map information +about the organizations where the user is a member. + +As a result, the token will contain a claim as follows: + +```json +"organization": { + "testcorp": { + "id": "42c3e46f-2477-44d7-a85b-d3b43f6b31fa", + "attr1": [ + "value1" + ] + } +} +``` + +The organization claim can be used by clients (for example, from ID Tokens) and resource servers (for example, from access tokens) +to authorize access to protected resources based on the organization where the user is a member. + +The `organization` scope is a built-in optional client scope at the realm. Therefore, this scope is added to any client created in the realm by default. It also defines the `Organization Membership` mapper that controls how the organization membership information is mapped to the tokens. + +NOTE: By default, the organization id and attributes are not included in the organization claim. To include them, edit the mapper and enable the *Add organization id* and *Add organization attributes* options, respectively. + +.Including attributes in the organization claim +image:images/organizations-add-org-attrs-in-claim.png[alt="Including attributes in the organization claim"] + +The `organization` scope is requested using different formats: + +[cols="2*", options="header"] +|=== +|Format +|Description +| `organization` | Maps to a single organization if the user is a member of a single organization. +Otherwise, if a member of multiple organizations, the user will be prompted to select an organization when authenticating to the realm. +| `organization:` | Maps to a single organization with the given alias. +| `organization:*` | Maps to all organizations the user is a member of. +|=== diff --git a/docs/documentation/server_admin/topics/overview/concepts.adoc b/docs/documentation/server_admin/topics/overview/concepts.adoc index b7e1c11b01b9..0eb947f166ff 100644 --- a/docs/documentation/server_admin/topics/overview/concepts.adoc +++ b/docs/documentation/server_admin/topics/overview/concepts.adoc @@ -64,7 +64,7 @@ protocol mappers:: protocol mappers. session:: When a user logs in, a session is created to manage the login session. A session contains information like when the user logged in and what - applications have participated within single-sign on during that session. Both admins and users can view session information. + applications have participated within single sign-on during that session. Both admins and users can view session information. user federation provider:: {project_name} can store and manage users. Often, companies already have LDAP or Active Directory services that store user and credential information. You can point {project_name} to validate credentials from those external stores and pull in identity information. @@ -72,7 +72,7 @@ identity provider:: An identity provider (IDP) is a service that can authenticate a user. {project_name} is an IDP. identity provider federation:: {project_name} can be configured to delegate authentication to one or more IDPs. Social login via - Facebook or Google+ is an example of identity provider federation. You can also hook {project_name} to delegate + Facebook or Google is an example of identity provider federation. You can also hook {project_name} to delegate authentication to any other OpenID Connect or SAML 2.0 IDP. identity provider mappers:: When doing IDP federation you can map incoming tokens and assertions to user and session attributes. This helps you propagate identity information from the external IDP diff --git a/docs/documentation/server_admin/topics/overview/features.adoc b/docs/documentation/server_admin/topics/overview/features.adoc index f9304b7a00cd..531f4422b943 100644 --- a/docs/documentation/server_admin/topics/overview/features.adoc +++ b/docs/documentation/server_admin/topics/overview/features.adoc @@ -3,7 +3,7 @@ {project_name} provides the following features: -* Single-Sign On and Single-Sign Out for browser applications. +* Single Sign-On and Single Sign-Out for browser applications. * OpenID Connect support. * OAuth 2.0 support. * SAML support. @@ -12,7 +12,7 @@ * User Federation - Sync users from LDAP and Active Directory servers. * Kerberos bridge - Automatically authenticate users that are logged-in to a Kerberos server. * Admin Console for central management of users, roles, role mappings, clients and configuration. -* Account Management console that allows users to centrally manage their account. +* Account Console that allows users to centrally manage their account. * Theme support - Customize all user facing pages to integrate with your applications and branding. * Two-factor Authentication - Support for TOTP/HOTP via Google Authenticator or FreeOTP. * Login flows - optional user self-registration, recover password, verify email, require password update, etc. @@ -23,9 +23,5 @@ ifeval::[{project_community}==true] * Service Provider Interfaces (SPI) - A number of SPIs to enable customizing various aspects of the server. Authentication flows, user federation providers, protocol mappers and many more. -* Client adapters for JavaScript applications, WildFly, JBoss EAP, Tomcat, Jetty, Spring, etc. -endif::[] -ifeval::[{project_product}==true] -* Client adapters for JavaScript applications, JBoss EAP, etc. endif::[] * Supports any platform/language that has an OpenID Connect Relying Party library or SAML 2.0 Service Provider library. diff --git a/docs/documentation/server_admin/topics/realms/email.adoc b/docs/documentation/server_admin/topics/realms/email.adoc index aa94bac7186b..43372b78ebfb 100644 --- a/docs/documentation/server_admin/topics/realms/email.adoc +++ b/docs/documentation/server_admin/topics/realms/email.adoc @@ -41,4 +41,93 @@ Encryption:: Tick one of these checkboxes to support sending emails for recovering usernames and passwords, especially if the SMTP server is on an external network. You will most likely need to change the *Port* to 465, the default port for SSL/TLS. Authentication:: - Set this switch to *ON* if your SMTP server requires authentication. When prompted, supply the *Username* and *Password*. The value of the *Password* field can refer a value from an external <<_vault-administration,vault>>. + Set this switch to *ON* if your SMTP server requires authentication. + +Username:: + All authentication-mechanisms require a username. + +Authentication Type:: + Choose the kind of authentication: 'password' or 'token'. + +Password:: + Only needed when *Authentication Type* 'password' is selected. + Supply the *Password*. The value of the *Password* field can refer a value from an external <<_vault-administration,vault>>. + +Auth Token URL:: + Only needed when *Authentication Type* 'token' is selected. + Supply the *Auth Token URL* that is used to fetch a token via client credentials grant. + +Auth Token Scope:: + Only needed when *Authentication Type* 'token' is selected. + Supply the *Auth Token Scope* that is used to fetch a token from the *Auth Token URL*. + +Auth Token ClientId:: + Only needed when *Authentication Type* 'token' is selected. + Supply the *Auth ClientId* that is used to fetch a token from the *Auth Token URL*. + +Auth Token Client Secret:: + Only needed when *Authentication Type* 'token' is selected. + Supply the *Auth Client Secret* that authenticates the client to fetch a token from the *Auth Token URL*. The value of the *Auth Client Secret* field can refer a value from an external <<_vault-administration,vault>>. + +ifeval::[{project_community}==true] + +== XOAUTH2 email configuration with third-party vendors + +The following section contains some hints on how to configure {project_name} email settings to use XOAUTH2 based authentication with some known third-party software SMTP servers. + +NOTE: This section has been contributed by the Keycloak community. As the Keycloak core team does not have means to test third-party providers, it is provided as-is. If you find this documentation outdated or incomplete, please contribute to improve it. + +=== Configuration for Microsoft Azure and Office365 + +Microsoft Azure allows 'Client Credentials Grant' using a client secret to gather an access token. +Microsoft Office365 supports SMTP with XOAUTH2 to authenticate with the gathered token. + +Links to relevant Microsoft documentation: + +- https://learn.microsoft.com/en-us/exchange/permissions-exo/application-rbac[Usage of role base access control for applications in exchange online] +- Settings in https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth[Authenticate an IMAP, POP or SMTP connection using OAuth] + +The following method for setting up {project_name} to send email with Azure and Office365 has been verified by a test. +There might be other variants to achieve the same depending on your environment. + +From:: +`@` + +Host:: +`smtp.office365.com` + +Port:: +`587` + +Encryption:: +Check Start TLS + +Username:: +`@` (might be the same of a different value than the sender value) + +Auth Token Url:: +`+https://login.microsoftonline.com//oauth2/v2.0/token+` ++ +Replace TenantID with the id of your Microsoft tenant, usually a UUID, in Azure or just copy the token url from the list of endpoints displayed in the Azure Console. + +Auth Token Scope:: +`+https://outlook.office.com/.default+` + +Auth Token ClientId:: +`` ++ +Replace ApplicationId with the id of your application in Azure, usually a UUID. + +Auth Token ClientSecret:: +`` + +=== Configuration for Google Mail + +This feature is not yet supported by {project_name}, because Google does not allow client-secrets for the Client Credentials Grant. + +=== Configuration for AWS + +XOAUTH2 is not supported by the AWS-SMTP service. +The AWS-service requires the use of a password. + +endif::[] diff --git a/docs/documentation/server_admin/topics/realms/keys.adoc b/docs/documentation/server_admin/topics/realms/keys.adoc index 4d613117ceed..826e8347b2fb 100644 --- a/docs/documentation/server_admin/topics/realms/keys.adoc +++ b/docs/documentation/server_admin/topics/realms/keys.adoc @@ -127,12 +127,20 @@ For the associated certificate chain to be loaded it must be imported to the Jav . Click the *Providers* tab. . Click *Add provider* and select *java-keystore*. . Enter a number in the *Priority* field. This number determines if the new key pair becomes the active key pair. -. Enter a value for *Keystore*. -. Enter a value for *Keystore Password*. -. Enter a value for *Key Alias*. -. Enter a value for *Key Password*. +. Enter the desired *Algorithm*. Note that the algorithm should match the key type (for example `RS256` requires a RSA private key, `ES256` a EC private key or `AES` an AES secret key). +. Enter a value for *Keystore*. Path to the keystore file. +. Enter the *Keystore Password*. The option can refer a value from an external <<_vault-administration,vault>>. +. Enter a value for *Keystore Type* (`JKS`, `PKCS12` or `BCFKS`). +. Enter a value for the *Key Alias* to load from the keystore. +. Enter the *Key Password*. The option can refer a value from an external <<_vault-administration,vault>>. +. Enter a value for *Key Use* (`sig` for signing or `enc` for encryption). Note that the use should match the algorithm type (for example `RS256` is `sig` but `RSA-OAEP` is `enc`) . Click *Save*. +[WARNING] +==== +Not all the keystore types support all types of keys. For example, `JKS` in all modes and `PKCS12` in fips mode (`BCFIPS` provider) cannot store secret key entries. +==== + ==== Making keys passive .Procedure 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..07fc870acd4d 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,15 +11,8 @@ realm and only be able to interact with customer-facing apps. .Procedure -. Point to the top of the left pane. - -. Click *Create Realm*. -+ -.Add realm menu -image:images/add-realm-menu.png[Add realm menu] - +. In the Admin Console, click *Create Realm* next to *Current realm*. . Enter a name for the realm. - . Click *Create*. + .Create realm diff --git a/docs/documentation/server_admin/topics/realms/proc-using-admin-console.adoc b/docs/documentation/server_admin/topics/realms/proc-using-admin-console.adoc index 0c4199c52b62..c2a917f9e040 100644 --- a/docs/documentation/server_admin/topics/realms/proc-using-admin-console.adoc +++ b/docs/documentation/server_admin/topics/realms/proc-using-admin-console.adoc @@ -4,30 +4,44 @@ You configure realms and perform most administrative tasks in the {project_name} .Prerequisites -* You need an administrator account. See xref:creating-first-admin_{context}[Creating the first administrator]. +To use the Admin Console, you need an administrator account. + +* If no administrators exist, see xref:creating-first-admin_{context}[Creating the first administrator]. +* If other administrators exist, ask an administrator to provide an account with privileges to manage realms. .Procedure . Go to the URL for the Admin Console. + For example, for localhost, use this URL: http://localhost:8080{kc_admins_path}/ + +. Enter the username and password you created on the Welcome Page or through environment variables as described in https://www.keycloak.org/server/configuration#_creating_the_initial_admin_user[Creating the initial admin user]. + .Login page image:images/login-page.png[Login page] - -. Enter the username and password you created on the Welcome Page or through environment variables as per https://www.keycloak.org/server/configuration#_creating_the_initial_admin_user[Creating the initial admin user] guide. ++ This action displays the Admin Console. + .Admin Console image:images/admin-console.png[Admin Console] . Note the menus and other options that you can use: -+ -* Click the menu labeled *Master* to pick a realm you want to manage or to create a new one. -+ + +* Click the *Current realm* to see if other realms are available to be managed. + +* Click *Create realm* to create another realm that you can manage. + * Click the top right list to view your account or log out. + +. Click *Realm settings* in the menu to see the fields and options for this realm. ++ +Click a question mark *?* icon to show the definition of a field such as *Frontend URL*. + + -* Hover over a question mark *?* icon to show a tooltip text that describes that field. The image above shows the tooltip in action. -* Click a question mark *?* icon to show a tooltip text that describes that field. The image above shows the tooltip in action. +.Realm settings +image:images/realm-settings.png[Realm settings] -NOTE: Export files from the Admin Console are not suitable for backups or data transfer between servers. Only boot-time exports are suitable for backups or data transfer between servers. +[NOTE] +==== +Export files from the Admin Console are not suitable for backups or data transfer between servers. Only boot-time exports are suitable for backups or data transfer between servers. +==== \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/realms/ssl.adoc b/docs/documentation/server_admin/topics/realms/ssl.adoc index 3ee8f95d7f28..ee57e4bfb0e0 100644 --- a/docs/documentation/server_admin/topics/realms/ssl.adoc +++ b/docs/documentation/server_admin/topics/realms/ssl.adoc @@ -17,7 +17,7 @@ image:images/general-tab.png[General Tab] . Set *Require SSL* to one of the following SSL modes: * *External requests* - Users can interact with {project_name} without SSL so long as they stick to private IP addresses such as `localhost`, `127.0.0.1`, `10.x.x.x`, `192.168.x.x`, and `172.16.x.x`. + Users can interact with {project_name} without SSL so long as they stick to private IPv4 addresses such as `localhost`, `127.0.0.1`, `10.x.x.x`, `192.168.x.x`, `172.16.x.x` or IPv6 link-local and unique-local addresses. If you try to access {project_name} without SSL from a non-private IP address, you will get an error. * *None* diff --git a/docs/documentation/server_admin/topics/realms/themes.adoc b/docs/documentation/server_admin/topics/realms/themes.adoc index 40b20aa6150e..18807dbd626b 100644 --- a/docs/documentation/server_admin/topics/realms/themes.adoc +++ b/docs/documentation/server_admin/topics/realms/themes.adoc @@ -5,7 +5,7 @@ For a given realm, you can change the appearance of any UI in {project_name} by .Procedure -. Click *Realm setting* in the menu. +. Click *Realm settings* in the menu. . Click the *Themes* tab. + .Themes tab @@ -17,7 +17,7 @@ Login theme:: Username password entry, OTP entry, new user registration, and other similar screens related to login. Account theme:: - Each user has a User Account Management UI. + The console used by the user to manage his or her account. Admin console theme:: The skin of the {project_name} Admin Console. diff --git a/docs/documentation/server_admin/topics/roles-groups/con-comparing-groups-roles.adoc b/docs/documentation/server_admin/topics/roles-groups/con-comparing-groups-roles.adoc index 7833f0065055..b4d46d56f47c 100644 --- a/docs/documentation/server_admin/topics/roles-groups/con-comparing-groups-roles.adoc +++ b/docs/documentation/server_admin/topics/roles-groups/con-comparing-groups-roles.adoc @@ -2,7 +2,7 @@ = Groups compared to roles [role="_abstract"] -Groups and roles have some similarities and differences. In {project_name}, groups are a collection of users to which you apply roles and attributes. Roles define types of users and applications assign permissions and access control to roles. +Groups and roles have some similarities and differences. In {project_name}, groups are a collection of users to which you apply roles and attributes. Roles define types of users, and applications assign permissions and access control to roles. <<_composite-roles,Composite Roles>> are similar to Groups as they provide the same functionality. The difference between them is conceptual. Composite roles apply the permission model to a set of services and applications. Use composite roles to manage applications and services. diff --git a/docs/documentation/server_admin/topics/roles-groups/con-role-scope-mappings.adoc b/docs/documentation/server_admin/topics/roles-groups/con-role-scope-mappings.adoc index 85a38e25754b..6bdb27602b58 100644 --- a/docs/documentation/server_admin/topics/roles-groups/con-role-scope-mappings.adoc +++ b/docs/documentation/server_admin/topics/roles-groups/con-role-scope-mappings.adoc @@ -4,9 +4,9 @@ = Role scope mappings [role="_abstract"] -On creation of an OIDC access token or SAML assertion, the user role mappings become claims within the token or assertion. Applications use these claims to make access decisions on the resources controlled by the application. {project_name} digitally signs access tokens and applications re-use them to invoke remotely secured REST services. However, these tokens have an associated risk. An attacker can obtain these tokens and use their permissions to compromise your networks. To prevent this situation, use _Role Scope Mappings_. +On creation of an OIDC access token or SAML assertion, the user role mappings become claims within the token or assertion. Applications use these claims to make access decisions on the resources controlled by the application. {project_name} digitally signs access tokens and applications reuse them to invoke remotely secured REST services. However, these tokens have an associated risk. An attacker can obtain these tokens and use their permissions to compromise your networks. To prevent this situation, use _Role Scope Mappings_. -_Role Scope Mappings_ limit the roles declared inside an access token. When a client requests a user authentication, the access token they receive contains only the role mappings that are explicitly specified for the client's scope. The result is that you limit the permissions of each individual access token instead of giving the client access to all the users permissions. +_Role Scope Mappings_ limit the roles declared inside an access token. When a client requests user authentication, the access token it receives contains only the role mappings that are explicitly specified for the client's scope. The result is that the permissions of each individual access token are limited instead of giving the client access to all the user's permissions. By default, each client gets all the role mappings of the user. You can view the role mappings for a client. @@ -26,3 +26,5 @@ You can also use <<_client_scopes, client scopes>> to define the same role scope .Partial scope image:images/client-scope.png[Partial scope] + +See the <<_oidc_token_role_mappings, Token Role mappings section>> for details about the algorithm that adds the roles to the token. diff --git a/docs/documentation/server_admin/topics/roles-groups/proc-creating-realm-roles.adoc b/docs/documentation/server_admin/topics/roles-groups/proc-creating-realm-roles.adoc index 2834a017a092..9a73621e6b0c 100644 --- a/docs/documentation/server_admin/topics/roles-groups/proc-creating-realm-roles.adoc +++ b/docs/documentation/server_admin/topics/roles-groups/proc-creating-realm-roles.adoc @@ -12,7 +12,4 @@ image:images/roles.png[] . Enter a *Description*. . Click *Save*. -.Add role -image:images/role.png[Add role] - The *description* field can be localized by specifying a substitution variable with `$\{var-name}` strings. The localized value is configured to your theme within the themes property files. See the link:{developerguide_link}[{developerguide_name}] for more details. diff --git a/docs/documentation/server_admin/topics/roles-groups/proc-managing-groups.adoc b/docs/documentation/server_admin/topics/roles-groups/proc-managing-groups.adoc index bc4661391002..bee710386a22 100644 --- a/docs/documentation/server_admin/topics/roles-groups/proc-managing-groups.adoc +++ b/docs/documentation/server_admin/topics/roles-groups/proc-managing-groups.adoc @@ -12,7 +12,16 @@ Groups are hierarchical. A group can have multiple subgroups but a group can hav If you have a parent group and a child group, and a user that belongs only to the child group, the user in the child group inherits the attributes and role mappings of both the parent group and the child group. -The following example includes a top-level *Sales* group and a child *North America* subgroup. +The hierarchy of a group is sometimes represented using the group path. The path is the complete list of names that represents the hierarchy of a specific group, from top to bottom and separated by slashes `/` (similar to files in a File System). For example a path can be `/top/level1/level2` which means that `top` is a top level group and is parent of `level1`, which in turn is parent of `level2`. This path represents unambiguously the hierarchy for the group `level2`. + +Because of historical reasons {project_name}, does not escape slashes in the group name itself. Therefore a group named `level1/group` under `top` uses the path `/top/level1/group`, which is misleading. {project_name} can be started with the option `+--spi-group--jpa--escape-slashes-in-group-path+` to `true` and then the slashes in the name are escaped with the character `~`. The escape char marks that the slash is part of the name and has no hierarchical meaning. The previous path example would be `/top/level1~/group` when escaped. + +[source,bash] +---- +bin/kc.[sh|bat] start --spi-group--jpa--escape-slashes-in-group-path=true +---- + +The following example includes a top-level *Sales* group and a child *North America* subgroup. To add a group: @@ -34,14 +43,13 @@ To add a user to a group: . Click *Users* in the menu. . Click the user that you want to perform a role mapping on. If the user is not displayed, click *View all users*. . Click *Groups*. -+ -.User groups -image:images/user-groups.png[] -+ . Click *Join Group*. . Select a group from the dialog. . Select a group from the *Available Groups* tree. . Click *Join*. ++ +.Join group +image:images/user-groups.png[] To remove a group from a user: diff --git a/docs/documentation/server_admin/topics/sessions.adoc b/docs/documentation/server_admin/topics/sessions.adoc index dc6c95720423..dfd332528ba0 100644 --- a/docs/documentation/server_admin/topics/sessions.adoc +++ b/docs/documentation/server_admin/topics/sessions.adoc @@ -1,4 +1,5 @@ +[[managing-user-sessions]] == Managing user sessions When users log into realms, {project_name} maintains a user session for each user and remembers each client visited by the user within the session. Realm administrators can perform multiple actions on each user session: diff --git a/docs/documentation/server_admin/topics/sessions/administering.adoc b/docs/documentation/server_admin/topics/sessions/administering.adoc index f53580cac37b..82ff217a9cdc 100644 --- a/docs/documentation/server_admin/topics/sessions/administering.adoc +++ b/docs/documentation/server_admin/topics/sessions/administering.adoc @@ -19,8 +19,8 @@ Clicking *Sign out all active sessions* does not revoke outstanding access token .Procedure . Click *Clients* in the menu. -. Click the *Sessions* tab. . Click a client to see that client's sessions. +. Click the *Sessions* tab. + .Client sessions image:images/client-sessions.png[Client sessions] @@ -29,8 +29,8 @@ image:images/client-sessions.png[Client sessions] .Procedure . Click *Users* in the menu. -. Click the *Sessions* tab. . Click a user to see that user's sessions. +. Click the *Sessions* tab. + .User sessions image:images/user-sessions.png[User sessions] diff --git a/docs/documentation/server_admin/topics/sessions/offline.adoc b/docs/documentation/server_admin/topics/sessions/offline.adoc index b280cc5d61e4..ab47f7e24bb4 100644 --- a/docs/documentation/server_admin/topics/sessions/offline.adoc +++ b/docs/documentation/server_admin/topics/sessions/offline.adoc @@ -8,9 +8,9 @@ During https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess[offli The client application is responsible for persisting the offline token in storage and then using it to retrieve new access tokens from the {project_name} server. -The difference between a refresh token and an offline token is that an offline token never expires and is not subject to the `SSO Session Idle` timeout and `SSO Session Max` lifespan. The offline token is valid after a user logout or server restart. You must use the offline token for a refresh token action at least once per thirty days or for the value of the <<_offline-session-idle, Offline Session Idle>>. +The difference between a refresh token and an offline token is that an offline token never expires and is not subject to the `SSO Session Idle` timeout and `SSO Session Max` lifespan. The offline token is valid after a user logout. You must use the offline token for a refresh token action at least once per thirty days or for the value of the <<_offline-session-idle, Offline Session Idle>>. -If you enable <<_offline-session-max-limited, Offline Session Max Limited>>, offline tokens expire after 60 days even if you use the offline token for a refresh token action. You can change this value, <<_offline-session-max, Offline Session Max>>, in the Admin Console. +If you enable <<_offline-session-max-limited, Offline Session Max Limited>>, offline tokens expire after 60 days even if you use the offline token for a refresh token action. You can change this value, <<_offline-session-max, Offline Session Max>>, in the Admin Console. When using offline access, client idle and max timeouts can be overridden at the <<_client_advanced_settings_oidc,client level>>. The options *Client Offline Session Idle* and *Client Offline Session Max*, in the client *Advanced Settings* tab, allow you to have a shorter offline timeouts for a specific application. Note that client session values also control the refresh token expiration but they never affect the global offline user SSO session. The option *Client Offline Session Max* is only evaluated in the client if <<_offline-session-max-limited, Offline Session Max Limited>> is *Enabled* at the realm level. @@ -21,3 +21,7 @@ Users can view and revoke offline tokens that {project_name} grants them in the To issue an offline token, users must have the role mapping for the realm-level `offline_access` role. Clients must also have that role in their scope. Clients must add an `offline_access` client scope as an `Optional client scope` to the role, which is done by default. Clients can request an offline token by adding the parameter `scope=offline_access` when sending their authorization request to {project_name}. The {project_name} OIDC client adapter automatically adds this parameter when you use it to access your application's secured URL (such as, $$http://localhost:8080/customer-portal/secured?scope=offline_access$$). The Direct Access Grant and Service Accounts support offline tokens if you include `scope=offline_access` in the authentication request body. + +{project_name} will limit its internal cache for offline user and offline client sessions to 10000 entries by default, which will reduce the overall memory usage for offline sessions. +Items which are evicted from memory will be loaded on-demand from the database when needed. +See the server configuration guide to change this default. diff --git a/docs/documentation/server_admin/topics/sessions/preloading.adoc b/docs/documentation/server_admin/topics/sessions/preloading.adoc deleted file mode 100644 index 9deed1a5e72a..000000000000 --- a/docs/documentation/server_admin/topics/sessions/preloading.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[offline-sessions-preloading]] - -=== Offline sessions preloading - -In addition to {jdgserver_name} caches, offline sessions are stored in a database which means they will be available even after server restart. -By default, the offline sessions are not preloaded from the database into the {jdgserver_name} caches during the server startup, because this -approach has a drawback if there are many offline sessions to be preloaded. It can significantly slow down the server startup time. -Therefore, the offline sessions are lazily fetched from the database by default. - -However, {project_name} can be configured to preload the offline sessions from the database into the {jdgserver_name} caches during the server startup. -It can be achieved by setting `preloadOfflineSessionsFromDatabase` property in the `userSessions` SPI to `true`. - -The following example shows how to configure offline sessions preloading. - -[source,bash] ----- -bin/kc.[sh|bat] start --spi-user-sessions-infinispan-preload-offline-sessions-from-database=true ----- diff --git a/docs/documentation/server_admin/topics/sessions/timeouts.adoc b/docs/documentation/server_admin/topics/sessions/timeouts.adoc index 472dcfbf97bd..22d44dad1211 100644 --- a/docs/documentation/server_admin/topics/sessions/timeouts.adoc +++ b/docs/documentation/server_admin/topics/sessions/timeouts.adoc @@ -93,6 +93,8 @@ image:images/tokens-tab.png[Tokens Tab] [NOTE] ==== +The following logic is only applied if persistent user sessions are not active: + For idle timeouts, a two-minute window of time exists that the session is active. For example, when you have the timeout set to 30 minutes, it will be 32 minutes before the session expires. This action is necessary for some scenarios in cluster and cross-data center environments where the token refreshes on one cluster node a short time before the expiration and the other cluster nodes incorrectly consider the session as expired because they have not yet received the message about a successful refresh from the refreshing node. 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 e541c41daac2..076632f810cf 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 @@ -20,7 +20,7 @@ to exchange the code for an _identity_ and _access_ and _refresh_ token. To pre [NOTE] ==== -A system is vulnerable to a stolen token for the lifetime of that token. For security and scalability reasons, access tokens are generally set to expire quickly so subsequent token requests fail. If a token expires, an application can obtain a new access token using the additional _refresh_ token sent by the login protocol. +A system is vulnerable to a stolen token for the lifetime of that token. For security and scalability reasons, access tokens are generally set to expire quickly so subsequent token requests fail. If a token expires, an application can obtain a new access token using the additional _refresh_ token sent by the login protocol. ==== [[_confidential-clients]] @@ -33,13 +33,13 @@ _Public_ clients are secure when HTTPS is strictly enforced and redirect URIs re ===== Implicit Flow -The Implicit Flow is a browser-based protocol. It is similar to the Authorization Code Flow but with fewer requests and no refresh tokens. +The Implicit Flow is a browser-based protocol. It is similar to the Authorization Code Flow but with fewer requests and no refresh tokens. [NOTE] ==== The possibility exists of _access_ tokens leaking in the browser history when tokens are transmitted via redirect URIs (see below). -Also, this flow does not provide clients with refresh tokens. Therefore, access tokens have to be long-lived or users have to re-authenticate when they expire. +Also, this flow does not provide clients with refresh tokens. Therefore, access tokens have to be long-lived or users have to re-authenticate when they expire. We do not advise using this flow. This flow is supported because it is in the OIDC and OAuth 2.0 specification. ==== @@ -72,12 +72,33 @@ The _Client Credentials Grant_ creates a token based on the metadata and permiss See the <<_service_accounts,Service Accounts>> chapter for more information. +[[_refresh_token_grant]] +===== Refresh token grant + +By default, {project_name} returns refresh tokens in the token responses from most of the flows. Some exceptions are implicit flow or client credentials grant described above. + +Refresh token is tied to the user session of the SSO browser session and can be valid for the lifetime of the user session. However, that client should send a refresh-token request at least once per +specified interval. Otherwise, the session can be considered "idle" and can expire. See the <<_timeouts,timeouts section>> for more information. + +{project_name} supports <<_offline-access,offline tokens>>, which can be used typically when client needs to use refresh token even if corresponding browser SSO session is already expired. + +[[_refresh_token_rotation]] +====== Refresh token rotation + +It is possible to specify that the refresh token is considered invalid once it is used. This means that client must always save the refresh token from the last refresh response because older refresh tokens, +which were already used, would not be considered valid anymore by {project_name}. This is possible to set with the use of _Revoke Refresh token_ option as specified in the <<_timeouts,timeouts section>>. + +{project_name} also supports the situation that no refresh token rotation exists. In this case, a refresh token is returned during login, but subsequent responses from refresh-token requests will not +return new refresh tokens. This practice is recommended for instance in the *FAPI 2 draft specification* in the link:{securing_apps_link}[securing apps] section. +In {project_name}, it is possible to skip refresh token rotation with the use of <<_client_policies,client policies>>. You can add executor `suppress-refresh-token-rotation` to some client +profile and configure client policy to specify for which clients would be the profile triggered, which means that for those clients the refresh token rotation is going to be skipped. + ===== Device authorization grant 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]] @@ -86,11 +107,11 @@ This is used by clients running on internet-connected devices that have limited This feature is used by clients who want to initiate the authentication flow by communicating with the OpenID Provider directly without redirect through the user's browser like OAuth 2.0's authorization code grant. Here's a brief summary of the protocol: . The client requests {project_name} an auth_req_id that identifies the authentication request made by the client. {project_name} creates the auth_req_id. -. After receiving this auth_req_id, this client repeatedly needs to poll {project_name} to obtain an Access Token, Refresh Token and ID Token from {project_name} in return for the auth_req_id until the user is authenticated. +. After receiving this auth_req_id, this client repeatedly needs to poll {project_name} to obtain an Access Token, Refresh Token and ID Token from {project_name} in return for the auth_req_id until the user is authenticated. An administrator can configure Client Initiated Backchannel Authentication (CIBA) related operations as `CIBA Policy` per realm. -Also please refer to other places of {project_name} documentation like link:{adapterguide_link}#_backchannel_authentication_endpoint[Backchannel Authentication Endpoint section] of {adapterguide_name} and link:{adapterguide_link}#_client_initiated_backchannel_authentication_grant[Client Initiated Backchannel Authentication Grant section] of {adapterguide_name}. +Also please refer to other places of {project_name} documentation like *Backchannel Authentication Endpoint* and *Client Initiated Backchannel Authentication Grant* in the link:{securing_apps_link}[securing apps] section. ====== CIBA Policy @@ -126,14 +147,14 @@ The configurable items and their description follow. The CIBA grant uses the following two providers. -. Authentication Channel Provider : provides the communication between {project_name} and the entity that actually authenticates the user via AD (Authentication Device). -. User Resolver Provider : get `UserModel` of {project_name} from the information provided by the client to identify the user. +. Authentication Channel Provider: provides the communication between {project_name} and the entity that actually authenticates the user via AD (Authentication Device). +. User Resolver Provider: get `UserModel` of {project_name} from the information provided by the client to identify the user. {project_name} has both default providers. However, the administrator needs to set up Authentication Channel Provider like this: [source,bash,subs="attributes+"] ---- -kc.[sh|bat] start --spi-ciba-auth-channel-ciba-http-auth-channel-http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path} +kc.[sh|bat] start --spi-ciba-auth-channel--ciba-http-auth-channel--http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path} ---- The configurable items and their description follow. @@ -156,10 +177,10 @@ Authentication Channel Provider is provided as SPI provider so that users of {pr If a user of {project_name} user want to use the HTTP Authentication Channel Provider, they need to know its contract between {project_name} and the authentication entity consisting of the following two parts. -Authentication Delegation Request/Response:: +Authentication Delegation Request/Response:: {project_name} sends an authentication request to the authentication entity. -Authentication Result Notification/ACK:: +Authentication Result Notification/ACK:: The authentication entity notifies the result of the authentication to {project_name}. Authentication Delegation Request/Response consists of the following messaging. @@ -292,7 +313,7 @@ User Resolver Provider is provided as SPI provider so that users of {project_nam ==== OIDC Logout -OIDC has four specifications relevant to logout mechanisms. These specifications are in draft status: +OIDC has four specifications relevant to logout mechanisms: . https://openid.net/specs/openid-connect-session-1_0.html[Session Management] . https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout] diff --git a/docs/documentation/server_admin/topics/sso-protocols/con-saml-bindings.adoc b/docs/documentation/server_admin/topics/sso-protocols/con-saml-bindings.adoc index 2822e32c5b17..d5e887346cb0 100644 --- a/docs/documentation/server_admin/topics/sso-protocols/con-saml-bindings.adoc +++ b/docs/documentation/server_admin/topics/sso-protocols/con-saml-bindings.adoc @@ -20,7 +20,7 @@ _Redirect_ binding uses a series of browser redirect URIs to exchange informatio ===== POST binding -_POST_ binding is similar to _Redirect_ binding but _POST_ binding exchanges XML documents using POST requests instead of using GET requests. _POST_ Binding uses JavaScript to make the browser send a POST request to the {project_name} server or application when exchanging documents. HTTP responds with an HTML document which contains an HTML form containing embedded JavaScript. When the page loads, the JavaScript automatically invokes the form. +_POST_ binding is similar to _Redirect_ binding but _POST_ binding exchanges XML documents using POST requests instead of using GET requests. _POST_ binding uses JavaScript to make the browser send a POST request to the {project_name} server or application when exchanging documents. HTTP responds with an HTML document which contains an HTML form containing embedded JavaScript. When the page loads, the JavaScript automatically invokes the form. _POST_ binding is recommended due to two restrictions: diff --git a/docs/documentation/server_admin/topics/sso-protocols/con-sso-docker.adoc b/docs/documentation/server_admin/topics/sso-protocols/con-sso-docker.adoc index 1903a0f888b9..9ea26483646a 100644 --- a/docs/documentation/server_admin/topics/sso-protocols/con-sso-docker.adoc +++ b/docs/documentation/server_admin/topics/sso-protocols/con-sso-docker.adoc @@ -27,4 +27,4 @@ NOTE: {project_name} does not create a browser SSO session after successful auth {project_name} has one endpoint for all Docker auth v2 requests. -`http(s)://authserver.host{kc_realms_path}/{realm-name}/protocol/docker-v2` +`http(s)://authserver.host{kc_realms_path}/{realm-name}/protocol/docker-v2/auth` diff --git a/docs/documentation/server_admin/topics/sso-protocols/oidc.adoc b/docs/documentation/server_admin/topics/sso-protocols/oidc.adoc index fe0e2397233d..177b1daf65ce 100644 --- a/docs/documentation/server_admin/topics/sso-protocols/oidc.adoc +++ b/docs/documentation/server_admin/topics/sso-protocols/oidc.adoc @@ -109,11 +109,11 @@ This is used by clients running on internet-connected devices that have limited This is used by clients who want to initiate the authentication flow by communicating with the OpenID Provider directly without redirect through the user's browser like OAuth 2.0's authorization code grant. Here's a brief summary of the protocol: . The client requests {project_name} an auth_req_id that identifies the authentication request made by the client. {project_name} creates the auth_req_id. -. After receiving this auth_req_id, this client repeatedly needs to poll {project_name} to obtain an Access Token, Refresh Token and ID Token from {project_name} in return for the auth_req_id until the user is authenticated. +. After receiving this auth_req_id, this client repeatedly needs to poll {project_name} to obtain an Access Token, Refresh Token and ID Token from {project_name} in return for the auth_req_id until the user is authenticated. An administrator can configure Client Initiated Backchannel Authentication (CIBA) related operations as `CIBA Policy` per realm. -Also please refer to other places of {project_name} documentation like link:{adapterguide_link}#_backchannel_authentication_endpoint[Backchannel Authentication Endpoint section] of {adapterguide_name} and link:{adapterguide_link}#_client_initiated_backchannel_authentication_grant[Client Initiated Backchannel Authentication Grant section] of {adapterguide_name}. +Also please refer to other places of {project_name} documentation like *Backchannel Authentication Endpoint* and *Client Initiated Backchannel Authentication Grant* in the link:{securing_apps_link}[securing apps] section. ====== CIBA Policy @@ -157,7 +157,7 @@ The CIBA grant uses the following two providers. [source,bash,subs="attributes+"] ---- -kc.[sh|bat] start --spi-ciba-auth-channel-ciba-http-auth-channel-http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path} +kc.[sh|bat] start --spi-ciba-auth-channel--ciba-http-auth-channel--http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path} ---- The configurable items and their description follow. @@ -180,10 +180,10 @@ Authentication Channel Provider is provided as SPI provider so that users of {pr If a user of {project_name} user want to use the HTTP Authentication Channel Provider, they need to know its contract between {project_name} and the authentication entity consisting of the following two parts. -Authentication Delegation Request/Response:: +Authentication Delegation Request/Response:: {project_name} sends an authentication request to the authentication entity. -Authentication Result Notification/ACK:: +Authentication Result Notification/ACK:: The authentication entity notifies the result of the authentication to {project_name}. Authentication Delegation Request/Response consists of the following messaging. diff --git a/docs/documentation/server_admin/topics/threat.adoc b/docs/documentation/server_admin/topics/threat.adoc index 8870348323d3..1deebe943c2e 100644 --- a/docs/documentation/server_admin/topics/threat.adoc +++ b/docs/documentation/server_admin/topics/threat.adoc @@ -2,4 +2,4 @@ [[mitigating_security_threats]] == Mitigating security threats -Security vulnerabilities exist in any authentication server. See the Internet Engineering Task Force's (IETF) https://datatracker.ietf.org/doc/html/rfc6819[OAuth 2.0 Threat Model] and the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-15[OAuth 2.0 Security Best Current Practice] for more information. +Security vulnerabilities exist in any authentication server. See the Internet Engineering Task Force's (IETF) https://datatracker.ietf.org/doc/html/rfc6819[OAuth 2.0 Threat Model] and the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics[OAuth 2.0 Security Best Current Practice] for more information. diff --git a/docs/documentation/server_admin/topics/threat/auth-sessions-limit.adoc b/docs/documentation/server_admin/topics/threat/auth-sessions-limit.adoc index c6961230ff5c..a7e1d4621aac 100644 --- a/docs/documentation/server_admin/topics/threat/auth-sessions-limit.adoc +++ b/docs/documentation/server_admin/topics/threat/auth-sessions-limit.adoc @@ -1,10 +1,7 @@ [[_limit-authentication-sessions]] === Limit Authentication Sessions -When a login page is opened for the first time in a web browser, {project_name} creates an object called authentication session that stores some useful information about the request. -Whenever a new login page is opened from a different tab in the same browser, {project_name} creates a new record called authentication sub-session that is stored within the authentication session. -Authentication requests can come from any type of clients such as the Admin CLI. In that case, a new authentication session is also created with one authentication sub-session. -Please note that authentication sessions can be created also in other ways than using a browser flow. The text below is applicable regardless of the source flow. +<<_authentication-sessions, Authentication sessions>> track the state of the authentication. The text below is applicable regardless of the source flow. NOTE: This section describes deployments that use the {jdgserver_name} provider for authentication sessions. @@ -28,7 +25,7 @@ The following example shows how to limit the number of active `AuthenticationSes [source,bash] ---- -bin/kc.[sh|bat] start --spi-authentication-sessions-infinispan-auth-sessions-limit=100 +bin/kc.[sh|bat] start --spi-authentication-sessions--infinispan--auth-sessions-limit=100 ---- ifeval::[{project_community}==true] @@ -36,6 +33,6 @@ The equivalent command for the new map storage: [source,bash] ---- -bin/kc.[sh|bat] start --spi-authentication-sessions-map-auth-sessions-limit=100 +bin/kc.[sh|bat] start --spi-authentication-sessions--map--auth-sessions-limit=100 ---- endif::[] diff --git a/docs/documentation/server_admin/topics/threat/brute-force.adoc b/docs/documentation/server_admin/topics/threat/brute-force.adoc index 2ac674ce9de4..63042fc7faca 100644 --- a/docs/documentation/server_admin/topics/threat/brute-force.adoc +++ b/docs/documentation/server_admin/topics/threat/brute-force.adoc @@ -2,80 +2,110 @@ [[password-guess-brute-force-attacks]] === Brute force attacks -A brute force attack attempts to guess a user's password by trying to log in multiple times. {project_name} has brute force detection capabilities and can temporarily disable a user account if the number of login failures exceeds a specified threshold. +A brute force attack attempts to guess a user's password by trying to log in multiple times. {project_name} has brute force detection capabilities and can permanently or temporarily disable a user account if the number of login failures exceeds a specified threshold. [NOTE] ==== -{project_name} disables brute force detection by default. Enable this feature to protect against brute force attacks. +When a user is locked and attempts to log in, {project_name} displays the default `Invalid username or password` error message. This message is the same error message as the message displayed for an invalid username or invalid password to ensure the attacker is unaware the account is disabled. ==== -.Procedure +[WARNING] +==== +Brute force detection is disabled by default. Enable this feature to protect against brute force attacks. +==== To enable this protection: . Click *Realm Settings* in the menu . Click the *Security Defenses* tab. . Click the *Brute Force Detection* tab. +. Choose the *Brute Force Mode* which best fit to your requirements. + .Brute force detection image:images/brute-force.png[] -{project_name} can deploy permanent lockout and temporary lockout actions when it detects an attack. Permanent lockout disables a user account until an administrator re-enables it. Temporary lockout disables a user account for a specific period of time. The time period that the account is disabled increases as the attack continues. +==== Lockout permanently +{project_name} disables a user account (blocking log in attempts) until an administrator re-enables it. -[NOTE] -==== -When a user is temporarily locked and attempts to log in, {project_name} displays the default `Invalid username or password` error message. This message is the same error message as the message displayed for an invalid username or invalid password to ensure the attacker is unaware the account is disabled. -==== +.Lockout permanently +image:images/brute-force-permanently.png[] -*Common Parameters* +*Permanent Lockout Parameters* |=== |Name |Description |Default |Max Login Failures |The maximum number of login failures. -|30 failures. +|30 failures |Quick Login Check Milliseconds |The minimum time between login attempts. -|1000 milliseconds. +|1000 milliseconds |Minimum Quick Login Wait |The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_. -|1 minute. +|1 minute |=== *Permanent Lockout Flow* + ==== . On successful login .. Reset `count` . On failed login .. Increment `count` -.. If `count` greater than _Max Login Failures_ -... Permanently disable user +.. If `count` is greater than or equals to `Max login failures` +... locks the user .. Else if the time between this failure and the last failure is less than _Quick Login Check Milliseconds_ -... Temporarily disable user for _Minimum Quick Login Wait_ +... Locks the user for the time specified at _Minimum Quick Login Wait_ +==== -When {project_name} disables a user, the user cannot log in until an administrator enables the user. Enabling an account resets the `count`. +[NOTE] +==== +Enabling an user account resets the `count`. ==== +==== Lockout temporarily +{project_name} disables a user account for a specific period of time. The time period that the account is disabled increases as the attack continues. + +.Lockout temporarily +image:images/brute-force-temporarily.png[] + *Temporary Lockout Parameters* |=== |Name |Description |Default +|Max Login Failures +|The maximum number of login failures. +|30 failures + +|Strategy to increase wait time +|Strategy to increase the time a user will be temporarily disabled when the user's login attempts exceed _Max Login Failures_ +|Multiple + |Wait Increment |The time added to the time a user is temporarily disabled when the user's login attempts exceed _Max Login Failures_. -|1 minute. +|1 minute |Max Wait |The maximum time a user is temporarily disabled. -|15 minutes. +|15 minutes |Failure Reset Time -|The time when the failure count resets. The timer runs from the last failed login. -|12 hours. +|The time when the failure count resets. The timer runs from the last failed login. Make sure this number is always greater than `Max wait`; otherwise the effective +wait time will never reach the value you have set to `Max wait`. +|12 hours + +|Quick Login Check Milliseconds +|The minimum time between login attempts. +|1000 milliseconds + +|Minimum Quick Login Wait +|The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_. +|1 minute |=== @@ -87,17 +117,143 @@ When {project_name} disables a user, the user cannot log in until an administrat .. If the time between this failure and the last failure is greater than _Failure Reset Time_ ... Reset `count` .. Increment `count` -.. Calculate `wait` using _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number -.. If `wait` equals 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_, set `wait` to _Minimum Quick Login Wait_. +.. Calculate `wait` according the brute force strategy defined (see below Strategies to set Wait Time). +.. If `wait` is less than or equals to 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_ +... set `wait` to _Minimum Quick Login Wait_ +.. if `wait` is greater than 0 ... Temporarily disable the user for the smallest of `wait` and _Max Wait_ seconds +==== + +[NOTE] +==== `count` does not increment when a temporarily disabled account commits a login failure. ==== -The downside of {project_name} brute force detection is that the server becomes vulnerable to denial of service attacks. When implementing a denial of service attack, an attacker can attempt to log in by guessing passwords for any accounts it knows and eventually causing {project_name} to disable the accounts. +*Strategies to set Wait Time* -Consider using intrusion prevention software (IPS). {project_name} logs every login failure and client IP address failure. You can point the IPS to the {project_name} server's log file, and the IPS can modify firewalls to block connections from these IP addresses. +{project_name} provides two strategies to calculate wait time: By multiples or Linear. By multiples is the first strategy introduced by {project_name}, so that is the default one. + +By multiples strategy, wait time is incremented when the number (or count) of failures are multiples of `Max Login Failure`. For instance, if you set `Max Login Failures` to `5` and a `Wait Increment` to `30` seconds, the effective time that an account is disabled after several failed authentication attempts will be: + +[cols="1,1,1,1"] +|=== +|`Number of Failures` | `Wait Increment` | `Max Login Failures` | `Effective Wait Time` +|1 |30 | 5 | 0 +|2 |30 | 5 | 0 +|3 |30 | 5 | 0 +|4 |30 | 5 | 0 +|**5** |**30** | 5 | **30** +|6 |30 | 5 | 30 +|7 |30 | 5 | 30 +|8 |30 | 5 | 30 +|9 |30 | 5 | 30 +|**10** |**30** | 5 | **60** +|=== + +At the fifth failed attempt, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds. + +The By multiple strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number. + +For linear strategy, wait time is incremented when the `count` (or number) of failures is greater than or equals to `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` to`30` seconds, the effective time that an account is disabled after several failed authentication attempts will be: + +[cols="1,1,1,1"] +|=== +|`Number of Failures` | `Wait Increment` | `Max Login Failures` | `Effective Wait Time` +|1 |30 | 5 | 0 +|2 |30 | 5 | 0 +|3 |30 | 5 | 0 +|4 |30 | 5 | 0 +|**5** |**30** | 5 | **30** +|**6** |**30** | 5 | **60** +|**7** |**30** | 5 | **90** +|**8** |**30** | 5 | **120** +|**9** |**30** | 5 | **150** +|**10** |**30** | 5 | **180** +|=== + +At the fifth failed attempt, the account is disabled for `30` seconds. Each new failure increases wait time according value specified at `wait increment`. + +The linear strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (1 + `count` - _Max Login Failures_). + +==== Lockout permanently after temporary lockout +Mixed mode. Locks user temporarily for specified number of times and then locks user permanently. + +.Lockout permanently after temporary lockout +image:images/brute-force-mixed.png[] + +*Permanent lockout after temporary lockouts Parameters* -==== Password policies +|=== +|Name |Description |Default + +|Max Login Failures +|The maximum number of login failures. +|30 failures + +|Maximum temporary Lockouts +|The maximum number of temporary lockouts permitted before permanent lockout occurs. +|1 + +|Strategy to increase wait time +|Strategy to increase the time a user will be temporarily disabled when the user's login attempts exceed _Max Login Failures_ +|Multiple + +|Wait Increment +|The time added to the time a user is temporarily disabled when the user's login attempts exceed _Max Login Failures_. +|1 minute + +|Max Wait +|The maximum time a user is temporarily disabled. +|15 minutes + +|Failure Reset Time +|The time when the failure count resets. The timer runs from the last failed login. Make sure this number is always greater than `Max wait`; otherwise the effective +wait time will never reach the value you have set to `Max wait`. +|12 hours + +|Quick Login Check Milliseconds +|The minimum time between login attempts. +|1000 milliseconds + +|Minimum Quick Login Wait +|The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_. +|1 minute -Ensure you have a complex password policy to force users to choose complex passwords. See the <<_password-policies, Password Policies>> chapter for more information. Prevent password guessing by setting up the {project_name} server to use one-time-passwords. +|=== + +*Permanent lockout after temporary lockouts Algorithm* +==== +. On successful login +.. Reset `count` +.. Reset `temporary lockout` counter +. On failed login +.. If the time between this failure and the last failure is greater than _Failure Reset Time_ +... Reset `count` +... Reset `temporary lockout` counter +.. Increment `count` +.. Calculate `wait` according the brute force strategy defined (see below Strategies to set Wait Time). +.. If `wait` is less than or equals to 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_ +... set `wait` to _Minimum Quick Login Wait_ +... set `quick login failure` to `true`` +.. if `wait` and `Maximum temporary Lockouts` is greater than 0 +... set `wait` to the smallest of `wait` and _Max Wait_ seconds +.. if `quick login failure` is `false` +... Increment `temporary lockout` counter +.. If `temporary lockout` counter exceeds `Maximum temporary lockouts` +... Permanently locks the user +.. Else +... Temporarily blocks the user according `wait` value + +==== + +[NOTE] +==== +`count` does not increment when a temporarily disabled account commits a login failure. +==== + +==== Downside of {project_name} brute force detection + +The downside of {project_name} brute force detection is that the server becomes vulnerable to denial of service attacks. When implementing a denial of service attack, an attacker can attempt to log in by guessing passwords for any accounts it knows and eventually causing {project_name} to disable the accounts. + +Consider using intrusion prevention software (IPS). {project_name} logs every login failure and client IP address failure. You can point the IPS to the {project_name} server's log file, and the IPS can modify firewalls to block connections from these IP addresses. diff --git a/docs/documentation/server_admin/topics/threat/csrf.adoc b/docs/documentation/server_admin/topics/threat/csrf.adoc index f5d3358c991d..685ff1527038 100644 --- a/docs/documentation/server_admin/topics/threat/csrf.adoc +++ b/docs/documentation/server_admin/topics/threat/csrf.adoc @@ -7,4 +7,4 @@ The OAuth 2.0 login specification requires that a state cookie matches against a The {project_name} Admin Console is a JavaScript/HTML5 application that makes REST calls to the backend {project_name} admin REST API. These calls all require bearer token authentication and consist of JavaScript Ajax calls, so CSRF is impossible. You can configure the admin REST API to validate the CORS origins. -The user account management section in {project_name} can be vulnerable to CSRF. To prevent CSRF attacks, {project_name} sets a state cookie and embeds the value of this cookie in hidden form fields or query parameters within action links. {project_name} checks the query/form parameter against the state cookie to verify that the user makes the call. +The Account Console in {project_name} can be vulnerable to CSRF. To prevent CSRF attacks, {project_name} sets a state cookie and embeds the value of this cookie in hidden form fields or query parameters within action links. {project_name} checks the query/form parameter against the state cookie to verify that the same user made the call. diff --git a/docs/documentation/server_admin/topics/threat/fapi-compliance.adoc b/docs/documentation/server_admin/topics/threat/fapi-compliance.adoc index 6330fa717334..ab927638e2c3 100644 --- a/docs/documentation/server_admin/topics/threat/fapi-compliance.adoc +++ b/docs/documentation/server_admin/topics/threat/fapi-compliance.adoc @@ -2,6 +2,6 @@ === FAPI compliance To make sure that {project_name} server will validate your client to be more secure and FAPI compliant, you can configure client policies -for the FAPI support. Details are described in the FAPI section of link:{adapterguide_link}#_fapi-support[{adapterguide_name}]. Among other things, this ensures some security best practices +for the FAPI support. *FAPI* details are described in the link:{securing_apps_link}[securing apps] section. Among other things, this ensures some security best practices described above like SSL required for clients, secure redirect URI used and more of similar best practices. diff --git a/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc new file mode 100644 index 000000000000..0044f005b534 --- /dev/null +++ b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc @@ -0,0 +1,5 @@ + +=== OAuth 2.1 compliance + +To make sure that {project_name} server will validate your client to be more secure and OAuth 2.1 compliant, you can configure client policies +for the OAuth 2.1 support. *OAuth 2.1* details are described in the link:{securing_apps_link}[securing apps] section. diff --git a/docs/documentation/server_admin/topics/threat/open-redirect.adoc b/docs/documentation/server_admin/topics/threat/open-redirect.adoc index a7417146079f..f37f95a79f54 100644 --- a/docs/documentation/server_admin/topics/threat/open-redirect.adoc +++ b/docs/documentation/server_admin/topics/threat/open-redirect.adoc @@ -6,3 +6,5 @@ An open redirector is an endpoint using a parameter to automatically redirect a {project_name} requires that all registered applications and clients register at least one redirection URI pattern. When a client requests that {project_name} performs a redirect, {project_name} checks the redirect URI against the list of valid registered URI patterns. Clients and applications must register as specific a URI pattern as possible to mitigate open redirector attacks. If an application requires a non http(s) custom scheme, it should be an explicit part of the validation pattern (for example `custom:/app/\*`). For security reasons a general pattern like `*` does not cover non http(s) schemes. + +By using <<_client_policies, Client Policies>>, an administrator can make sure that clients cannot register open redirect URLs such as `*`. diff --git a/docs/documentation/server_admin/topics/threat/password-db-compromised.adoc b/docs/documentation/server_admin/topics/threat/password-db-compromised.adoc index 0d19b566accd..9b1503989a1c 100644 --- a/docs/documentation/server_admin/topics/threat/password-db-compromised.adoc +++ b/docs/documentation/server_admin/topics/threat/password-db-compromised.adoc @@ -1,4 +1,4 @@ === Password database compromised -{project_name} does not store passwords in raw text but as hashed text, using the PBKDF2 hashing algorithm. {project_name} performs 27,500 hashing iterations, the number of iterations recommended by the security community. This number of hashing iterations can adversely affect performance as PBKDF2 hashing uses a significant amount of CPU resources. \ No newline at end of file +{project_name} does not store passwords in raw text but as hashed text, using the `PBKDF2-HMAC-SHA512` message digest algorithm. {project_name} performs `210,000` hashing iterations, the number of iterations recommended by the security community. This number of hashing iterations can adversely affect performance as PBKDF2 hashing uses a significant amount of CPU resources. \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/threat/password.adoc b/docs/documentation/server_admin/topics/threat/password.adoc new file mode 100644 index 000000000000..307a4bdce82d --- /dev/null +++ b/docs/documentation/server_admin/topics/threat/password.adoc @@ -0,0 +1,4 @@ + +=== Password policies + +Ensure you have a complex password policy to force users to choose complex passwords. See the <<_password-policies, Password Policies>> chapter for more information. Prevent password guessing by setting up the {project_name} server to use one-time-passwords. diff --git a/docs/documentation/server_admin/topics/threat/read-only-attributes.adoc b/docs/documentation/server_admin/topics/threat/read-only-attributes.adoc index 5403525a3181..e6c7de36b32a 100644 --- a/docs/documentation/server_admin/topics/threat/read-only-attributes.adoc +++ b/docs/documentation/server_admin/topics/threat/read-only-attributes.adoc @@ -29,11 +29,11 @@ This is the list of the read-only attributes, which are used internally by the { System administrators have a way to add additional attributes to this list. The configuration is currently available at the server level. -You can add this configuration by using the `spi-user-profile-declarative-user-profile-read-only-attributes` and ``spi-user-profile-declarative-user-profile-admin-read-only-attributes` options. For example: +You can add this configuration by using the `spi-user-profile--declarative-user-profile--read-only-attributes` and `spi-user-profile--declarative-user-profile--admin-read-only-attributes` options. For example: [source,bash,options="nowrap"] ---- -kc.[sh|bat] start --spi-user-profile-declarative-user-profile-read-only-attributes=foo,bar* +kc.[sh|bat] start --spi-user-profile--declarative-user-profile--read-only-attributes=foo,bar* ---- For this example, users and administrators would not be able to update attribute `foo`. Users would not be able to edit any attributes starting with the `bar`. diff --git a/docs/documentation/server_admin/topics/threat/redirect.adoc b/docs/documentation/server_admin/topics/threat/redirect.adoc index eeca92d712a0..1a65d74b868e 100644 --- a/docs/documentation/server_admin/topics/threat/redirect.adoc +++ b/docs/documentation/server_admin/topics/threat/redirect.adoc @@ -3,3 +3,5 @@ === Unspecific redirect URIs Make your registered redirect URIs as specific as feasible. Registering vague redirect URIs for xref:con-oidc-auth-flows_{context}[Authorization Code Flows] can allow malicious clients to impersonate another client with broader access. Impersonation can happen if two clients live under the same domain, for example. + +You can use secure redirect uris enforcer executor for your realm. The result makes sure that client administrators are able to register only clients with specific redirect-uris matching various requirements such as requiring that a URL cannot have wildcards in the context path or can be limited to specified permitted domains. See <<_client_policies, Client Policies>> for details about how to configure client policies with a specific executor. \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/threat/validate-user-attributes.adoc b/docs/documentation/server_admin/topics/threat/validate-user-attributes.adoc new file mode 100644 index 000000000000..f80ddb76df5a --- /dev/null +++ b/docs/documentation/server_admin/topics/threat/validate-user-attributes.adoc @@ -0,0 +1,10 @@ +[[validate_user_attributes]] +=== Validate user attributes + +With the functionality in <>, administrators can restrict the data users enter for attributes, for example, in user registration or the account console. + +Administrators should not allow unmanaged attributes for users to prevent attackers adding an unlimited number of attributes. +Attributes should have a validation that restricts the amount of data entered by attackers. + +When using regular expressions to validate user attributes, avoid regular expressions that use an excessive amount of memory or CPU. +See https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS[OWASP's Regular expression Denial of Service] for details. \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/user-federation.adoc b/docs/documentation/server_admin/topics/user-federation.adoc index 41d4a1c6b036..baa263f6db1a 100644 --- a/docs/documentation/server_admin/topics/user-federation.adoc +++ b/docs/documentation/server_admin/topics/user-federation.adoc @@ -18,7 +18,7 @@ To add a storage provider, perform the following procedure: .User federation image:images/user-federation.png[User federation] + -. Select the provider type card from the listed cards. +. Choose to add a *Kerberos* or *LDAP* provider + {project_name} brings you to that provider's configuration page. diff --git a/docs/documentation/server_admin/topics/user-federation/ldap.adoc b/docs/documentation/server_admin/topics/user-federation/ldap.adoc index 4df2909017e9..965bc0ba54bd 100644 --- a/docs/documentation/server_admin/topics/user-federation/ldap.adoc +++ b/docs/documentation/server_admin/topics/user-federation/ldap.adoc @@ -17,12 +17,15 @@ image:images/user-federation.png[User federation] . Click *Add LDAP providers*. + {project_name} brings you to the LDAP configuration page. ++ +.Add LDAP provider +image:images/user-fed-ldap.png[User federation] ==== Storage mode {project_name} imports users from LDAP into the local {project_name} user database. This copy of the user database synchronizes on-demand or through a periodic background task. An exception exists for synchronizing passwords. {project_name} never imports passwords. Password validation always occurs on the LDAP server. -The advantage of synchronization is that all {project_name} features work efficiently because any required extra per-user data is stored locally. The disadvantage is that each time {project_name} queries a specific user for the first time, {project_name} performs a corresponding database insert. +The advantage of synchronization is that all {project_name} features work efficiently because any required extra per-user data is stored locally. The disadvantage is that each time {project_name} queries a specific user for the first time, {project_name} performs a corresponding database insert. Also, when imported users are returned as part of a search operation, a corresponding LDAP search is performed for each one to check if the user still exists in LDAP and do some basic validation. You can synchronize the import with your LDAP server. Import synchronization is unnecessary when LDAP mappers always read particular attributes from the LDAP rather than the database. @@ -37,6 +40,13 @@ If you disable *Import Users*, you cannot save user profile attributes into the When you attempt to change the non-LDAP mapped user data, the user update is not possible. For example, you cannot disable the LDAP mapped user unless the user's `enabled` flag maps to an LDAP attribute. ==== +[NOTE] +==== +When working with imported users, {project_name} performs a LDAP search when the user is queried to validate the user and decorate it so that the configured mappers work properly. This means that extra care must be taken when performing unfiltered user searches that may fetch a big number of users as a LDAP search will be issued for every imported user that is found, possibly affecting the performance in a negative way. + +Operations that fetch a single user (for example during login) are usually cached and should not be impacted by this extra LDAP search that is performed when the user is fetched for the first time. +==== + ==== Edit mode Users and admins can modify user metadata, users through the <<_account-service, Account Console>>, and administrators through the Admin Console. The `Edit Mode` configuration on the LDAP configuration page defines the user's LDAP update privileges. @@ -69,16 +79,22 @@ Toggle this switch to *ON* if you want new users created by {project_name} added Allow Kerberos authentication:: Enable Kerberos/SPNEGO authentication in the realm with user data provisioned from LDAP. For more information, see the <<_kerberos,Kerberos section>>. +Remove invalid users during searches:: +Remove users from the local database if they are not available from the user storage when executing searches. If this is true, users no longer available from their corresponding user storage will be deleted from the local database whenever trying to look up users. If false, then users previously imported from the user storage will be kept in the local database, as read-only and disabled, even if that user is no longer available from the user storage. For example, user was deleted directly from LDAP or the `Users DN` is invalid. Note that this behavior will only happen when the user is not yet cached. + +Relative User Creation DN:: +Relative DN from the `Users DN` where new users will be created. This allows users to be created in a sub-DN of the parent `Users DN` when using a `subtree` search scope. For example, if the `Users DN` is set to `ou=people,dc=myorg,dc=com` and the `Relative User Creation DN` is set to `ou=engineering`, users will be fetched from the `Users DN` and all sub-DNs, but new users will be stored in `ou=engineering,ou=people,dc=myorg,dc=com`. In other words, {project_name} concatenates the `Relative User Creation DN` with the `Users DN` (a comma is added automatically when concatenating the DNs) and uses this resulting DN to store users + +A similar property is also available in the group and role mappers, allowing groups and roles to be added to a sub-DN of the base DN that is used to search for the groups/roles. + Other options:: Hover the mouse pointer over the tooltips in the Admin Console to see more details about these options. ==== Connecting to LDAP over SSL -When you configure a secure connection URL to your LDAP store (for example,`ldaps://myhost.com:636`), {project_name} uses SSL to communicate with the LDAP server. Configure a truststore on the {project_name} server side so that {project_name} can trust the SSL connection to LDAP. +When you configure a secure connection URL to your LDAP store (for example,`ldaps://myhost.com:636`), {project_name} uses SSL to communicate with the LDAP server. Configure a truststore on the {project_name} server side so that {project_name} can trust the SSL connection to LDAP - see https://www.keycloak.org/server/keycloak-truststore[Configuring a Truststore] {section}. -Configure the global truststore for {project_name} with the Truststore SPI. For more information about configuring the global truststore, see the https://www.keycloak.org/server/keycloak-truststore[Configuring a Truststore] {section}. If you do not configure the Truststore SPI, the truststore falls back to the default mechanism provided by Java, which can be the file supplied by the `javax.net.ssl.trustStore` system property or the cacerts file from the JDK if the system property is unset. - -The `Use Truststore SPI` configuration property, in the LDAP federation provider configuration, controls the truststore SPI. By default, {project_name} sets the property to `Always`, which is adequate for most deployments. {project_name} uses the Truststore SPI if the connection URL to LDAP starts with `ldaps` only. +The `Use Truststore SPI` configuration property is deprecated. It should normally be left as `Always`. ==== Synchronizing LDAP users to {project_name} @@ -143,11 +159,44 @@ User Attribute mappers that map basic {project_name} user attributes, such as us When {project_name} updates a password, {project_name} sends the password in plain-text format. This action is different from updating the password in the built-in {project_name} database, where {project_name} hashes and salts the password before sending it to the database. For LDAP, {project_name} relies on the LDAP server to hash and salt the password. -By default, LDAP servers such as MSAD, RHDS, or FreeIPA hash and salt passwords. Other LDAP servers such as OpenLDAP or ApacheDS store the passwords in plain-text unless you use the _LDAPv3 Password Modify Extended Operation_ as described in https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3[RFC3062]. Enable the LDAPv3 Password Modify Extended Operation in the LDAP configuration page. See the documentation of your LDAP server for more details. +By default, LDAP servers such as MSAD, RHDS, or FreeIPA hash and salt passwords. Other LDAP servers such as OpenLDAP store the passwords in plain-text unless you use the _LDAPv3 Password Modify Extended Operation_ as described in https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3[RFC3062]. Enable the LDAPv3 Password Modify Extended Operation in the LDAP configuration page. See the documentation of your LDAP server for more details. https://directory.apache.org/apacheds/advanced-ug/4.1.1.4-ss-password-hash.html[Configure ApacheDS to hash and salt passwords automatically] by enabling the passwordHashing interceptor. WARNING: Always verify that user passwords are properly hashed and not stored as plaintext by inspecting a changed directory entry using `ldapsearch` and base64 decode the `userPassword` attribute value. +[[_ldap_connection_pool]] +==== Configuring the connection pool + +For more efficiency when managing LDAP connections and to improve performance when handling multiple connections, you can +enable connection pooling. By doing that, when a connection is closed, it will be returned to the pool for future use therefore +reducing the cost of creating new connections all the time. + +The LDAP connection pool configuration is configured using the following system properties: + +[cols="2*", options="header"] +|=== +|Name +|Description +| `com.sun.jndi.ldap.connect.pool.authentication` | A list of space-separated authentication types of connections that may be pooled. Valid types are "none", "simple", and "DIGEST-MD5" +| `com.sun.jndi.ldap.connect.pool.initsize` | The string representation of an integer that represents the number of connections per connection identity to create when initially creating a connection for the identity +| `com.sun.jndi.ldap.connect.pool.maxsize` | The string representation of an integer that represents the maximum number of connections per connection identity that can be maintained concurrently +| `com.sun.jndi.ldap.connect.pool.prefsize` | The string representation of an integer that represents the preferred number of connections per connection identity that should be maintained concurrently +| `com.sun.jndi.ldap.connect.pool.timeout` | The string representation of an integer that represents the number of milliseconds that an idle connection may remain in the pool without being closed and removed from the pool +| `com.sun.jndi.ldap.connect.pool.protocol` | A list of space-separated protocol types of connections that may be pooled. Valid types are "plain" and "ssl" +| `com.sun.jndi.ldap.connect.pool.debug` | A string that indicates the level of debug output to produce. Valid values are "fine" (trace connection creation and removal) and "all" (all debugging information) +|=== + +By default, connection pooling is enabled for both `plain` and `ssl` protocols. + +For more details, see the link:https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/config.html[Java LDAP Connection Pooling Configuration] documentation. + +To set any of these properties, you can set the `JAVA_OPTS_APPEND` environment variable: + +[source,bash] +---- +export JAVA_OPTS_APPEND=-Dcom.sun.jndi.ldap.connect.pool.initsize=10 -Dcom.sun.jndi.ldap.connect.pool.maxsize=50 +---- + [[_ldap_troubleshooting]] ==== Troubleshooting @@ -174,11 +223,10 @@ Mapper for provider: XXX, Mapper name: YYY, Provider: ZZZ ... ``` Note those messages are displayed just with the enabled DEBUG logging. -- For tracking the performance or connection pooling issues, consider setting the value of property `Connection Pool Debug Level` of -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. +- For tracking the performance or connection pooling issues, consider setting the value of property `com.sun.jndi.ldap.connect.pool.debug` to `all`. This change adds many additional messages to the server log with the included logging for the LDAP connection +pooling. As a result, you can track the issues related to connection pooling or performance. For more details, see link:#_ldap_connection_pool[Configuring the connection pool]. -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 @@ -187,4 +235,7 @@ 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/user-federation/sssd.adoc b/docs/documentation/server_admin/topics/user-federation/sssd.adoc index 56bd160016ff..08206bb71c28 100644 --- a/docs/documentation/server_admin/topics/user-federation/sssd.adoc +++ b/docs/documentation/server_admin/topics/user-federation/sssd.adoc @@ -3,9 +3,9 @@ === SSSD and FreeIPA Identity Management integration -{project_name} includes the https://fedoraproject.org/wiki/Features/SSSD[System Security Services Daemon (SSSD)] plugin. SSSD is part of the Fedora and Red Hat Enterprise Linux (RHEL), and it provides access to multiple identities and authentication providers. SSSD also provides benefits such as failover and offline support. For more information, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system-level_authentication_guide/sssd[the Red Hat Enterprise Linux Identity Management documentation]. +{project_name} includes the https://fedoraproject.org/wiki/Features/SSSD[System Security Services Daemon (SSSD)] plugin. SSSD is part of the Fedora and Red Hat Enterprise Linux (RHEL), and it provides access to multiple identities and authentication providers. SSSD also provides benefits such as failover and offline support. For more information, see https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/system-level_authentication_guide/sssd[the Red Hat Enterprise Linux Identity Management documentation]. -SSSD integrates with the FreeIPA identity management (IdM) server, providing authentication and access control. With this integration, {project_name} can authenticate against privileged access management (PAM) services and retrieve user data from SSSD. For more information about using Red Hat Identity Management in Linux environments, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/linux_domain_identity_authentication_and_policy_guide/index[the Red Hat Enterprise Linux Identity Management documentation]. +SSSD integrates with the FreeIPA identity management (IdM) server, providing authentication and access control. With this integration, {project_name} can authenticate against privileged access management (PAM) services and retrieve user data from SSSD. For more information about using Red Hat Identity Management in Linux environments, see https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/linux_domain_identity_authentication_and_policy_guide/index[the Red Hat Enterprise Linux Identity Management documentation]. image:images/keycloak-sssd-freeipa-integration-overview.png[] @@ -13,7 +13,7 @@ image:images/keycloak-sssd-freeipa-integration-overview.png[] [NOTE] ==== -{project_name} registers groups and roles automatically but does not synchronize them. Any changes made by the {project_name} administrator in {project_name} do not synchronize with SSSD. +{project_name} registers groups and roles automatically but does not synchronize them. The groups are imported from SSSD the first time the user is accessed and then they are managed entirely inside {project_name}. Any changes made by the administrator in {project_name} do not synchronize with SSSD or vice-versa. ==== ==== FreeIPA/IdM server @@ -31,7 +31,7 @@ The https://quay.io/repository/freeipa/freeipa-server?tab=tags/[FreeIPA Containe -v /var/lib/ipa-data:/data:Z freeipa/freeipa-server ---- + -The parameter `-h` with `server.freeipa.local` represents the FreeIPA/IdM server hostname. +The parameter `-h` with `server.freeipa.local` represents the FreeIPA/IdM server hostname. Change `YOUR_PASSWORD` to a password of your own. . After the container starts, change the `/etc/hosts` file to include: @@ -156,7 +156,7 @@ ipaapi:x:992:988:IPA Framework User:/:/sbin/nologin {project_name} uses https://github.com/hypfvieh/dbus-java[DBus-Java] project to communicate at a low level with D-Bus and https://github.com/java-native-access/jna[JNA] to authenticate via Operating System Pluggable Authentication Modules (PAM). -Although now {project_name} contains all the needed libraries to run the `SSSD` provider, JDK version 17 is needed. Therefore the `SSSD` provider will only be displayed when the host configuration is correct and JDK 17 is used to run {project_name}. +Although now {project_name} contains all the needed libraries to run the `SSSD` provider, JDK version 21 is needed. Therefore the `SSSD` provider will only be displayed when the host configuration is correct and JDK 21 is used to run {project_name}. ==== Configuring a federated SSSD store diff --git a/docs/documentation/server_admin/topics/users/con-aia.adoc b/docs/documentation/server_admin/topics/users/con-aia.adoc new file mode 100644 index 000000000000..b4bc11576bc5 --- /dev/null +++ b/docs/documentation/server_admin/topics/users/con-aia.adoc @@ -0,0 +1,82 @@ +// Module included in the following assemblies: +// +// server_admin/topics/users.adoc + +[id="con-aia_{context}"] += Application initiated actions + +Application initiated actions (AIA) allow client applications to request a user to perform an action on the {project_name} side. Usually, when an OIDC client application +wants a user to log in, it redirects that user to the login URL as described in the <>. After login, the user is redirected back to the client application. +The user performs the actions that were required by the administrator as described in the <> +and then is immediately redirected back to the application. However, AIA allows the client application to request some required actions from the user during login. This can be +done even if the user is already authenticated on the client and has an active SSO session. It is triggered by adding the `kc_action` parameter to the OIDC login URL with the value containing the requested action. +For instance `kc_action=UPDATE_PASSWORD` parameter. + +A user may cancel an application initiated action. In this case the user is redirected back to the client application. +The redirect URI will contain the query parameters `kc_action_status=cancelled` and `kc_action` with the name of the cancelled action. + +NOTE: The `kc_action` and `kc_action_status` parameters are a {project_name} proprietary mechanism unsupported by the OIDC specification. + +NOTE: Application initiated actions are supported only for OIDC clients. + +So if AIA is used, an example flow is similar to the following: + +* A client application redirects the user to the OIDC login URL with the additional parameter such as `kc_action=UPDATE_PASSWORD` + +* There is a `browser` flow always triggered as described in the <<_authentication-flows, Authentication flows section>>. If the user was not authenticated, that user needs to authenticate as during normal login. +In case the user was already authenticated, that user might be automatically re-authenticated by an SSO cookie without needing to actively re-authenticate and supply the credentials again. In this case, that user will be +directly redirected to the screen with the particular action (update password in this case). However, in some cases, active re-authentication is required even if the user has an SSO +cookie (See <> for the details). + +* The screen with particular action (in this case `update password`) is displayed to the user, so that user needs to perform a particular action + +* Then user is redirected back to the client application + +Note that AIA are used by the {project_name} <<_account-service, Account Console>> to request update password or to reset other credentials such as OTP or WebAuthn. + +WARNING: Even if the parameter `kc_action` was used, it is not sufficient to assume that the user always performs the action. For example, a user could have manually deleted +the `kc_action` parameter from the browser URL. Therefore, no guarantee exists that the user has an OTP for the account after the client requested `kc_action=CONFIGURE_TOTP`. If you +want to verify that the user configured two-factor authenticator, the client application may need to check it was configured. For instance +by checking the claims like `acr` in the tokens. + +[id="con-aia-reauth_{context}"] +== Re-authentication during AIA + +In case the user is already authenticated due to an active SSO session, that user usually does not need to actively re-authenticate. However, if that user actively authenticated longer than five minutes ago, +the client can still request re-authentication when some AIA is requested. Exceptions exist from this guideline as follows: + +* For every required action it is possible to configure the max age on the required action itself in the <>. + If the policy is not configured, it defaults to five minutes. + +* The action `delete_account` will always require the user to actively re-authenticate + +* The action `UPDATE_PASSWORD` might require the user to actively re-authenticate according to the configured <>. +In case the policy is not configured, it is also possible to configure it on the required action itself in the <> +when configuring the particular required action. If the policy is not configured in any of those places, it defaults to five minutes. + +* If you want to use a shorter re-authentication, you can still use a parameter query parameter such as `max_age` with the specified shorter value or eventually `prompt=login`, which will always require user to +actively re-authenticate as described in the OIDC specification. Note that using `max_age` for a longer value than the default five minutes (or the one specifically configured for the required action) is not supported. +The `max_age` can be currently used only to make the value shorter than the default five minutes. + +* If <<_step-up-flow,Step-up authentication>> is enabled and the action is to add or delete a credential, authentication is required with the level corresponding +to the given credential. This requirement exists in case the user already has the credential of the particular level. For example, if `otp` and `webauthn` are configured in the authentication flow as 2nd-factor authenticators +(both in the authentication flow at level 2) and the user already has a 2nd-factor credential (`otp` or `webauthn` in this case), the user is required to authenticate with an existing 2nd-factor credential to add another 2nd-level credential. +In the same manner, deleting an existing 2nd-factor credential (`otp` or `webauthn` in this case), authentication with an existing 2nd-factor level credential is required. The requirement exists for security reasons. + +[id="con-aia-parameterized_{context}"] +== Parameterized AIA + +Some AIA can require the parameter to be sent together with the action name. For instance, the `Delete Credential` action can be triggered only by AIA and it requires a parameter to be sent together with the name +of the action, which points to the ID of the removed credential. So the URL for this example would be `kc_action=delete_credential:ce1008ac-f811-427f-825a-c0b878d1c24b`. In this case, the +part after the colon character (`ce1008ac-f811-427f-825a-c0b878d1c24b`) contains the ID of the credential of the particular user, which is to be deleted. The `Delete Credential` action +displays the confirmation screen where the user can confirm agreement to delete the credential. + +NOTE: The <<_account-service,{project_name} Account Console>> typically uses the `Delete Credential` action when deleting a 2nd-factor credential. You can check the Account Console for examples if you want +to use this action directly from your own applications. However, relying on the Account Console is best instead of managing credentials from your own applications. + +[id="con-aia-available-actions_{context}"] +== Available actions + +To see all available actions, log in to the Admin Console and select `master` realm. Then go to the right top corner and click on the name of the user -> select `Realm info` -> tab `Provider info`. Then in the table, find SPI `required-action` . In the +2nd column, there are available providers. Those can be used as values of the `kc_action` parameter (unless parameterized as described above). But note that this can be further restricted based on what actions are enabled for your realm in +the <>. diff --git a/docs/documentation/server_admin/topics/users/con-required-actions.adoc b/docs/documentation/server_admin/topics/users/con-required-actions.adoc index d5ed189190c2..ff6c33a99f6e 100644 --- a/docs/documentation/server_admin/topics/users/con-required-actions.adoc +++ b/docs/documentation/server_admin/topics/users/con-required-actions.adoc @@ -7,9 +7,13 @@ You can set the actions that a user must perform at the first login. These actions are required after the user provides credentials. After the first login, these actions are no longer required. You add required actions on the *Details* tab of that user. +Some required actions are automatically triggered for the user during login even if they are not explicitly added to this user by the administrator. For example `Update password` action can be +triggered if <<_password-policies, Password policies>> are configured in a way that the user password needs to be changed every X days. Or `verify profile` +action can require the user to update the <> as long as some user attributes do not match the requirements according to the user profile configuration. + The following are examples of required action types: -Update Password:: +Update Password:: The user must change their password. Configure OTP:: @@ -21,3 +25,7 @@ Verify Email:: Update Profile:: The user must update profile information, such as name, address, email, and phone number. +NOTE: Some actions do not makes sense to be added to the user account directly. For example, the `Update User Locale` is a helper action to handle some localization related parameters. Another +example is the `Delete Credential` action, which is supposed to be triggered as a <>. Regarding this one, if the administrator wants to delete the credential of some +user, that administrator can do it directly in the Admin Console. The `Delete Credential` action is dedicated to be used for example by the <<_account-service,{project_name} Account Console>>. + diff --git a/docs/documentation/server_admin/topics/users/con-user-impersonation.adoc b/docs/documentation/server_admin/topics/users/con-user-impersonation.adoc index 419dbe2fe89c..8b8c2eeb99a4 100644 --- a/docs/documentation/server_admin/topics/users/con-user-impersonation.adoc +++ b/docs/documentation/server_admin/topics/users/con-user-impersonation.adoc @@ -14,12 +14,12 @@ Any user with the `impersonation` role in the realm can impersonate a user. . Click a user to impersonate. . From the *Actions* list, select *Impersonate*. + -image:images/user-details.png[] +image:images/user-impersonate-action.png[] * If the administrator and the user are in the same realm, then the administrator will be logged out and automatically logged in as the user being impersonated. * If the administrator and user are in different realms, the administrator will remain logged in, and additionally will be logged in as the user in that user's realm. -In both instances, the *User Account Management* page of the impersonated user is displayed. +In both instances, the *Account Console* of the impersonated user is displayed. .Additional resources * For more information on assigning administration permissions, see the <<_admin_permissions,Admin Console Access Control>> chapter. diff --git a/docs/documentation/server_admin/topics/users/proc-configuring-user-attributes.adoc b/docs/documentation/server_admin/topics/users/proc-configuring-user-attributes.adoc deleted file mode 100644 index 528c090ccd28..000000000000 --- a/docs/documentation/server_admin/topics/users/proc-configuring-user-attributes.adoc +++ /dev/null @@ -1,27 +0,0 @@ -// Module included in the following assemblies: -// -// server_admin/topics/users.adoc - -[id="proc-configuring-user-attributes_{context}"] -= Configuring user attributes - -User attributes provide a customized experience for each user. You can create a personalized identity for each user in the console by configuring user attributes. - -.Users -image:images/user-attributes.png[] - -.Prerequisite -* You are in the realm where the user exists. - -.Procedure -. Click *Users* in the menu. -. Select a user to manage. -. Click the *Attributes* tab. -. Enter the attribute name in the *Key* field. -. Enter the attribute value in the *Value* field. -. Click *Save*. - - -NOTE: Some read-only attributes are not supposed to be updated by the administrators. This includes attributes that are read-only -by design like for example `LDAP_ID`, which is filled automatically by the LDAP provider. Some other attributes should be read-only for -typical user administrators due to security reasons. See the details in the xref:read_only_user_attributes[Mitigating security threats] chapter. diff --git a/docs/documentation/server_admin/topics/users/proc-enabling-recaptcha.adoc b/docs/documentation/server_admin/topics/users/proc-enabling-recaptcha.adoc index 45bef02f82ac..12cbc03ac0e9 100644 --- a/docs/documentation/server_admin/topics/users/proc-enabling-recaptcha.adoc +++ b/docs/documentation/server_admin/topics/users/proc-enabling-recaptcha.adoc @@ -6,45 +6,88 @@ = Enabling reCAPTCHA [role="_abstract"] -To safeguard registration against bots, {project_name} has integration with Google reCAPTCHA. +To safeguard registration against bots, {project_name} has integration with Google reCAPTCHA (see <>) and reCAPTCHA Enterprise (see <>). +The default theme (`register.ftl`) supports both v2 (visible, checkbox-based) and v3 (score-based, invisible) reCAPTCHA (see https://cloud.google.com/recaptcha/docs/choose-key-type[Choose the appropriate reCAPTCHA key type]). -Once reCAPTCHA is enabled, you can edit `register.ftl` in your login theme to configure the placement and styling of the reCAPTCHA button on the registration page. +[[procedure_recaptcha]] +== Setting up Google reCAPTCHA -.Procedure . Enter the following URL in a browser: + [source,bash,subs=+attributes] ---- -https://developers.google.com/recaptcha/ +https://www.google.com/recaptcha/admin/create ---- -. Create an API key to get your reCAPTCHA site key and secret. Note the reCAPTCHA site key and secret for future use in this procedure. +. Create a reCAPTCHA and choose between Challenge v2 (visible checkbox) or Score-based, v3 (invisible) to get your reCAPTCHA site key and secret. Note them down for future use in this procedure. + -NOTE: The localhost works by default. You do not have to specify a domain. +NOTE: localhost domains are not supported by default. If you wish to continue supporting them for development you can add them to the list of supported domains for your site key. + . Navigate to the {project_name} admin console. -. Click *Authentication* in the menu. +. Click *Authentication* in the menu. . Click the *Flows* tab. . Select *Registration* from the list. . Set the *reCAPTCHA* requirement to *Required*. This enables reCAPTCHA. . Click the *gear icon* ⚙️ on the *reCAPTCHA* row. -. Click the *Config* link. + -.Recaptcha config page +.reCAPTCHA config image:images/recaptcha-config.png[] -.. Enter the *Recaptcha Site Key* generated from the Google reCAPTCHA website. -.. Enter the *Recaptcha Secret* generated from the Google reCAPTCHA website. +.. Enter the *reCAPTCHA Site Key* generated from the Google reCAPTCHA website. +.. Enter the *reCAPTCHA Secret* generated from the Google reCAPTCHA website. +.. Toggle **reCAPTCHA v3** according to your Site Key type: on for score-based reCAPTCHA (v3), off for challenge reCAPTCHA (v2). +.. (Optional) Toggle *Use recaptcha.net* to use `www.recaptcha.net` instead of `www.google.com` domain for cookies. See https://developers.google.com/recaptcha/docs/faq[reCAPTCHA faq] for more information. . Authorize Google to use the registration page as an iframe. + NOTE: In {project_name}, websites cannot include a login page dialog in an iframe. This restriction is to prevent clickjacking attacks. You need to change the default HTTP response headers that is set in {project_name}. + -.. Click *Realm Settings* in the menu. -.. Click the *Security Defenses* tab. -.. Enter `https://www.google.com` in the field for the *X-Frame-Options* header. -.. Enter `https://www.google.com` in the field for the *Content-Security-Policy* header. +.. Click *Realm Settings* in the menu. +.. Click the *Security Defenses* tab. +.. Enter `https://www.google.com` in the field for the *X-Frame-Options* header (or `https//www.recaptcha.net` if you enabled *Use recaptcha.net*). +.. Enter `https://www.google.com` in the field for the *Content-Security-Policy* header (or `https//www.recaptcha.net` if you enabled *Use recaptcha.net*). + + +[[procedure_recaptcha_enterprise]] +== Setting up Google reCAPTCHA Enterprise +. Enter the following URL in a browser: ++ +[source,bash,subs=+attributes] +---- +https://developers.google.com/recaptcha/ +---- + +. Create a key for a "Website" platform, and choose the desired key type. Leave the defaults for v3 reCAPTCHA (invisible), or toggle *Use checkbox challenge* for a v2 reCAPTCHA (visible). Note the site key for future use in this procedure. ++ +NOTE: The localhost works by default. You do not have to specify a domain. ++ +. On your Google Cloud Project, go to *Credentials* and create an API key. ++ +NOTE: For better security, click on *edit api key* and add an API restriction to restrict the key to the *reCAPTCHA Enterprise API* only. ++ +. Navigate to the {project_name} Admin Console. +. Click *Authentication* in the menu. +. Click the *Flows* tab. +. Duplicate the "registration" flow. +. Bind the new flow to the *Registration flow*. +. Edit the new flow: +.. Delete the *reCAPTCHA* step. +.. Add the step *reCAPTCHA Enterprise* as a sub-step of "registration form" (first step of the flow). +. Set the *reCAPTCHA Enterprise* requirement to *Required*. +. Click the *gear icon* ⚙️ on the *reCAPTCHA Enterprise* row. + ++ +.reCAPTCHA Enterprise config +image:images/recaptcha-enterprise-config.png[] + +.. Enter the *Recaptcha Project ID* of your Google Cloud console project. +.. Enter the *Recaptcha Site Key* generated at the beginning of the procedure. +.. Enter the *Recaptcha API Key* generated at the beginning of the procedure. +.. Toggle **reCAPTCHA v3** according to your Site Key type: on for score-based reCAPTCHA (v3), off for challenge reCAPTCHA (v2). +.. (Optional) Customize the *Min. Score Threshold* as you see fit. Set it to the minimum score, between 0.0 and 1.0, that a user should achieve on reCAPTCHA to be allowed to register. See https://cloud.google.com/recaptcha/docs/interpret-assessment-website#interpret_scores[interpret scores]. +.. (Optional) Toggle *Use recaptcha.net* to use `www.recaptcha.net` instead of `www.google.com` domain for cookies. See https://developers.google.com/recaptcha/docs/faq[reCAPTCHA faq] for more information. +. Authorize Google to use the registration page as an iframe. See the last steps of <> for a detailed procedure. [role="_additional-resources"] .Additional resources diff --git a/docs/documentation/server_admin/topics/users/proc-searching-user.adoc b/docs/documentation/server_admin/topics/users/proc-searching-user.adoc index 5f102d742e5f..ecc6fdada9fc 100644 --- a/docs/documentation/server_admin/topics/users/proc-searching-user.adoc +++ b/docs/documentation/server_admin/topics/users/proc-searching-user.adoc @@ -10,6 +10,8 @@ Search for a user to view detailed information about the user, such as the user' .Prerequisite * You are in the realm where the user exists. +== Default search + .Procedure . Click *Users* in the main menu. This *Users* page is displayed. . Type the full name, last name, first name, or email address of the user you want to search for in the search box. The search returns all users that match your criteria. @@ -19,8 +21,21 @@ The criteria used to match users depends on the syntax used on the search box: .. `"somevalue"` -> performs exact search of the string `"somevalue"`; .. `\*somevalue*` -> performs infix search, akin to a `LIKE '%somevalue%'` DB query; .. `somevalue*` or `somevalue` -> performs prefix search, akin to a `LIKE 'somevalue%'` DB query. -+ -NOTE: Searches performed in the *Users* page encompasses searching both {project_name}'s database and configured user federated backends, such as LDAP. Users found in federated backends will be imported into {project_name}'s database if they don't already exist there. -+ -.Additional resources + +== Attribute search + +.Procedure +. Click *Users* in the main menu. This *Users* page is displayed. +. Click *Default search* button and switch it to *Attribute search*. +. Click *Select attributes* button and specify the attributes to search by. +. Check *Exact search* checkbox to perform exact match or keep it unchecked to use an infix search for attribute values. +. Click *Search* button to perform the search. It returns all users that match the criteria. + + +[NOTE] +==== +Searches performed in the *Users* page encompass both {project_name}'s database and configured user federation backends, such as LDAP. Users found in federated backends will be imported into {project_name}'s database if they don't already exist there. +==== + +.Additional Resources * For more information on user federation, see <<_user-storage-federation,User Federation>>. diff --git a/docs/documentation/server_admin/topics/users/user-profile.adoc b/docs/documentation/server_admin/topics/users/user-profile.adoc index be05e39701b0..c4343efe7de5 100644 --- a/docs/documentation/server_admin/topics/users/user-profile.adoc +++ b/docs/documentation/server_admin/topics/users/user-profile.adoc @@ -1,9 +1,15 @@ [[user-profile]] -= Defining a user profile += Managing user attributes -In {project_name} a user is associated with a set of attributes. These attributes are used to better describe and identify users within {project_name} as well as to pass over additional information about them to applications. +In {project_name} a user is associated with a set of attributes. These attributes are used to better describe and identify +users within {project_name} as well as to pass over additional information about them to applications. -A user profile defines a well-defined schema for representing user attributes and how they are managed within a realm. By providing a consistent view over user information, it allows administrators to control the different aspects on how attributes are managed as well as to make it much easier to extend {project_name} to support additional attributes. +A user profile defines a well-defined schema for representing user attributes and how they are managed within a realm. +By providing a consistent view over user information, it allows administrators to control the different aspects on how +attributes are managed as well as to make it much easier to extend {project_name} to support additional attributes. + +Although the user profile is mainly targeted for attributes that end-users can manage (e.g.: first and last names, phone, etc) +it also serves for managing any other metadata you want to associate with your users. Among other capabilities, user profile enables administrators to: @@ -13,46 +19,108 @@ Among other capabilities, user profile enables administrators to: * Dynamically enforce user profile compliance so that user information is always updated and in compliance with the metadata and rules associated with attributes * Define validation rules on a per-attribute basis by leveraging the built-in validators or writing custom ones * Dynamically render forms that users interact with like registration, update profile, brokering, and personal information in the account console, according to the attribute definitions and without any need to manually change themes. +* Customize user management interfaces in the administration console so that attributes are rendered dynamically based on the user profile schema -The User Profile capabilities are backed by the User Profile SPI. By default, these capabilities are disabled and realms are configured to use a default configuration that keeps backward compatibility with the legacy behavior. +The user profile schema or configuration uses a <<_user-profile-json-configuration,JSON>> format to represent attributes and their metadata. From the administration console, +you are able to manage the configuration by clicking on the `Realm Settings` on the left side menu and then clicking on the `User Profile` tab on that page. -[NOTE] -==== -The legacy behavior is about keeping the default constraints used by {project_name} when managing users root attributes such as username, email, first and last name, without any restriction on how custom attributes are managed. Regarding user flows such as registration, profile update, brokering, and managing accounts through the account console, users are restricted to use the attributes aforementioned with the possibility to change theme templates to support additional attributes. +In the next sections, we'll be looking at how to create your own user profile schema or configuration, and how to manage attributes. -If you are already using {project_name}, the legacy behavior is what you have been using so far. -==== +== Understanding the Default Configuration -Differently than the legacy behavior, the declarative provider gives you a lot more flexibility to define the user profile configuration to a realm through the administration console and a well-defined JSON schema. +By default, {project_name} provides a basic user profile configuration covering some of the most common user attributes: -In the next sections, we'll be looking at how to use the declarative provider to define your own user profile configuration. +[cols="2*", options="header"] +|=== +|Name +|Description +| `username` | The username +| `email` | End-User's preferred e-mail address. +| `firstName` | Given name(s) or first name(s) of the end-user +| `lastName` | Surname(s) or last name(s) of the End-User +|=== + +In {project_name}, both `username` and `email` attributes have a special handling as they are often used to identify, authenticate, +and link user accounts. For those attributes, you are limited to changing their settings, and you can not remove them. [NOTE] ==== -In the future, the legacy behavior will no longer be supported in {project_name}. Ideally, you should start looking at the new capabilities provided by the User Profile and migrate your realms accordingly. +The behavior of both `username` and `email` attributes changes accordingly to the `Login` settings of your realm. For instance, +changing the `Email as username` or the `Edit username` settings will override any configuration you have set in the user profile configuration. ==== -== Enabling the User Profile +As you will see in the following sections, you are free to change the default configuration by bringing your own attributes +or changing the settings for any of the available attributes to better fit it to your needs. + +== Understanding the User Profile Contexts + +In {project_name}, users are managed through different contexts: + +* Registration +* Update Profile +* Reviewing Profile when authenticating through a broker or social provider +* Account Console +* Administrative (e.g.: administration console and Admin REST API) + +Except for the `Administrative` context, all other contexts are considered end-user contexts as they are related to user self-service +flows. + +Knowing these contexts is important to understand where your user profile configuration will take effect when managing users. +Regardless of the context where the user is being managed, the same user profile configuration will be used to render UIs and validate +attribute values. -:tech_feature_name: Declarative User Profile -:tech_feature_setting: -Dkeycloak.profile.feature.declarative_user_profile=enabled -:tech_feature_id: declarative-user-profile -include::../templates/techpreview.adoc[] +As you will see in the following sections, you might restrict certain attributes to be available only from the administrative context and disable them +completely for end-users. The other way around is also true if you don't want administrators to have access to certain user attributes but only the end-user. -In addition to enabling the `declarative_user_profile` feature, you should enable User Profile for a realm. To do that, click on the `Realm Settings` link on -the left side menu and turn on the `User Profile Enabled` switch. +[[_understanding-managed-and-unmanaged-attributes]] +== Understanding Managed and Unmanaged Attributes -image:images/user-profile-enabling.png[] +By default, {project_name} will only recognize the attributes defined in your user profile configuration. +The server ignores any other attribute not explicitly defined there. -Once you enable it and click on the `Save` button, you can access the `User Profile` tab from where you can manage the configuration for user attributes. +By being strict about which user attributes can be set to your users, as well as how their values are validated, +{project_name} can add another defense barrier to your realm and help you to prevent unexpected attributes and values associated to your users. -By enabling the user profile for a realm, {project_name} is going to impose additional constraints on how attributes are managed based on the user profile configuration. In summary, here is the list of what you should expect when the feature is enabled: +That said, user attributes can be categorized as follows: -* From an administration point of view, the `Attributes` tab at the user details page will only show the attributes defined in the user profile configuration. The conditions defined on a per-attribute basis will also be taken into account when managing attributes. +* *Managed*. These are attributes controlled by your user profile, to which you want to allow end-users and administrators +to manage from any user profile context. +For these attributes, you want complete control on how and when they are managed. +* *Unmanaged*. These are attributes you do not explicitly define in your user profile so that they are completely ignored by {project_name}, by default. -* User facing forms like registration, update profile, brokering, and personal info in the account console, are going to be rendered dynamically based on the user profile configuration. For that, {project_name} is going to rely on different templates to render these forms dynamically. +Although unmanaged attributes are disabled by default, you can configure your realm using different policies to define how they are handled by the server. +For that, click on the `Realm Settings` at the left side menu, click on the `General` tab, and then choose any of the following options from the `Unmanaged Attributes` setting: -In the next topics, we'll be exploring how to manage the user profile configuration and how it affects your realm. +* *Disabled*. This is the default policy so that unmanaged attributes are disabled from all user profile contexts. +* *Enabled*. This policy enables unmanaged attributes to all user profile contexts. +* *Admin can view*. This policy enables unmanaged attributes only from the administrative context as read-only. +* *Admin can edit*. This policy enables unmanaged attributes only from the administrative context for reads and writes. + +These policies give you a fine-grained control over how the server will handle unmanaged attributes. +You can choose to completely disable or only support unmanaged attributes when managing users through the administrative context. + +When unmanaged attributes are enabled (even if partially) you can manage them from the administration console at the `Attributes` tab in the User Details UI. +If the policy is set to `Disabled` this tab is not available. + +As a security recommendation, try to adhere to the most strict policy as much as possible (e.g.: `Disabled` or `Admin can edit`) to prevent unexpected +attributes (and values) set to your users when they are managing their profile through end-user contexts. +Avoid setting the `Enabled` policy and prefer defining all the attributes that end-users can manage in your user profile configuration, under your control. + +[NOTE] +==== +The `Enabled` policy is targeted for realms migrating from previous versions of {project_name} and to avoid breaking +behavior when using custom themes and extending the server with their own custom user attributes. +==== + +As you will see in the following sections, you can also restrict the audience for an attribute by choosing if it should be visible or writable by users and/or administrators. + +For unmanaged attributes, the maximum length is 2048 characters. +To specify a different minimum or maximum length, change the unmanaged attribute to a managed attribute and add a `length` validator. + +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 {project_Name} and reference them by ID or URL. == Managing the User Profile @@ -62,24 +130,20 @@ The user profile configuration is managed on a per-realm basis. For that, click .User Profile Tab image:images/user-profile-tab.png[] -In the `Attributes` sub-tab you have a list of the attributes currently associated with the user profile. By default, the configuration is created based on the user root attributes and each attribute is configured with some defaults in terms of validation and permissioning. +In the `Attributes` sub-tab you have a list of all managed attributes. In the `Attribute Groups` sub-tab you can manage attribute groups. An attribute group allows you to correlate attributes so that they are displayed together when rendering user facing forms. -[NOTE] -==== -For now, attribute groups are only used for rendering purposes but in the future they should also enable defining top-level configurations to the attributes they are linked to. -==== - -In the `JSON Editor` sub-tab you can view and edit the configuration using a well-defined JSON schema. Any change you make when at any other tab are reflected in the JSON configuration shown at this tab. +In the `JSON Editor` sub-tab you can view and edit the <<_user-profile-json-configuration,JSON>> configuration. You can use this tab +to grab your current configuration or manage it manually. Any change you make to this tab is reflected in the other tabs, and vice-versa. -In the next section, you are going to learn how to manage the configuration from the `Attributes` sub-tab. +In the next section, you are going to learn how to manage attributes. == Managing Attributes -At the `Attributes` sub-tab you can create, edit, and delete the attributes associated with the user profile. +At the `Attributes` sub-tab you can create, edit, and delete the managed attributes. -To define a new attribute and associate it with the user profile, click on the *Create attribute* button at the top the attribute listing. +To define a new attribute and associate it with the user profile, click on the *Create attribute* button at the top of the attribute listing. .Attribute Configuration image:images/user-profile-create-attribute.png[] @@ -87,74 +151,74 @@ image:images/user-profile-create-attribute.png[] When configuring the attribute you can define the following settings: Name:: -The name of the attribute. +The name of the attribute, used to uniquely identify an attribute. Display name:: -A user-friendly name for the attribute, mainly used when rendering user-facing forms. It supports internationalization so that values can be loaded from message bundles. +A user-friendly name for the attribute, mainly used when rendering user-facing forms. It also supports link:#_using-internationalized-messages[Using Internationalized Messages] + +Multivalued:: +If enabled, the attribute supports multiple values and UIs are rendered accordingly to allow setting many values. When enabling this +setting, make sure to add a validator to set a hard limit to the number of values. Attribute Group:: The attribute group to which the attribute belongs to, if any. -Enabled when scope:: -Allows you to define a list of scopes to dynamically enable an attribute. If not set, the attribute is always enabled and its constraints are always enforced when managing user profiles as well as when rendering user-facing forms. Otherwise, the same constraints only apply when any of the scopes in the list is requested by clients. - +Enabled when:: +Enables or disables an attribute. If set to `Always`, the attribute is available from any user profile context. +If set to `Scopes are requested`, the attribute is only available when the client acting on behalf of the user is requesting a +set of one or more scopes. You can use this option to dynamically enforce certain attributes depending on the client scopes +being requested. For the administration console, scopes are not evaluated and the attribute is always enabled. +That is because filtering attributes by scopes only works when running end-user authentication flows. Required:: -Set the attribute as required. If not enabled, the attribute is optional. Otherwise, the attribute must be provided by users and administrators with the possibility to also make the attribute required only for users or administrators as well as based on the scopes requested by clients. +Set the conditions to mark an attribute as required. If disabled, the attribute is optional. +If enabled, you can set the `Required for` setting to mark the attribute as required depending on the user profile context so that +the attribute is required for end-users (via end-user contexts) or to administrators (via administrative context), or both. +You can also set the `Required when` setting to mark the attribute as required only when a set of one or more client scopes are requested. +If set to `Always`, the attribute is required from any user profile context. +If set to `Scopes are requested`, the attribute is only required when the client acting on behalf of the user is requesting a +set of one or more scopes. For the account and administration consoles, scopes are not evaluated and the attribute is not required. +That is because filtering attributes by scopes only works when running authentication flows. Permission:: -In this section, you can define read and write permissions for users and administrators. +In this section, you can define read and write permissions when the attribute is being managed from an end-user or administrative context. +The `Who can edit` setting mark an attribute as writable by `User` and/or `Admin`, from an end-user and administrative context, respectively. +The `Who can view` setting mark an attribute as read-only by `User` and/or `Admin` from an end-user and administrative context, respectively. Validation:: -In this section, you can define the validations that will be performed when managing the attribute value. {project_name} provides a set of built-in validators you can choose from with the possibility to add your own. +In this section, you can define the validations that will be performed when managing the attribute value. +{project_name} provides a set of built-in validators you can choose from with the possibility to add your own. For more details, look at +the link:#_validating-attributes[Validating Attributes] section. Annotation:: In this section, you can associate annotations to the attribute. Annotations are mainly useful to pass over additional metadata to frontends for rendering purposes. +For more details, look at the link:#_defining-ui-annotations[Defining UI Annotations] section. -=== Managing Permissions - -In the `Permission` section, you can define the level of access users and administrators have to read and write to an attribute. - -.Attribute Permission -image:images/user-profile-permission.png[] - -For that, you can use the following settings: +When you create an attribute, the attribute is only available from administrative contexts to avoid unexpectedly exposing attributes to end-users. +Effectively, the attribute won't be accessible to end-users when they are managing their profile through the end-user contexts. You can change the `Permissions` settings anytime accordingly +to your needs. -Can user view?:: -If enabled, users can view the attribute. Otherwise, users don't have access to the attribute. +[[_validating-attributes]] +== Validating Attributes -Can user edit?:: -If enabled, users can view and edit the attribute. Otherwise, users don't have access to write to the attribute. +You can enable validation to managed attributes to make sure the attribute value conforms to specific rules. +For that, you can add or remove validators from the `Validations` settings when managing an attribute. -Can admin view?:: -If enabled, administrators can view the attribute. Otherwise, administrators don't have access to the attribute. - -Can admin edit?:: -If enabled, administrators can view and edit the attribute. Otherwise, administrators don't have access to write to the attribute. - -[NOTE] -==== -When you create an attribute, no permission is set to the attribute. Effectively, the attribute won't be accessible by either users or administrators. Once you create the attribute, make sure to set the permissions accordingly to that the attribute is only visible by the target audience. -==== - -Permissioning has a direct impact on how and who can manage the attribute, as well as on how the attribute is rendered in user-facing forms. - -For instance, by marking an attribute as only viewable by users, the administrators won't have access to the attribute when managing users through the administration console (neither from the User API). Also, users won't be able to change the attribute when updating their profiles. An interesting configuration if user attributes are fetched from an existing identity store (federation) and you just want to make attributes visible to users without any possibility to update the attribute other than through the source identity store. +.Attribute Validation +image:images/user-profile-validation.png[] -Similarly, you can also mark an attribute as writable only for administrators with read-only access for users. In this case, only administrators are going to be allowed to manage the attribute. +Validation happens at any time when writing to an attribute, and they can throw errors that will be shown in UIs when the value +fails a validation. -Depending on your privacy requirements, you might also want attributes inaccessible to administrators but with read-write permissions for users. +For security reasons, every attribute that is editable by users should have a validation to restrict the size of the values users enter. +If no `length` validator has been specified, {project_name} defaults to a maximum length of 2048 characters. -Make sure to set the correct permissions whenever you add a new attribute to the user profile configuration. +=== Built-in Validators -=== Managing validations +{project_name} provides some built-in validators that you can choose from, and you are also able to provide +your own validators by extending the `Validator SPI`. -In the `Validation` section, you can choose from different forms of validation to make sure the attribute value conforms to specific rules. - -.Attribute Validation -image:images/user-profile-validation.png[] - -{project_name} provides different validators out of the box: +The list below provides a list of all the built-in validators: [cols="3*", options="header"] |=== @@ -209,6 +273,10 @@ image:images/user-profile-validation.png[] |Check if the value has a valid format based on the realm and/or user locale. | None +|iso-date +|Check if the value has a valid format based on ISO 8601. This validator can be used with inputs using the html5-date input type. +| None + |person-name-prohibited-characters | Check if the value is a valid person name as an additional barrier for attacks such as script injection. The validation is based on a default RegEx pattern that blocks characters not common in person names. | @@ -217,6 +285,7 @@ image:images/user-profile-validation.png[] |username-prohibited-characters | Check if the value is a valid username as an additional barrier for attacks such as script injection. The validation is based on a default RegEx pattern that blocks characters not common in usernames. +When the realm setting `Email as username` is enabled, this validator is skipped to allow email values. | *error-message*: the key of the error message in i18n bundle. If not set a generic message is used. @@ -227,252 +296,49 @@ image:images/user-profile-validation.png[] *options*: array of strings containing allowed values. -|=== - -==== Managing annotations - -In order to pass additional information to frontends, attributes can be decorated with -annotations to dictate how attributes are rendered. This capability is mainly useful when extending {project_name} themes -to render pages dynamically based on the annotations associated with attributes. -This mechanism is used for example to link:#_configuring_form_input_field_for_attribute[configure Form input filed for attribute]. - -.Attribute Annotation -image:images/user-profile-annotation.png[] - -== Managing Attribute Groups - -At the `Attribute Groups` sub-tab you can create, edit, and delete attribute groups. An attribute group allows you to define a container for correlated attributes so that they are rendered together when at the user-facing forms. - -.Attribute Group List -image:images/user-profile-attribute-group-list.png[] - -[NOTE] -==== -You can't delete attribute groups that are bound to attributes. For that, you should first update the attributes to remove the binding. -==== - -To create a new group, click on the *Create attributes group* button on the top of the attribute groups listing. - -.Attribute Group Configuration -image:images/user-profile-create-attribute-group.png[] - -When configuring the group you can define the following settings: - -Name:: -The name of the group. - -Display name:: -A user-friendly name for the group, mainly used when rendering user-facing forms. It supports internationalization so that values can be loaded from message bundles. - -Display description:: -A user-friendly text that will be displayed as a tooltip when rendering user-facing forms. - -Annotation:: -In this section, you can associate annotations to the attribute. Annotations are mainly useful to pass over additional metadata to frontends for rendering purposes. - -== Using the JSON configuration - -The user profile configuration is stored using a well-defined JSON schema. You can choose from editing the user profile configuration directly by clicking on the `JSON Editor` sub-tab. - -.JSON Configuration -image:images/user-profile-json-config.png[] - -The JSON schema is defined as follows: - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "required": { - "roles": [ "user", "admin" ], - "scopes": [ "foo", "bar" ] - }, - "permissions": { - "view": [ "admin", "user" ], - "edit": [ "admin", "user" ] - }, - "validations": { - "email": { - "max-local-length": 64 - }, - "length": { - "max": 255 - } - }, - "annotations": { - "myannotation": "myannotation-value" - } - } - ], - "groups": [ - { - "name": "personalInfo", - "displayHeader": "Personal Information" - } - ] -} ----- - -The schema supports as many attributes as you need. - -For each attribute you should define a `name` and, optionally, the `required`, `permission`, and the `annotations` settings. - -=== Required property - -The `required` setting defines whether an attribute is required. {project_name} allows you to set an attribute as required based on different conditions. - -When the `required` setting is defined as an empty object, the attribute is always required. - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "required": {} - ] -} ----- - -On the other hand, you can choose to make the attribute required only for users, or administrators, or both. As well as mark the attribute as required only in case a specific scope is requested when the user is authenticating in {project_name}. - -To mark an attribute as required for a user and/or administrator, set the `roles` property as follows: - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "required": { - "roles": ["user"] - } - ] -} ----- - -The `roles` property expects an array whose values can be either `user` or `admin`, depending on whether the attribute is required by the user or the administrator, respectively. - -Similarly, you can choose to make the attribute required when a set of one or more scopes is requested by a client when authenticating a user. For that, you can use the `scopes` property as follows: - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "required": { - "scopes": ["foo"] - } - ] -} ----- - -The `scopes` property is an array whose values can be any string representing a client scope. - -=== Permissions property - -The attribute-level `permissions` property can be used to define the read and write permissions to an attribute. The permissions are set based on whether these operations can be performed on the attribute by a user, or administrator, or both. - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "permissions": { - "view": ["admin"], - "edit": ["user"] - } - ] -} ----- - -Both `view` and `edit` properties expect an array whose values can be either `user` or `admin`, depending on whether the attribute is viewable or editable by the user or the administrator, respectively. - -When the `edit` permission is granted, the `view` permission is implicitly granted. - -=== Annotations property - -The attribute-level `annotation` property can be used to associate additional metadata to attributes. Annotations are mainly useful for passing over additional information about attributes to frontends rendering user attributes based on the user profile configuration. Each annotation is a key/value pair. - -[source,json] ----- -{ - "attributes": [ - { - "name": "myattribute", - "annotations": { - "foo": ["foo-value"], - "bar": ["bar-value"] - } - ] -} ----- - -== Using dynamic forms - -One of the main capabilities of User Profile is the possibility to dynamically render user-facing forms based on attributes metadata. When you have the feature enabled to your realm, forms like registration and update profile are rendered using specific theme templates to dynamically render pages based on the user profile configuration. - -That said, you shouldn't need to customize templates at all if the default rendering mechanisms serves to your needs. In case you still need customizations to themes, here are the templates you should be looking at: +|up-username-not-idn-homograph +|The field can contain only latin characters and common unicode characters. Useful for the fields, which can be subject of IDN homograph attacks (typically username). +| -[cols="2*", options="header"] -|=== -|Template -|Description +*error-message*: the key of the error message in i18n bundle. If not set a generic message is used. -| base/login/update-user-profile.ftl -| The template that renders the update profile page. +|multivalued +|Validates the size of a multivalued attribute. +| -| base/login/register-user-profile.ftl -| The template that renders the registration page. +*min*: an integer to define the minimum allowed count of attribute values. -| base/login/idp-review-user-profile.ftl -| The template that renders the page to review/update the user profile when federating users through brokering. +*max*: an integer to define the maximum allowed count of attribute values. -| base/login/user-profile-commons.ftl -| The template that renders input fields in forms based on attributes configuration. Used from all three page templates described above. New input types can be implemented here. |=== -The default rendering mechanism provides the following capabilities: - -* Dynamically display fields based on the permissions set to attributes. -* Dynamically render markers for required fields based on the constraints set to the attributes. -* Dynamically render field input type (text, date, number, select, multiselect) set to an attribute. -* Dynamically render read-only fields depending on the permissions set to an attribute. -* Dynamically order fields depending on the order set to the attributes. -* Dynamically group fields that belong to a same attribute group. - -=== Ordering attributes +[[_defining-ui-annotations]] +== Defining UI Annotations -The attributes order is set by dragging and dropping the attribute rows on the attribute listing page. - -.Ordering Attributes -image:images/user-profile-attribute-list-order.png[] +In order to pass additional information to frontends, attributes can be decorated with +annotations to dictate how attributes are rendered. This capability is mainly useful when extending {project_name} themes +to render pages dynamically based on the annotations associated with attributes. -The order you set in this page is respected when fields are rendered in dynamic forms. +Annotations are used, for example, for link:#_changing-the-html-type-for-an-attribute[Changing the HTML `type` for an Attribute] and link:#_changing-the-dom-representation-of-an-attribute[Changing the DOM representation of an Attribute], as you will +see in the following sections. -=== Grouping attributes +.Attribute Annotation +image:images/user-profile-annotation.png[] -When dynamic forms are rendered, they will try to group together attributes that belong to a same attribute group. - -.Dynamic Update Profile Form -image:images/user-profile-update-profile.png[] +An annotation is a key/value pair shared with the UI so that they can change how the HTML element corresponding to the attribute is rendered. +You can set any annotation you want to an attribute as long as the annotation is supported by the theme your realm is using. [NOTE] ==== -When attributes are linked to an attribute group, the attribute order is also important to make sure attributes within the same group are close together, within a same group header. Otherwise, if attributes within a group do not have a sequential order you might have the same group header rendered multiple times in the dynamic form. +The only restriction you have is to avoid using annotations using the `kc` prefix in their keys because these annotations +using this prefix are reserved for {project_name}. ==== -[[_configuring_form_input_field_for_attribute]] -=== Configuring Form input filed for Attributes +=== Built-in Annotations -{project_name} provides built-in annotations to configure which input type will be used for the attribute in dynamic forms and other aspects of it's visualization. +The following annotations are supported by {project_name} built-in themes: -Available annotations are: [cols="2*", options="header"] |=== |Name @@ -536,6 +402,16 @@ Useful for numeric fields. |inputTypeStep |HTML input `step` attribute applied to the field - Specifies the interval between legal numbers in an input field. Useful for numeric fields. +|Number Format +|If set, the `data-kcNumberFormat` attribute is added to the field to format the value based on a given format. This annotation is targeted for numbers where the format is based on the +number of digits expected in a determined position. For instance, a format `(\{2}) \{5}-\{4}` will format the field value to `(00) 00000-0000`. + +|Number UnFormat +|If set, the `data-kcNumberUnFormat` attribute is added to the field to format the value based on a given format before submitting the form. This annotation +is useful if you do not want to store any format for a specific attribute but only format the value on the client side. For instance, if the current value +is `(00) 00000-0000`, the value will change to `00000000000` if you set the value `\{11}` to this annotation or any other format you want by specifying a set of one or more group of digits. +Make sure to add validators to perform server-side validations before storing values. + |=== [NOTE] @@ -545,7 +421,11 @@ Field types use HTML form field tags and attributes applied to them - they behav Visual rendering also depends on css styles applied in the used theme. ==== -Available `inputType` annotation values: +[[_changing-the-html-type-for-an-attribute]] +=== Changing the HTML `type` for an Attribute + +You can change the `type` of a HTML5 input element by setting the `inputType` annotation. The available types are: + [cols="3*", options="header"] |=== |Name @@ -619,7 +499,7 @@ Available `inputType` annotation values: |=== [[_managing_options_for_select_fields]] -==== Defining options for select and multiselect fields +=== Defining options for select and multiselect fields Options for select and multiselect fields are taken from validation applied to the attribute to be sure validation and field options presented in UI are always consistent. By default, options are taken from built-in `options` validation. @@ -639,7 +519,7 @@ image:images/user-profile-select-options-simple-i18n.png[] Localized UI label texts for option value have to be provided by `userprofile.jobtitle.sweng` and `userprofile.jobtitle.swarch` keys then, using common localization mechanism. -You can also use `inputOptionLabels` annotation to provide labels for individual options. It contains map of labels for option - key in the map is +You can also use `inputOptionLabels` annotation to provide labels for individual options. It contains a map of labels for option - key in the map is option value (defined in validation), and value in the map is UI label text itself or its internationalization pattern (like `${i18n.key}`) for that option. [NOTE] @@ -710,10 +590,292 @@ provided by built-in `options` validation. .Options provided by custom validator image:images/user-profile-select-options-custom-validator.png[] +[[_changing-the-dom-representation-of-an-attribute]] +=== Changing the DOM representation of an Attribute + +You can enable additional client-side behavior by setting annotations with the `kc` prefix. These annotations are going to +translate into an HTML attribute in the corresponding element of an attribute, prefixed with `data-`, and a script with +the same name will be loaded to the dynamic pages so that you can select elements from the DOM based on the custom `data-` attribute +and decorate them accordingly by modifying their DOM representation. + +For instance, if you add a `kcMyCustomValidation` annotation to an attribute, the HTML attribute `data-kcMyCustomValidation` is added to +the corresponding HTML element for the attribute, and a JavaScript module is loaded from your custom theme at `/resources/js/kcMyCustomValidation.js`. +See the {developerguide_link}[{developerguide_name}] for more information about how to deploy a custom JavaScript module to your theme. + +The JavaScript module can run any code to customize the DOM and the elements rendered for each attribute. For that, +you can use the `userProfile.js` module to register an annotation descriptor for your custom annotation as follows: + +[source,javascript] +---- +import { registerElementAnnotatedBy } from "./userProfile.js"; -== Forcing User Profile compliance +registerElementAnnotatedBy({ + name: 'kcMyCustomValidation', + onAdd(element) { + var listener = function (event) { + // do something on keyup + }; -In order to make sure user profiles are in compliance with the configuration, administrators may use the `VerifyProfile` required action to eventually force users to update their profiles when authenticating to {project_name}. + element.addEventListener("keyup", listener); + + // returns a cleanup function to remove the event listener + return () => element.removeEventListener("keyup", listener); + } +}); +---- + +The `registerElementAnnotatedBy` is a method to register annotation descriptors. A descriptor is an object with a `name`, +referencing the annotation name, +and a `onAdd` function. Whenever the page is rendered or an attribute with the annotation is added to the DOM, the `onAdd` +function is invoked so that you can customize the behavior for the element. + +The `onAdd` function can also return a function to perform a cleanup. For instance, if you are adding event listeners +to elements, you might want to remove them in case the element is removed from the DOM. + +Alternatively, you can also use any JavaScript code you want if the `userProfile.js` is not enough for your needs: + +[source,javascript] +---- +document.querySelectorAll(`[data-kcMyCustomValidation]`).forEach((element) => { + var listener = function (evt) { + // do something on keyup + }; + + element.addEventListener("keyup", listener); + }); +---- + +== Managing Attribute Groups + +At the `Attribute Groups` sub-tab you can create, edit, and delete attribute groups. An attribute group allows you to define a container for correlated attributes so that they are rendered together when at the user-facing forms. + +.Attribute Group List +image:images/user-profile-attribute-group-list.png[] + +[NOTE] +==== +You can't delete attribute groups that are bound to attributes. For that, you should first update the attributes to remove the binding. +==== + +To create a new group, click on the *Create attributes group* button on the top of the attribute groups listing. + +.Attribute Group Configuration +image:images/user-profile-create-attribute-group.png[] + +When configuring the group you can define the following settings: + +Name:: +The name of the attribute, used to uniquely identify an attribute. + +Display name:: +A user-friendly name for the attribute, mainly used when rendering user-facing forms. It also supports link:#_using-internationalized-messages[Using Internationalized Messages] + +Display description:: +A user-friendly text that will be displayed as a tooltip when rendering user-facing forms. It also supports link:#_using-internationalized-messages[Using Internationalized Messages] + +Annotation:: +In this section, you can associate annotations to the attribute. Annotations are mainly useful to pass over additional metadata to frontends for rendering purposes. + +[[_user-profile-json-configuration]] +== Using the JSON configuration + +The user profile configuration is stored using a well-defined JSON schema. You can choose from editing the user profile configuration directly by clicking on the `JSON Editor` sub-tab. + +.JSON Configuration +image:images/user-profile-json-config.png[] + +The JSON schema is defined as follows: + +[source,json] +---- +{ + "unmanagedAttributePolicy": "DISABLED", + "attributes": [ + { + "name": "myattribute", + "multivalued": false, + "displayName": "My Attribute", + "group": "personalInfo", + "required": { + "roles": [ "user", "admin" ], + "scopes": [ "foo", "bar" ] + }, + "permissions": { + "view": [ "admin", "user" ], + "edit": [ "admin", "user" ] + }, + "validations": { + "email": { + "max-local-length": 64 + }, + "length": { + "max": 255 + } + }, + "annotations": { + "myannotation": "myannotation-value" + } + } + ], + "groups": [ + { + "name": "personalInfo", + "displayHeader": "Personal Information", + "annotations": { + "foo": ["foo-value"], + "bar": ["bar-value"] + } + } + ] +} +---- + +The schema supports as many attributes and groups as you need. + +The `unmanagedAttributePolicy` property defines the unmanaged attribute policy by setting one of following values. For more details, +look at the link:#_understanding-managed-and-unmanaged-attributes[Understanding Managed and Unmanaged Attributes]. + +* `DISABLED` +* `ENABLED` +* `ADMIN_VIEW` +* `ADMIN_EDIT` + +=== Attribute Schema + +For each attribute you should define a `name` and, optionally, the `required`, `permission`, and the `annotations` settings. + +The `required` property defines whether an attribute is required. {project_name} allows you to set an attribute as required based on different conditions. + +When the `required` property is defined as an empty object, the attribute is always required. + +[source,json] +---- +{ + "attributes": [ + { + "name": "myattribute", + "required": {} + ] +} +---- + +On the other hand, you can choose to make the attribute required only for users, or administrators, or both. As well as mark the attribute as required only in case a specific scope is requested when the user is authenticating in {project_name}. + +To mark an attribute as required for a user and/or administrator, set the `roles` property as follows: + +[source,json] +---- +{ + "attributes": [ + { + "name": "myattribute", + "required": { + "roles": ["user"] + } + ] +} +---- + +The `roles` property expects an array whose values can be either `user` or `admin`, depending on whether the attribute is required by the user or the administrator, respectively. + +Similarly, you can choose to make the attribute required when a set of one or more scopes is requested by a client when authenticating a user. For that, you can use the `scopes` property as follows: + +[source,json] +---- +{ + "attributes": [ + { + "name": "myattribute", + "required": { + "scopes": ["foo"] + } + ] +} +---- + +The `scopes` property is an array whose values can be any string representing a client scope. + +The attribute-level `permissions` property can be used to define the read and write permissions to an attribute. The permissions are set based on whether these operations can be performed on the attribute by a user, or administrator, or both. + +[source,json] +---- +{ + "attributes": [ + { + "name": "myattribute", + "permissions": { + "view": ["admin"], + "edit": ["user"] + } + ] +} +---- + +Both `view` and `edit` properties expect an array whose values can be either `user` or `admin`, depending on whether the attribute is viewable or editable by the user or the administrator, respectively. + +When the `edit` permission is granted, the `view` permission is implicitly granted. + +The attribute-level `annotation` property can be used to associate additional metadata to attributes. Annotations are mainly useful for passing over additional information about attributes to frontends rendering user attributes based on the user profile configuration. Each annotation is a key/value pair. + +[source,json] +---- +{ + "attributes": [ + { + "name": "myattribute", + "annotations": { + "foo": ["foo-value"], + "bar": ["bar-value"] + } + ] +} +---- + +=== Attribute Group Schema + +For each attribute group you should define a `name` and, optionally, the `annotations` settings. + +The attribute-level `annotation` property can be used to associate additional metadata to attributes. Annotations are mainly useful for passing over additional information about attributes to frontends rendering user attributes based on the user profile configuration. Each annotation is a key/value pair. + +== Customizing How UIs are Rendered + +The UIs from all the user profile contexts (including the administration console) are rendered dynamically accordingly to your +user profile configuration. + +The default rendering mechanism provides the following capabilities: + +* Show or hide fields based on the permissions set to attributes. +* Render markers for required fields based on the constraints set to the attributes. +* Change the field input type (text, date, number, select, multiselect) set to an attribute. +* Mark fields as read-only depending on the permissions set to an attribute. +* Order fields depending on the order set to the attributes. +* Group fields that belong to the same attribute group. +* Dynamically group fields that belong to the same attribute group. + +=== Ordering attributes + +The attribute order is set by dragging and dropping the attribute rows on the attribute listing page. + +.Ordering Attributes +image:images/user-profile-attribute-list-order.png[] + +The order you set in this page is respected when fields are rendered in dynamic forms. + +=== Grouping attributes + +When dynamic forms are rendered, they will try to group together attributes that belong to the same attribute group. + +.Dynamic Update Profile Form +image:images/user-profile-update-profile.png[] + +[NOTE] +==== +When attributes are linked to an attribute group, the attribute order is also important to make sure attributes within the same group are close together, within a same group header. Otherwise, if attributes within a group do not have a sequential order you might have the same group header rendered multiple times in the dynamic form. +==== + +== Enabling Progressive Profiling + +In order to make sure end-user profiles are in compliance with the configuration, administrators can use the `VerifyProfile` required action to eventually force users to update their profiles when authenticating to {project_name}. [NOTE] ==== @@ -722,24 +884,22 @@ The `VerifyProfile` action is similar to the `UpdateProfile` action. However, it When enabled, the `VerifyProfile` action is going to perform the following steps when the user is authenticating: -* Check whether the user profile is fully compliant with the user profile configuration set to the realm. +* Check whether the user profile is fully compliant with the user profile configuration set to the realm. That means running +validations and make sure all of them are successful. * If not, perform an additional step during the authentication so that the user can update any missing or invalid attribute. * If the user profile is compliant with the configuration, no additional step is performed, and the user continues with the authentication process. -By default, the `VerifyProfile` action is disabled. To enabled it, click on the -`Authentication` link on the left side menu and then click on the `Required Actions` tab. At this tab, select the *Enabled* switch of the `VerifyProfile` action. +The `VerifyProfile` action is enabled by default. To disable it, click on the +`Authentication` link on the left side menu and then click on the `Required Actions` tab. At this tab, use the *Enabled* switch of the `VerifyProfile` action to disable it. .Registering the VerifyProfile Required Action image:images/user-profile-register-verify-profile-action.png[] -== Migrating to User Profile - -Before enabling the User Profile capabilities to a realm, there are some important considerations you should be aware of. By providing a single place to manage attribute metadata, the feature is very strict about the attributes that can be set to users and how they are managed. - -In terms of user management, administrators are able to manage only the attributes defined in the user profile configuration. Any other attribute set to the user and not yet defined in the user profile configuration won't be accessible. It is recommended to update your user profile configuration with all the user attributes you want to expose either to users or administrators. - -The same recommendation applies for those accessing the User REST API to query user information. +[[_using-internationalized-messages]] +== Using Internationalized Messages -In regards to {project_name} internal user attributes such as `LDAP_ID`, `LDAP_ENTRY_DN`, or `KERBEROS_PRINCIPAL`, if you want to be able to access those attributes you should have them as attributes in your user profile configuration. The recommendation is to mark these attributes as viewable only to administrators so that you can look at them when managing the user attributes through the administration console or querying users via User API. +If you want to use internationalized messages when configuring attributes, attributes groups, and annotations, you can +set their display name, description, and values, using a placeholder that will translate to a message from a message bundle. -In regards to theming, if you already have customizations to the legacy templates (those hardcoded with user root attributes) your custom templates won't be used when rendering user-facing forms but the new templates that render these forms dynamically. Ideally, you should avoid having any customizations to templates and try to stick with the behavior provided by these new templates to dynamically render forms for you. If they are still not enough to address your requirements, you can either customize them or provide us with any feedback so that we discuss whether it makes sense to enhance the new templates. +For that, you can use a placeholder to resolve messages keys such as `${myAttributeName}`, where `myAttributeName` is the key for a message in a message bundle. For more details, +look at link:{developerguide_link}#messages[{developerguide_name}] about how to add message bundles to custom themes. diff --git a/docs/documentation/server_admin/topics/vault.adoc b/docs/documentation/server_admin/topics/vault.adoc index 6206d6f2081d..83e21b06c742 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: @@ -26,13 +26,14 @@ In the <<_ldap,LDAP settings>> of LDAP-based user federation. OIDC identity provider secret:: In the _Client Secret_ inside identity provider <<_identity_broker_oidc,OpenID Connect Config>> +[[_vault-key-resolvers]] === Key resolvers All built-in providers support the configuration of key resolvers. A key resolver implements the algorithm or strategy for combining the realm name with the key, obtained from the `${vault.key}` expression, into the final entry name used to retrieve the secret from the vault. {project_name} uses the `keyResolvers` property to configure the resolvers that the provider uses. The value is a comma-separated list of resolver names. An example of the configuration for the `files-plaintext` provider follows: [source,bash] ---- -kc.[sh|bat] start --spi-vault-file-key-resolvers=REALM_UNDERSCORE_KEY,KEY_ONLY +kc.[sh|bat] start --spi-vault--file--key-resolvers=REALM_UNDERSCORE_KEY,KEY_ONLY ---- The resolvers run in the same order you declare them in the configuration. For each resolver, {project_name} uses the last entry name the resolver produces, which combines the realm with the vault key to search for the vault's secret. If {project_name} finds a secret, it returns the secret. If not, {project_name} uses the next resolver. This search continues until {project_name} finds a non-empty secret or runs out of resolvers. If {project_name} finds no secret, {project_name} returns an empty secret. @@ -45,13 +46,13 @@ A list of the currently available resolvers follows: |Name |Description | KEY_ONLY -| {project_name} ignores the realm name and uses the key from the vault expression. +| {project_name} ignores the realm name and uses the key from the vault expression. {project_name} escapes occurrences of underscores in the key with another underscore character. For example, if the key is called `my_secret`, {project_name} searches for an entry in the vault named `my++__++secret`. This is to prevent conflicts with the default `REALM_UNDERSCORE_KEY` resolver. | REALM_UNDERSCORE_KEY | {project_name} combines the realm and key by using an underscore character. {project_name} escapes occurrences of underscores in the realm or key with another underscore character. For example, if the realm is called `master_realm` and the key is `smtp_key`, the combined key is `master+++__+++realm_smtp+++__+++key`. | REALM_FILESEPARATOR_KEY -| {project_name} combines the realm and key by using the platform file separator character. +| {project_name} combines the realm and key by using the platform file separator character. The vault expression prohibits the use of characters that could cause path traversal, thus preventing access to secrets outside the corresponding realm. ifeval::[{project_community}==true] | FACTORY_PROVIDED @@ -60,4 +61,4 @@ endif::[] |=== -If you have not configured a resolver for the built-in providers, {project_name} selects the `REALM_UNDERSCORE_KEY`. \ No newline at end of file +If you have not configured a resolver for the built-in providers, {project_name} selects the `REALM_UNDERSCORE_KEY`. diff --git a/docs/documentation/server_development/images/empty-user-federation-page.png b/docs/documentation/server_development/images/empty-user-federation-page.png index 5576a174f900..1ff3e73829c9 100644 Binary files a/docs/documentation/server_development/images/empty-user-federation-page.png and b/docs/documentation/server_development/images/empty-user-federation-page.png differ diff --git a/docs/documentation/server_development/images/keycloak_logo.png b/docs/documentation/server_development/images/keycloak_logo.png old mode 100755 new mode 100644 diff --git a/docs/documentation/server_development/images/storage-provider-created.png b/docs/documentation/server_development/images/storage-provider-created.png index 7e5757ce8cba..44e93e31d871 100644 Binary files a/docs/documentation/server_development/images/storage-provider-created.png and b/docs/documentation/server_development/images/storage-provider-created.png differ diff --git a/docs/documentation/server_development/images/user-federation-page.png b/docs/documentation/server_development/images/user-federation-page.png index 787eb9e08aff..595038863db6 100644 Binary files a/docs/documentation/server_development/images/user-federation-page.png and b/docs/documentation/server_development/images/user-federation-page.png differ diff --git a/docs/documentation/server_development/topics.adoc b/docs/documentation/server_development/topics.adoc index e802fc26e8fd..d1a915fc80bc 100644 --- a/docs/documentation/server_development/topics.adoc +++ b/docs/documentation/server_development/topics.adoc @@ -1,10 +1,10 @@ include::topics/preface.adoc[] include::topics/admin-rest-api.adoc[] include::topics/themes.adoc[] +include::topics/themes-react.adoc[] include::topics/themes-selector.adoc[] include::topics/themes-resources.adoc[] include::topics/locale-selector.adoc[] -include::topics/custom-attributes.adoc[] include::topics/identity-brokering.adoc[] include::topics/identity-brokering/tokens.adoc[] include::topics/identity-brokering/account-linking.adoc[] diff --git a/docs/documentation/server_development/topics/action-token-handler-spi.adoc b/docs/documentation/server_development/topics/action-token-handler-spi.adoc index 951d97917172..9e4806292db7 100644 --- a/docs/documentation/server_development/topics/action-token-handler-spi.adoc +++ b/docs/documentation/server_development/topics/action-token-handler-spi.adoc @@ -67,7 +67,7 @@ action token is invalidated. As action token is just a signed JWT with few mandatory fields (see <<_action_token_anatomy,Anatomy of action token>> above), it can be serialized and signed as such using Keycloak's `JWSBuilder` class. This way has been already implemented in `serialize(session, realm, uriInfo)` method of `org.keycloak.authentication.actiontoken.DefaultActionToken` -and can be leveraged by implementors by using that class for tokens instead of plain `JsonWebToken`. +and can be leveraged by implementers by using that class for tokens instead of plain `JsonWebToken`. The following example shows the implementation of a simple action token. Note that the class must have a private constructor without any arguments. This is necessary to deserialize the token class from JWT. diff --git a/docs/documentation/server_development/topics/admin-rest-api.adoc b/docs/documentation/server_development/topics/admin-rest-api.adoc index 37e74ad85eec..69e095b0e657 100644 --- a/docs/documentation/server_development/topics/admin-rest-api.adoc +++ b/docs/documentation/server_development/topics/admin-rest-api.adoc @@ -67,35 +67,7 @@ curl \ "http://localhost:8080{kc_realms_path}/master/protocol/openid-connect/token" ---- -ifeval::[{project_community}==true] -=== Example using Java - -There's a Java client library for the Admin REST API that makes it easy to use from Java. To use it from your application add a dependency on the -`keycloak-admin-client` library. - -The following example shows how to use the Java client library to get the details of the master realm: - -[source,java,subs="attributes+"] ----- - -import org.keycloak.admin.client.Keycloak; -import org.keycloak.representations.idm.RealmRepresentation; -... - -Keycloak keycloak = Keycloak.getInstance( - "http://localhost:8080{kc_base_path}", - "master", - "admin", - "password", - "admin-cli"); -RealmRepresentation realm = keycloak.realm("master").toRepresentation(); ----- - -Complete Javadoc for the admin client is available at {apidocs_link}[{apidocs_name}]. -endif::[] - === Additional resources [role="_additional-resources"] * {adminguide_link}[{adminguide_name}] -* {adapterguide_link}[{adapterguide_name}] * {apidocs_link}[{apidocs_name}] diff --git a/docs/documentation/server_development/topics/auth-spi.adoc b/docs/documentation/server_development/topics/auth-spi.adoc index e042c3954cf7..ab74c04ea19b 100644 --- a/docs/documentation/server_development/topics/auth-spi.adoc +++ b/docs/documentation/server_development/topics/auth-spi.adoc @@ -1,6 +1,7 @@ [[_auth_spi]] == Authentication SPI + {project_name} includes a range of different authentication mechanisms: kerberos, password, otp and others. These mechanisms may not meet all of your requirements and you may want to plug in your own custom ones. {project_name} provides an authentication SPI that you can use to write new plugins. @@ -8,7 +9,7 @@ The Admin Console supports applying, ordering, and configuring these new mechani {project_name} also supports a simple registration form. Different aspects of this form can be enabled and disabled for example -Recaptcha support can be turned off and on. +reCAPTCHA support can be turned off and on. The same authentication SPI can be used to add another page to the registration flow or reimplement it entirely. There's also an additional fine-grained SPI you can use to add specific validations and user extensions to the built-in registration form. @@ -65,7 +66,7 @@ Cookie - ALTERNATIVE Kerberos - ALTERNATIVE Forms subflow - ALTERNATIVE Username/Password Form - REQUIRED - Conditional OTP subflow - CONDITIONAL + Conditional 2FA subflow - CONDITIONAL Condition - User Configured - REQUIRED OTP Form - REQUIRED ---- @@ -114,14 +115,14 @@ Let's walk through the steps from when a client first redirects to keycloak to a A failureChallenge() means that there is a challenge, but that the flow should log this as an error in the error log. This error log can be used to lock accounts or IP Addresses that have had too many login failures. If the username and password is valid, the provider associated the UserModel with the AuthenticationSessionModel and returns a status of success(). -. The next execution is a subflow called Conditional OTP. The executions for this subflow are loaded and the same processing logic occurs. Its Requirement is +. The next execution is a subflow called Conditional 2FA. The executions for this subflow are loaded and the same processing logic occurs. Its Requirement is Conditional. This means that the flow will first evaluate all conditional executors that it contains. Conditional executors are authenticators that implement `ConditionalAuthenticator`, and must implement the method `boolean matchCondition(AuthenticationFlowContext context)`. A conditional subflow will call the `matchCondition` method of all conditional executions it contains, and if all of them evaluate to true, it will act as if it was a required subflow. If not, it will act as if it was a disabled subflow. Conditional authenticators are only used for this purpose, and are not used as authenticators. This means that even if the conditional authenticator evaluates to "true", then this will not mark a flow or subflow as successful. For example, a flow containing only a Conditional subflow with only a conditional authenticator will never allow a user to log in. -. The first execution of the Conditional OTP subflow is the Condition - User Configured. +. The first execution of the Conditional 2FA subflow is the Condition - User Configured. This provider requires that a user has been associated with the flow. This requirement is satisfied because the UsernamePassword provider already associated the user with the flow. This provider's `matchCondition` method will evaluate the `configuredFor` method for all other Authenticators in its current subflow. If the subflow contains @@ -133,7 +134,7 @@ Let's walk through the steps from when a client first redirects to keycloak to a Since a user is required for this provider, the provider is also asked if the user is configured to use this provider. If user is not configured, then the flow will then set up a required action that the user must perform after authentication is complete. For OTP, this means the OTP setup page. If the user is configured, he will be asked to enter his otp code. In our scenario, because of the conditional - sub-flow, the user will never see the OTP login page, unless the Conditional OTP subflow is set to Required. + sub-flow, the user will never see the OTP login page, unless the Conditional 2FA subflow is set to Required. . After the flow is complete, the authentication processor creates a UserSessionModel and associates it with the AuthenticationSessionModel. It then checks to see if the user is required to complete any required actions before logging in. . First, each required action's evaluateTriggers() method is called. @@ -150,7 +151,7 @@ Let's walk through the steps from when a client first redirects to keycloak to a In this section, we'll take a look at the Authenticator interface. For this, we are going to implement an authenticator that requires that a user enter in the answer to a secret question like "What is your mother's maiden name?". -This example is fully implemented and contained in the examples/providers/authenticator directory of the demo distribution of {project_name}. +This example is fully implemented and contained in the {quickstartRepo_link}[{quickstartRepo_name}] repository under `extension/authenticator`. To create an authenticator, you must at minimum implement the org.keycloak.authentication.AuthenticatorFactory and Authenticator interfaces. The Authenticator interface defines the logic. The AuthenticatorFactory is responsible for creating instances of an Authenticator. @@ -940,7 +941,7 @@ It is entirely possible for you to implement your own flow with a set of Authent But what you'll usually want to do is just add a bit of validation to the out-of-the-box registration page. An additional SPI was created to be able to do this. It basically allows you to add validation of form elements on the page as well as to initialize UserModel attributes and data after the user has been registered. -We'll look at both the implementation of the user profile registration processing as well as the registration Google Recaptcha plugin. +We'll look at both the implementation of the user profile registration processing as well as the registration Google reCAPTCHA Enterprise plugin. ==== Implementation FormAction interface @@ -953,18 +954,24 @@ Rendering is done in the buildPage() method, validation is done in the validate( @Override public void buildPage(FormContext context, LoginFormsProvider form) { - AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig(); - if (captchaConfig == null || captchaConfig.getConfig() == null - || captchaConfig.getConfig().get(SITE_KEY) == null - || captchaConfig.getConfig().get(SITE_SECRET) == null - ) { + Map config = context.getAuthenticatorConfig().getConfig(); + if (config == null + || Stream.of(PROJECT_ID, SITE_KEY, API_KEY, ACTION) + .anyMatch(key -> Strings.isNullOrEmpty(config.get(key))) + || parseDoubleFromConfig(config, SCORE_THRESHOLD) == null) { form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED)); return; } - String siteKey = captchaConfig.getConfig().get(SITE_KEY); + + String userLanguageTag = context.getSession().getContext().resolveLocale(context.getUser()) + .toLanguageTag(); + boolean invisible = Boolean.parseBoolean(config.getOrDefault(INVISIBLE, "true")); + form.setAttribute("recaptchaRequired", true); - form.setAttribute("recaptchaSiteKey", siteKey); - form.addScript("https://www.google.com/recaptcha/api.js"); + form.setAttribute("recaptchaSiteKey", config.get(SITE_KEY)); + form.setAttribute("recaptchaAction", config.get(ACTION)); + form.setAttribute("recaptchaVisible", !invisible); + form.addScript("https://www.google.com/recaptcha/enterprise.js?hl=" + userLanguageTag); } ---- @@ -975,11 +982,11 @@ You can add additional attributes to the form provider so that they can be displ The code above is from the registration recaptcha plugin. Recaptcha requires some specific settings that must be obtained from configuration. FormActions are configured in the exact same as Authenticators are. -In this example, we pull the Google Recaptcha site key from configuration and add it as an attribute to the form provider. -Our registration template file can read this attribute now. +In this example, we pull the Google Recaptcha site key and other options from Recaptcha configuration and add them as attributes to the form provider. +Our registration template file, register.ftl, can now have access to those attributes. Recaptcha also has the requirement of loading a JavaScript script. -You can do this by calling LoginFormsProvider.addScript() passing in the URL. +You can do this by calling LoginFormsProvider.addScript(), passing in the URL. For user profile processing, there is no additional information that it needs to add to the form, so its buildPage() method is empty. @@ -993,33 +1000,25 @@ Let's look at the Recaptcha's plugin first. @Override public void validate(ValidationContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - List errors = new ArrayList<>(); - boolean success = false; - String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE); - if (!Validation.isBlank(captcha)) { - AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig(); - String secret = captchaConfig.getConfig().get(SITE_SECRET); - success = validateRecaptcha(context, success, captcha, secret); - } - if (success) { + if (!Validation.isBlank(captcha) && validateRecaptcha(context, captcha)) { context.success(); } else { + List errors = new ArrayList<>(); errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED)); formData.remove(G_RECAPTCHA_RESPONSE); context.validationError(formData, errors); - return; - - } } + ---- Here we obtain the form data that the Recaptcha widget adds to the form. We obtain the Recaptcha secret key from configuration. We then validate the recaptcha. If successful, ValidationContext.success() is called. +We clear the captcha token from the form using formData.remove, but keep other form data untouched. If not, we invoke ValidationContext.validationError() passing in the formData (so the user doesn't have to re-enter data), we also specify an error message we want displayed. The error message must point to a message bundle property in the internationalized message bundles. For other registration extensions validate() might be validating the format of a form element, for example an alternative email attribute. @@ -1195,7 +1194,7 @@ or during `Service account` authentication (represented by OAuth2 `Client Creden [role="_additional-resource"] .Additional resources -* For more details about {project_name} adapter and OAuth2 flows see link:{adapterguide_link}[{adapterguide_name}]. +* For more details about {project_name} adapter and OAuth2 flows see link:{securing_apps_link}[{securing_apps_name}]. ==== Default implementations diff --git a/docs/documentation/server_development/topics/custom-attributes.adoc b/docs/documentation/server_development/topics/custom-attributes.adoc deleted file mode 100644 index 8b05e4982173..000000000000 --- a/docs/documentation/server_development/topics/custom-attributes.adoc +++ /dev/null @@ -1,68 +0,0 @@ - -[[_custom_user_attributes]] -== Custom user attributes - -You can add custom user attributes to the registration page and account management console with a custom theme. - -=== Registration page - -Use this procedure to enter custom attributes in the registration page. - -.Procedure - -. Copy the template `themes/base/login/register.ftl` to the login type of your custom theme. - -. Open the copy in an editor. -+ -For example, to add a mobile number to the registration page, add the following snippet to the form: -+ -[source,html] ----- -
      -
      - -
      - -
      - -
      -
      ----- - -. Ensure the name of the input html element starts with `user.attributes`. In the example above, the attribute will be stored by {project_name} with the name `mobile`. - -. To see the changes, make sure your realm is using your custom theme for the login theme and open the registration page. - -=== Account Management Console - -Use this procedure to manage custom attributes in the user profile page in the account management console. - -.Procedure -. Copy the template `themes/base/account/account.ftl` to the -account type of your custom theme. - -. Open the copy in an editor. -+ -As an example to add a mobile number to the account page add the following snippet to the form: -+ -[source,html] ----- -
      -
      - -
      - -
      - -
      -
      ----- - -. Ensure the name of the input html element starts with `user.attributes`. - -. To see the changes, make sure your realm is using your custom theme for the account theme and open the user profile page in the account management console. - -[role="_additional-resources"] -=== Additional resources - -* See <<_themes,Themes>> for how to create a custom theme. diff --git a/docs/documentation/server_development/topics/extensions.adoc b/docs/documentation/server_development/topics/extensions.adoc index 2e88d7018fef..99a0105b0e55 100644 --- a/docs/documentation/server_development/topics/extensions.adoc +++ b/docs/documentation/server_development/topics/extensions.adoc @@ -24,13 +24,15 @@ Object getResource(); ---- -Use this method to return an object, which acts as a https://github.com/jax-rs[JAX-RS Resource]. For more details, see the Javadoc and our examples. -There is a very simple example in the example distribution in `providers/rest` and there is a more advanced example in `providers/domain-extension`, -which shows how to add an authenticated REST endpoint and other functionalities like <<_extensions_spi, Adding your own SPI>> -or <<_extensions_jpa,Extending the datamodel with custom JPA entities>>. +Use this method to return an object, which acts as a https://github.com/jax-rs[JAX-RS Resource]. +Your JAX-RS resource is only recognized by the server and registered as a valid endpoint if it includes the following configuration: +- adding an empty file named `beans.xml` under `META-INF` +- annotating the JAX-RS class with the annotation `jakarta.ws.rs.ext.Provider`. 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 @@ -42,10 +44,6 @@ A custom SPI is especially useful with Custom REST endpoints. Use this procedure + [source,java] ---- -package org.keycloak.examples.domainextension.spi; - -import ... - public class ExampleSpi implements Spi { @Override @@ -77,7 +75,7 @@ public class ExampleSpi implements Spi { + [source] ---- -org.keycloak.examples.domainextension.spi.ExampleSpi +ExampleSpi ---- . Create the interfaces `ExampleServiceProviderFactory`, which extends from `ProviderFactory` and `ExampleService`, which extends from `Provider`. @@ -86,8 +84,6 @@ is always scoped per application, however `ExampleService` is scoped per-request . Finally you need to implement your providers in the same manner as described in the <<_providers,Service Provider Interfaces>> chapter. -For more details, take a look at the example distribution at `providers/domain-extension`, which shows an Example SPI similar to the one above. - [role="_additional-resources"] .Additional resources * <<_extensions_rest,Custom REST endpoints>> @@ -141,7 +137,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 @@ -150,6 +146,4 @@ is always done at the server startup, so to trigger a DB update of your schema, it's the file `META-INF/example-changelog.xml` which must be packed in same JAR as the JPA entities and `ExampleJpaEntityProvider`) and then restart server. The DB schema will be automatically updated at startup. -For more details, take a look at the example distribution at example `providers/domain-extension`, which shows the `ExampleJpaEntityProvider` and `example-changelog.xml` described above. - NOTE: Don't forget to always back up your database before doing any changes in the Liquibase changelog and triggering a DB update. diff --git a/docs/documentation/server_development/topics/identity-brokering/account-linking.adoc b/docs/documentation/server_development/topics/identity-brokering/account-linking.adoc index d0d77ce1db60..02a629066807 100644 --- a/docs/documentation/server_development/topics/identity-brokering/account-linking.adoc +++ b/docs/documentation/server_development/topics/identity-brokering/account-linking.adoc @@ -1,4 +1,5 @@ +[_client-initiated-account-linking] === Client initiated account linking Some applications want to integrate with social providers like Facebook, but do not want to provide an option to login via @@ -13,16 +14,37 @@ back to the server. The server establishes the link and redirects back to the a There are some preconditions that must be met by the client application before it can initiate this protocol: * The desired identity provider must be configured and enabled for the user's realm in the admin console. -* The user account must already be logged in as an existing user via the OIDC protocol * The user must have an `account.manage-account` or `account.manage-account-links` role mapping. * The application must be granted the scope for those roles within its access token + +The protocol is realized by the link:{adminguide_link}#con-aia_server_administration_guide[Application-initiated action (AIA)]. If you want the user, who is authenticated in your client application, to link +to the identity provider, attach the parameter `kc_action` with the value `idp_link:` to the OIDC authentication URL and redirect the user to this URL. For example, +to request linking to the identity provider with the alias `my-oidc-provider`, attach the parameter such as: + +[source,subs="attributes+"] +---- +kc_action=idp_link:my-oidc-provider +---- + +==== Refreshing external tokens + +If you use the external token generated by logging into the provider (such as a Facebook or GitHub token), you can refresh this token by re-initiating the account linking API. + +==== Legacy client initiated account linking + +WARNING: The legacy client initiated account linking is using a custom protocol that is not based on AIA. If you are use this protocol, consider migrating +your client application to the AIA based protocol described above because legacy client initiated account linking might be removed in the future {project_name} versions. + +In addition to the preconditions above, the legacy client initiated account linking has another precondition: + +* The user account must already be logged in as an existing user via the OIDC protocol * The application must have access to its access token as it needs information within it to generate the redirect URL. To initiate the login, the application must fabricate a URL and redirect the user's browser to this URL. The URL looks like this: [source,subs="attributes+"] ---- -/{auth-server-root}{kc_realms_path}/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash} +/{auth-server-root}{kc_realms_path}/{realm-name}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash} ---- Here's a description of each path and query param: @@ -68,7 +90,7 @@ Here's an example of Java Servlet code that generates the URL to establish the a request.getSession().setAttribute("hash", hash); String redirectUri = ...; String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl) - .path("{kc_realms_path}/{realm}/broker/{provider}/link") + .path("{kc_realms_path}/{realm-name}/broker/{provider}/link") .queryParam("nonce", nonce) .queryParam("hash", hash) .queryParam("client_id", clientId) @@ -87,8 +109,3 @@ to the application. If there is an error condition and the auth server deems it [WARNING] While this API guarantees that the application initiated the request, it does not completely prevent CSRF attacks for this operation. The application is still responsible for guarding against CSRF attacks target at itself. - -==== Refreshing external tokens - -If you are using the external token generated by logging into the provider (i.e. a Facebook or GitHub token), you can refresh this token by re-initiating the account linking API. - diff --git a/docs/documentation/server_development/topics/identity-brokering/tokens.adoc b/docs/documentation/server_development/topics/identity-brokering/tokens.adoc index a11b0abb3823..40c02c6154d3 100644 --- a/docs/documentation/server_development/topics/identity-brokering/tokens.adoc +++ b/docs/documentation/server_development/topics/identity-brokering/tokens.adoc @@ -10,7 +10,7 @@ To retrieve a token for a particular identity provider you need to send a reques [source,subs="attributes+"] ---- -GET {kc_realms_path}/{realm}/broker/{provider_alias}/token HTTP/1.1 +GET {kc_realms_path}/{realm-name}/broker/{provider_alias}/token HTTP/1.1 Host: localhost:8080 Authorization: Bearer ---- diff --git a/docs/documentation/server_development/topics/providers.adoc b/docs/documentation/server_development/topics/providers.adoc index 5dca6c65b445..e5d36452021d 100644 --- a/docs/documentation/server_development/topics/providers.adoc +++ b/docs/documentation/server_development/topics/providers.adoc @@ -73,7 +73,7 @@ public class MyThemeSelectorProvider implements ThemeSelectorProvider { } @Override - public void close() { + public void close() { } } ---- @@ -91,7 +91,7 @@ For example, to configure a provider you can set options as follows: [source,bash] ---- -bin/kc.[sh|bat] --spi-theme-selector-my-theme-selector-enabled=true --spi-theme-selector-my-theme-selector-theme=my-theme +bin/kc.[sh|bat] --spi-theme-selector--my-theme-selector--enabled=true --spi-theme-selector--my-theme-selector--theme=my-theme ---- Then you can retrieve the config in the `ProviderFactory` init method: @@ -122,6 +122,44 @@ public class MyThemeSelectorProvider implements ThemeSelectorProvider { } ---- +The pom.xml file for your SPI requires a `dependencyManagement` section with an import reference to the {project_name} version that is intended for the SPI. In this example, replace the occurrence of `VERSION` with {project_versionMvn}, which is the current version of {project_name}. + +[source,xml] +---- + + + 4.0.0 + + org.example + test-lib + 1.0-SNAPSHOT + + + + + org.keycloak + keycloak-parent + VERSION + pom + import + + + + + + + org.keycloak + keycloak-model-jpa + provided + + + + +---- +<1> Replace `VERSION` with the current version of {project_name} + [[_override_builtin_providers]] ==== Override built-in providers @@ -196,7 +234,7 @@ one of them needs to be specified as the default one. For example such as: [source,bash] ---- -bin/kc.[sh|bat] build --spi-hostname-provider=default +bin/kc.[sh|bat] build --spi-hostname--provider=default ---- The value `default` used as the value of `default-provider` must match the ID returned by the `ProviderFactory.getId()` of the particular provider factory implementation. @@ -216,11 +254,37 @@ need to be retrieved with the usage of `KeycloakSessionFactory`. It is not recom === Registering provider implementations -Providers are registered with the server by simply copying them to the `providers` directory. +Providers are registered with the server by simply copying the JAR file to the `providers` directory. 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 the `kc.[sh|bat] build` command. +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 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 --help +---- + +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. +==== + ==== Disabling a provider @@ -229,12 +293,16 @@ For example to disable the Infinispan user cache provider use: [source,bash] ---- -bin/kc.[sh|bat] build --spi-user-cache-infinispan-enabled=false +bin/kc.[sh|bat] build --spi-user-cache--infinispan--enabled=false ---- [[_script_providers]] === JavaScript providers +:tech_feature_name: Scripts +:tech_feature_id: scripts +include::./templates/techpreview.adoc[] + {project_name} has the ability to execute scripts during runtime in order to allow administrators to customize specific functionalities: * Authenticator @@ -359,7 +427,8 @@ my-script-mapper.js The `META-INF/keycloak-scripts.json` is a file descriptor that provides metadata information about the scripts you want to deploy. It is a JSON file with the following structure: -```json +[source,json] +---- { "authenticators": [ { @@ -390,7 +459,7 @@ The `META-INF/keycloak-scripts.json` is a file descriptor that provides metadata } ] } -``` +---- This file should reference the different types of script providers that you want to deploy: @@ -428,8 +497,7 @@ The name of the script file. This property is *mandatory* and should map to a fi ==== Deploy the script JAR Once you have a JAR file with a descriptor and the scripts you want to deploy, you just need to copy the JAR to the {project_name} `providers/` directory, then run `bin/kc.[sh|bat] build`. -Note that you also need to enable the `scripts` feature. === Available SPIs -If you want to see list of all available SPIs at runtime, you can check `Server Info` page in Admin Console as described in <<_providers_admin_console,Admin Console>> section. +If you want to see list of all available SPIs at runtime, you can check `Provider Info` page in Admin Console as described in <<_providers_admin_console,Admin Console>> section. diff --git a/docs/documentation/server_development/topics/saml-role-mappings-spi.adoc b/docs/documentation/server_development/topics/saml-role-mappings-spi.adoc index 8c99f124498e..98a7c7009b6d 100644 --- a/docs/documentation/server_development/topics/saml-role-mappings-spi.adoc +++ b/docs/documentation/server_development/topics/saml-role-mappings-spi.adoc @@ -11,7 +11,7 @@ Implementations can not only map roles into other roles but also add or remove r roles assigned to the SAML principal) depending on the use case. For details about the configuration of the role mappings provider for the SAML adapter as well as a description of the default -implementations available see the link:{adapterguide_link}[{adapterguide_name}]. +implementations available see the link:{securing_apps_link}[{securing_apps_name}]. === Implementing a custom role mappings provider @@ -26,4 +26,4 @@ of the custom implementation must be added to the archive that also contains the When the SP application is deployed, the role mappings provider that will be used is selected by the id that was set in `keycloak-saml.xml` or in the `keycloak-saml` subsystem. So to enable your custom provider simply make sure that its id is -properly set in the adapter configuration. \ No newline at end of file +properly set in the adapter configuration. diff --git a/docs/documentation/server_development/topics/themes-react.adoc b/docs/documentation/server_development/topics/themes-react.adoc new file mode 100644 index 000000000000..c922522c473f --- /dev/null +++ b/docs/documentation/server_development/topics/themes-react.adoc @@ -0,0 +1,65 @@ +[[_theme_react]] +== Themes based on React + +The admin console and account console are based on React. +To fully customize these you can use the React based npm packages. +There are two packages: + +* `@keycloak/keycloak-admin-ui`: This is the base theme for the admin console. +* `@keycloak/keycloak-account-ui`: This is the base theme for the account console. + +Both packages are available on npm. + +=== Installing the packages + +To install the packages, run the following command: + +[source,bash] +---- +pnpm install @keycloak/keycloak-account-ui +---- + +=== Using the packages + +To use these pages you'll need to add KeycloakProvider in your component hierarchy to setup what client, realm and url to use. + +[source,javascript] +---- +import { KeycloakProvider } from "@keycloak/keycloak-ui-shared"; + +//... + + + {/* rest of your application */} + +---- + +=== Translating the pages + +The pages are translated using the `i18next` library. +You can set it up as described on their https://react.i18next.com/[website]. +If you want to use the translations that are provided then you need to add `i18next-fetch-backend` to your project and add: + +[source,javascript] +---- +backend: { + loadPath: `http://localhost:8080/resources/master/account/{lng}}`, + parse: (data: string) => { + const messages = JSON.parse(data); + + return Object.fromEntries( + messages.map(({ key, value }) => [key, value]) + ); + }, +}, +---- + +=== Using the pages + +All "pages" are React components that can be used in your application. +To see what components are available, see the https://github.com/keycloak/keycloak/blob/main/js/apps/account-ui/src/index.ts[source]. +Or have a look at the https://github.com/keycloak/keycloak-quickstarts/tree/main/extension/extend-account-console-node[quick start] to see how to use them. diff --git a/docs/documentation/server_development/topics/themes.adoc b/docs/documentation/server_development/topics/themes.adoc old mode 100755 new mode 100644 index 2ffbe6ea66c5..d52feea014bc --- a/docs/documentation/server_development/topics/themes.adoc +++ b/docs/documentation/server_development/topics/themes.adoc @@ -11,7 +11,7 @@ image::images/login-sunrise.png[caption="",title="Login page with sunrise exampl A theme can provide one or more types to customize different aspects of {project_name}. The types available are: -* Account - Account management +* Account - Account Console * Admin - Admin Console * Email - Emails * Login - Login forms @@ -32,13 +32,13 @@ NOTE: To set the theme for the `master` Admin Console you need to set the Admin + . To see the changes to the Admin Console refresh the page. -. Change the welcome theme by using the `spi-theme-welcome-theme` option. +. Change the welcome theme by using the `spi-theme--welcome-theme` option. . For example: + [source,bash] ---- -bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme +bin/kc.[sh|bat] start --spi-theme--welcome-theme=custom-theme ---- [[_default-themes]] @@ -59,9 +59,8 @@ A theme consists of: * Scripts * Theme properties -Unless you plan to replace every single page you should extend another theme. Most likely you will want to extend the {project_name} theme, but you could also -consider extending the base theme if you are significantly changing the look and feel of the pages. The base theme primarily consists of HTML templates and -message bundles, while the {project_name} theme primarily contains images and stylesheets. +Unless you plan to replace every single page you should extend another theme. Most likely you will want to extend some existing theme. Alternatively, if you intend to provide your own implementation of the admin or account console, +consider extending the `base` theme. The `base` theme consists of a message bundle and therefore such implementation needs to start from scratch, including implementation of the main `index.ftl` Freemarker template, but it can leverage existing translations from the message bundle. When extending a theme you can override individual resources (templates, stylesheets, etc.). If you decide to override HTML templates bear in mind that you may need to update your custom template when upgrading to a new release. @@ -75,7 +74,7 @@ restarting {project_name}. + [source,bash] ---- -bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false +bin/kc.[sh|bat] start --spi-theme--static-max-age=-1 --spi-theme--cache-themes=false --spi-theme--cache-templates=false ---- . Create a directory in the `themes` directory. @@ -89,7 +88,7 @@ For example, to add the login type to the `mytheme` theme, create the directory . For each type create a file `theme.properties` which allows setting some configuration for the theme. + -For example, to configure the theme `themes/mytheme/login` to extend the base theme and import some common resources, create the file `themes/mytheme/login/theme.properties` with following contents: +For example, to configure the theme `themes/mytheme/login` to extend the `base` theme and import some common resources, create the file `themes/mytheme/login/theme.properties` with following contents: + [source] ---- @@ -106,7 +105,7 @@ You have now created a theme with support for the login type. . For *Login Theme* select *mytheme* and click *Save*. . Open the login page for the realm. + -You can do this either by logging in through your application or by opening the Account Management console (`/realms/{realm name}/account`). +You can do this either by logging in through your application or by opening the Account Console (`/realms/{realm-name}/account`). . To see the effect of changing the parent theme, set `parent=keycloak` in `theme.properties` and refresh the login page. @@ -124,11 +123,14 @@ It can be useful for instance if you redeployed custom providers or custom theme Theme properties are set in the file `/theme.properties` in the theme directory. -* parent - Parent theme to extend -* import - Import resources from another theme -* common - Override the common resource path. The default value is `common/keycloak` when not specified. This value would be used as value of suffix of `${url.resourcesCommonPath}`, which is used typically in freemarker templates (prefix of `${url.resoucesCommonPath}` value is theme root uri). -* styles - Space-separated list of styles to include -* locales - Comma-separated list of supported locales +`parent`:: Parent theme to extend +`import`:: Import resources from another theme +`common`:: Override the common resource path. The default value is `common/keycloak` when not specified. This value would be used as value of suffix of `${url.resourcesCommonPath}`, which is used typically in freemarker templates (prefix of `${url.resoucesCommonPath}` value is theme root uri). +`styles`:: Space-separated list of styles to include +`locales`:: Comma-separated list of supported locales +`contentHashPattern`:: Regex pattern of a file path in the theme where files have a content hash as part of their file name. +A content hash is usually an abbreviated hash of the file's contents. The hash will change when the contents of the file have changed, and is usually created using the bundling process of the JavaScript application bundling. +When the preview feature `rolling-updates:v2` is enabled, this allows for a more seamless rolling upgrade. There are a list of properties that can be used to change the css class used for certain element types. For a list of these properties look at the theme.properties file in the corresponding type of the keycloak theme (`themes/keycloak//theme.properties`). @@ -154,6 +156,7 @@ unixHome=${env.HOME:Unix home not found} windowsHome=${env.HOMEPATH:Windows home not found} ---- +[[_theme_stylesheet]] ==== Add a stylesheet to a theme You can add one or more stylesheets to a theme. @@ -238,7 +241,39 @@ Or to use directly in HTML templates add the following to a custom HTML template [source,html] ---- - +My image description +---- + +[[_theme_custom_footer]] +==== Adding a custom footer to a login theme + +In order to use a custom footer, create a `footer.ftl` file in your custom login theme with the desired content. + +An example for a custom `footer.ftl` may look like this: +``` +<#macro content> +<#-- footer at the end of the login box --> +
      + +
      + +``` + +==== Adding an image to an email theme + +To make images available to the theme add them to the `/email/resources/img` directory of your theme. These can be used from within directly in HTML templates. + +For example to add an image to the `mytheme` copy an image to `themes/mytheme/email/resources/img/logo.jpg`. + +To use directly in HTML templates add the following to a custom HTML template: + +[source,html] +---- +My image description ---- ==== Messages @@ -329,12 +364,24 @@ kcLogoIdP-myProvider = fa fa-lock ---- All icons are available on the official website of PatternFly4. -Icons for social providers are already defined in base login theme properties (`themes/keycloak/login/theme.properties`), where you can inspire yourself. +Icons for social providers are already defined in `base` login theme properties (`themes/keycloak/login/theme.properties`), where you can inspire yourself. ==== Creating a custom HTML template -{project_name} uses https://freemarker.apache.org/[Apache Freemarker] templates to generate HTML. You can override individual templates in your own theme by -creating `/