From 2b38242ebaea27e2b4e1282475a93185b0742e2a Mon Sep 17 00:00:00 2001
From: Damian Ujma <92095156+damianujma@users.noreply.github.com>
Date: Wed, 14 May 2025 15:25:45 +0200
Subject: [PATCH] ACS-9416 Backport ACS-9414 Enhance the Identity Provider
configuration ( #3263) (#3342)
---
.github/workflows/ci.yml | 188 +-
.secrets.baseline | 10 +-
...dentityServiceAuthenticationComponent.java | 244 ++-
.../IdentityServiceConfig.java | 743 ++++----
.../IdentityServiceFacade.java | 514 ++---
.../IdentityServiceFacadeFactoryBean.java | 1650 +++++++++--------
...IdentityServiceJITProvisioningHandler.java | 174 +-
.../IdentityServiceRemoteUserMapper.java | 362 ++--
.../SpringBasedIdentityServiceFacade.java | 113 +-
...ntityServiceAdminConsoleAuthenticator.java | 73 +-
.../AccessTokenToDecodedTokenUserMapper.java | 66 +
.../user/DecodedTokenUser.java | 44 +
.../{ => user}/OIDCUserInfo.java | 2 +-
.../user/TokenUserToOIDCUserMapper.java | 76 +
.../user/UserInfoAttrMapping.java | 41 +
...dentity-service-authentication-context.xml | 23 +-
...identity-service-authentication.properties | 9 +-
.../java/org/alfresco/AllUnitTestsSuite.java | 447 ++---
.../ClientRegistrationProviderUnitTest.java | 83 +-
...ityServiceAuthenticationComponentTest.java | 345 ++--
...tityServiceJITProvisioningHandlerTest.java | 65 +-
...ServiceJITProvisioningHandlerUnitTest.java | 157 +-
.../IdentityServiceRemoteUserMapperTest.java | 29 +-
...ingBasedIdentityServiceFacadeUnitTest.java | 197 +-
...viceAdminConsoleAuthenticatorUnitTest.java | 39 +-
...TokenToDecodedTokenUserMapperUnitTest.java | 109 ++
.../TokenUserToOIDCUserMapperUnitTest.java | 95 +
.../test/resources/alfresco-global.properties | 3 +
28 files changed, 3262 insertions(+), 2639 deletions(-)
create mode 100644 repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapper.java
create mode 100644 repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/DecodedTokenUser.java
rename repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/{ => user}/OIDCUserInfo.java (99%)
create mode 100644 repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapper.java
create mode 100644 repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/UserInfoAttrMapping.java
create mode 100644 repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapperUnitTest.java
create mode 100644 repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapperUnitTest.java
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3926433b33..95710e18bb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,14 +44,10 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
- - id: changed-files
- uses: Alfresco/alfresco-build-tools/.github/actions/github-list-changes@v8.13.0
- with:
- write-list-to-env: true
- - uses: Alfresco/alfresco-build-tools/.github/actions/pre-commit@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/pre-commit@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Prepare maven cache and check compilation"
@@ -69,12 +65,12 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- - uses: Alfresco/alfresco-build-tools/.github/actions/veracode@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/veracode@v8.16.0
continue-on-error: true
with:
srcclr-api-token: ${{ secrets.SRCCLR_API_TOKEN }}
@@ -92,10 +88,10 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/github-download-file@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/github-download-file@v8.16.0
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
repository: "Alfresco/veracode-baseline-archive"
@@ -148,10 +144,10 @@ jobs:
!contains(github.event.head_commit.message, '[skip tests]') &&
!contains(github.event.head_commit.message, '[force]')
steps:
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
- - uses: Alfresco/ya-pmd-scan@v4.1.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
+ - uses: Alfresco/ya-pmd-scan@v4.3.0
with:
classpath-build-command: "mvn test-compile -ntp -Pags -pl \"-:alfresco-community-repo-docker\""
@@ -181,14 +177,14 @@ jobs:
testAttributes: "-Dtest=AllMmtUnitTestSuite"
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} - ${{ matrix.testModule }}
@@ -219,7 +215,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -261,9 +257,9 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -276,7 +272,7 @@ jobs:
run: docker compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} - ${{ matrix.testSuite }}
@@ -307,7 +303,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -340,9 +336,9 @@ jobs:
version: ['10.5', '10.6']
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: Run MariaDB ${{ matrix.version }} database
@@ -351,7 +347,7 @@ jobs:
MARIADB_VERSION: ${{ matrix.version }}
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} - ${{ matrix.version }}
@@ -382,7 +378,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -411,9 +407,9 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run MariaDB 10.11 database"
@@ -422,7 +418,7 @@ jobs:
MARIADB_VERSION: 10.11
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -453,7 +449,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -482,9 +478,9 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run MySQL 8 database"
@@ -493,7 +489,7 @@ jobs:
MYSQL_VERSION: 8
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -524,7 +520,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -552,9 +548,9 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run PostgreSQL 14.15 database"
@@ -563,7 +559,7 @@ jobs:
POSTGRES_VERSION: 14.15
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -594,7 +590,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -622,9 +618,9 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run PostgreSQL 15.10 database"
@@ -633,7 +629,7 @@ jobs:
POSTGRES_VERSION: 15.10
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -664,7 +660,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -692,9 +688,9 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run PostgreSQL 16.6 database"
@@ -703,7 +699,7 @@ jobs:
POSTGRES_VERSION: 16.6
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -734,7 +730,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -760,16 +756,16 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run ActiveMQ"
run: docker compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile activemq up -d
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -800,7 +796,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -860,9 +856,9 @@ jobs:
mvn-options: '-Dencryption.ssl.keystore.location=${CI_WORKSPACE}/keystores/alfresco/alfresco.keystore -Dencryption.ssl.truststore.location=${CI_WORKSPACE}/keystores/alfresco/alfresco.truststore'
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Set transformers tag"
@@ -885,7 +881,7 @@ jobs:
run: docker compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} - ${{ matrix.testSuite }} ${{ matrix.idp }}
@@ -916,7 +912,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -974,9 +970,9 @@ jobs:
REQUIRES_LOCAL_IMAGES: true
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -992,7 +988,7 @@ jobs:
run: mvn install -pl :alfresco-community-repo-integration-test -am -DskipTests -Pall-tas-tests
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} - ${{ matrix.test-name }}
@@ -1030,7 +1026,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.tests.outcome }}
@@ -1056,16 +1052,16 @@ jobs:
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run Postgres 16.6 database"
run: docker compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile postgres up -d
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -1096,7 +1092,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -1130,9 +1126,9 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -1140,7 +1136,7 @@ jobs:
bash ./scripts/ci/build.sh
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} 0${{ matrix.part }} - (PostgreSQL) ${{ matrix.test-name }}
@@ -1176,9 +1172,9 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -1186,7 +1182,7 @@ jobs:
bash ./scripts/ci/build.sh
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }} 0${{ matrix.part }} - (MySQL) ${{ matrix.test-name }}
@@ -1218,9 +1214,9 @@ jobs:
REQUIRES_LOCAL_IMAGES: true
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -1234,7 +1230,7 @@ jobs:
mvn -B install -pl :alfresco-governance-services-automation-community-rest-api -am -Pags -Pall-tas-tests -DskipTests
- name: "Prepare Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-prepare@v8.16.0
id: rp-prepare
with:
rp-launch-prefix: ${{ env.RP_LAUNCH_PREFIX }}
@@ -1266,7 +1262,7 @@ jobs:
continue-on-error: true
- name: "Summarize Report Portal"
if: github.ref_name == 'master'
- uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.13.0
+ uses: Alfresco/alfresco-build-tools/.github/actions/reportportal-summarize@v8.16.0
id: rp-summarize
with:
tests-outcome: ${{ steps.run-tests.outcome }}
@@ -1308,9 +1304,9 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v4
- - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.13.0
- - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.13.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
+ - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
diff --git a/.secrets.baseline b/.secrets.baseline
index b160381264..c1b3709032 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -133,21 +133,21 @@
"filename": ".github/workflows/ci.yml",
"hashed_secret": "b86dc2f033a63f2b7b9e7d270ab806d2910d7572",
"is_verified": false,
- "line_number": 299
+ "line_number": 295
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/ci.yml",
"hashed_secret": "1bfb0e20f886150ba59b853bcd49dea893e00966",
"is_verified": false,
- "line_number": 374
+ "line_number": 370
},
{
"type": "Secret Keyword",
"filename": ".github/workflows/ci.yml",
"hashed_secret": "128f14373ccfaff49e3664045d3a11b50cbb7b39",
"is_verified": false,
- "line_number": 908
+ "line_number": 904
}
],
".github/workflows/master_release.yml": [
@@ -1607,7 +1607,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
- "line_number": 46,
+ "line_number": 48,
"is_secret": false
}
],
@@ -1868,5 +1868,5 @@
}
]
},
- "generated_at": "2025-02-26T15:13:52Z"
+ "generated_at": "2025-05-13T13:17:41Z"
}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
index 90d0d9e0a6..e48f9bf7f0 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
@@ -1,123 +1,121 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2023 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import org.alfresco.repo.management.subsystems.ActivateableBean;
-import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
-import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-/**
- *
- * Authenticates a user against Identity Service (Keycloak/Authorization Server).
- * {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the
- * user credentials are valid.
- *
- * The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator
- * will just fall through to the next one in the chain.
- *
- */
-public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
-{
- private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
- /** client used to authenticate user credentials against Authorization Server **/
- private IdentityServiceFacade identityServiceFacade;
- /** enabled flag for the identity service subsystem**/
- private boolean active;
-
- private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
-
- private boolean allowGuestLogin;
-
- public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
- {
- this.identityServiceFacade = identityServiceFacade;
- }
-
- public void setAllowGuestLogin(boolean allowGuestLogin)
- {
- this.allowGuestLogin = allowGuestLogin;
- }
-
- public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
- {
- this.jitProvisioningHandler = jitProvisioningHandler;
- }
-
- @Override
- public void authenticateImpl(String userName, char[] password) throws AuthenticationException
- {
- if (identityServiceFacade == null)
- {
- if (LOGGER.isDebugEnabled())
- {
- LOGGER.debug("IdentityServiceFacade was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
- }
-
- throw new AuthenticationException("User not authenticated because IdentityServiceFacade was not set.");
- }
-
- try
- {
- // Attempt to verify user credentials
- IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.password(userName, String.valueOf(password)));
-
- String normalizedUsername = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(accessTokenAuthorization.getAccessToken().getTokenValue())
- .map(OIDCUserInfo::username)
- .orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint."));
- // Verification was successful so treat as authenticated user
- setCurrentUser(normalizedUsername);
- }
- catch (IdentityServiceFacadeException e)
- {
- throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
- }
- catch (RuntimeException e)
- {
- throw new AuthenticationException("Failed to verify user credentials.", e);
- }
- }
-
- public void setActive(boolean active)
- {
- this.active = active;
- }
-
- @Override
- public boolean isActive()
- {
- return active;
- }
-
- @Override
- protected boolean implementationAllowsGuestLogin()
- {
- return allowGuestLogin;
- }
-}
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2023 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
+import org.alfresco.repo.security.authentication.AuthenticationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+
+/**
+ *
+ * Authenticates a user against Identity Service (Keycloak/Authorization Server). {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the user credentials are valid.
+ * The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator will just fall through to the next one in the chain.
+ *
+ */
+public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
+{
+ private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
+ /** client used to authenticate user credentials against Authorization Server **/
+ private IdentityServiceFacade identityServiceFacade;
+ /** enabled flag for the identity service subsystem **/
+ private boolean active;
+
+ private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
+
+ private boolean allowGuestLogin;
+
+ public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
+ {
+ this.identityServiceFacade = identityServiceFacade;
+ }
+
+ public void setAllowGuestLogin(boolean allowGuestLogin)
+ {
+ this.allowGuestLogin = allowGuestLogin;
+ }
+
+ public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
+ {
+ this.jitProvisioningHandler = jitProvisioningHandler;
+ }
+
+ @Override
+ public void authenticateImpl(String userName, char[] password) throws AuthenticationException
+ {
+ if (identityServiceFacade == null)
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("IdentityServiceFacade was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
+ }
+
+ throw new AuthenticationException("User not authenticated because IdentityServiceFacade was not set.");
+ }
+
+ try
+ {
+ // Attempt to verify user credentials
+ IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.password(userName, String.valueOf(password)));
+
+ String normalizedUsername = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(accessTokenAuthorization.getAccessToken().getTokenValue())
+ .map(OIDCUserInfo::username)
+ .orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint."));
+ // Verification was successful so treat as authenticated user
+ setCurrentUser(normalizedUsername);
+ }
+ catch (IdentityServiceFacadeException e)
+ {
+ throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
+ }
+ catch (RuntimeException e)
+ {
+ throw new AuthenticationException("Failed to verify user credentials.", e);
+ }
+ }
+
+ public void setActive(boolean active)
+ {
+ this.active = active;
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return active;
+ }
+
+ @Override
+ protected boolean implementationAllowsGuestLogin()
+ {
+ return allowGuestLogin;
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
index 5189de298a..7593e982e9 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
@@ -1,330 +1,413 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2025 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
-import org.springframework.web.util.UriComponentsBuilder;
-
-/**
- * Class to hold configuration for the Identity Service.
- *
- * @author Gavin Cornwell
- */
-@SuppressWarnings("PMD.ExcessivePublicCount")
-public class IdentityServiceConfig
-{
- private static final String REALMS = "realms";
-
- private int clientConnectionTimeout;
- private int clientSocketTimeout;
- private String issuerUrl;
- private String audience;
- // client id
- private String resource;
- private String clientSecret;
- private String authServerUrl;
- private String realm;
- private int connectionPoolSize;
- private boolean allowAnyHostname;
- private boolean disableTrustManager;
- private String truststore;
- private String truststorePassword;
- private String clientKeystore;
- private String clientKeystorePassword;
- private String clientKeyPassword;
- private String realmKey;
- private int publicKeyCacheTtl;
- private boolean publicClient;
- private String principalAttribute;
- private boolean clientIdValidationDisabled;
- private String adminConsoleRedirectPath;
- private String signatureAlgorithms;
-
- /**
- *
- * @return Client connection timeout in milliseconds.
- */
- public int getClientConnectionTimeout()
- {
- return clientConnectionTimeout;
- }
-
- /**
- *
- * @param clientConnectionTimeout Client connection timeout in milliseconds.
- */
- public void setClientConnectionTimeout(int clientConnectionTimeout)
- {
- this.clientConnectionTimeout = clientConnectionTimeout;
- }
-
- /**
- *
- * @return Client socket timeout in milliseconds.s
- */
- public int getClientSocketTimeout()
- {
- return clientSocketTimeout;
- }
-
- /**
- *
- * @param clientSocketTimeout Client socket timeout in milliseconds.
- */
- public void setClientSocketTimeout(int clientSocketTimeout)
- {
- this.clientSocketTimeout = clientSocketTimeout;
- }
-
- public void setConnectionPoolSize(int connectionPoolSize)
- {
- this.connectionPoolSize = connectionPoolSize;
- }
-
- public int getConnectionPoolSize()
- {
- return connectionPoolSize;
- }
-
- public String getIssuerUrl()
- {
- return issuerUrl;
- }
-
- public void setIssuerUrl(String issuerUrl)
- {
- this.issuerUrl = issuerUrl;
- }
-
- public String getAudience()
- {
- return audience;
- }
-
- public void setAudience(String audience)
- {
- this.audience = audience;
- }
-
- public String getAuthServerUrl()
- {
- return Optional.ofNullable(realm)
- .filter(StringUtils::isNotBlank)
- .filter(realm -> StringUtils.isNotBlank(authServerUrl))
- .map(realm -> UriComponentsBuilder.fromUriString(authServerUrl)
- .pathSegment(REALMS, realm)
- .build()
- .toString())
- .orElse(authServerUrl);
- }
-
- public void setAuthServerUrl(String authServerUrl)
- {
- this.authServerUrl = authServerUrl;
- }
-
- public String getRealm()
- {
- return realm;
- }
-
- public void setRealm(String realm)
- {
- this.realm = realm;
- }
-
- public String getResource()
- {
- return resource;
- }
-
- public void setResource(String resource)
- {
- this.resource = resource;
- }
-
- public void setClientSecret(String clientSecret)
- {
- this.clientSecret = clientSecret;
- }
-
- public String getClientSecret()
- {
- return Optional.ofNullable(clientSecret)
- .orElse("");
- }
-
- public void setAllowAnyHostname(boolean allowAnyHostname)
- {
- this.allowAnyHostname = allowAnyHostname;
- }
-
- public boolean isAllowAnyHostname()
- {
- return allowAnyHostname;
- }
-
- public void setDisableTrustManager(boolean disableTrustManager)
- {
- this.disableTrustManager = disableTrustManager;
- }
-
- public boolean isDisableTrustManager()
- {
- return disableTrustManager;
- }
-
- public void setTruststore(String truststore)
- {
- this.truststore = truststore;
- }
-
- public String getTruststore()
- {
- return truststore;
- }
-
- public void setTruststorePassword(String truststorePassword)
- {
- this.truststorePassword = truststorePassword;
- }
-
- public String getTruststorePassword()
- {
- return truststorePassword;
- }
-
- public void setClientKeystore(String clientKeystore)
- {
- this.clientKeystore = clientKeystore;
- }
-
- public String getClientKeystore()
- {
- return clientKeystore;
- }
-
- public void setClientKeystorePassword(String clientKeystorePassword)
- {
- this.clientKeystorePassword = clientKeystorePassword;
- }
-
- public String getClientKeystorePassword()
- {
- return clientKeystorePassword;
- }
-
- public void setClientKeyPassword(String clientKeyPassword)
- {
- this.clientKeyPassword = clientKeyPassword;
- }
-
- public String getClientKeyPassword()
- {
- return clientKeyPassword;
- }
-
- public void setRealmKey(String realmKey)
- {
- this.realmKey = realmKey;
- }
-
- public String getRealmKey()
- {
- return realmKey;
- }
-
- public void setPublicKeyCacheTtl(int publicKeyCacheTtl)
- {
- this.publicKeyCacheTtl = publicKeyCacheTtl;
- }
-
- public int getPublicKeyCacheTtl()
- {
- return publicKeyCacheTtl;
- }
-
- public void setPublicClient(boolean publicClient)
- {
- this.publicClient = publicClient;
- }
-
- public boolean isPublicClient()
- {
- return publicClient;
- }
-
- public String getPrincipalAttribute()
- {
- return principalAttribute;
- }
-
- public void setPrincipalAttribute(String principalAttribute)
- {
- this.principalAttribute = principalAttribute;
- }
-
- public boolean isClientIdValidationDisabled()
- {
- return clientIdValidationDisabled;
- }
-
- public void setClientIdValidationDisabled(boolean clientIdValidationDisabled)
- {
- this.clientIdValidationDisabled = clientIdValidationDisabled;
- }
-
- public String getAdminConsoleRedirectPath()
- {
- return adminConsoleRedirectPath;
- }
-
- public void setAdminConsoleRedirectPath(String adminConsoleRedirectPath)
- {
- this.adminConsoleRedirectPath = adminConsoleRedirectPath;
- }
-
- public Set getSignatureAlgorithms()
- {
- return Stream.of(signatureAlgorithms.split(","))
- .map(String::trim)
- .map(SignatureAlgorithm::from)
- .filter(Objects::nonNull)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- public void setSignatureAlgorithms(String signatureAlgorithms)
- {
- this.signatureAlgorithms = signatureAlgorithms;
- }
-}
\ No newline at end of file
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Class to hold configuration for the Identity Service.
+ *
+ * @author Gavin Cornwell
+ */
+@SuppressWarnings("PMD.ExcessivePublicCount")
+public class IdentityServiceConfig
+{
+ private static final String REALMS = "realms";
+
+ private int clientConnectionTimeout;
+ private int clientSocketTimeout;
+ private String issuerUrl;
+ private String audience;
+ // client id
+ private String resource;
+ private String clientSecret;
+ private String authServerUrl;
+ private String realm;
+ private int connectionPoolSize;
+ private boolean allowAnyHostname;
+ private boolean disableTrustManager;
+ private String truststore;
+ private String truststorePassword;
+ private String clientKeystore;
+ private String clientKeystorePassword;
+ private String clientKeyPassword;
+ private String realmKey;
+ private int publicKeyCacheTtl;
+ private boolean publicClient;
+ private String principalAttribute;
+ private boolean clientIdValidationDisabled;
+ private String adminConsoleRedirectPath;
+ private String signatureAlgorithms;
+ private String adminConsoleScopes;
+ private String passwordGrantScopes;
+ private String issuerAttribute;
+ private String firstNameAttribute;
+ private String lastNameAttribute;
+ private String emailAttribute;
+ private long jwtClockSkewMs;
+
+ /**
+ *
+ * @return Client connection timeout in milliseconds.
+ */
+ public int getClientConnectionTimeout()
+ {
+ return clientConnectionTimeout;
+ }
+
+ /**
+ *
+ * @param clientConnectionTimeout
+ * Client connection timeout in milliseconds.
+ */
+ public void setClientConnectionTimeout(int clientConnectionTimeout)
+ {
+ this.clientConnectionTimeout = clientConnectionTimeout;
+ }
+
+ /**
+ *
+ * @return Client socket timeout in milliseconds.s
+ */
+ public int getClientSocketTimeout()
+ {
+ return clientSocketTimeout;
+ }
+
+ /**
+ *
+ * @param clientSocketTimeout
+ * Client socket timeout in milliseconds.
+ */
+ public void setClientSocketTimeout(int clientSocketTimeout)
+ {
+ this.clientSocketTimeout = clientSocketTimeout;
+ }
+
+ public void setConnectionPoolSize(int connectionPoolSize)
+ {
+ this.connectionPoolSize = connectionPoolSize;
+ }
+
+ public int getConnectionPoolSize()
+ {
+ return connectionPoolSize;
+ }
+
+ public String getIssuerUrl()
+ {
+ return issuerUrl;
+ }
+
+ public void setIssuerUrl(String issuerUrl)
+ {
+ this.issuerUrl = issuerUrl;
+ }
+
+ public String getAudience()
+ {
+ return audience;
+ }
+
+ public void setAudience(String audience)
+ {
+ this.audience = audience;
+ }
+
+ public String getAuthServerUrl()
+ {
+ return Optional.ofNullable(realm)
+ .filter(StringUtils::isNotBlank)
+ .filter(realm -> StringUtils.isNotBlank(authServerUrl))
+ .map(realm -> UriComponentsBuilder.fromUriString(authServerUrl)
+ .pathSegment(REALMS, realm)
+ .build()
+ .toString())
+ .orElse(authServerUrl);
+ }
+
+ public void setAuthServerUrl(String authServerUrl)
+ {
+ this.authServerUrl = authServerUrl;
+ }
+
+ public String getRealm()
+ {
+ return realm;
+ }
+
+ public void setRealm(String realm)
+ {
+ this.realm = realm;
+ }
+
+ public String getResource()
+ {
+ return resource;
+ }
+
+ public void setResource(String resource)
+ {
+ this.resource = resource;
+ }
+
+ public void setClientSecret(String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ }
+
+ public String getClientSecret()
+ {
+ return Optional.ofNullable(clientSecret)
+ .orElse("");
+ }
+
+ public void setAllowAnyHostname(boolean allowAnyHostname)
+ {
+ this.allowAnyHostname = allowAnyHostname;
+ }
+
+ public boolean isAllowAnyHostname()
+ {
+ return allowAnyHostname;
+ }
+
+ public void setDisableTrustManager(boolean disableTrustManager)
+ {
+ this.disableTrustManager = disableTrustManager;
+ }
+
+ public boolean isDisableTrustManager()
+ {
+ return disableTrustManager;
+ }
+
+ public void setTruststore(String truststore)
+ {
+ this.truststore = truststore;
+ }
+
+ public String getTruststore()
+ {
+ return truststore;
+ }
+
+ public void setTruststorePassword(String truststorePassword)
+ {
+ this.truststorePassword = truststorePassword;
+ }
+
+ public String getTruststorePassword()
+ {
+ return truststorePassword;
+ }
+
+ public void setClientKeystore(String clientKeystore)
+ {
+ this.clientKeystore = clientKeystore;
+ }
+
+ public String getClientKeystore()
+ {
+ return clientKeystore;
+ }
+
+ public void setClientKeystorePassword(String clientKeystorePassword)
+ {
+ this.clientKeystorePassword = clientKeystorePassword;
+ }
+
+ public String getClientKeystorePassword()
+ {
+ return clientKeystorePassword;
+ }
+
+ public void setClientKeyPassword(String clientKeyPassword)
+ {
+ this.clientKeyPassword = clientKeyPassword;
+ }
+
+ public String getClientKeyPassword()
+ {
+ return clientKeyPassword;
+ }
+
+ public void setRealmKey(String realmKey)
+ {
+ this.realmKey = realmKey;
+ }
+
+ public String getRealmKey()
+ {
+ return realmKey;
+ }
+
+ public void setPublicKeyCacheTtl(int publicKeyCacheTtl)
+ {
+ this.publicKeyCacheTtl = publicKeyCacheTtl;
+ }
+
+ public int getPublicKeyCacheTtl()
+ {
+ return publicKeyCacheTtl;
+ }
+
+ public void setPublicClient(boolean publicClient)
+ {
+ this.publicClient = publicClient;
+ }
+
+ public boolean isPublicClient()
+ {
+ return publicClient;
+ }
+
+ public String getPrincipalAttribute()
+ {
+ return principalAttribute;
+ }
+
+ public void setPrincipalAttribute(String principalAttribute)
+ {
+ this.principalAttribute = principalAttribute;
+ }
+
+ public boolean isClientIdValidationDisabled()
+ {
+ return clientIdValidationDisabled;
+ }
+
+ public void setClientIdValidationDisabled(boolean clientIdValidationDisabled)
+ {
+ this.clientIdValidationDisabled = clientIdValidationDisabled;
+ }
+
+ public String getAdminConsoleRedirectPath()
+ {
+ return adminConsoleRedirectPath;
+ }
+
+ public void setAdminConsoleRedirectPath(String adminConsoleRedirectPath)
+ {
+ this.adminConsoleRedirectPath = adminConsoleRedirectPath;
+ }
+
+ public Set getSignatureAlgorithms()
+ {
+ return Stream.of(signatureAlgorithms.split(","))
+ .map(String::trim)
+ .map(SignatureAlgorithm::from)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public void setSignatureAlgorithms(String signatureAlgorithms)
+ {
+ this.signatureAlgorithms = signatureAlgorithms;
+ }
+
+ public String getIssuerAttribute()
+ {
+ return issuerAttribute;
+ }
+
+ public void setIssuerAttribute(String issuerAttribute)
+ {
+ this.issuerAttribute = issuerAttribute;
+ }
+
+ public Set getAdminConsoleScopes()
+ {
+ return Stream.of(adminConsoleScopes.split(","))
+ .map(String::trim)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public void setAdminConsoleScopes(String adminConsoleScopes)
+ {
+ this.adminConsoleScopes = adminConsoleScopes;
+ }
+
+ public Set getPasswordGrantScopes()
+ {
+ return Stream.of(passwordGrantScopes.split(","))
+ .map(String::trim)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public void setPasswordGrantScopes(String passwordGrantScopes)
+ {
+ this.passwordGrantScopes = passwordGrantScopes;
+ }
+
+ public void setFirstNameAttribute(String firstNameAttribute)
+ {
+ this.firstNameAttribute = firstNameAttribute;
+ }
+
+ public void setLastNameAttribute(String lastNameAttribute)
+ {
+ this.lastNameAttribute = lastNameAttribute;
+ }
+
+ public void setEmailAttribute(String emailAttribute)
+ {
+ this.emailAttribute = emailAttribute;
+ }
+
+ public void setJwtClockSkewMs(long jwtClockSkewMs)
+ {
+ this.jwtClockSkewMs = jwtClockSkewMs;
+ }
+
+ public String getFirstNameAttribute()
+ {
+ return firstNameAttribute;
+ }
+
+ public String getLastNameAttribute()
+ {
+ return lastNameAttribute;
+ }
+
+ public String getEmailAttribute()
+ {
+ return emailAttribute;
+ }
+
+ public long getJwtClockSkewMs()
+ {
+ return jwtClockSkewMs;
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java
index cf764b6d11..3054869dbe 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java
@@ -1,249 +1,265 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2025 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import static java.util.Objects.nonNull;
-import static java.util.Objects.requireNonNull;
-
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-
-/**
- * Allows to interact with the Identity Service
- */
-public interface IdentityServiceFacade
-{
- /**
- * Returns {@link AccessToken} based authorization for provided {@link AuthorizationGrant}.
- * @param grant the OAuth2 grant provided by the Resource Owner.
- * @return {@link AccessTokenAuthorization} containing access token and optional refresh token.
- * @throws {@link AuthorizationException} when provided grant cannot be exchanged for the access token.
- */
- AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException;
-
- /**
- * Decodes the access token into the {@link DecodedAccessToken} which contains claims connected with a given token.
- * @param token {@link String} with encoded access token value.
- * @return {@link DecodedAccessToken} containing decoded claims.
- * @throws {@link TokenDecodingException} when token decoding failed.
- */
- DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
-
- /**
- * Gets claims about the authenticated user,
- * such as name and email address, via the UserInfo endpoint of the OpenID provider.
- * @param token {@link String} with encoded access token value.
- * @param principalAttribute {@link String} the attribute name used to access the user's name from the user info response.
- * @return {@link OIDCUserInfo} containing user claims.
- */
- Optional getUserInfo(String token, String principalAttribute);
-
- /**
- * Gets a client registration
- */
- ClientRegistration getClientRegistration();
-
- class IdentityServiceFacadeException extends RuntimeException
- {
- public IdentityServiceFacadeException(String message)
- {
- super(message);
- }
-
- IdentityServiceFacadeException(String message, Throwable cause)
- {
- super(message, cause);
- }
- }
-
- class AuthorizationException extends IdentityServiceFacadeException
- {
- AuthorizationException(String message)
- {
- super(message);
- }
-
- AuthorizationException(String message, Throwable cause)
- {
- super(message, cause);
- }
- }
-
- class UserInfoException extends IdentityServiceFacadeException
- {
-
- UserInfoException(String message)
- {
- super(message);
- }
-
- UserInfoException(String message, Throwable cause)
- {
- super(message, cause);
- }
- }
-
- class TokenDecodingException extends IdentityServiceFacadeException
- {
- TokenDecodingException(String message)
- {
- super(message);
- }
-
- TokenDecodingException(String message, Throwable cause)
- {
- super(message, cause);
- }
- }
-
- /**
- * Represents access token authorization with optional refresh token.
- */
- interface AccessTokenAuthorization
- {
- /**
- * Required {@link AccessToken}
- * @return {@link AccessToken}
- */
- AccessToken getAccessToken();
-
- /**
- * Optional refresh token.
- * @return Refresh token or {@code null}
- */
- String getRefreshTokenValue();
- }
-
- interface AccessToken {
- String getTokenValue();
- Instant getExpiresAt();
- }
-
- interface DecodedAccessToken extends AccessToken
- {
- Object getClaim(String claim);
- }
-
- class AuthorizationGrant {
- private final String username;
- private final String password;
- private final String refreshToken;
- private final String authorizationCode;
- private final String redirectUri;
-
- private AuthorizationGrant(String username, String password, String refreshToken, String authorizationCode, String redirectUri)
- {
- this.username = username;
- this.password = password;
- this.refreshToken = refreshToken;
- this.authorizationCode = authorizationCode;
- this.redirectUri = redirectUri;
- }
-
- public static AuthorizationGrant password(String username, String password)
- {
- return new AuthorizationGrant(requireNonNull(username), requireNonNull(password), null, null, null);
- }
-
- public static AuthorizationGrant refreshToken(String refreshToken)
- {
- return new AuthorizationGrant(null, null, requireNonNull(refreshToken), null, null);
- }
-
- public static AuthorizationGrant authorizationCode(String authorizationCode, String redirectUri)
- {
- return new AuthorizationGrant(null, null, null, requireNonNull(authorizationCode), requireNonNull(redirectUri));
- }
-
- boolean isPassword()
- {
- return nonNull(username);
- }
-
- boolean isRefreshToken()
- {
- return nonNull(refreshToken);
- }
-
- boolean isAuthorizationCode()
- {
- return nonNull(authorizationCode);
- }
-
- String getUsername()
- {
- return username;
- }
-
- String getPassword()
- {
- return password;
- }
-
- String getRefreshToken()
- {
- return refreshToken;
- }
-
- String getAuthorizationCode()
- {
- return authorizationCode;
- }
-
- String getRedirectUri()
- {
- return redirectUri;
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o)
- {
- return true;
- }
- if (o == null || getClass() != o.getClass())
- {
- return false;
- }
- AuthorizationGrant that = (AuthorizationGrant) o;
- return Objects.equals(username, that.username) &&
- Objects.equals(password, that.password) &&
- Objects.equals(refreshToken, that.refreshToken) &&
- Objects.equals(authorizationCode, that.authorizationCode) &&
- Objects.equals(redirectUri, that.redirectUri);
- }
-
- @Override
- public int hashCode()
- {
- return Objects.hash(username, password, refreshToken, authorizationCode, redirectUri);
- }
- }
-}
\ No newline at end of file
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import static java.util.Objects.nonNull;
+import static java.util.Objects.requireNonNull;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+
+import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
+
+/**
+ * Allows to interact with the Identity Service
+ */
+public interface IdentityServiceFacade
+{
+ /**
+ * Returns {@link AccessToken} based authorization for provided {@link AuthorizationGrant}.
+ *
+ * @param grant
+ * the OAuth2 grant provided by the Resource Owner.
+ * @return {@link AccessTokenAuthorization} containing access token and optional refresh token.
+ * @throws {@link
+ * AuthorizationException} when provided grant cannot be exchanged for the access token.
+ */
+ AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException;
+
+ /**
+ * Decodes the access token into the {@link DecodedAccessToken} which contains claims connected with a given token.
+ *
+ * @param token
+ * {@link String} with encoded access token value.
+ * @return {@link DecodedAccessToken} containing decoded claims.
+ * @throws {@link
+ * TokenDecodingException} when token decoding failed.
+ */
+ DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
+
+ /**
+ * Gets claims about the authenticated user, such as name and email address, via the UserInfo endpoint of the OpenID provider.
+ *
+ * @param token
+ * {@link String} with encoded access token value.
+ * @param userInfoAttrMapping
+ * {@link UserInfoAttrMapping} containing the mapping of claims.
+ * @return {@link DecodedTokenUser} containing user claims or {@link Optional#empty()} if the token does not contain a username claim.
+ */
+ Optional getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping);
+
+ /**
+ * Gets a client registration
+ */
+ ClientRegistration getClientRegistration();
+
+ class IdentityServiceFacadeException extends RuntimeException
+ {
+ public IdentityServiceFacadeException(String message)
+ {
+ super(message);
+ }
+
+ IdentityServiceFacadeException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+
+ class AuthorizationException extends IdentityServiceFacadeException
+ {
+ AuthorizationException(String message)
+ {
+ super(message);
+ }
+
+ AuthorizationException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+
+ class UserInfoException extends IdentityServiceFacadeException
+ {
+
+ UserInfoException(String message)
+ {
+ super(message);
+ }
+
+ UserInfoException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+
+ class TokenDecodingException extends IdentityServiceFacadeException
+ {
+ TokenDecodingException(String message)
+ {
+ super(message);
+ }
+
+ TokenDecodingException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Represents access token authorization with optional refresh token.
+ */
+ interface AccessTokenAuthorization
+ {
+ /**
+ * Required {@link AccessToken}
+ *
+ * @return {@link AccessToken}
+ */
+ AccessToken getAccessToken();
+
+ /**
+ * Optional refresh token.
+ *
+ * @return Refresh token or {@code null}
+ */
+ String getRefreshTokenValue();
+ }
+
+ interface AccessToken
+ {
+ String getTokenValue();
+
+ Instant getExpiresAt();
+ }
+
+ interface DecodedAccessToken extends AccessToken
+ {
+ Object getClaim(String claim);
+ }
+
+ class AuthorizationGrant
+ {
+ private final String username;
+ private final String password;
+ private final String refreshToken;
+ private final String authorizationCode;
+ private final String redirectUri;
+
+ private AuthorizationGrant(String username, String password, String refreshToken, String authorizationCode, String redirectUri)
+ {
+ this.username = username;
+ this.password = password;
+ this.refreshToken = refreshToken;
+ this.authorizationCode = authorizationCode;
+ this.redirectUri = redirectUri;
+ }
+
+ public static AuthorizationGrant password(String username, String password)
+ {
+ return new AuthorizationGrant(requireNonNull(username), requireNonNull(password), null, null, null);
+ }
+
+ public static AuthorizationGrant refreshToken(String refreshToken)
+ {
+ return new AuthorizationGrant(null, null, requireNonNull(refreshToken), null, null);
+ }
+
+ public static AuthorizationGrant authorizationCode(String authorizationCode, String redirectUri)
+ {
+ return new AuthorizationGrant(null, null, null, requireNonNull(authorizationCode), requireNonNull(redirectUri));
+ }
+
+ boolean isPassword()
+ {
+ return nonNull(username);
+ }
+
+ boolean isRefreshToken()
+ {
+ return nonNull(refreshToken);
+ }
+
+ boolean isAuthorizationCode()
+ {
+ return nonNull(authorizationCode);
+ }
+
+ String getUsername()
+ {
+ return username;
+ }
+
+ String getPassword()
+ {
+ return password;
+ }
+
+ String getRefreshToken()
+ {
+ return refreshToken;
+ }
+
+ String getAuthorizationCode()
+ {
+ return authorizationCode;
+ }
+
+ String getRedirectUri()
+ {
+ return redirectUri;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ AuthorizationGrant that = (AuthorizationGrant) o;
+ return Objects.equals(username, that.username) &&
+ Objects.equals(password, that.password) &&
+ Objects.equals(refreshToken, that.refreshToken) &&
+ Objects.equals(authorizationCode, that.authorizationCode) &&
+ Objects.equals(redirectUri, that.redirectUri);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(username, password, refreshToken, authorizationCode, redirectUri);
+ }
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java
index a3465ec3c9..bcafe791d4 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java
@@ -1,818 +1,832 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2025 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import static java.util.Objects.requireNonNull;
-import static java.util.Optional.ofNullable;
-import static java.util.function.Predicate.not;
-
-import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
-import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.security.interfaces.RSAPublicKey;
-import java.time.Duration;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import com.nimbusds.jose.JOSEObjectType;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
-import com.nimbusds.jose.proc.BadJOSEException;
-import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jose.util.ResourceRetriever;
-import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.id.Identifier;
-import com.nimbusds.oauth2.sdk.id.Issuer;
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
-
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.hc.client5.http.classic.HttpClient;
-import org.apache.hc.client5.http.config.ConnectionConfig;
-import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
-import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
-import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
-import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
-import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
-import org.apache.hc.client5.http.ssl.TrustAllStrategy;
-import org.apache.hc.core5.ssl.SSLContextBuilder;
-import org.apache.hc.core5.ssl.SSLContexts;
-import org.springframework.beans.factory.FactoryBean;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.RequestEntity;
-import org.springframework.http.ResponseEntity;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpRequestFactory;
-import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
-import org.springframework.http.converter.FormHttpMessageConverter;
-import org.springframework.security.converter.RsaKeyConverters;
-import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
-import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
-import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
-import org.springframework.security.oauth2.core.AuthorizationGrantType;
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
-import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.core.OAuth2Error;
-import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
-import org.springframework.security.oauth2.core.OAuth2TokenValidator;
-import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
-import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
-import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
-import org.springframework.security.oauth2.jwt.Jwt;
-import org.springframework.security.oauth2.jwt.JwtClaimNames;
-import org.springframework.security.oauth2.jwt.JwtClaimValidator;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
-import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
-import org.springframework.web.client.RestOperations;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
-
-/**
- * Creates an instance of {@link IdentityServiceFacade}.
- * This factory can return a null if it is disabled.
- */
-public class IdentityServiceFacadeFactoryBean implements FactoryBean
-{
- private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
-
- private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
-
- private boolean enabled;
- private SpringBasedIdentityServiceFacadeFactory factory;
-
- public void setEnabled(boolean enabled)
- {
- this.enabled = enabled;
- }
-
- public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
- {
- factory = new SpringBasedIdentityServiceFacadeFactory(
- new HttpClientProvider(identityServiceConfig)::createHttpClient,
- new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
- new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder
- );
- }
-
- @Override
- public IdentityServiceFacade getObject() throws Exception
- {
- // The creation of the client can be disabled for testing or when the username/password authentication is not required,
- // for instance when Identity Service is configured for 'bearer only' authentication or Direct Access Grants are disabled.
- if (!enabled)
- {
- return null;
- }
-
- return new LazyInstantiatingIdentityServiceFacade(factory::createIdentityServiceFacade);
- }
-
- @Override
- public Class> getObjectType()
- {
- return IdentityServiceFacade.class;
- }
-
- @Override
- public boolean isSingleton()
- {
- return true;
- }
-
- private static IdentityServiceFacadeException authorizationServerCantBeUsedException(RuntimeException cause)
- {
- return new IdentityServiceFacadeException("Unable to use the Authorization Server.", cause);
- }
-
- // The target facade is created lazily to improve resiliency on Identity Service
- // (Keycloak/Authorization Server) failures when Spring Context is starting up.
- static class LazyInstantiatingIdentityServiceFacade implements IdentityServiceFacade
- {
- private final AtomicReference targetFacade = new AtomicReference<>();
- private final Supplier targetFacadeCreator;
-
- LazyInstantiatingIdentityServiceFacade(Supplier targetFacadeCreator)
- {
- this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
- }
-
- @Override
- public AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException
- {
- return getTargetFacade().authorize(grant);
- }
-
- @Override
- public DecodedAccessToken decodeToken(String token) throws TokenDecodingException
- {
- return getTargetFacade().decodeToken(token);
- }
-
- @Override
- public Optional getUserInfo(String token, String principalAttribute)
- {
- return getTargetFacade().getUserInfo(token, principalAttribute);
- }
-
- @Override
- public ClientRegistration getClientRegistration()
- {
- return getTargetFacade().getClientRegistration();
- }
-
- private IdentityServiceFacade getTargetFacade()
- {
- return ofNullable(targetFacade.get())
- .orElseGet(() -> targetFacade.updateAndGet(prev ->
- ofNullable(prev).orElseGet(this::createTargetFacade)));
- }
-
- private IdentityServiceFacade createTargetFacade()
- {
- try
- {
- return targetFacadeCreator.get();
- }
- catch (IdentityServiceFacadeException e)
- {
- throw e;
- }
- catch (RuntimeException e)
- {
- LOGGER.warn("Failed to instantiate IdentityServiceFacade.", e);
- throw authorizationServerCantBeUsedException(e);
- }
- }
- }
-
- private static class SpringBasedIdentityServiceFacadeFactory
- {
- private final Supplier httpClientProvider;
- private final Function clientRegistrationProvider;
- private final BiFunction jwtDecoderProvider;
-
- SpringBasedIdentityServiceFacadeFactory(
- Supplier httpClientProvider,
- Function clientRegistrationProvider,
- BiFunction jwtDecoderProvider)
- {
- this.httpClientProvider = requireNonNull(httpClientProvider);
- this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
- this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider);
- }
-
- private IdentityServiceFacade createIdentityServiceFacade()
- {
- //Here we preserve the behaviour of previously used Keycloak Adapter
- // * Client is authenticating itself using basic auth
- // * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
-
- final ClientHttpRequestFactory httpRequestFactory = new CustomClientHttpRequestFactory(
- httpClientProvider.get());
- final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
- final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
- final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate,
- clientRegistration.getProviderDetails());
-
- return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory),
- clientRegistration, jwtDecoder);
- }
-
- private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
- {
- final RestTemplate restTemplate = new RestTemplate(
- Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
- restTemplate.setRequestFactory(requestFactory);
- restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
-
- return restTemplate;
- }
- }
-
- private static class HttpClientProvider
- {
- private final IdentityServiceConfig config;
-
- private HttpClientProvider(IdentityServiceConfig config)
- {
- this.config = requireNonNull(config);
- }
-
- private HttpClient createHttpClient()
- {
- try
- {
- HttpClientBuilder clientBuilder = HttpClients.custom();
- applyConfiguration(clientBuilder);
- return clientBuilder.build();
- }
- catch (Exception e)
- {
- throw new IllegalStateException("Failed to create ClientHttpRequestFactory. " + e.getMessage(), e);
- }
- }
-
- private void applyConfiguration(HttpClientBuilder builder) throws Exception
- {
- final PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
-
- applyConnectionConfiguration(connectionManagerBuilder);
- applySSLConfiguration(connectionManagerBuilder);
-
- builder.setConnectionManager(connectionManagerBuilder.build());
- }
-
- private void applyConnectionConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
- {
- final ConnectionConfig connectionConfig = ConnectionConfig.custom()
- .setConnectTimeout(config.getClientConnectionTimeout(), TimeUnit.MILLISECONDS)
- .setSocketTimeout(config.getClientSocketTimeout(), TimeUnit.MILLISECONDS)
- .build();
-
- connectionManagerBuilder.setMaxConnTotal(config.getConnectionPoolSize());
- connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
- }
-
- private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
- throws Exception
- {
- SSLContextBuilder sslContextBuilder = null;
- if (config.isDisableTrustManager())
- {
- sslContextBuilder = SSLContexts.custom()
- .loadTrustMaterial(TrustAllStrategy.INSTANCE);
-
- }
- else if (isDefined(config.getTruststore()))
- {
- final char[] truststorePassword = asCharArray(config.getTruststorePassword(), null);
- sslContextBuilder = SSLContexts.custom()
- .loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
- }
-
- if (isDefined(config.getClientKeystore()))
- {
- if (sslContextBuilder == null)
- {
- sslContextBuilder = SSLContexts.custom();
- }
- final char[] keystorePassword = asCharArray(config.getClientKeystorePassword(), null);
- final char[] keyPassword = asCharArray(config.getClientKeyPassword(), keystorePassword);
- sslContextBuilder.loadKeyMaterial(new File(config.getClientKeystore()), keystorePassword, keyPassword);
- }
-
- final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder.create();
-
- if (sslContextBuilder != null)
- {
- sslConnectionSocketFactoryBuilder.setSslContext(sslContextBuilder.build());
- }
-
- if (config.isDisableTrustManager() || config.isAllowAnyHostname())
- {
- sslConnectionSocketFactoryBuilder.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
- }
- final SSLConnectionSocketFactory sslConnectionSocketFactory = sslConnectionSocketFactoryBuilder.build();
- connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
- }
-
- private char[] asCharArray(String value, char... nullValue)
- {
- return ofNullable(value)
- .filter(not(String::isBlank))
- .map(String::toCharArray)
- .orElse(nullValue);
- }
- }
-
- static class ClientRegistrationProvider
- {
- private final IdentityServiceConfig config;
-
- private static final Set SCOPES = Set.of("openid", "profile", "email");
-
- ClientRegistrationProvider(IdentityServiceConfig config)
- {
- this.config = requireNonNull(config);
- }
-
- public ClientRegistration createClientRegistration(final RestOperations rest)
- {
- return possibleMetadataURIs()
- .stream()
- .map(u -> extractMetadata(rest, u))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .findFirst()
- .map(this::validateDiscoveryDocument)
- .map(this::createBuilder)
- .map(this::configureClientAuthentication)
- .map(Builder::build)
- .orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
- }
-
- private OIDCProviderMetadata validateDiscoveryDocument(OIDCProviderMetadata metadata)
- {
- validateOIDCEndpoint(metadata.getTokenEndpointURI(), "Token");
- validateOIDCEndpoint(metadata.getAuthorizationEndpointURI(), "Authorization");
- validateOIDCEndpoint(metadata.getUserInfoEndpointURI(), "User Info");
- validateOIDCEndpoint(metadata.getJWKSetURI(), "JWK Set");
-
- if (metadata.getIssuer() != null)
- {
- try
- {
- URI metadataIssuerURI = new URI(metadata.getIssuer().getValue());
- validateOIDCEndpoint(metadataIssuerURI, "Issuer");
- if (StringUtils.isNotBlank(config.getIssuerUrl()) &&
- !metadataIssuerURI.equals(URI.create(config.getIssuerUrl())))
- {
- throw new IdentityServiceException("Failed to create ClientRegistration. "
- + "The Issuer value from the OIDC Discovery Endpoint does not align with the provided Issuer. Expected `%s` but found `%s`"
- .formatted(config.getIssuerUrl(), metadata.getIssuer().getValue()));
- }
- }
- catch (URISyntaxException e)
- {
- throw new IdentityServiceException("The provided Issuer value could not be parsed as a URI reference.", e);
- }
- }
- else
- {
- throw new IdentityServiceException("The Issuer retrieved from the OIDC Discovery Endpoint cannot be null.");
- }
-
- return metadata;
- }
-
- private void validateOIDCEndpoint(URI value, String endpointName)
- {
- if (value == null || value.toASCIIString().isBlank())
- {
- throw new IdentityServiceException("The `%s` Endpoint retrieved from the OIDC Discovery Endpoint cannot be empty.".formatted(endpointName));
- }
- }
-
- private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata)
- {
- final String authUri = Optional.of(metadata)
- .map(OIDCProviderMetadata::getAuthorizationEndpointURI)
- .map(URI::toASCIIString)
- .orElse(null);
-
- final String issuerUri = Optional.of(metadata)
- .map(OIDCProviderMetadata::getIssuer)
- .map(Issuer::getValue)
- .orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ?
- config.getAuthServerUrl() :
- config.getIssuerUrl());
-
- return ClientRegistration
- .withRegistrationId("ids")
- .authorizationUri(authUri)
- .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
- .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
- .issuerUri(issuerUri)
- .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
- .scope(getSupportedScopes(metadata.getScopes()))
- .providerConfigurationMetadata(createMetadata(metadata))
- .authorizationGrantType(AuthorizationGrantType.PASSWORD);
- }
-
- private Map createMetadata(OIDCProviderMetadata metadata)
- {
- Map configurationMetadata = new LinkedHashMap<>();
- if(metadata.getScopes() != null)
- {
- configurationMetadata.put(SCOPES_SUPPORTED.getValue(), metadata.getScopes());
- }
- if(StringUtils.isNotBlank(config.getAudience()))
- {
- configurationMetadata.put(AUDIENCE.getValue(), config.getAudience());
- }
- return configurationMetadata;
- }
-
- private Builder configureClientAuthentication(Builder builder)
- {
- builder.clientId(config.getResource());
- if (config.isPublicClient())
- {
- return builder.clientSecret(null)
- .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
- }
- return builder.clientSecret(config.getClientSecret())
- .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
- }
-
- private Set getSupportedScopes(Scope scopes)
- {
- return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
- .map(Identifier::getValue)
- .collect(Collectors.toSet());
- }
-
- private Optional extractMetadata(RestOperations rest, URI metadataUri)
- {
- final String response;
- try
- {
- final ResponseEntity r = rest.exchange(RequestEntity.get(metadataUri).build(), String.class);
- if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
- {
- LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode()
- + ", has body: " + r.hasBody() + ".");
- return Optional.empty();
- }
- response = r.getBody();
- }
- catch (Exception e)
- {
- LOGGER.warn("Failed to get response from " + metadataUri + ". " + e.getMessage(), e);
- return Optional.empty();
- }
- try
- {
- return Optional.of(OIDCProviderMetadata.parse(response));
- }
- catch (Exception e)
- {
- LOGGER.warn("Failed to parse metadata. " + e.getMessage(), e);
- return Optional.empty();
- }
- }
-
- private Collection possibleMetadataURIs()
- {
- if (StringUtils.isBlank(config.getAuthServerUrl()) && StringUtils.isBlank(config.getIssuerUrl()))
- {
- throw new IdentityServiceException(
- "Failed to create ClientRegistration. The values of issuer url and auth server url cannot both be empty.");
- }
-
- String baseUrl = StringUtils.isNotBlank(config.getAuthServerUrl()) ?
- config.getAuthServerUrl() :
- config.getIssuerUrl();
-
- return List.of(UriComponentsBuilder.fromUriString(baseUrl)
- .pathSegment(".well-known", "openid-configuration")
- .build().toUri());
- }
- }
-
- static class JwtDecoderProvider
- {
- private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
- private final IdentityServiceConfig config;
- private final Set signatureAlgorithms;
-
- JwtDecoderProvider(IdentityServiceConfig config)
- {
- this.config = requireNonNull(config);
- this.signatureAlgorithms = ofNullable(config.getSignatureAlgorithms())
- .filter(not(Set::isEmpty))
- .orElseGet(() -> {
- LOGGER.warn("Unable to find any valid signature algorithms in the configuration. "
- + "Using the default signature algorithm: " + DEFAULT_SIGNATURE_ALGORITHM.getName() + ".");
- return Set.of(DEFAULT_SIGNATURE_ALGORITHM);
- });
- }
-
- public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
- {
- try
- {
- final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
-
- decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
- decoder.setClaimSetConverter(
- new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
-
- return decoder;
- }
- catch (RuntimeException e)
- {
- LOGGER.warn("Failed to create JwtDecoder.", e);
- throw authorizationServerCantBeUsedException(e);
- }
- }
-
- private NimbusJwtDecoder buildJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
- {
- if (isDefined(config.getRealmKey()))
- {
- final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
- return NimbusJwtDecoder.withPublicKey(publicKey)
- .signatureAlgorithm(DEFAULT_SIGNATURE_ALGORITHM)
- .build();
- }
- final String jwkSetUri = requireValidJwkSetUri(providerDetails);
- final NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder decoderBuilder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
- signatureAlgorithms.forEach(decoderBuilder::jwsAlgorithm);
- return decoderBuilder
- .restOperations(rest)
- .jwtProcessorCustomizer(this::reconfigureJWKSCache)
- .build();
- }
-
- private void reconfigureJWKSCache(ConfigurableJWTProcessor jwtProcessor)
- {
- final Optional> jwkSource = ofNullable(jwtProcessor)
- .map(ConfigurableJWTProcessor::getJWSKeySelector)
- .filter(JWSVerificationKeySelector.class::isInstance)
- .map(o -> (JWSVerificationKeySelector) o)
- .map(JWSVerificationKeySelector::getJWKSource)
- .filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet) o);
- if (jwkSource.isEmpty())
- {
- LOGGER.warn("Not able to reconfigure the JWK Cache. Unexpected JWKSource.");
- return;
- }
-
- final Optional jwkSetUrl = jwkSource.map(RemoteJWKSet::getJWKSetURL);
- if (jwkSetUrl.isEmpty())
- {
- LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown JWKSetURL.");
- return;
- }
-
- final Optional resourceRetriever = jwkSource.map(RemoteJWKSet::getResourceRetriever);
- if (resourceRetriever.isEmpty())
- {
- LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown ResourceRetriever.");
- return;
- }
-
- final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1,
- TimeUnit.SECONDS);
- final JWKSource cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(),
- resourceRetriever.get(), cache);
-
- jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
- signatureAlgorithms.stream()
- .map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName()))
- .collect(Collectors.toSet()),
- cachingJWKSource));
- jwtProcessor.setJWSTypeVerifier(new CustomJOSEObjectTypeVerifier(JOSEObjectType.JWT, AT_JWT));
- }
-
- private OAuth2TokenValidator createJwtTokenValidator(ProviderDetails providerDetails)
- {
- List> validators = new ArrayList<>();
- validators.add(new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)));
- validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
- if (!config.isClientIdValidationDisabled())
- {
- validators.add(new JwtClaimValidator("azp", config.getResource()::equals));
- }
- if (StringUtils.isNotBlank(config.getAudience()))
- {
- validators.add(new JwtAudienceValidator(config.getAudience()));
- }
- return new DelegatingOAuth2TokenValidator<>(validators);
- }
-
- private RSAPublicKey parsePublicKey(String pem)
- {
- try
- {
- return tryToParsePublicKey(pem);
- }
- catch (Exception e)
- {
- if (isPemFormatException(e))
- {
- //For backward compatibility with Keycloak adapter
- return tryToParsePublicKey("-----BEGIN PUBLIC KEY-----\n" + pem + "\n-----END PUBLIC KEY-----");
- }
- throw e;
- }
- }
-
- private RSAPublicKey tryToParsePublicKey(String pem)
- {
- final InputStream pemStream = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8));
- return RsaKeyConverters.x509().convert(pemStream);
- }
-
- private boolean isPemFormatException(Exception e)
- {
- return e.getMessage() != null && e.getMessage().contains("-----BEGIN PUBLIC KEY-----");
- }
-
- private String requireValidJwkSetUri(ProviderDetails providerDetails)
- {
- final String uri = providerDetails.getJwkSetUri();
- if (!isDefined(uri))
- {
- OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
- "Failed to find a Signature Verifier for: '"
- + providerDetails.getIssuerUri()
- + "'. Check to ensure you have configured the JwkSet URI.",
- null);
- throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
- }
- return uri;
- }
- }
-
- static class JwtIssuerValidator implements OAuth2TokenValidator
- {
- private final String requiredIssuer;
-
- public JwtIssuerValidator(String issuer)
- {
- this.requiredIssuer = requireNonNull(issuer, "issuer cannot be null");
- }
-
- @Override
- public OAuth2TokenValidatorResult validate(Jwt token)
- {
- requireNonNull(token, "token cannot be null");
- final Object issuer = token.getClaim(JwtClaimNames.ISS);
- if (issuer != null && requiredIssuer.equals(issuer.toString()))
- {
- return OAuth2TokenValidatorResult.success();
- }
-
- final OAuth2Error error = new OAuth2Error(
- OAuth2ErrorCodes.INVALID_TOKEN,
- "The iss claim is not valid. Expected `%s` but got `%s`.".formatted(requiredIssuer, issuer),
- "https://tools.ietf.org/html/rfc6750#section-3.1");
- return OAuth2TokenValidatorResult.failure(error);
- }
-
- }
-
- static class JwtAudienceValidator implements OAuth2TokenValidator
- {
- private final String configuredAudience;
-
- public JwtAudienceValidator(String configuredAudience)
- {
- this.configuredAudience = configuredAudience;
- }
-
- @Override
- public OAuth2TokenValidatorResult validate(Jwt token)
- {
- requireNonNull(token, "token cannot be null");
- final Object audience = token.getClaim(JwtClaimNames.AUD);
- if (audience != null)
- {
- if(audience instanceof List && ((List) audience).contains(configuredAudience))
- {
- return OAuth2TokenValidatorResult.success();
- }
- if(audience instanceof String && audience.equals(configuredAudience))
- {
- return OAuth2TokenValidatorResult.success();
- }
- }
-
- final OAuth2Error error = new OAuth2Error(
- OAuth2ErrorCodes.INVALID_TOKEN,
- "The aud claim is not valid. Expected configured audience `%s` not found.".formatted(configuredAudience),
- "https://tools.ietf.org/html/rfc6750#section-3.1");
- return OAuth2TokenValidatorResult.failure(error);
- }
- }
-
- static class CustomClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory
- {
- CustomClientHttpRequestFactory(HttpClient httpClient)
- {
- super(httpClient);
- }
-
- @Override
- public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException
- {
- /*
- * This is to avoid the Brotli content encoding that is not well-supported by the combination of
- * the Apache Http Client and the Spring RestTemplate
- */
- ClientHttpRequest request = super.createRequest(uri, httpMethod);
- request.getHeaders()
- .add("Accept-Encoding", "gzip, deflate");
- return request;
- }
- }
-
- static class CustomJOSEObjectTypeVerifier extends DefaultJOSEObjectTypeVerifier
- {
- public CustomJOSEObjectTypeVerifier(JOSEObjectType... allowedTypes)
- {
- super(Set.of(allowedTypes));
- }
-
- @Override
- public void verify(JOSEObjectType type, SecurityContext context) throws BadJOSEException
- {
- super.verify(type, context);
- }
- }
-
- private static boolean isDefined(String value)
- {
- return value != null && !value.isBlank();
- }
-}
\ No newline at end of file
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Predicate.not;
+
+import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
+import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.Identifier;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.converter.RsaKeyConverters;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
+import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.jwt.JwtClaimValidator;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
+import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
+
+/**
+ * Creates an instance of {@link IdentityServiceFacade}.
+ * This factory can return a null if it is disabled.
+ */
+public class IdentityServiceFacadeFactoryBean implements FactoryBean
+{
+ private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
+
+ private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
+ private static final String DEFAULT_ISSUER_ATTR = "issuer";
+
+ private boolean enabled;
+ private SpringBasedIdentityServiceFacadeFactory factory;
+
+ public void setEnabled(boolean enabled)
+ {
+ this.enabled = enabled;
+ }
+
+ public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
+ {
+ factory = new SpringBasedIdentityServiceFacadeFactory(
+ new HttpClientProvider(identityServiceConfig)::createHttpClient,
+ new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
+ new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder);
+ }
+
+ @Override
+ public IdentityServiceFacade getObject() throws Exception
+ {
+ // The creation of the client can be disabled for testing or when the username/password authentication is not required,
+ // for instance when Identity Service is configured for 'bearer only' authentication or Direct Access Grants are disabled.
+ if (!enabled)
+ {
+ return null;
+ }
+
+ return new LazyInstantiatingIdentityServiceFacade(factory::createIdentityServiceFacade);
+ }
+
+ @Override
+ public Class> getObjectType()
+ {
+ return IdentityServiceFacade.class;
+ }
+
+ @Override
+ public boolean isSingleton()
+ {
+ return true;
+ }
+
+ private static IdentityServiceFacadeException authorizationServerCantBeUsedException(RuntimeException cause)
+ {
+ return new IdentityServiceFacadeException("Unable to use the Authorization Server.", cause);
+ }
+
+ // The target facade is created lazily to improve resiliency on Identity Service
+ // (Keycloak/Authorization Server) failures when Spring Context is starting up.
+ static class LazyInstantiatingIdentityServiceFacade implements IdentityServiceFacade
+ {
+ private final AtomicReference targetFacade = new AtomicReference<>();
+ private final Supplier targetFacadeCreator;
+
+ LazyInstantiatingIdentityServiceFacade(Supplier targetFacadeCreator)
+ {
+ this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
+ }
+
+ @Override
+ public AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException
+ {
+ return getTargetFacade().authorize(grant);
+ }
+
+ @Override
+ public DecodedAccessToken decodeToken(String token) throws TokenDecodingException
+ {
+ return getTargetFacade().decodeToken(token);
+ }
+
+ @Override
+ public Optional getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
+ {
+ return getTargetFacade().getUserInfo(token, userInfoAttrMapping);
+ }
+
+ @Override
+ public ClientRegistration getClientRegistration()
+ {
+ return getTargetFacade().getClientRegistration();
+ }
+
+ private IdentityServiceFacade getTargetFacade()
+ {
+ return ofNullable(targetFacade.get())
+ .orElseGet(() -> targetFacade.updateAndGet(prev -> ofNullable(prev).orElseGet(this::createTargetFacade)));
+ }
+
+ private IdentityServiceFacade createTargetFacade()
+ {
+ try
+ {
+ return targetFacadeCreator.get();
+ }
+ catch (IdentityServiceFacadeException e)
+ {
+ throw e;
+ }
+ catch (RuntimeException e)
+ {
+ LOGGER.warn("Failed to instantiate IdentityServiceFacade.", e);
+ throw authorizationServerCantBeUsedException(e);
+ }
+ }
+ }
+
+ private static class SpringBasedIdentityServiceFacadeFactory
+ {
+ private final Supplier httpClientProvider;
+ private final Function clientRegistrationProvider;
+ private final BiFunction jwtDecoderProvider;
+
+ SpringBasedIdentityServiceFacadeFactory(
+ Supplier httpClientProvider,
+ Function clientRegistrationProvider,
+ BiFunction jwtDecoderProvider)
+ {
+ this.httpClientProvider = requireNonNull(httpClientProvider);
+ this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
+ this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider);
+ }
+
+ private IdentityServiceFacade createIdentityServiceFacade()
+ {
+ // Here we preserve the behaviour of previously used Keycloak Adapter
+ // * Client is authenticating itself using basic auth
+ // * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
+
+ final ClientHttpRequestFactory httpRequestFactory = new CustomClientHttpRequestFactory(
+ httpClientProvider.get());
+ final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
+ final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
+ final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate,
+ clientRegistration.getProviderDetails());
+
+ return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory),
+ clientRegistration, jwtDecoder);
+ }
+
+ private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
+ {
+ final RestTemplate restTemplate = new RestTemplate(
+ Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter(), new MappingJackson2HttpMessageConverter()));
+ restTemplate.setRequestFactory(requestFactory);
+ restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+
+ return restTemplate;
+ }
+ }
+
+ private static class HttpClientProvider
+ {
+ private final IdentityServiceConfig config;
+
+ private HttpClientProvider(IdentityServiceConfig config)
+ {
+ this.config = requireNonNull(config);
+ }
+
+ private HttpClient createHttpClient()
+ {
+ try
+ {
+ HttpClientBuilder clientBuilder = HttpClients.custom();
+ applyConfiguration(clientBuilder);
+ return clientBuilder.build();
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException("Failed to create ClientHttpRequestFactory. " + e.getMessage(), e);
+ }
+ }
+
+ private void applyConfiguration(HttpClientBuilder builder) throws Exception
+ {
+ final PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
+
+ applyConnectionConfiguration(connectionManagerBuilder);
+ applySSLConfiguration(connectionManagerBuilder);
+
+ builder.setConnectionManager(connectionManagerBuilder.build());
+ }
+
+ private void applyConnectionConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
+ {
+ final ConnectionConfig connectionConfig = ConnectionConfig.custom()
+ .setConnectTimeout(config.getClientConnectionTimeout(), TimeUnit.MILLISECONDS)
+ .setSocketTimeout(config.getClientSocketTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ connectionManagerBuilder.setMaxConnTotal(config.getConnectionPoolSize());
+ connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
+ }
+
+ private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
+ throws Exception
+ {
+ SSLContextBuilder sslContextBuilder = null;
+ if (config.isDisableTrustManager())
+ {
+ sslContextBuilder = SSLContexts.custom()
+ .loadTrustMaterial(TrustAllStrategy.INSTANCE);
+
+ }
+ else if (isDefined(config.getTruststore()))
+ {
+ final char[] truststorePassword = asCharArray(config.getTruststorePassword(), null);
+ sslContextBuilder = SSLContexts.custom()
+ .loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
+ }
+
+ if (isDefined(config.getClientKeystore()))
+ {
+ if (sslContextBuilder == null)
+ {
+ sslContextBuilder = SSLContexts.custom();
+ }
+ final char[] keystorePassword = asCharArray(config.getClientKeystorePassword(), null);
+ final char[] keyPassword = asCharArray(config.getClientKeyPassword(), keystorePassword);
+ sslContextBuilder.loadKeyMaterial(new File(config.getClientKeystore()), keystorePassword, keyPassword);
+ }
+
+ final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder.create();
+
+ if (sslContextBuilder != null)
+ {
+ sslConnectionSocketFactoryBuilder.setSslContext(sslContextBuilder.build());
+ }
+
+ if (config.isDisableTrustManager() || config.isAllowAnyHostname())
+ {
+ sslConnectionSocketFactoryBuilder.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
+ }
+ final SSLConnectionSocketFactory sslConnectionSocketFactory = sslConnectionSocketFactoryBuilder.build();
+ connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
+ }
+
+ private char[] asCharArray(String value, char... nullValue)
+ {
+ return ofNullable(value)
+ .filter(not(String::isBlank))
+ .map(String::toCharArray)
+ .orElse(nullValue);
+ }
+ }
+
+ static class ClientRegistrationProvider
+ {
+ private final IdentityServiceConfig config;
+
+ ClientRegistrationProvider(IdentityServiceConfig config)
+ {
+ this.config = requireNonNull(config);
+ }
+
+ public ClientRegistration createClientRegistration(final RestOperations rest)
+ {
+ return possibleMetadataURIs()
+ .stream()
+ .map(u -> extractMetadata(rest, u))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst()
+ .map(this::validateDiscoveryDocument)
+ .map(this::createBuilder)
+ .map(this::configureClientAuthentication)
+ .map(Builder::build)
+ .orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
+ }
+
+ private OIDCProviderMetadata validateDiscoveryDocument(OIDCProviderMetadata metadata)
+ {
+ validateOIDCEndpoint(metadata.getTokenEndpointURI(), "Token");
+ validateOIDCEndpoint(metadata.getAuthorizationEndpointURI(), "Authorization");
+ validateOIDCEndpoint(metadata.getUserInfoEndpointURI(), "User Info");
+ validateOIDCEndpoint(metadata.getJWKSetURI(), "JWK Set");
+
+ if (metadata.getIssuer() != null)
+ {
+ try
+ {
+ URI metadataIssuerURI = new URI(metadata.getIssuer().getValue());
+ validateOIDCEndpoint(metadataIssuerURI, "Issuer");
+ if (StringUtils.isNotBlank(config.getIssuerUrl()) &&
+ !metadataIssuerURI.equals(URI.create(config.getIssuerUrl())))
+ {
+ throw new IdentityServiceException("Failed to create ClientRegistration. "
+ + "The Issuer value from the OIDC Discovery Endpoint does not align with the provided Issuer. Expected `%s` but found `%s`"
+ .formatted(config.getIssuerUrl(), metadata.getIssuer().getValue()));
+ }
+ }
+ catch (URISyntaxException e)
+ {
+ throw new IdentityServiceException("The provided Issuer value could not be parsed as a URI reference.", e);
+ }
+ }
+ else
+ {
+ throw new IdentityServiceException("The Issuer retrieved from the OIDC Discovery Endpoint cannot be null.");
+ }
+
+ return metadata;
+ }
+
+ private void validateOIDCEndpoint(URI value, String endpointName)
+ {
+ if (value == null || value.toASCIIString().isBlank())
+ {
+ throw new IdentityServiceException("The `%s` Endpoint retrieved from the OIDC Discovery Endpoint cannot be empty.".formatted(endpointName));
+ }
+ }
+
+ private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata)
+ {
+ final String authUri = Optional.of(metadata)
+ .map(OIDCProviderMetadata::getAuthorizationEndpointURI)
+ .map(URI::toASCIIString)
+ .orElse(null);
+
+ var metadataIssuer = getMetadataIssuer(metadata, config);
+ final String issuerUri = metadataIssuer
+ .orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ? config.getAuthServerUrl() : config.getIssuerUrl());
+
+ final var usernameAttribute = StringUtils.isNotBlank(config.getPrincipalAttribute()) ? config.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
+
+ return ClientRegistration
+ .withRegistrationId("ids")
+ .authorizationUri(authUri)
+ .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
+ .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
+ .issuerUri(issuerUri)
+ .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
+ .userNameAttributeName(usernameAttribute)
+ .scope(getSupportedScopes(metadata.getScopes()))
+ .providerConfigurationMetadata(createMetadata(metadata))
+ .authorizationGrantType(AuthorizationGrantType.PASSWORD);
+ }
+
+ private Map createMetadata(OIDCProviderMetadata metadata)
+ {
+ Map configurationMetadata = new LinkedHashMap<>();
+ if (metadata.getScopes() != null)
+ {
+ configurationMetadata.put(SCOPES_SUPPORTED.getValue(), metadata.getScopes());
+ }
+ if (StringUtils.isNotBlank(config.getAudience()))
+ {
+ configurationMetadata.put(AUDIENCE.getValue(), config.getAudience());
+ }
+ return configurationMetadata;
+ }
+
+ private Builder configureClientAuthentication(Builder builder)
+ {
+ builder.clientId(config.getResource());
+ if (config.isPublicClient())
+ {
+ return builder.clientSecret(null)
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+ }
+ return builder.clientSecret(config.getClientSecret())
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+ }
+
+ private Set getSupportedScopes(Scope scopes)
+ {
+ return scopes.stream()
+ .filter(this::hasPasswordGrantScope)
+ .map(Identifier::getValue)
+ .collect(Collectors.toSet());
+ }
+
+ private boolean hasPasswordGrantScope(Scope.Value scope)
+ {
+ return config.getPasswordGrantScopes().contains(scope.getValue());
+ }
+
+ private Optional extractMetadata(RestOperations rest, URI metadataUri)
+ {
+ final String response;
+ try
+ {
+ final ResponseEntity r = rest.exchange(RequestEntity.get(metadataUri).build(), String.class);
+ if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
+ {
+ LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode()
+ + ", has body: " + r.hasBody() + ".");
+ return Optional.empty();
+ }
+ response = r.getBody();
+ }
+ catch (Exception e)
+ {
+ LOGGER.warn("Failed to get response from " + metadataUri + ". " + e.getMessage(), e);
+ return Optional.empty();
+ }
+ try
+ {
+ return Optional.of(OIDCProviderMetadata.parse(response));
+ }
+ catch (Exception e)
+ {
+ LOGGER.warn("Failed to parse metadata. " + e.getMessage(), e);
+ return Optional.empty();
+ }
+ }
+
+ private Collection possibleMetadataURIs()
+ {
+ if (StringUtils.isBlank(config.getAuthServerUrl()) && StringUtils.isBlank(config.getIssuerUrl()))
+ {
+ throw new IdentityServiceException(
+ "Failed to create ClientRegistration. The values of issuer url and auth server url cannot both be empty.");
+ }
+
+ String baseUrl = StringUtils.isNotBlank(config.getAuthServerUrl()) ? config.getAuthServerUrl() : config.getIssuerUrl();
+
+ return List.of(UriComponentsBuilder.fromUriString(baseUrl)
+ .pathSegment(".well-known", "openid-configuration")
+ .build().toUri());
+ }
+ }
+
+ private static Optional getMetadataIssuer(OIDCProviderMetadata metadata, IdentityServiceConfig config)
+ {
+ return DEFAULT_ISSUER_ATTR.equals(config.getIssuerAttribute()) ? Optional.of(metadata)
+ .map(OIDCProviderMetadata::getIssuer)
+ .map(Issuer::getValue)
+ : Optional.of(metadata)
+ .map(OIDCProviderMetadata::getCustomParameters)
+ .map(map -> map.get(config.getIssuerAttribute()))
+ .filter(String.class::isInstance)
+ .map(String.class::cast);
+ }
+
+ static class JwtDecoderProvider
+ {
+ private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
+ private final IdentityServiceConfig config;
+ private final Set signatureAlgorithms;
+
+ JwtDecoderProvider(IdentityServiceConfig config)
+ {
+ this.config = requireNonNull(config);
+ this.signatureAlgorithms = ofNullable(config.getSignatureAlgorithms())
+ .filter(not(Set::isEmpty))
+ .orElseGet(() -> {
+ LOGGER.warn("Unable to find any valid signature algorithms in the configuration. "
+ + "Using the default signature algorithm: " + DEFAULT_SIGNATURE_ALGORITHM.getName() + ".");
+ return Set.of(DEFAULT_SIGNATURE_ALGORITHM);
+ });
+ }
+
+ public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
+ {
+ try
+ {
+ final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
+
+ decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
+ decoder.setClaimSetConverter(
+ new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
+
+ return decoder;
+ }
+ catch (RuntimeException e)
+ {
+ LOGGER.warn("Failed to create JwtDecoder.", e);
+ throw authorizationServerCantBeUsedException(e);
+ }
+ }
+
+ private NimbusJwtDecoder buildJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
+ {
+ if (isDefined(config.getRealmKey()))
+ {
+ final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
+ return NimbusJwtDecoder.withPublicKey(publicKey)
+ .signatureAlgorithm(DEFAULT_SIGNATURE_ALGORITHM)
+ .build();
+ }
+ final String jwkSetUri = requireValidJwkSetUri(providerDetails);
+ final NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder decoderBuilder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
+ signatureAlgorithms.forEach(decoderBuilder::jwsAlgorithm);
+ return decoderBuilder
+ .restOperations(rest)
+ .jwtProcessorCustomizer(this::reconfigureJWKSCache)
+ .build();
+ }
+
+ private void reconfigureJWKSCache(ConfigurableJWTProcessor jwtProcessor)
+ {
+ final Optional> jwkSource = ofNullable(jwtProcessor)
+ .map(ConfigurableJWTProcessor::getJWSKeySelector)
+ .filter(JWSVerificationKeySelector.class::isInstance)
+ .map(o -> (JWSVerificationKeySelector) o)
+ .map(JWSVerificationKeySelector::getJWKSource)
+ .filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet) o);
+ if (jwkSource.isEmpty())
+ {
+ LOGGER.warn("Not able to reconfigure the JWK Cache. Unexpected JWKSource.");
+ return;
+ }
+
+ final Optional jwkSetUrl = jwkSource.map(RemoteJWKSet::getJWKSetURL);
+ if (jwkSetUrl.isEmpty())
+ {
+ LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown JWKSetURL.");
+ return;
+ }
+
+ final Optional resourceRetriever = jwkSource.map(RemoteJWKSet::getResourceRetriever);
+ if (resourceRetriever.isEmpty())
+ {
+ LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown ResourceRetriever.");
+ return;
+ }
+
+ final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1,
+ TimeUnit.SECONDS);
+ final JWKSource cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(),
+ resourceRetriever.get(), cache);
+
+ jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
+ signatureAlgorithms.stream()
+ .map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName()))
+ .collect(Collectors.toSet()),
+ cachingJWKSource));
+ jwtProcessor.setJWSTypeVerifier(new CustomJOSEObjectTypeVerifier(JOSEObjectType.JWT, AT_JWT));
+ }
+
+ private OAuth2TokenValidator createJwtTokenValidator(ProviderDetails providerDetails)
+ {
+ List> validators = new ArrayList<>();
+ validators.add(new JwtTimestampValidator(Duration.of(config.getJwtClockSkewMs(), ChronoUnit.MILLIS)));
+ validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
+ if (!config.isClientIdValidationDisabled())
+ {
+ validators.add(new JwtClaimValidator("azp", config.getResource()::equals));
+ }
+ if (StringUtils.isNotBlank(config.getAudience()))
+ {
+ validators.add(new JwtAudienceValidator(config.getAudience()));
+ }
+ return new DelegatingOAuth2TokenValidator<>(validators);
+ }
+
+ private RSAPublicKey parsePublicKey(String pem)
+ {
+ try
+ {
+ return tryToParsePublicKey(pem);
+ }
+ catch (Exception e)
+ {
+ if (isPemFormatException(e))
+ {
+ // For backward compatibility with Keycloak adapter
+ return tryToParsePublicKey("-----BEGIN PUBLIC KEY-----\n" + pem + "\n-----END PUBLIC KEY-----");
+ }
+ throw e;
+ }
+ }
+
+ private RSAPublicKey tryToParsePublicKey(String pem)
+ {
+ final InputStream pemStream = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8));
+ return RsaKeyConverters.x509().convert(pemStream);
+ }
+
+ private boolean isPemFormatException(Exception e)
+ {
+ return e.getMessage() != null && e.getMessage().contains("-----BEGIN PUBLIC KEY-----");
+ }
+
+ private String requireValidJwkSetUri(ProviderDetails providerDetails)
+ {
+ final String uri = providerDetails.getJwkSetUri();
+ if (!isDefined(uri))
+ {
+ OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
+ "Failed to find a Signature Verifier for: '"
+ + providerDetails.getIssuerUri()
+ + "'. Check to ensure you have configured the JwkSet URI.",
+ null);
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+ return uri;
+ }
+ }
+
+ static class JwtIssuerValidator implements OAuth2TokenValidator
+ {
+ private final String requiredIssuer;
+
+ public JwtIssuerValidator(String issuer)
+ {
+ this.requiredIssuer = requireNonNull(issuer, "issuer cannot be null");
+ }
+
+ @Override
+ public OAuth2TokenValidatorResult validate(Jwt token)
+ {
+ requireNonNull(token, "token cannot be null");
+ final Object issuer = token.getClaim(JwtClaimNames.ISS);
+ if (issuer != null && requiredIssuer.equals(issuer.toString()))
+ {
+ return OAuth2TokenValidatorResult.success();
+ }
+
+ final OAuth2Error error = new OAuth2Error(
+ OAuth2ErrorCodes.INVALID_TOKEN,
+ "The iss claim is not valid. Expected `%s` but got `%s`.".formatted(requiredIssuer, issuer),
+ "https://tools.ietf.org/html/rfc6750#section-3.1");
+ return OAuth2TokenValidatorResult.failure(error);
+ }
+
+ }
+
+ static class JwtAudienceValidator implements OAuth2TokenValidator
+ {
+ private final String configuredAudience;
+
+ public JwtAudienceValidator(String configuredAudience)
+ {
+ this.configuredAudience = configuredAudience;
+ }
+
+ @Override
+ public OAuth2TokenValidatorResult validate(Jwt token)
+ {
+ requireNonNull(token, "token cannot be null");
+ final Object audience = token.getClaim(JwtClaimNames.AUD);
+ if (audience != null)
+ {
+ if (audience instanceof List && ((List) audience).contains(configuredAudience))
+ {
+ return OAuth2TokenValidatorResult.success();
+ }
+ if (audience instanceof String && audience.equals(configuredAudience))
+ {
+ return OAuth2TokenValidatorResult.success();
+ }
+ }
+
+ final OAuth2Error error = new OAuth2Error(
+ OAuth2ErrorCodes.INVALID_TOKEN,
+ "The aud claim is not valid. Expected configured audience `%s` not found.".formatted(configuredAudience),
+ "https://tools.ietf.org/html/rfc6750#section-3.1");
+ return OAuth2TokenValidatorResult.failure(error);
+ }
+ }
+
+ static class CustomClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory
+ {
+ CustomClientHttpRequestFactory(HttpClient httpClient)
+ {
+ super(httpClient);
+ }
+
+ @Override
+ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException
+ {
+ /* This is to avoid the Brotli content encoding that is not well-supported by the combination of the Apache Http Client and the Spring RestTemplate */
+ ClientHttpRequest request = super.createRequest(uri, httpMethod);
+ request.getHeaders()
+ .add("Accept-Encoding", "gzip, deflate");
+ return request;
+ }
+ }
+
+ static class CustomJOSEObjectTypeVerifier extends DefaultJOSEObjectTypeVerifier
+ {
+ public CustomJOSEObjectTypeVerifier(JOSEObjectType... allowedTypes)
+ {
+ super(Set.of(allowedTypes));
+ }
+
+ @Override
+ public void verify(JOSEObjectType type, SecurityContext context) throws BadJOSEException
+ {
+ super.verify(type, context);
+ }
+ }
+
+ private static boolean isDefined(String value)
+ {
+ return value != null && !value.isBlank();
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandler.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandler.java
index 8c77c73054..599bc06292 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandler.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandler.java
@@ -30,59 +30,38 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import java.util.function.BiFunction;
import java.util.function.Predicate;
-import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
-import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import org.apache.commons.lang3.StringUtils;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
+import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapper;
+import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapper;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
-import org.apache.commons.lang3.StringUtils;
/**
- * This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo}
- * from {@link IdentityServiceFacade.DecodedAccessToken} or {@link UserInfo}
- * and creates a new user if it does not exist in the repository.
+ * This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository.
*/
public class IdentityServiceJITProvisioningHandler
{
- private final IdentityServiceConfig identityServiceConfig;
private final IdentityServiceFacade identityServiceFacade;
private final PersonService personService;
private final TransactionService transactionService;
-
- private final BiFunction> mapTokenToUserInfoResponse = (token, usernameMappingClaim) -> {
- Optional firstName = Optional.ofNullable(token)
- .map(jwtToken -> jwtToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
- .filter(String.class::isInstance)
- .map(String.class::cast);
- Optional lastName = Optional.ofNullable(token)
- .map(jwtToken -> jwtToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
- .filter(String.class::isInstance)
- .map(String.class::cast);
- Optional email = Optional.ofNullable(token)
- .map(jwtToken -> jwtToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
- .filter(String.class::isInstance)
- .map(String.class::cast);
-
- return Optional.ofNullable(token.getClaim(Optional.ofNullable(usernameMappingClaim)
- .filter(StringUtils::isNotBlank)
- .orElse(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)))
- .filter(String.class::isInstance)
- .map(String.class::cast)
- .map(this::normalizeUserId)
- .map(username -> new OIDCUserInfo(username, firstName.orElse(""), lastName.orElse(""), email.orElse("")));
- };
+ private final IdentityServiceConfig identityServiceConfig;
+ private UserInfoAttrMapping userInfoAttrMapping;
+ private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
+ private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
- PersonService personService,
- TransactionService transactionService,
- IdentityServiceConfig identityServiceConfig)
+ PersonService personService,
+ TransactionService transactionService,
+ IdentityServiceConfig identityServiceConfig)
{
this.identityServiceFacade = identityServiceFacade;
this.personService = personService;
@@ -90,94 +69,95 @@ public class IdentityServiceJITProvisioningHandler
this.identityServiceConfig = identityServiceConfig;
}
+ /**
+ * Extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository. Call to the UserInfo endpoint is made only if the token does not contain a username claim or if user needs to be created and some of the {@link OIDCUserInfo} fields are empty.
+ */
public Optional extractUserInfoAndCreateUserIfNeeded(String bearerToken)
{
- Optional userInfoResponse = Optional.ofNullable(bearerToken)
- .filter(Predicate.not(String::isEmpty))
- .flatMap(token -> extractUserInfoResponseFromAccessToken(token)
- .filter(userInfo -> StringUtils.isNotEmpty(userInfo.username()))
- .or(() -> extractUserInfoResponseFromEndpoint(token)));
-
- if (transactionService.isReadOnly() || userInfoResponse.isEmpty())
+ if (userInfoAttrMapping == null)
{
- return userInfoResponse;
+ initMappers(identityServiceConfig);
}
- return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>()
+
+ Optional oidcUserInfo = Optional.ofNullable(bearerToken)
+ .filter(Predicate.not(String::isEmpty))
+ .flatMap(token -> extractUserInfoResponseFromAccessToken(token).filter(decodedTokenUser -> StringUtils.isNotEmpty(decodedTokenUser.username()))
+ .or(() -> extractUserInfoResponseFromEndpoint(token, userInfoAttrMapping)))
+ .map(tokenUserToOIDCUserMapper::toOIDCUser);
+
+ if (transactionService.isReadOnly() || oidcUserInfo.isEmpty())
{
+ return oidcUserInfo;
+ }
+ return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<>() {
@Override
public Optional doWork() throws Exception
{
- return userInfoResponse.map(userInfo -> {
- if (userInfo.username() != null && personService.createMissingPeople()
- && !personService.personExists(userInfo.username()))
+ return oidcUserInfo.map(oidcUser -> {
+ if (userDoesNotExistsAndCanBeCreated(oidcUser))
{
- if (!userInfo.allFieldsNotEmpty())
+ if (!oidcUser.allFieldsNotEmpty())
{
- userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
+ oidcUser = extractUserInfoResponseFromEndpoint(bearerToken, userInfoAttrMapping)
+ .map(tokenUserToOIDCUserMapper::toOIDCUser)
+ .orElse(oidcUser);
}
- Map properties = new HashMap<>();
- properties.put(ContentModel.PROP_USERNAME, userInfo.username());
- properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
- properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
- properties.put(ContentModel.PROP_EMAIL, userInfo.email());
- properties.put(ContentModel.PROP_ORGID, "");
- properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
-
- properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
- properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
-
- personService.createPerson(properties);
+ createPerson(oidcUser);
}
- return userInfo;
+ return oidcUser;
});
}
+
}, AuthenticationUtil.getSystemUserName());
}
- private Optional extractUserInfoResponseFromAccessToken(String bearerToken)
+ private void initMappers(IdentityServiceConfig identityServiceConfig)
+ {
+ this.userInfoAttrMapping = initUserInfoAttrMapping(identityServiceConfig);
+ this.tokenUserToOIDCUserMapper = new TokenUserToOIDCUserMapper(personService);
+ this.tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
+ }
+
+ private boolean userDoesNotExistsAndCanBeCreated(OIDCUserInfo userInfo)
+ {
+ return userInfo.username() != null && personService.createMissingPeople()
+ && !personService.personExists(userInfo.username());
+ }
+
+ private Optional extractUserInfoResponseFromAccessToken(String bearerToken)
{
return Optional.ofNullable(bearerToken)
- .map(identityServiceFacade::decodeToken)
- .flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
- identityServiceConfig.getPrincipalAttribute()));
+ .map(identityServiceFacade::decodeToken)
+ .flatMap(tokenToDecodedTokenUserMapper::toDecodedTokenUser);
}
- private Optional extractUserInfoResponseFromEndpoint(String bearerToken)
+ private Optional extractUserInfoResponseFromEndpoint(String bearerToken, UserInfoAttrMapping userInfoAttrMapping)
{
- return identityServiceFacade.getUserInfo(bearerToken,
- StringUtils.isNotBlank(identityServiceConfig.getPrincipalAttribute()) ?
- identityServiceConfig.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)
- .filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty())
- .map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()),
- Optional.ofNullable(userInfo.firstName()).orElse(""),
- Optional.ofNullable(userInfo.lastName()).orElse(""),
- Optional.ofNullable(userInfo.email()).orElse("")));
+ return identityServiceFacade.getUserInfo(bearerToken, userInfoAttrMapping)
+ .filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty());
}
- /**
- * Normalizes a user id, taking into account existing user accounts and case sensitivity settings.
- *
- * @param userId the user id
- * @return the string
- */
- private String normalizeUserId(final String userId)
+ private void createPerson(OIDCUserInfo userInfo)
{
- if (userId == null)
- {
- return null;
- }
+ Map properties = new HashMap<>();
+ properties.put(ContentModel.PROP_USERNAME, userInfo.username());
+ properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
+ properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
+ properties.put(ContentModel.PROP_EMAIL, userInfo.email());
+ properties.put(ContentModel.PROP_ORGID, "");
+ properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
+ properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
+ properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
- String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork()
- {
- @Override
- public String doWork() throws Exception
- {
- return personService.getUserIdentifier(userId);
- }
- }, AuthenticationUtil.getSystemUserName());
-
- return normalized == null ? userId : normalized;
+ personService.createPerson(properties);
}
-}
\ No newline at end of file
+ private UserInfoAttrMapping initUserInfoAttrMapping(IdentityServiceConfig identityServiceConfig)
+ {
+ return new UserInfoAttrMapping(identityServiceFacade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
+ identityServiceConfig.getFirstNameAttribute(),
+ identityServiceConfig.getLastNameAttribute(),
+ identityServiceConfig.getEmailAttribute());
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
index 2c91ef75ac..5302116dc7 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
@@ -1,181 +1,181 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2023 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import jakarta.servlet.http.HttpServletRequest;
-
-import java.util.Optional;
-
-import org.alfresco.repo.management.subsystems.ActivateableBean;
-import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
-
-/**
- * A {@link RemoteUserMapper} implementation that detects and validates JWTs
- * issued by the Alfresco Identity Service.
- *
- * @author Gavin Cornwell
- */
-public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
-{
- private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
-
- /** Is the mapper enabled */
- private boolean isEnabled;
-
- /** Are token validation failures handled silently? */
- private boolean isValidationFailureSilent;
-
- private BearerTokenResolver bearerTokenResolver;
-
- private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
-
- /**
- * Sets the active flag
- *
- * @param isEnabled true to enable the subsystem
- */
- public void setActive(boolean isEnabled)
- {
- this.isEnabled = isEnabled;
- }
-
- /**
- * Determines whether token validation failures are silent
- *
- * @param silent true to silently fail, false to throw an exception
- */
- public void setValidationFailureSilent(boolean silent)
- {
- this.isValidationFailureSilent = silent;
- }
-
- public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
- {
- this.bearerTokenResolver = bearerTokenResolver;
- }
-
- public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
- {
- this.jitProvisioningHandler = jitProvisioningHandler;
- }
-
- /*
- * (non-Javadoc)
- * @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(jakarta.servlet.http.HttpServletRequest)
- */
- @Override
- public String getRemoteUser(HttpServletRequest request)
- {
- LOGGER.trace("Retrieving username from http request...");
-
- if (!this.isEnabled)
- {
- LOGGER.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
- return null;
- }
- try
- {
- String normalizedUserId = extractUserFromHeader(request);
-
-
- if (normalizedUserId != null)
- {
- // Normalize the user ID taking into account case sensitivity settings
- LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
- return normalizedUserId;
- }
- }
- catch (IdentityServiceFacadeException e)
- {
- if (!isValidationFailureSilent)
- {
- throw new AuthenticationException("Failed to extract username from token: " + e.getMessage(), e);
- }
- LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
- }
- catch (RuntimeException e)
- {
- LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
- }
- LOGGER.trace("Could not identify a userId. Returning null.");
- return null;
- }
-
- /*
- * (non-Javadoc)
- * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive()
- */
- public boolean isActive()
- {
- return this.isEnabled;
- }
-
- /**
- * Extracts the user name from the JWT in the given request.
- *
- * @param request The request containing the JWT
- * @return The username or null if it can not be determined
- */
- private String extractUserFromHeader(HttpServletRequest request)
- {
- // try authenticating with bearer token first
- LOGGER.debug("Trying bearer token...");
-
- final String bearerToken;
- try
- {
- bearerToken = bearerTokenResolver.resolve(request);
- }
- catch (OAuth2AuthenticationException e)
- {
- LOGGER.debug("Failed to resolve Bearer token.", e);
- return null;
- }
-
- final Optional possibleUsername = jitProvisioningHandler
- .extractUserInfoAndCreateUserIfNeeded(bearerToken)
- .map(OIDCUserInfo::username);
-
- if (possibleUsername.isEmpty())
- {
- LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
- return null;
- }
-
- String normalizedUsername = possibleUsername.get();
- LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(normalizedUsername));
-
- return normalizedUsername;
- }
-
-}
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import java.util.Optional;
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+import org.alfresco.repo.security.authentication.AuthenticationException;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+
+/**
+ * A {@link RemoteUserMapper} implementation that detects and validates JWTs issued by the Alfresco Identity Service.
+ *
+ * @author Gavin Cornwell
+ */
+public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
+{
+ private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
+
+ /** Is the mapper enabled */
+ private boolean isEnabled;
+
+ /** Are token validation failures handled silently? */
+ private boolean isValidationFailureSilent;
+
+ private BearerTokenResolver bearerTokenResolver;
+
+ private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
+
+ /**
+ * Sets the active flag
+ *
+ * @param isEnabled
+ * true to enable the subsystem
+ */
+ public void setActive(boolean isEnabled)
+ {
+ this.isEnabled = isEnabled;
+ }
+
+ /**
+ * Determines whether token validation failures are silent
+ *
+ * @param silent
+ * true to silently fail, false to throw an exception
+ */
+ public void setValidationFailureSilent(boolean silent)
+ {
+ this.isValidationFailureSilent = silent;
+ }
+
+ public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
+ {
+ this.bearerTokenResolver = bearerTokenResolver;
+ }
+
+ public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
+ {
+ this.jitProvisioningHandler = jitProvisioningHandler;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(jakarta.servlet.http.HttpServletRequest) */
+ @Override
+ public String getRemoteUser(HttpServletRequest request)
+ {
+ LOGGER.trace("Retrieving username from http request...");
+
+ if (!this.isEnabled)
+ {
+ LOGGER.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
+ return null;
+ }
+ try
+ {
+ String normalizedUserId = extractUserFromHeader(request);
+
+ if (normalizedUserId != null)
+ {
+ // Normalize the user ID taking into account case sensitivity settings
+ LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
+ return normalizedUserId;
+ }
+ }
+ catch (IdentityServiceFacadeException e)
+ {
+ if (!isValidationFailureSilent)
+ {
+ throw new AuthenticationException("Failed to extract username from token: " + e.getMessage(), e);
+ }
+ LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
+ }
+ catch (RuntimeException e)
+ {
+ LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
+ }
+ LOGGER.trace("Could not identify a userId. Returning null.");
+ return null;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive() */
+ public boolean isActive()
+ {
+ return this.isEnabled;
+ }
+
+ /**
+ * Extracts the user name from the JWT in the given request.
+ *
+ * @param request
+ * The request containing the JWT
+ * @return The username or null if it can not be determined
+ */
+ private String extractUserFromHeader(HttpServletRequest request)
+ {
+ // try authenticating with bearer token first
+ LOGGER.debug("Trying bearer token...");
+
+ final String bearerToken;
+ try
+ {
+ bearerToken = bearerTokenResolver.resolve(request);
+ }
+ catch (OAuth2AuthenticationException e)
+ {
+ LOGGER.debug("Failed to resolve Bearer token.", e);
+ return null;
+ }
+
+ final Optional possibleUsername = jitProvisioningHandler
+ .extractUserInfoAndCreateUserIfNeeded(bearerToken)
+ .map(OIDCUserInfo::username);
+
+ if (possibleUsername.isEmpty())
+ {
+ LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
+ return null;
+ }
+
+ String normalizedUsername = possibleUsername.get();
+ LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(normalizedUsername));
+
+ return normalizedUsername;
+ }
+
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java
index 8eb0eff806..de39c53b27 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java
@@ -30,21 +30,12 @@ import static java.util.Objects.requireNonNull;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
-import java.util.function.Predicate;
-import com.nimbusds.oauth2.sdk.ErrorObject;
-import com.nimbusds.oauth2.sdk.ParseException;
-import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
-import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
-import com.nimbusds.openid.connect.sdk.UserInfoRequest;
-import com.nimbusds.openid.connect.sdk.UserInfoResponse;
-import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
@@ -59,27 +50,35 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRe
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
+import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
+
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);
private static final Instant SOME_INSIGNIFICANT_DATE_IN_THE_PAST = Instant.MIN.plusSeconds(12345);
private final Map clients;
+ private final DefaultOAuth2UserService defaultOAuth2UserService;
private final ClientRegistration clientRegistration;
private final JwtDecoder jwtDecoder;
@@ -93,6 +92,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
+ this.defaultOAuth2UserService = createOAuth2UserService(restOperations);
}
@Override
@@ -121,51 +121,18 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
@Override
- public Optional getUserInfo(String tokenParameter, String principalAttribute)
+ public Optional getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
{
- return Optional.ofNullable(tokenParameter)
- .filter(Predicate.not(String::isEmpty))
- .flatMap(token -> Optional.ofNullable(clientRegistration)
- .map(ClientRegistration::getProviderDetails)
- .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint)
- .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUri)
- .flatMap(uri -> {
- try
- {
- return Optional.of(
- new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send());
- }
- catch (IOException | URISyntaxException e)
- {
- LOGGER.warn("Failed to get user information. Reason: " + e.getMessage());
- return Optional.empty();
- }
- })
- .flatMap(httpResponse -> {
- try
- {
- UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
-
- if (userInfoResponse instanceof UserInfoErrorResponse userInfoErrorResponse)
- {
- String errorMessage = Optional.ofNullable(userInfoErrorResponse.getErrorObject())
- .map(ErrorObject::getDescription)
- .orElse("No error description found");
- LOGGER.warn("User Info Request failed: " + errorMessage);
- throw new UserInfoException(errorMessage);
- }
- return Optional.of(userInfoResponse);
- }
- catch (ParseException e)
- {
- LOGGER.warn("Failed to parse user info response. Reason: " + e.getMessage());
- return Optional.empty();
- }
- })
- .map(UserInfoResponse::toSuccessResponse)
- .map(UserInfoSuccessResponse::getUserInfo))
- .map(userInfo -> new OIDCUserInfo(userInfo.getStringClaim(principalAttribute), userInfo.getGivenName(),
- userInfo.getFamilyName(), userInfo.getEmailAddress()));
+ try
+ {
+ return Optional.ofNullable(defaultOAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, getSpringAccessToken(token))))
+ .flatMap(oAuth2User -> mapOAuth2UserToDecodedTokenUser(oAuth2User, userInfoAttrMapping));
+ }
+ catch (OAuth2AuthenticationException exception)
+ {
+ LOGGER.warn("User Info Request failed: " + exception.getMessage());
+ return Optional.empty();
+ }
}
@Override
@@ -202,11 +169,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
if (grant.isRefreshToken())
{
- final OAuth2AccessToken expiredAccessToken = new OAuth2AccessToken(
- TokenType.BEARER,
- "JUST_FOR_FULFILLING_THE_SPRING_API",
- SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
- SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
+ final OAuth2AccessToken expiredAccessToken = getSpringAccessToken("JUST_FOR_FULFILLING_THE_SPRING_API");
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken,
@@ -258,6 +221,26 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
return client;
}
+ private static DefaultOAuth2UserService createOAuth2UserService(RestOperations rest)
+ {
+ final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
+ userService.setRestOperations(rest);
+ return userService;
+ }
+
+ private Optional mapOAuth2UserToDecodedTokenUser(OAuth2User oAuth2User, UserInfoAttrMapping userInfoAttrMapping)
+ {
+ var preferredUsername = Optional.ofNullable(oAuth2User.getAttribute(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .filter(StringUtils::isNotEmpty);
+ var userName = Optional.ofNullable(oAuth2User.getName()).filter(username -> !username.isEmpty()).or(() -> preferredUsername);
+ return userName.map(name -> DecodedTokenUser.validateAndCreate(name,
+ oAuth2User.getAttribute(userInfoAttrMapping.firstNameClaim()),
+ oAuth2User.getAttribute(userInfoAttrMapping.lastNameClaim()),
+ oAuth2User.getAttribute(userInfoAttrMapping.emailClaim())));
+ }
+
private static OAuth2AccessTokenResponseClient createPasswordClient(RestOperations rest,
ClientRegistration clientRegistration)
{
@@ -288,6 +271,16 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
};
}
+ private static OAuth2AccessToken getSpringAccessToken(String token)
+ {
+ // Just for fulfilling the Spring API
+ return new OAuth2AccessToken(
+ TokenType.BEARER,
+ token,
+ SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
+ SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
+ }
+
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java
index fa9e38de53..8907c2f808 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java
@@ -37,13 +37,19 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.State;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.web.util.UriComponentsBuilder;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
@@ -53,16 +59,9 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
-import org.springframework.web.util.UriComponentsBuilder;
/**
- * An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID
- * or to initiate the OIDC authorization code flow.
+ * An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow.
*/
public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
{
@@ -71,7 +70,6 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
- private static final Set SCOPES = Set.of("openid", "profile", "email", "offline_access");
private IdentityServiceConfig identityServiceConfig;
private IdentityServiceFacade identityServiceFacade;
@@ -145,7 +143,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
try
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
- authorizationCode(code, request.getRequestURL().toString()));
+ authorizationCode(code, request.getRequestURL().toString()));
addCookies(response, accessTokenAuthorization);
bearerToken = accessTokenAuthorization.getAccessToken().getTokenValue();
}
@@ -154,8 +152,8 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
if (LOGGER.isWarnEnabled())
{
LOGGER.warn(
- "Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
- exception.getMessage());
+ "Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
+ exception.getMessage());
}
}
return bearerToken;
@@ -188,7 +186,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
{
cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken().getTokenValue(), response);
cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(
- accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
+ accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response);
}
@@ -198,13 +196,13 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
State state = new State();
UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri())
- .queryParam("client_id", clientRegistration.getClientId())
- .queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
- .queryParam("response_type", "code")
- .queryParam("scope", String.join("+", getScopes(clientRegistration)))
- .queryParam("state", state.toString());
+ .queryParam("client_id", clientRegistration.getClientId())
+ .queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
+ .queryParam("response_type", "code")
+ .queryParam("scope", String.join("+", getScopes(clientRegistration)))
+ .queryParam("state", state.toString());
- if(StringUtils.isNotBlank(identityServiceConfig.getAudience()))
+ if (StringUtils.isNotBlank(identityServiceConfig.getAudience()))
{
authRequestBuilder.queryParam("audience", identityServiceConfig.getAudience());
}
@@ -215,20 +213,25 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set getScopes(ClientRegistration clientRegistration)
{
return Optional.ofNullable(clientRegistration.getProviderDetails())
- .map(ProviderDetails::getConfigurationMetadata)
- .map(metadata -> metadata.get(SCOPES_SUPPORTED.getValue()))
- .filter(Scope.class::isInstance)
- .map(Scope.class::cast)
- .map(this::getSupportedScopes)
- .orElse(clientRegistration.getScopes());
+ .map(ProviderDetails::getConfigurationMetadata)
+ .map(metadata -> metadata.get(SCOPES_SUPPORTED.getValue()))
+ .filter(Scope.class::isInstance)
+ .map(Scope.class::cast)
+ .map(this::getSupportedScopes)
+ .orElse(clientRegistration.getScopes());
}
private Set getSupportedScopes(Scope scopes)
{
return scopes.stream()
- .filter(scope -> SCOPES.contains(scope.getValue()))
- .map(Identifier::getValue)
- .collect(Collectors.toSet());
+ .filter(this::hasAdminConsoleScope)
+ .map(Identifier::getValue)
+ .collect(Collectors.toSet());
+ }
+
+ private boolean hasAdminConsoleScope(Scope.Value scope)
+ {
+ return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
}
private String getRedirectUri(String requestURL)
@@ -263,7 +266,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private AccessTokenAuthorization doRefreshAuthToken(String refreshToken)
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
- AuthorizationGrant.refreshToken(refreshToken));
+ AuthorizationGrant.refreshToken(refreshToken));
if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
{
throw new AuthenticationException("AccessTokenResponse is null or empty");
@@ -284,7 +287,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
}
public void setIdentityServiceFacade(
- IdentityServiceFacade identityServiceFacade)
+ IdentityServiceFacade identityServiceFacade)
{
this.identityServiceFacade = identityServiceFacade;
}
@@ -295,13 +298,13 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
}
public void setCookiesService(
- AdminConsoleAuthenticationCookiesService cookiesService)
+ AdminConsoleAuthenticationCookiesService cookiesService)
{
this.cookiesService = cookiesService;
}
public void setIdentityServiceConfig(
- IdentityServiceConfig identityServiceConfig)
+ IdentityServiceConfig identityServiceConfig)
{
this.identityServiceConfig = identityServiceConfig;
}
@@ -316,4 +319,4 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
{
this.isEnabled = isEnabled;
}
-}
\ No newline at end of file
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapper.java
new file mode 100644
index 0000000000..e94d6824e5
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapper.java
@@ -0,0 +1,66 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+import java.util.Optional;
+
+import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
+import org.apache.commons.lang3.StringUtils;
+
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
+
+public class AccessTokenToDecodedTokenUserMapper
+{
+ private static final String DEFAULT_USERNAME_CLAIM = PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
+
+ private final UserInfoAttrMapping userInfoAttrMapping;
+
+ public AccessTokenToDecodedTokenUserMapper(UserInfoAttrMapping userInfoAttrMapping)
+ {
+ this.userInfoAttrMapping = userInfoAttrMapping;
+ }
+
+ /**
+ * Maps the given {@link IdentityServiceFacade.DecodedAccessToken} to a {@link DecodedTokenUser}.
+ *
+ * @param token
+ * the token to map
+ * @return the mapped {@link DecodedTokenUser} or {@link Optional#empty()} if the token does not contain a username claim
+ */
+ public Optional toDecodedTokenUser(IdentityServiceFacade.DecodedAccessToken token)
+ {
+ Object firstName = token.getClaim(userInfoAttrMapping.firstNameClaim());
+ Object lastName = token.getClaim(userInfoAttrMapping.lastNameClaim());
+ Object email = token.getClaim(userInfoAttrMapping.emailClaim());
+
+ return Optional.ofNullable(token.getClaim(Optional.ofNullable(userInfoAttrMapping.usernameClaim())
+ .filter(StringUtils::isNotBlank)
+ .orElse(DEFAULT_USERNAME_CLAIM)))
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .map(username -> DecodedTokenUser.validateAndCreate(username, firstName, lastName, email));
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/DecodedTokenUser.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/DecodedTokenUser.java
new file mode 100644
index 0000000000..305c108944
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/DecodedTokenUser.java
@@ -0,0 +1,44 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+import java.util.Optional;
+
+public record DecodedTokenUser(String username, String firstName, String lastName, String email)
+{
+
+ private static final String EMPTY_STRING = "";
+
+ public static DecodedTokenUser validateAndCreate(String username, Object firstName, Object lastName, Object email)
+ {
+ return new DecodedTokenUser(username, getStringVal(firstName), getStringVal(lastName), getStringVal(email));
+ }
+
+ private static String getStringVal(Object firstName)
+ {
+ return Optional.ofNullable(firstName).filter(String.class::isInstance).map(String.class::cast).orElse(EMPTY_STRING);
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/OIDCUserInfo.java
similarity index 99%
rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java
rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/OIDCUserInfo.java
index 5f8a3c25d6..8d8e98f995 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/OIDCUserInfo.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice;
+package org.alfresco.repo.security.authentication.identityservice.user;
import java.util.stream.Stream;
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapper.java
new file mode 100644
index 0000000000..5e3cde4be9
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapper.java
@@ -0,0 +1,76 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.service.cmr.security.PersonService;
+
+public class TokenUserToOIDCUserMapper
+{
+ private final PersonService personService;
+
+ public TokenUserToOIDCUserMapper(PersonService personService)
+ {
+ this.personService = personService;
+ }
+
+ /**
+ * Maps a decoded token user to an OIDC user where the user id (username) is normalized.
+ *
+ * @param decodedTokenUser
+ * the decoded token user
+ * @return the OIDC user
+ */
+ public OIDCUserInfo toOIDCUser(DecodedTokenUser decodedTokenUser)
+ {
+ return new OIDCUserInfo(usernameToUserId(decodedTokenUser.username()), decodedTokenUser.firstName(), decodedTokenUser.lastName(), decodedTokenUser.email());
+ }
+
+ /**
+ * Normalizes a username, taking into account existing user accounts and case sensitivity settings.
+ *
+ * @param caseSensitiveUserName
+ * the case-sensitive username
+ * @return the string
+ */
+ private String usernameToUserId(final String caseSensitiveUserName)
+ {
+ if (caseSensitiveUserName == null)
+ {
+ return null;
+ }
+
+ String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() {
+ @Override
+ public String doWork() throws Exception
+ {
+ return personService.getUserIdentifier(caseSensitiveUserName);
+ }
+ }, AuthenticationUtil.getSystemUserName());
+
+ return normalized == null ? caseSensitiveUserName : normalized;
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/UserInfoAttrMapping.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/UserInfoAttrMapping.java
new file mode 100644
index 0000000000..d748dfe2fd
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/user/UserInfoAttrMapping.java
@@ -0,0 +1,41 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+/**
+ * The UserInfoAttrMapping record represents the mapping of claims fetched from the UserInfo endpoint to create an Alfresco user.
+ *
+ * @param usernameClaim
+ * the claim that represents the username
+ * @param firstNameClaim
+ * the claim that represents the first name
+ * @param lastNameClaim
+ * the claim that represents the last name
+ * @param emailClaim
+ * the claim that represents the email
+ */
+public record UserInfoAttrMapping(String usernameClaim, String firstNameClaim, String lastNameClaim, String emailClaim)
+{}
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
index 748baf8cec..8bc16b3c06 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
@@ -149,6 +149,15 @@
${identity-service.principal-attribute:preferred_username}
+
+ ${identity-service.first-name-attribute:given_name}
+
+
+ ${identity-service.last-name-attribute:family_name}
+
+
+ ${identity-service.email-attribute:email}
+
${identity-service.client-id.validation.disabled:true}
@@ -158,6 +167,18 @@
${identity-service.signature-algorithms:RS256,PS256}
+
+ ${identity-service.admin-console.scopes:openid,profile,email,offline_access}
+
+
+ ${identity-service.password-grant.scopes:openid,profile,email}
+
+
+ ${identity-service.issuer-attribute:issuer}
+
+
+ ${identity-service.jwt-clock-skew-ms:0}
+
@@ -219,4 +240,4 @@
-
\ No newline at end of file
+
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
index 7357b01644..e6d517c1ad 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
@@ -12,4 +12,11 @@ identity-service.resource=alfresco
identity-service.credentials.secret=
identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
-identity-service.signature-algorithms=RS256,PS256
\ No newline at end of file
+identity-service.signature-algorithms=RS256,PS256
+identity-service.first-name-attribute=given_name
+identity-service.last-name-attribute=family_name
+identity-service.email-attribute=email
+identity-service.admin-console.scopes=openid,profile,email,offline_access
+identity-service.password-grant.scopes=openid,profile,email
+identity-service.issuer-attribute=issuer
+identity-service.jwt-clock-skew-ms=0
diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
index cd6d76158e..fd5e9136a8 100644
--- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
+++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
@@ -4,27 +4,31 @@
* %%
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
- *
+ *
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
* #L%
*/
package org.alfresco;
+import org.junit.experimental.categories.Categories;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
import org.alfresco.repo.security.authentication.identityservice.ClientRegistrationProviderUnitTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
@@ -33,237 +37,236 @@ import org.alfresco.repo.security.authentication.identityservice.SpringBasedIden
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
-import org.junit.experimental.categories.Categories;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
/**
- * All Repository project UNIT test classes (no application context) should be added to this test suite.
- * Tests marked as DBTests are automatically excluded and are run as part of {@link AllDBTestsTestSuite}.
+ * All Repository project UNIT test classes (no application context) should be added to this test suite. Tests marked as DBTests are automatically excluded and are run as part of {@link AllDBTestsTestSuite}.
*/
@RunWith(Categories.class)
@Categories.ExcludeCategory({DBTests.class, NonBuildTests.class})
-@Suite.SuiteClasses({
- org.alfresco.repo.site.SiteMembershipTest.class,
- org.alfresco.encryption.EncryptorTest.class,
- org.alfresco.encryption.KeyStoreKeyProviderTest.class,
- org.alfresco.filesys.config.ServerConfigurationBeanTest.class,
- org.alfresco.filesys.repo.rules.ShuffleTest.class,
- org.alfresco.opencmis.AlfrescoCmisExceptionInterceptorTest.class,
- org.alfresco.repo.admin.Log4JHierarchyInitTest.class,
- org.alfresco.repo.attributes.PropTablesCleanupJobTest.class,
- org.alfresco.repo.cache.AbstractCacheFactoryTest.class,
- org.alfresco.repo.cache.DefaultCacheFactoryTest.class,
- org.alfresco.repo.cache.DefaultSimpleCacheTest.class,
- org.alfresco.repo.cache.InMemoryCacheStatisticsTest.class,
- org.alfresco.repo.cache.TransactionStatsTest.class,
- org.alfresco.repo.cache.lookup.EntityLookupCacheTest.class,
- org.alfresco.repo.calendar.CalendarHelpersTest.class,
- org.alfresco.repo.copy.CopyServiceImplUnitTest.class,
- org.alfresco.repo.dictionary.RepoDictionaryDAOTest.class,
- org.alfresco.repo.forms.processor.node.FieldProcessorTest.class,
- org.alfresco.repo.forms.processor.workflow.TaskFormProcessorTest.class,
- org.alfresco.repo.forms.processor.workflow.WorkflowFormProcessorTest.class,
- org.alfresco.repo.invitation.site.InviteSenderTest.class,
- org.alfresco.repo.invitation.site.InviteModeratedSenderTest.class,
- org.alfresco.repo.jscript.ScriptSearchTest.class,
- org.alfresco.repo.lock.LockUtilsTest.class,
- org.alfresco.repo.lock.mem.LockStoreImplTest.class,
- org.alfresco.repo.management.CheckRequiredClassesForLoggingConsoleUnitTest.class,
- org.alfresco.repo.management.subsystems.CryptodocSwitchableApplicationContextFactoryTest.class,
- org.alfresco.repo.module.ModuleDetailsImplTest.class,
- org.alfresco.repo.module.ModuleVersionNumberTest.class,
- org.alfresco.repo.module.DeprecatedModulesValidatorTest.class,
- org.alfresco.repo.node.integrity.IntegrityEventTest.class,
- org.alfresco.repo.policy.MTPolicyComponentTest.class,
- org.alfresco.repo.policy.PolicyComponentTest.class,
- org.alfresco.repo.rendition.RenditionNodeManagerTest.class,
- org.alfresco.repo.rendition.RenditionServiceImplTest.class,
- org.alfresco.repo.replication.ReplicationServiceImplTest.class,
- org.alfresco.repo.rule.RuleServiceImplUnitTest.class,
- org.alfresco.repo.service.StoreRedirectorProxyFactoryTest.class,
- org.alfresco.repo.site.RoleComparatorImplTest.class,
- org.alfresco.repo.template.UnsafeMethodsTest.class,
- org.alfresco.repo.tenant.MultiTAdminServiceImplTest.class,
- org.alfresco.repo.thumbnail.ThumbnailServiceImplParameterTest.class,
- org.alfresco.repo.transfer.ContentChunkerImplTest.class,
- org.alfresco.repo.transfer.HttpClientTransmitterImplTest.class,
- org.alfresco.repo.transfer.manifest.TransferManifestTest.class,
- org.alfresco.repo.transfer.TransferVersionCheckerImplTest.class,
- org.alfresco.service.cmr.calendar.CalendarRecurrenceHelperTest.class,
- org.alfresco.service.cmr.calendar.CalendarTimezoneHelperTest.class,
- org.alfresco.tools.RenameUserTest.class,
- org.alfresco.util.VersionNumberTest.class,
- org.alfresco.util.FileNameValidatorTest.class,
- org.alfresco.util.HttpClientHelperTest.class,
- org.alfresco.util.JSONtoFmModelTest.class,
- org.alfresco.util.ModelUtilTest.class,
- org.alfresco.util.PropertyMapTest.class,
- org.alfresco.util.ValueProtectingMapTest.class,
- org.alfresco.util.json.ExceptionJsonSerializerTest.class,
- org.alfresco.util.collections.CollectionUtilsTest.class,
- org.alfresco.util.schemacomp.DbObjectXMLTransformerTest.class,
- org.alfresco.util.schemacomp.DbPropertyTest.class,
- org.alfresco.util.schemacomp.DefaultComparisonUtilsTest.class,
- org.alfresco.util.schemacomp.DifferenceTest.class,
- org.alfresco.util.schemacomp.MultiFileDumperTest.class,
- org.alfresco.util.schemacomp.RedundantDbObjectTest.class,
- org.alfresco.util.schemacomp.SchemaComparatorTest.class,
- org.alfresco.util.schemacomp.SchemaToXMLTest.class,
- org.alfresco.util.schemacomp.ValidatingVisitorTest.class,
- org.alfresco.util.schemacomp.ValidationResultTest.class,
- org.alfresco.util.schemacomp.XMLToSchemaTest.class,
- org.alfresco.util.schemacomp.model.ColumnTest.class,
- org.alfresco.util.schemacomp.model.ForeignKeyTest.class,
- org.alfresco.util.schemacomp.model.IndexTest.class,
- org.alfresco.util.schemacomp.model.PrimaryKeyTest.class,
- org.alfresco.util.schemacomp.model.SchemaTest.class,
- org.alfresco.util.schemacomp.model.SequenceTest.class,
- org.alfresco.util.schemacomp.model.TableTest.class,
- org.alfresco.util.schemacomp.validator.IndexColumnsValidatorTest.class,
- org.alfresco.util.schemacomp.validator.NameValidatorTest.class,
- org.alfresco.util.schemacomp.validator.SchemaVersionValidatorTest.class,
- org.alfresco.util.schemacomp.validator.TypeNameOnlyValidatorTest.class,
- org.alfresco.util.test.OmittedTestClassFinderUnitTest.class,
- org.alfresco.util.test.junitrules.RetryAtMostRuleTest.class,
- org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class,
- org.alfresco.repo.search.impl.solr.AbstractSolrQueryHTTPClientTest.class,
- org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
- org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
- org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngineTest.class,
- org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorLimitsTest.class,
- org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorPermissionsTest.class,
- org.alfresco.repo.search.impl.solr.DbOrIndexSwitchingQueryLanguageTest.class,
- org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class,
- org.alfresco.repo.search.impl.solr.SolrSQLHttpClientTest.class,
- org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class,
- org.alfresco.repo.search.impl.solr.SolrJSONResultTest.class,
- org.alfresco.repo.search.impl.solr.SolrSQLJSONResultMetadataSetTest.class,
- org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class,
- org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
- org.alfresco.util.BeanExtenderUnitTest.class,
- org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
- IdentityServiceFacadeFactoryBeanTest.class,
- LazyInstantiatingIdentityServiceFacadeUnitTest.class,
- SpringBasedIdentityServiceFacadeUnitTest.class,
- IdentityServiceJITProvisioningHandlerUnitTest.class,
- AdminConsoleAuthenticationCookiesServiceUnitTest.class,
- AdminConsoleHttpServletRequestWrapperUnitTest.class,
- IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
- ClientRegistrationProviderUnitTest.class,
- org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
- org.alfresco.repo.security.authentication.PasswordHashingTest.class,
- org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
- org.alfresco.repo.security.permissions.PermissionCheckCollectionTest.class,
- org.alfresco.repo.security.sync.LDAPUserRegistryTest.class,
- org.alfresco.traitextender.TraitExtenderIntegrationTest.class,
- org.alfresco.traitextender.AJExtensionsCompileTest.class,
+@Suite.SuiteClasses(value = {
+ org.alfresco.repo.site.SiteMembershipTest.class,
+ org.alfresco.encryption.EncryptorTest.class,
+ org.alfresco.encryption.KeyStoreKeyProviderTest.class,
+ org.alfresco.filesys.config.ServerConfigurationBeanTest.class,
+ org.alfresco.filesys.repo.rules.ShuffleTest.class,
+ org.alfresco.opencmis.AlfrescoCmisExceptionInterceptorTest.class,
+ org.alfresco.repo.admin.Log4JHierarchyInitTest.class,
+ org.alfresco.repo.attributes.PropTablesCleanupJobTest.class,
+ org.alfresco.repo.cache.AbstractCacheFactoryTest.class,
+ org.alfresco.repo.cache.DefaultCacheFactoryTest.class,
+ org.alfresco.repo.cache.DefaultSimpleCacheTest.class,
+ org.alfresco.repo.cache.InMemoryCacheStatisticsTest.class,
+ org.alfresco.repo.cache.TransactionStatsTest.class,
+ org.alfresco.repo.cache.lookup.EntityLookupCacheTest.class,
+ org.alfresco.repo.calendar.CalendarHelpersTest.class,
+ org.alfresco.repo.copy.CopyServiceImplUnitTest.class,
+ org.alfresco.repo.dictionary.RepoDictionaryDAOTest.class,
+ org.alfresco.repo.forms.processor.node.FieldProcessorTest.class,
+ org.alfresco.repo.forms.processor.workflow.TaskFormProcessorTest.class,
+ org.alfresco.repo.forms.processor.workflow.WorkflowFormProcessorTest.class,
+ org.alfresco.repo.invitation.site.InviteSenderTest.class,
+ org.alfresco.repo.invitation.site.InviteModeratedSenderTest.class,
+ org.alfresco.repo.jscript.ScriptSearchTest.class,
+ org.alfresco.repo.lock.LockUtilsTest.class,
+ org.alfresco.repo.lock.mem.LockStoreImplTest.class,
+ org.alfresco.repo.management.CheckRequiredClassesForLoggingConsoleUnitTest.class,
+ org.alfresco.repo.management.subsystems.CryptodocSwitchableApplicationContextFactoryTest.class,
+ org.alfresco.repo.module.ModuleDetailsImplTest.class,
+ org.alfresco.repo.module.ModuleVersionNumberTest.class,
+ org.alfresco.repo.module.DeprecatedModulesValidatorTest.class,
+ org.alfresco.repo.node.integrity.IntegrityEventTest.class,
+ org.alfresco.repo.policy.MTPolicyComponentTest.class,
+ org.alfresco.repo.policy.PolicyComponentTest.class,
+ org.alfresco.repo.rendition.RenditionNodeManagerTest.class,
+ org.alfresco.repo.rendition.RenditionServiceImplTest.class,
+ org.alfresco.repo.replication.ReplicationServiceImplTest.class,
+ org.alfresco.repo.rule.RuleServiceImplUnitTest.class,
+ org.alfresco.repo.service.StoreRedirectorProxyFactoryTest.class,
+ org.alfresco.repo.site.RoleComparatorImplTest.class,
+ org.alfresco.repo.template.UnsafeMethodsTest.class,
+ org.alfresco.repo.tenant.MultiTAdminServiceImplTest.class,
+ org.alfresco.repo.thumbnail.ThumbnailServiceImplParameterTest.class,
+ org.alfresco.repo.transfer.ContentChunkerImplTest.class,
+ org.alfresco.repo.transfer.HttpClientTransmitterImplTest.class,
+ org.alfresco.repo.transfer.manifest.TransferManifestTest.class,
+ org.alfresco.repo.transfer.TransferVersionCheckerImplTest.class,
+ org.alfresco.service.cmr.calendar.CalendarRecurrenceHelperTest.class,
+ org.alfresco.service.cmr.calendar.CalendarTimezoneHelperTest.class,
+ org.alfresco.tools.RenameUserTest.class,
+ org.alfresco.util.VersionNumberTest.class,
+ org.alfresco.util.FileNameValidatorTest.class,
+ org.alfresco.util.HttpClientHelperTest.class,
+ org.alfresco.util.JSONtoFmModelTest.class,
+ org.alfresco.util.ModelUtilTest.class,
+ org.alfresco.util.PropertyMapTest.class,
+ org.alfresco.util.ValueProtectingMapTest.class,
+ org.alfresco.util.json.ExceptionJsonSerializerTest.class,
+ org.alfresco.util.collections.CollectionUtilsTest.class,
+ org.alfresco.util.schemacomp.DbObjectXMLTransformerTest.class,
+ org.alfresco.util.schemacomp.DbPropertyTest.class,
+ org.alfresco.util.schemacomp.DefaultComparisonUtilsTest.class,
+ org.alfresco.util.schemacomp.DifferenceTest.class,
+ org.alfresco.util.schemacomp.MultiFileDumperTest.class,
+ org.alfresco.util.schemacomp.RedundantDbObjectTest.class,
+ org.alfresco.util.schemacomp.SchemaComparatorTest.class,
+ org.alfresco.util.schemacomp.SchemaToXMLTest.class,
+ org.alfresco.util.schemacomp.ValidatingVisitorTest.class,
+ org.alfresco.util.schemacomp.ValidationResultTest.class,
+ org.alfresco.util.schemacomp.XMLToSchemaTest.class,
+ org.alfresco.util.schemacomp.model.ColumnTest.class,
+ org.alfresco.util.schemacomp.model.ForeignKeyTest.class,
+ org.alfresco.util.schemacomp.model.IndexTest.class,
+ org.alfresco.util.schemacomp.model.PrimaryKeyTest.class,
+ org.alfresco.util.schemacomp.model.SchemaTest.class,
+ org.alfresco.util.schemacomp.model.SequenceTest.class,
+ org.alfresco.util.schemacomp.model.TableTest.class,
+ org.alfresco.util.schemacomp.validator.IndexColumnsValidatorTest.class,
+ org.alfresco.util.schemacomp.validator.NameValidatorTest.class,
+ org.alfresco.util.schemacomp.validator.SchemaVersionValidatorTest.class,
+ org.alfresco.util.schemacomp.validator.TypeNameOnlyValidatorTest.class,
+ org.alfresco.util.test.OmittedTestClassFinderUnitTest.class,
+ org.alfresco.util.test.junitrules.RetryAtMostRuleTest.class,
+ org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class,
+ org.alfresco.repo.search.impl.solr.AbstractSolrQueryHTTPClientTest.class,
+ org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
+ org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
+ org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngineTest.class,
+ org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorLimitsTest.class,
+ org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorPermissionsTest.class,
+ org.alfresco.repo.search.impl.solr.DbOrIndexSwitchingQueryLanguageTest.class,
+ org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class,
+ org.alfresco.repo.search.impl.solr.SolrSQLHttpClientTest.class,
+ org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class,
+ org.alfresco.repo.search.impl.solr.SolrJSONResultTest.class,
+ org.alfresco.repo.search.impl.solr.SolrSQLJSONResultMetadataSetTest.class,
+ org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class,
+ org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
+ org.alfresco.util.BeanExtenderUnitTest.class,
+ org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
+ IdentityServiceFacadeFactoryBeanTest.class,
+ LazyInstantiatingIdentityServiceFacadeUnitTest.class,
+ SpringBasedIdentityServiceFacadeUnitTest.class,
+ IdentityServiceJITProvisioningHandlerUnitTest.class,
+ AccessTokenToDecodedTokenUserMapperUnitTest.class,
+ TokenUserToOIDCUserMapperUnitTest.class,
+ AdminConsoleAuthenticationCookiesServiceUnitTest.class,
+ AdminConsoleHttpServletRequestWrapperUnitTest.class,
+ IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
+ ClientRegistrationProviderUnitTest.class,
+ org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
+ org.alfresco.repo.security.authentication.PasswordHashingTest.class,
+ org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
+ org.alfresco.repo.security.permissions.PermissionCheckCollectionTest.class,
+ org.alfresco.repo.security.sync.LDAPUserRegistryTest.class,
+ org.alfresco.traitextender.TraitExtenderIntegrationTest.class,
+ org.alfresco.traitextender.AJExtensionsCompileTest.class,
- org.alfresco.repo.virtual.page.PageCollatorTest.class,
- org.alfresco.repo.virtual.ref.GetChildByIdMethodTest.class,
- org.alfresco.repo.virtual.ref.GetParentReferenceMethodTest.class,
- org.alfresco.repo.virtual.ref.NewVirtualReferenceMethodTest.class,
- org.alfresco.repo.virtual.ref.PlainReferenceParserTest.class,
- org.alfresco.repo.virtual.ref.PlainStringifierTest.class,
- org.alfresco.repo.virtual.ref.ProtocolTest.class,
- org.alfresco.repo.virtual.ref.ReferenceTest.class,
- org.alfresco.repo.virtual.ref.ResourceParameterTest.class,
- org.alfresco.repo.virtual.ref.StringParameterTest.class,
- org.alfresco.repo.virtual.ref.VirtualProtocolTest.class,
- org.alfresco.repo.virtual.store.ReferenceComparatorTest.class,
+ org.alfresco.repo.virtual.page.PageCollatorTest.class,
+ org.alfresco.repo.virtual.ref.GetChildByIdMethodTest.class,
+ org.alfresco.repo.virtual.ref.GetParentReferenceMethodTest.class,
+ org.alfresco.repo.virtual.ref.NewVirtualReferenceMethodTest.class,
+ org.alfresco.repo.virtual.ref.PlainReferenceParserTest.class,
+ org.alfresco.repo.virtual.ref.PlainStringifierTest.class,
+ org.alfresco.repo.virtual.ref.ProtocolTest.class,
+ org.alfresco.repo.virtual.ref.ReferenceTest.class,
+ org.alfresco.repo.virtual.ref.ResourceParameterTest.class,
+ org.alfresco.repo.virtual.ref.StringParameterTest.class,
+ org.alfresco.repo.virtual.ref.VirtualProtocolTest.class,
+ org.alfresco.repo.virtual.store.ReferenceComparatorTest.class,
- org.alfresco.repo.virtual.ref.ZeroReferenceParserTest.class,
- org.alfresco.repo.virtual.ref.ZeroStringifierTest.class,
+ org.alfresco.repo.virtual.ref.ZeroReferenceParserTest.class,
+ org.alfresco.repo.virtual.ref.ZeroStringifierTest.class,
- org.alfresco.repo.virtual.ref.HashStringifierTest.class,
- org.alfresco.repo.virtual.ref.NodeRefRadixHasherTest.class,
- org.alfresco.repo.virtual.ref.NumericPathHasherTest.class,
- org.alfresco.repo.virtual.ref.StoredPathHasherTest.class,
+ org.alfresco.repo.virtual.ref.HashStringifierTest.class,
+ org.alfresco.repo.virtual.ref.NodeRefRadixHasherTest.class,
+ org.alfresco.repo.virtual.ref.NumericPathHasherTest.class,
+ org.alfresco.repo.virtual.ref.StoredPathHasherTest.class,
- org.alfresco.repo.virtual.template.VirtualQueryImplTest.class,
- org.alfresco.repo.virtual.store.TypeVirtualizationMethodUnitTest.class,
+ org.alfresco.repo.virtual.template.VirtualQueryImplTest.class,
+ org.alfresco.repo.virtual.store.TypeVirtualizationMethodUnitTest.class,
- org.alfresco.repo.security.authentication.AuthenticationServiceImplTest.class,
- org.alfresco.util.EmailHelperTest.class,
- org.alfresco.repo.action.ParameterDefinitionImplTest.class,
- org.alfresco.repo.action.ActionDefinitionImplTest.class,
- org.alfresco.repo.action.ActionConditionDefinitionImplTest.class,
- org.alfresco.repo.action.ActionImplTest.class,
- org.alfresco.repo.action.ActionConditionImplTest.class,
- org.alfresco.repo.action.CompositeActionImplTest.class,
- org.alfresco.repo.action.CompositeActionConditionImplTest.class,
- org.alfresco.repo.action.executer.TransformActionExecuterTest.class,
- org.alfresco.repo.action.executer.ImporterActionExecutorUnitTest.class,
- org.alfresco.repo.audit.AuditableAnnotationTest.class,
- org.alfresco.repo.audit.PropertyAuditFilterTest.class,
- org.alfresco.repo.audit.access.NodeChangeTest.class,
- org.alfresco.repo.content.ContentServiceImplUnitTest.class,
- org.alfresco.repo.content.directurl.SystemWideDirectUrlConfigUnitTest.class,
- org.alfresco.repo.content.directurl.ContentStoreDirectUrlConfigUnitTest.class,
- org.alfresco.repo.content.LimitedStreamCopierTest.class,
- org.alfresco.repo.content.filestore.FileIOTest.class,
- org.alfresco.repo.content.filestore.SpoofedTextContentReaderTest.class,
- org.alfresco.repo.content.ContentDataTest.class,
- org.alfresco.repo.content.replication.AggregatingContentStoreUnitTest.class,
- org.alfresco.service.cmr.repository.TransformationOptionLimitsTest.class,
- org.alfresco.service.cmr.repository.TransformationOptionPairTest.class,
- org.alfresco.repo.content.transform.TransformerConfigTestSuite.class,
- org.alfresco.repo.content.transform.TransformerDebugTest.class,
- org.alfresco.service.cmr.repository.TemporalSourceOptionsTest.class,
- org.alfresco.repo.content.metadata.MetadataExtracterLimitsTest.class,
- org.alfresco.repo.content.caching.quota.StandardQuotaStrategyMockTest.class,
- org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategyTest.class,
- org.alfresco.repo.content.caching.CachingContentStoreTest.class,
- org.alfresco.repo.content.caching.ContentCacheImplTest.class,
- org.alfresco.repo.domain.permissions.FixedAclUpdaterUnitTest.class,
- org.alfresco.repo.domain.propval.PropertyTypeConverterTest.class,
- org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
- org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
- org.alfresco.repo.search.DocumentNavigatorTest.class,
- org.alfresco.util.NumericEncodingTest.class,
- org.alfresco.repo.search.impl.parsers.CMIS_FTSTest.class,
- org.alfresco.repo.search.impl.parsers.CMISTest.class,
- org.alfresco.repo.search.impl.parsers.FTSTest.class,
- org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest.class,
- org.alfresco.repo.security.authentication.AuthorizationTest.class,
- org.alfresco.repo.security.permissions.PermissionCheckedCollectionTest.class,
- org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSetTest.class,
- org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterUtilsTest.class,
- org.alfresco.repo.security.authentication.ChainingAuthenticationServiceTest.class,
- org.alfresco.repo.security.authentication.NameBasedUserNameGeneratorTest.class,
- org.alfresco.repo.version.common.VersionImplTest.class,
- org.alfresco.repo.version.common.VersionHistoryImplTest.class,
- org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicyTest.class,
- org.alfresco.repo.workflow.activiti.WorklfowObjectFactoryTest.class,
- org.alfresco.repo.workflow.activiti.properties.ActivitiPriorityPropertyHandlerTest.class,
- org.alfresco.repo.workflow.WorkflowSuiteContextShutdownTest.class,
- org.alfresco.repo.search.LuceneUtilsTest.class,
+ org.alfresco.repo.security.authentication.AuthenticationServiceImplTest.class,
+ org.alfresco.util.EmailHelperTest.class,
+ org.alfresco.repo.action.ParameterDefinitionImplTest.class,
+ org.alfresco.repo.action.ActionDefinitionImplTest.class,
+ org.alfresco.repo.action.ActionConditionDefinitionImplTest.class,
+ org.alfresco.repo.action.ActionImplTest.class,
+ org.alfresco.repo.action.ActionConditionImplTest.class,
+ org.alfresco.repo.action.CompositeActionImplTest.class,
+ org.alfresco.repo.action.CompositeActionConditionImplTest.class,
+ org.alfresco.repo.action.executer.TransformActionExecuterTest.class,
+ org.alfresco.repo.action.executer.ImporterActionExecutorUnitTest.class,
+ org.alfresco.repo.audit.AuditableAnnotationTest.class,
+ org.alfresco.repo.audit.PropertyAuditFilterTest.class,
+ org.alfresco.repo.audit.access.NodeChangeTest.class,
+ org.alfresco.repo.content.ContentServiceImplUnitTest.class,
+ org.alfresco.repo.content.directurl.SystemWideDirectUrlConfigUnitTest.class,
+ org.alfresco.repo.content.directurl.ContentStoreDirectUrlConfigUnitTest.class,
+ org.alfresco.repo.content.LimitedStreamCopierTest.class,
+ org.alfresco.repo.content.filestore.FileIOTest.class,
+ org.alfresco.repo.content.filestore.SpoofedTextContentReaderTest.class,
+ org.alfresco.repo.content.ContentDataTest.class,
+ org.alfresco.repo.content.replication.AggregatingContentStoreUnitTest.class,
+ org.alfresco.service.cmr.repository.TransformationOptionLimitsTest.class,
+ org.alfresco.service.cmr.repository.TransformationOptionPairTest.class,
+ org.alfresco.repo.content.transform.TransformerConfigTestSuite.class,
+ org.alfresco.repo.content.transform.TransformerDebugTest.class,
+ org.alfresco.service.cmr.repository.TemporalSourceOptionsTest.class,
+ org.alfresco.repo.content.metadata.MetadataExtracterLimitsTest.class,
+ org.alfresco.repo.content.caching.quota.StandardQuotaStrategyMockTest.class,
+ org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategyTest.class,
+ org.alfresco.repo.content.caching.CachingContentStoreTest.class,
+ org.alfresco.repo.content.caching.ContentCacheImplTest.class,
+ org.alfresco.repo.domain.permissions.FixedAclUpdaterUnitTest.class,
+ org.alfresco.repo.domain.propval.PropertyTypeConverterTest.class,
+ org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
+ org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
+ org.alfresco.repo.search.DocumentNavigatorTest.class,
+ org.alfresco.util.NumericEncodingTest.class,
+ org.alfresco.repo.search.impl.parsers.CMIS_FTSTest.class,
+ org.alfresco.repo.search.impl.parsers.CMISTest.class,
+ org.alfresco.repo.search.impl.parsers.FTSTest.class,
+ org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest.class,
+ org.alfresco.repo.security.authentication.AuthorizationTest.class,
+ org.alfresco.repo.security.permissions.PermissionCheckedCollectionTest.class,
+ org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSetTest.class,
+ org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterUtilsTest.class,
+ org.alfresco.repo.security.authentication.ChainingAuthenticationServiceTest.class,
+ org.alfresco.repo.security.authentication.NameBasedUserNameGeneratorTest.class,
+ org.alfresco.repo.version.common.VersionImplTest.class,
+ org.alfresco.repo.version.common.VersionHistoryImplTest.class,
+ org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicyTest.class,
+ org.alfresco.repo.workflow.activiti.WorklfowObjectFactoryTest.class,
+ org.alfresco.repo.workflow.activiti.properties.ActivitiPriorityPropertyHandlerTest.class,
+ org.alfresco.repo.workflow.WorkflowSuiteContextShutdownTest.class,
+ org.alfresco.repo.search.LuceneUtilsTest.class,
- org.alfresco.heartbeat.HBDataCollectorServiceImplTest.class,
- org.alfresco.heartbeat.jobs.LockingJobTest.class,
- org.alfresco.heartbeat.jobs.QuartzJobSchedulerTest.class,
- org.alfresco.heartbeat.AuthoritiesDataCollectorTest.class,
- org.alfresco.heartbeat.ConfigurationDataCollectorTest.class,
- org.alfresco.heartbeat.InfoDataCollectorTest.class,
- org.alfresco.heartbeat.ModelUsageDataCollectorTest.class,
- org.alfresco.heartbeat.SessionsUsageDataCollectorTest.class,
- org.alfresco.heartbeat.SystemUsageDataCollectorTest.class,
+ org.alfresco.heartbeat.HBDataCollectorServiceImplTest.class,
+ org.alfresco.heartbeat.jobs.LockingJobTest.class,
+ org.alfresco.heartbeat.jobs.QuartzJobSchedulerTest.class,
+ org.alfresco.heartbeat.AuthoritiesDataCollectorTest.class,
+ org.alfresco.heartbeat.ConfigurationDataCollectorTest.class,
+ org.alfresco.heartbeat.InfoDataCollectorTest.class,
+ org.alfresco.heartbeat.ModelUsageDataCollectorTest.class,
+ org.alfresco.heartbeat.SessionsUsageDataCollectorTest.class,
+ org.alfresco.heartbeat.SystemUsageDataCollectorTest.class,
- org.alfresco.util.BeanExtenderUnitTest.class,
- org.alfresco.util.bean.HierarchicalBeanLoaderTest.class,
- org.alfresco.util.resource.HierarchicalResourceLoaderTest.class,
- org.alfresco.repo.events.ClientUtilTest.class,
- org.alfresco.repo.rendition2.RenditionService2Test.class,
- org.alfresco.repo.rendition2.TransformationOptionsConverterTest.class,
+ org.alfresco.util.BeanExtenderUnitTest.class,
+ org.alfresco.util.bean.HierarchicalBeanLoaderTest.class,
+ org.alfresco.util.resource.HierarchicalResourceLoaderTest.class,
+ org.alfresco.repo.events.ClientUtilTest.class,
+ org.alfresco.repo.rendition2.RenditionService2Test.class,
+ org.alfresco.repo.rendition2.TransformationOptionsConverterTest.class,
- org.alfresco.repo.event2.RepoEvent2UnitSuite.class,
+ org.alfresco.repo.event2.RepoEvent2UnitSuite.class,
- org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class,
- org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class,
- org.alfresco.repo.serviceaccount.ServiceAccountRegistryImplTest.class
+ org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class,
+ org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class,
+ org.alfresco.repo.serviceaccount.ServiceAccountRegistryImplTest.class
})
public class AllUnitTestsSuite
-{
-}
\ No newline at end of file
+{}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/ClientRegistrationProviderUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/ClientRegistrationProviderUnitTest.java
index 40357e5ab7..4efd45cfb0 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/ClientRegistrationProviderUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/ClientRegistrationProviderUnitTest.java
@@ -38,8 +38,7 @@ import java.util.Set;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
-
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.ClientRegistrationProvider;
+import net.minidev.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -51,12 +50,17 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.web.client.RestTemplate;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.ClientRegistrationProvider;
+
public class ClientRegistrationProviderUnitTest
{
private static final String CLIENT_ID = "alfresco";
private static final String OPENID_CONFIGURATION = "{\"token_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.serviceonline.alfresco/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.serviceonline.alfresco/alfresco/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.service.alfresco/oidc/userinfo\",\"authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.serviceonline.alfresco/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"serviceonline.alfresco\",\"cloud_graph_host_name\":\"graph.oidc.net\",\"msgraph_host\":\"graph.service.alfresco\",\"rbac_url\":\"https://pas.oidc.alfresco\"}";
private static final String DISCOVERY_PATH_SEGMENTS = "/.well-known/openid-configuration";
private static final String AUTH_SERVER = "https://login.serviceonline.alfresco";
+ private static final String ADMIN_CONSOLE_SCOPES = "openid,email,profile,offline_access";
+ private static final String PSSWD_GRANT_SCOPES = "openid,email,profile";
+ private static final String ISSUER_ATRR = "issuer";
private IdentityServiceConfig config;
private RestTemplate restTemplate;
@@ -70,6 +74,9 @@ public class ClientRegistrationProviderUnitTest
config = new IdentityServiceConfig();
config.setAuthServerUrl(AUTH_SERVER);
config.setResource(CLIENT_ID);
+ config.setAdminConsoleScopes(ADMIN_CONSOLE_SCOPES);
+ config.setPasswordGrantScopes(PSSWD_GRANT_SCOPES);
+ config.setIssuerAttribute(ISSUER_ATRR);
restTemplate = mock(RestTemplate.class);
ResponseEntity responseEntity = mock(ResponseEntity.class);
@@ -90,7 +97,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
- restTemplate);
+ restTemplate);
assertThat(clientRegistration).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
@@ -99,7 +106,7 @@ public class ClientRegistrationProviderUnitTest
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
- AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
+ AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -112,7 +119,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
- restTemplate);
+ restTemplate);
assertThat(clientRegistration).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
@@ -121,7 +128,7 @@ public class ClientRegistrationProviderUnitTest
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
- AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
+ AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -134,7 +141,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -148,7 +155,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -161,7 +168,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -174,7 +181,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -187,7 +194,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -200,7 +207,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
- () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
+ () -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -215,7 +222,7 @@ public class ClientRegistrationProviderUnitTest
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
- AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
+ AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -227,10 +234,10 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
- restTemplate);
+ restTemplate);
assertThat(
- clientRegistration.getScopes().containsAll(
- Set.of("openid", "profile", "email"))).isTrue();
+ clientRegistration.getScopes().containsAll(
+ Set.of("openid", "profile", "email"))).isTrue();
}
}
@@ -243,7 +250,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
- restTemplate);
+ restTemplate);
assertThat(clientRegistration.getScopes().size()).isEqualTo(1);
assertThat(clientRegistration.getScopes().stream().findFirst().get()).isEqualTo("openid");
}
@@ -260,7 +267,45 @@ public class ClientRegistrationProviderUnitTest
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
- "https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
+ "https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
}
}
-}
\ No newline at end of file
+
+ @Test
+ public void shouldUseDefaultIssuerAttribute()
+ {
+ config.setIssuerUrl(null);
+ try (MockedStatic providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
+ {
+ providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
+
+ ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
+ restTemplate);
+ assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0");
+
+ }
+ }
+
+ @Test
+ public void shouldUseCustomIssuerAttribute()
+ {
+ try (MockedStatic providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
+ {
+ config.setIssuerAttribute("access_token_issuer");
+ when(oidcResponse.getCustomParameters()).thenReturn(createJSONObject("access_token_issuer", "https://login.serviceonline.alfresco/alfresco/v2.0/at_trust"));
+ providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
+
+ ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
+ restTemplate);
+ assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0/at_trust");
+
+ }
+ }
+
+ private static JSONObject createJSONObject(String fieldName, String fieldValue)
+ {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.appendField(fieldName, fieldValue);
+ return jsonObject;
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
index 5ca4b96162..2833b364c5 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
@@ -1,172 +1,173 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2023 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.net.ConnectException;
-import java.util.Optional;
-
-import org.alfresco.error.ExceptionStackUtil;
-import org.alfresco.repo.security.authentication.AuthenticationContext;
-import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
-import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
-import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.security.PersonService;
-import org.alfresco.service.transaction.TransactionService;
-import org.alfresco.util.BaseSpringTest;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-
-public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
-{
- private final IdentityServiceAuthenticationComponent authComponent = new IdentityServiceAuthenticationComponent();
-
- @Autowired
- private AuthenticationContext authenticationContext;
-
- @Autowired
- private TransactionService transactionService;
-
- @Autowired
- private UserRegistrySynchronizer userRegistrySynchronizer;
-
- @Autowired
- private NodeService nodeService;
-
- @Autowired
- private PersonService personService;
-
-
- private IdentityServiceJITProvisioningHandler jitProvisioning;
- private IdentityServiceFacade mockIdentityServiceFacade;
-
- @Before
- public void setUp()
- {
- authComponent.setAuthenticationContext(authenticationContext);
- authComponent.setTransactionService(transactionService);
- authComponent.setUserRegistrySynchronizer(userRegistrySynchronizer);
- authComponent.setNodeService(nodeService);
- authComponent.setPersonService(personService);
-
- jitProvisioning = mock(IdentityServiceJITProvisioningHandler.class);
- mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
- authComponent.setJitProvisioningHandler(jitProvisioning);
- authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
- }
-
- @After
- public void tearDown()
- {
- authenticationContext.clearCurrentSecurityContext();
- }
-
- @Test (expected=AuthenticationException.class)
- public void testAuthenticationFail()
- {
- final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
-
- doThrow(new AuthorizationException("Failed")).when(mockIdentityServiceFacade).authorize(grant);
-
- authComponent.authenticateImpl("username", "password".toCharArray());
- }
-
- @Test(expected = AuthenticationException.class)
- public void testAuthenticationFail_connectionException()
- {
- final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
-
- doThrow(new AuthorizationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
- .when(mockIdentityServiceFacade).authorize(grant);
-
- try
- {
- authComponent.authenticateImpl("username", "password".toCharArray());
- }
- catch (RuntimeException ex)
- {
- Throwable cause = ExceptionStackUtil.getCause(ex, ConnectException.class);
- assertNotNull(cause);
- throw ex;
- }
- }
-
- @Test (expected=AuthenticationException.class)
- public void testAuthenticationFail_otherException()
- {
- final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
-
- doThrow(new RuntimeException("Some other errors!"))
- .when(mockIdentityServiceFacade)
- .authorize(grant);
-
- authComponent.authenticateImpl("username", "password".toCharArray());
- }
-
- @Test
- public void testAuthenticationPass()
- {
- final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
- AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
- IdentityServiceFacade.AccessToken accessToken = mock(IdentityServiceFacade.AccessToken.class);
-
- when(authorization.getAccessToken()).thenReturn(accessToken);
- when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
- when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
- when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN"))
- .thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
-
- authComponent.authenticateImpl("username", "password".toCharArray());
-
- // Check that the authenticated user has been set
- assertEquals("User has not been set as expected.","username", authenticationContext.getCurrentUserName());
- }
-
- @Test (expected= AuthenticationException.class)
- public void testFallthroughWhenIdentityServiceFacadeIsNull()
- {
- authComponent.setIdentityServiceFacade(null);
- authComponent.authenticateImpl("username", "password".toCharArray());
- }
-
- @Test
- public void testSettingAllowGuestUser()
- {
- authComponent.setAllowGuestLogin(true);
- assertTrue(authComponent.guestUserAuthenticationAllowed());
-
- authComponent.setAllowGuestLogin(false);
- assertFalse(authComponent.guestUserAuthenticationAllowed());
- }
-}
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.ConnectException;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.alfresco.error.ExceptionStackUtil;
+import org.alfresco.repo.security.authentication.AuthenticationContext;
+import org.alfresco.repo.security.authentication.AuthenticationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.BaseSpringTest;
+
+public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
+{
+ private final IdentityServiceAuthenticationComponent authComponent = new IdentityServiceAuthenticationComponent();
+
+ @Autowired
+ private AuthenticationContext authenticationContext;
+
+ @Autowired
+ private TransactionService transactionService;
+
+ @Autowired
+ private UserRegistrySynchronizer userRegistrySynchronizer;
+
+ @Autowired
+ private NodeService nodeService;
+
+ @Autowired
+ private PersonService personService;
+
+ private IdentityServiceJITProvisioningHandler jitProvisioning;
+ private IdentityServiceFacade mockIdentityServiceFacade;
+
+ @Before
+ public void setUp()
+ {
+ authComponent.setAuthenticationContext(authenticationContext);
+ authComponent.setTransactionService(transactionService);
+ authComponent.setUserRegistrySynchronizer(userRegistrySynchronizer);
+ authComponent.setNodeService(nodeService);
+ authComponent.setPersonService(personService);
+
+ jitProvisioning = mock(IdentityServiceJITProvisioningHandler.class);
+ mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
+ authComponent.setJitProvisioningHandler(jitProvisioning);
+ authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
+ }
+
+ @After
+ public void tearDown()
+ {
+ authenticationContext.clearCurrentSecurityContext();
+ }
+
+ @Test(expected = AuthenticationException.class)
+ public void testAuthenticationFail()
+ {
+ final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
+
+ doThrow(new AuthorizationException("Failed")).when(mockIdentityServiceFacade).authorize(grant);
+
+ authComponent.authenticateImpl("username", "password".toCharArray());
+ }
+
+ @Test(expected = AuthenticationException.class)
+ public void testAuthenticationFail_connectionException()
+ {
+ final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
+
+ doThrow(new AuthorizationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
+ .when(mockIdentityServiceFacade).authorize(grant);
+
+ try
+ {
+ authComponent.authenticateImpl("username", "password".toCharArray());
+ }
+ catch (RuntimeException ex)
+ {
+ Throwable cause = ExceptionStackUtil.getCause(ex, ConnectException.class);
+ assertNotNull(cause);
+ throw ex;
+ }
+ }
+
+ @Test(expected = AuthenticationException.class)
+ public void testAuthenticationFail_otherException()
+ {
+ final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
+
+ doThrow(new RuntimeException("Some other errors!"))
+ .when(mockIdentityServiceFacade)
+ .authorize(grant);
+
+ authComponent.authenticateImpl("username", "password".toCharArray());
+ }
+
+ @Test
+ public void testAuthenticationPass()
+ {
+ final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
+ AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
+ IdentityServiceFacade.AccessToken accessToken = mock(IdentityServiceFacade.AccessToken.class);
+
+ when(authorization.getAccessToken()).thenReturn(accessToken);
+ when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
+ when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
+ when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN"))
+ .thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
+
+ authComponent.authenticateImpl("username", "password".toCharArray());
+
+ // Check that the authenticated user has been set
+ assertEquals("User has not been set as expected.", "username", authenticationContext.getCurrentUserName());
+ }
+
+ @Test(expected = AuthenticationException.class)
+ public void testFallthroughWhenIdentityServiceFacadeIsNull()
+ {
+ authComponent.setIdentityServiceFacade(null);
+ authComponent.authenticateImpl("username", "password".toCharArray());
+ }
+
+ @Test
+ public void testSettingAllowGuestUser()
+ {
+ authComponent.setAllowGuestLogin(true);
+ assertTrue(authComponent.guestUserAuthenticationAllowed());
+
+ authComponent.setAllowGuestLogin(false);
+ assertFalse(authComponent.guestUserAuthenticationAllowed());
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java
index 2c2a5ce00c..52e4714d27 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java
@@ -25,29 +25,29 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.BaseSpringTest;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
@SuppressWarnings("PMD.AvoidAccessibilityAlteration")
public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
@@ -61,12 +61,12 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private final boolean isAuth0Enabled = Optional.ofNullable(System.getProperty("auth0.enabled"))
- .map(Boolean::valueOf)
- .orElse(false);
+ .map(Boolean::valueOf)
+ .orElse(false);
private final String userPassword = Optional.ofNullable(System.getProperty("admin.password"))
- .filter(password -> isAuth0Enabled)
- .orElse("password");
+ .filter(password -> isAuth0Enabled)
+ .orElse("password");
@Before
public void setup()
@@ -75,16 +75,16 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
nodeService = (NodeService) applicationContext.getBean("nodeService");
transactionService = (TransactionService) applicationContext.getBean("transactionService");
DefaultChildApplicationContextManager childApplicationContextManager = (DefaultChildApplicationContextManager) applicationContext
- .getBean("Authentication");
+ .getBean("Authentication");
ChildApplicationContextFactory childApplicationContextFactory = childApplicationContextManager.getChildApplicationContextFactory(
- "identity-service1");
+ "identity-service1");
identityServiceFacade = (IdentityServiceFacade) childApplicationContextFactory.getApplicationContext()
- .getBean("identityServiceFacade");
+ .getBean("identityServiceFacade");
jitProvisioningHandler = (IdentityServiceJITProvisioningHandler) childApplicationContextFactory.getApplicationContext()
- .getBean("jitProvisioningHandler");
+ .getBean("jitProvisioningHandler");
IdentityServiceConfig identityServiceConfig = (IdentityServiceConfig) childApplicationContextFactory.getApplicationContext()
- .getBean("identityServiceConfig");
+ .getBean("identityServiceConfig");
identityServiceConfig.setAllowAnyHostname(true);
identityServiceConfig.setClientKeystore(null);
identityServiceConfig.setDisableTrustManager(true);
@@ -95,12 +95,11 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
{
assertFalse(personService.personExists(IDS_USERNAME));
- IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
- identityServiceFacade.authorize(
+ IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
Optional userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- accessTokenAuthorization.getAccessToken().getTokenValue());
+ accessTokenAuthorization.getAccessToken().getTokenValue());
NodeRef person = personService.getPerson(IDS_USERNAME);
@@ -125,23 +124,26 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertFalse(personService.personExists(IDS_USERNAME));
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
- IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
- identityServiceFacade.authorize(
+ IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
+ UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(principalAttribute, "given_name", "family_name", "email");
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
+ ClientRegistration clientRegistration = mock(ClientRegistration.class, RETURNS_DEEP_STUBS);
+ when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(principalAttribute);
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
- when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
+ when(idsServiceFacadeMock.getUserInfo(accessToken, userInfoAttrMapping)).thenReturn(identityServiceFacade.getUserInfo(accessToken, userInfoAttrMapping));
+ when(idsServiceFacadeMock.getClientRegistration()).thenReturn(clientRegistration);
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
- .getDeclaredField("identityServiceFacade");
+ .getDeclaredField("identityServiceFacade");
declaredField.setAccessible(true);
declaredField.set(jitProvisioningHandler, idsServiceFacadeMock);
Optional userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- accessToken);
+ accessToken);
declaredField.set(jitProvisioningHandler, identityServiceFacade);
@@ -153,7 +155,7 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
- verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, principalAttribute);
+ verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, userInfoAttrMapping);
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());
@@ -166,18 +168,17 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
@After
public void tearDown()
{
- AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork()
- {
+ AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() {
@Override
public Void doWork() throws Exception
{
transactionService.getRetryingTransactionHelper()
- .doInTransaction((RetryingTransactionCallback) () -> {
- personService.deletePerson(IDS_USERNAME);
- return null;
- });
+ .doInTransaction((RetryingTransactionCallback) () -> {
+ personService.deletePerson(IDS_USERNAME);
+ return null;
+ });
return null;
}
});
}
-}
\ No newline at end of file
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java
index ed0071fe97..2b5223f68f 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java
@@ -38,12 +38,17 @@ import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
-
-import org.alfresco.service.cmr.security.PersonService;
-import org.alfresco.service.transaction.TransactionService;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Answers;
import org.mockito.Mock;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+
+import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
+import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.transaction.TransactionService;
public class IdentityServiceJITProvisioningHandlerUnitTest
{
@@ -51,6 +56,9 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
@Mock
private IdentityServiceFacade identityServiceFacade;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private ClientRegistration clientRegistration;
+
@Mock
private PersonService personService;
@@ -64,11 +72,22 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
private IdentityServiceConfig identityServiceConfig;
@Mock
- private OIDCUserInfo userInfo;
+ private DecodedTokenUser decodedTokenUser;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
+ private UserInfoAttrMapping expectedMapping;
+
private static final String JWT_TOKEN = "myToken";
+ private static final String USERNAME = "johny123";
+ private static final String FIRST_NAME = "John";
+ private static final String LAST_NAME = "Doe";
+ private static final String EMAIL = "johny123@email.com";
+
+ public static final String USERNAME_CLAIM = "nickname";
+ public static final String EMAIL_CLAIM = "email";
+ public static final String FIRST_NAME_CLAIM = "given_name";
+ public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
@@ -78,149 +97,147 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(transactionService.isReadOnly()).thenReturn(false);
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
- jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
- personService, transactionService, identityServiceConfig);
+ when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
+ when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(USERNAME_CLAIM);
+ when(identityServiceConfig.getEmailAttribute()).thenReturn(EMAIL_CLAIM);
+ when(identityServiceConfig.getFirstNameAttribute()).thenReturn(FIRST_NAME_CLAIM);
+ when(identityServiceConfig.getLastNameAttribute()).thenReturn(LAST_NAME_CLAIM);
+ expectedMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
+ jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
}
@Test
public void shouldExtractUserInfoForExistingUser()
{
- when(personService.personExists("johny123")).thenReturn(true);
- when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
+ when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ when(personService.personExists(USERNAME)).thenReturn(true);
+ when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
+ jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
+ assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
- verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
{
- when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
- when(personService.personExists("johny123")).thenReturn(true);
- when(decodedAccessToken.getClaim("nickname")).thenReturn("johny123");
+ when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
+ when(personService.personExists(USERNAME)).thenReturn(true);
+ when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(USERNAME);
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
+ assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
- verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, "nickname");
+ verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromAccessTokenAndCreateUser()
{
- when(personService.personExists("johny123")).thenReturn(false);
-
- when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
- when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn("John");
- when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn("Doe");
- when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn("johny123@email.com");
+ when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ when(personService.personExists(USERNAME)).thenReturn(false);
+ when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
+ when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn(FIRST_NAME);
+ when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn(LAST_NAME);
+ when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn(EMAIL);
+ jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
- assertEquals("John", result.get().firstName());
- assertEquals("Doe", result.get().lastName());
- assertEquals("johny123@email.com", result.get().email());
+ assertEquals(USERNAME, result.get().username());
+ assertEquals(FIRST_NAME, result.get().firstName());
+ assertEquals(LAST_NAME, result.get().lastName());
+ assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
- verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromUserInfoEndpointAndCreateUser()
{
- when(userInfo.username()).thenReturn("johny123");
- when(userInfo.firstName()).thenReturn("John");
- when(userInfo.lastName()).thenReturn("Doe");
- when(userInfo.email()).thenReturn("johny123@email.com");
-
- when(personService.personExists("johny123")).thenReturn(false);
-
- when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
- when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
+ when(decodedTokenUser.username()).thenReturn(USERNAME);
+ when(decodedTokenUser.firstName()).thenReturn(FIRST_NAME);
+ when(decodedTokenUser.lastName()).thenReturn(LAST_NAME);
+ when(decodedTokenUser.email()).thenReturn(EMAIL);
+ when(personService.personExists(USERNAME)).thenReturn(false);
+ when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
+ when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
- assertEquals("John", result.get().firstName());
- assertEquals("Doe", result.get().lastName());
- assertEquals("johny123@email.com", result.get().email());
+ assertEquals(USERNAME, result.get().username());
+ assertEquals(FIRST_NAME, result.get().firstName());
+ assertEquals(LAST_NAME, result.get().lastName());
+ assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
- verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
-
- when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
+ when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
- verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsername()
{
- when(personService.personExists("johny123")).thenReturn(true);
-
+ when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
-
- when(userInfo.username()).thenReturn("johny123");
- when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
-
+ when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
+ assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
- verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsernameWithProvidedPrincipalAttribute()
{
- when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
- when(personService.personExists("johny123")).thenReturn(true);
-
- when(decodedAccessToken.getClaim("nickname")).thenReturn("");
-
- when(userInfo.username()).thenReturn("johny123");
- when(identityServiceFacade.getUserInfo(JWT_TOKEN, "nickname")).thenReturn(Optional.of(userInfo));
+ when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
+ when(personService.personExists(USERNAME)).thenReturn(true);
+ when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("");
+ when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
- JWT_TOKEN);
+ JWT_TOKEN);
assertTrue(result.isPresent());
- assertEquals("johny123", result.get().username());
+ assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
- verify(identityServiceFacade).getUserInfo(JWT_TOKEN, "nickname");
+ verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
@@ -232,8 +249,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
- verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
- verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
+ verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
+ verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
}
-}
\ No newline at end of file
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
index f137bffa02..72e29c2d27 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
@@ -34,17 +34,18 @@ import java.time.Instant;
import java.util.Map;
import java.util.Vector;
import java.util.function.Supplier;
+import jakarta.servlet.http.HttpServletRequest;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
-
-import jakarta.servlet.http.HttpServletRequest;
import junit.framework.TestCase;
+import org.mockito.Mockito;
+import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
+
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
-import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
/**
* Tests the Identity Service based authentication subsystem.
@@ -68,7 +69,9 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
public void testWrongTokenWithSilentValidation()
{
- final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected ");}));
+ final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {
+ throw new TokenDecodingException("Expected ");
+ }));
mapper.setValidationFailureSilent(true);
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
@@ -79,7 +82,9 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
public void testWrongTokenWithoutSilentValidation()
{
- final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected");}));
+ final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {
+ throw new TokenDecodingException("Expected");
+ }));
mapper.setValidationFailureSilent(false);
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
@@ -92,12 +97,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
private IdentityServiceRemoteUserMapper givenMapper(Map> tokenToUser)
{
final TransactionService transactionService = mock(TransactionService.class);
- final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
+ final IdentityServiceFacade facade = mock(IdentityServiceFacade.class, Mockito.RETURNS_DEEP_STUBS);
final PersonService personService = mock(PersonService.class);
final IdentityServiceConfig identityServiceConfig = mock(IdentityServiceConfig.class);
when(transactionService.isReadOnly()).thenReturn(true);
when(facade.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
+ when(facade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
+ .thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
@@ -108,21 +115,21 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
mapper.setActive(true);
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
-
return mapper;
}
/**
* Utility method for creating a mocked Servlet request with a token.
*
- * @param token The token to add to the Authorization header
+ * @param token
+ * The token to add to the Authorization header
* @return The mocked request object
*/
private HttpServletRequest createMockTokenRequest(String token)
{
// Mock a request with the token in the Authorization header (if supplied)
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
-
+
Vector authHeaderValues = new Vector<>(1);
if (token != null)
{
@@ -133,7 +140,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
.thenReturn(authHeaderValues.elements());
when(mockRequest.getHeader(AUTHORIZATION_HEADER))
.thenReturn(authHeaderValues.isEmpty() ? null : authHeaderValues.get(0));
-
+
return mockRequest;
}
@@ -166,4 +173,4 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
return PersonClaims.PREFERRED_USERNAME_CLAIM_NAME.equals(claim) ? usernameSupplier.get() : null;
}
}
-}
\ No newline at end of file
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java
index 271d9b80b4..34670b0a88 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java
@@ -1,98 +1,99 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2025 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.security.authentication.identityservice;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
-import org.junit.Test;
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.core.AuthorizationGrantType;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.web.client.RestOperations;
-
-public class SpringBasedIdentityServiceFacadeUnitTest
-{
- private static final String USER_NAME = "user";
- private static final String PASSWORD = "password";
- private static final String TOKEN = "tEsT-tOkEn";
-
- @Test
- public void shouldThrowVerificationExceptionOnFailure()
- {
- final RestOperations restOperations = mock(RestOperations.class);
- final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
- when(restOperations.exchange(any(), any(Class.class))).thenThrow(new RuntimeException("Expected"));
-
- final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
-
- assertThatExceptionOfType(AuthorizationException.class)
- .isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
- .havingCause().withNoCause().withMessage("Expected");
- }
-
- @Test
- public void shouldThrowTokenExceptionOnFailure()
- {
- final RestOperations restOperations = mock(RestOperations.class);
- final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
- when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
-
- final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
-
- assertThatExceptionOfType(TokenDecodingException.class)
- .isThrownBy(() -> facade.decodeToken(TOKEN))
- .havingCause().withNoCause().withMessage("Expected");
- }
-
-
- @Test
- public void shouldReturnEmptyOptionalOnFailure()
- {
- final RestOperations restOperations = mock(RestOperations.class);
- final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
- final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
-
-
- assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
- }
-
- private ClientRegistration testRegistration()
- {
- return ClientRegistration.withRegistrationId("test")
- .tokenUri("http://localhost")
- .clientId("test")
- .userInfoUri("http://localhost/userinfo")
- .authorizationGrantType(AuthorizationGrantType.PASSWORD)
- .build();
- }
-}
\ No newline at end of file
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.web.client.RestOperations;
+
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
+import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
+
+public class SpringBasedIdentityServiceFacadeUnitTest
+{
+ private static final String USER_NAME = "user";
+ private static final String PASSWORD = "password";
+ private static final String TOKEN = "tEsT-tOkEn";
+ private static final UserInfoAttrMapping USER_INFO_ATTR_MAPPING = new UserInfoAttrMapping("preferred_username", "given_name", "family_name", "email");
+
+ @Test
+ public void shouldThrowVerificationExceptionOnFailure()
+ {
+ final RestOperations restOperations = mock(RestOperations.class);
+ final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+ when(restOperations.exchange(any(), any(Class.class))).thenThrow(new RuntimeException("Expected"));
+
+ final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
+
+ assertThatExceptionOfType(AuthorizationException.class)
+ .isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
+ .havingCause().withNoCause().withMessage("Expected");
+ }
+
+ @Test
+ public void shouldThrowTokenExceptionOnFailure()
+ {
+ final RestOperations restOperations = mock(RestOperations.class);
+ final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+ when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
+
+ final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
+
+ assertThatExceptionOfType(TokenDecodingException.class)
+ .isThrownBy(() -> facade.decodeToken(TOKEN))
+ .havingCause().withNoCause().withMessage("Expected");
+ }
+
+ @Test
+ public void shouldReturnEmptyOptionalOnFailure()
+ {
+ final RestOperations restOperations = mock(RestOperations.class);
+ final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+ final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
+
+ assertThat(facade.getUserInfo(TOKEN, USER_INFO_ATTR_MAPPING).isEmpty()).isTrue();
+ }
+
+ private ClientRegistration testRegistration()
+ {
+ return ClientRegistration.withRegistrationId("test")
+ .tokenUri("http://localhost")
+ .clientId("test")
+ .userInfoUri("http://localhost/userinfo")
+ .authorizationGrantType(AuthorizationGrantType.PASSWORD)
+ .build();
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
index 78eda0036b..6d1ca62de4 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
@@ -38,18 +38,11 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
-
-import com.nimbusds.oauth2.sdk.Scope;
-
+import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+
+import com.nimbusds.oauth2.sdk.Scope;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -58,6 +51,14 @@ import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+
@SuppressWarnings("PMD.AvoidStringBufferField")
public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
@@ -118,7 +119,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
- String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
+ String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
@@ -134,7 +135,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
- String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
+ String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(accessToken.getTokenValue()).thenReturn("REFRESHED_JWT_TOKEN");
when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
@@ -155,10 +156,11 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
+ when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
- .formatted("http://localhost:8080", redirectPath);
+ .formatted("http://localhost:8080", redirectPath);
authenticator.requestAuthentication(request, response);
@@ -178,9 +180,10 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAudience()).thenReturn(audience);
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
+ when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
- .formatted("http://localhost:8080", redirectPath);
+ .formatted("http://localhost:8080", redirectPath);
authenticator.requestAuthentication(request, response);
@@ -200,7 +203,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
- String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
+ String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class);
@@ -221,8 +224,8 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
when(identityServiceFacade.authorize(
- AuthorizationGrant.authorizationCode("auth_code", adminConsoleURL.toString())))
- .thenReturn(accessTokenAuthorization);
+ AuthorizationGrant.authorizationCode("auth_code", adminConsoleURL.toString())))
+ .thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
@@ -242,4 +245,4 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
assertEquals("admin", username);
}
-}
\ No newline at end of file
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapperUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapperUnitTest.java
new file mode 100644
index 0000000000..f41a313e51
--- /dev/null
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/AccessTokenToDecodedTokenUserMapperUnitTest.java
@@ -0,0 +1,109 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.util.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
+
+public class AccessTokenToDecodedTokenUserMapperUnitTest
+{
+
+ @Mock
+ private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
+
+ private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
+
+ public static final String USERNAME_CLAIM = "nickname";
+ public static final String EMAIL_CLAIM = "email";
+ public static final String FIRST_NAME_CLAIM = "given_name";
+ public static final String LAST_NAME_CLAIM = "family_name";
+
+ @Before
+ public void setup()
+ {
+ initMocks(this);
+ UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
+ tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
+ }
+
+ @Test
+ public void shouldMapToDecodedTokenUserWithAllFieldsPopulated()
+ {
+ when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
+ when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
+ when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
+ when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
+
+ Optional result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
+
+ assertTrue(result.isPresent());
+ assertEquals("johny123", result.get().username());
+ assertEquals("John", result.get().firstName());
+ assertEquals("Doe", result.get().lastName());
+ assertEquals("johny123@email.com", result.get().email());
+ }
+
+ @Test
+ public void shouldMapToDecodedTokenUserWithSomeFieldsEmpty()
+ {
+ when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
+ when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("");
+ when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
+ when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("");
+
+ Optional result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
+
+ assertTrue(result.isPresent());
+ assertEquals("johny123", result.get().username());
+ assertEquals("", result.get().firstName());
+ assertEquals("Doe", result.get().lastName());
+ assertEquals("", result.get().email());
+ }
+
+ @Test
+ public void shouldReturnEmptyOptionalForNullUsername()
+ {
+ when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(null);
+ when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
+ when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
+ when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
+
+ Optional result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
+
+ assertFalse(result.isPresent());
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapperUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapperUnitTest.java
new file mode 100644
index 0000000000..6602f7dab5
--- /dev/null
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/user/TokenUserToOIDCUserMapperUnitTest.java
@@ -0,0 +1,95 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.user;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import org.alfresco.service.cmr.security.PersonService;
+
+public class TokenUserToOIDCUserMapperUnitTest
+{
+
+ @Mock
+ private PersonService personService;
+
+ @InjectMocks
+ private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
+
+ @Before
+ public void setup()
+ {
+ initMocks(this);
+ }
+
+ @Test
+ public void shouldMapToOIDCUserWithAllFieldsPopulated()
+ {
+ DecodedTokenUser decodedTokenUser = new DecodedTokenUser("JOHNY123", "John", "Doe", "johny123@email.com");
+ when(personService.getUserIdentifier("JOHNY123")).thenReturn("johny123");
+
+ OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
+
+ assertEquals("johny123", oidcUserInfo.username());
+ assertEquals("John", oidcUserInfo.firstName());
+ assertEquals("Doe", oidcUserInfo.lastName());
+ assertEquals("johny123@email.com", oidcUserInfo.email());
+ }
+
+ @Test
+ public void shouldMapToOIDCUserWithSomeFieldsEmpty()
+ {
+ DecodedTokenUser decodedTokenUser = new DecodedTokenUser("johny123", "", "Doe", "");
+ when(personService.getUserIdentifier("johny123")).thenReturn("johny123");
+
+ OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
+
+ assertEquals("johny123", oidcUserInfo.username());
+ assertEquals("", oidcUserInfo.firstName());
+ assertEquals("Doe", oidcUserInfo.lastName());
+ assertEquals("", oidcUserInfo.email());
+ }
+
+ @Test
+ public void shouldReturnNullForNullUsername()
+ {
+ DecodedTokenUser decodedTokenUser = new DecodedTokenUser(null, "John", "Doe", "johny123@email.com");
+
+ OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
+
+ assertNull(oidcUserInfo.username());
+ assertEquals("John", oidcUserInfo.firstName());
+ assertEquals("Doe", oidcUserInfo.lastName());
+ assertEquals("johny123@email.com", oidcUserInfo.email());
+ }
+}
diff --git a/repository/src/test/resources/alfresco-global.properties b/repository/src/test/resources/alfresco-global.properties
index 20f5389408..53848254c4 100644
--- a/repository/src/test/resources/alfresco-global.properties
+++ b/repository/src/test/resources/alfresco-global.properties
@@ -28,6 +28,9 @@ identity-service.register-node-at-startup=true
identity-service.register-node-period=50
identity-service.token-store=SESSION
identity-service.principal-attribute=preferred_username
+identity-service.first-name-attribute=given_name
+identity-service.last-name-attribute=family_name
+identity-service.email-attribute=email
identity-service.turn-off-change-session-id-on-login=true
identity-service.token-minimum-time-to-live=10
identity-service.min-time-between-jwks-requests=60