mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS-6601 Implement Repository OIDC Compliance (#2447)
* ACS-6677 Enhance OIDC Configuration Flexibility (#2426) * ACS-6603 Implement OIDC Compliance (#2442) * ACS-6677 Enhance OIDC Configuration Flexibility * ACS-6677 Revert changing http header * ACS-6677 Add unit test to suite * ACS-6677 Rename var * ACS-6677 Fix PMD issues * ACS-6677 Fix PMD issues * ACS-6677 Improve code * ACS-6677 Fix compatibility * ACS-6677 Add JwtAudienceValidator * ACS-6677 Change domain * ACS-6603 Oidc compliance * ACS-6603 Add Auth0 test * ACS-6603 Reformat * ACS-6603 Enable User Info Endpoint test + Refactor * ACS-6603 Change test condition * ACS-6603 Add state parameter + reformat stream * ACS-6603 Use enum type
This commit is contained in:
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -26,6 +26,9 @@ env:
|
|||||||
CI_WORKSPACE: ${{ github.workspace }}
|
CI_WORKSPACE: ${{ github.workspace }}
|
||||||
TAS_ENVIRONMENT: ./packaging/tests/environment
|
TAS_ENVIRONMENT: ./packaging/tests/environment
|
||||||
TAS_SCRIPTS: ../alfresco-community-repo/packaging/tests/scripts
|
TAS_SCRIPTS: ../alfresco-community-repo/packaging/tests/scripts
|
||||||
|
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_OIDC_ADMIN_CLIENT_ID }}
|
||||||
|
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_OIDC_CLIENT_SECRET }}
|
||||||
|
AUTH0_ADMIN_PASSWORD: ${{ secrets.AUTH0_OIDC_ADMIN_PASSWORD }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
@@ -387,7 +390,7 @@ jobs:
|
|||||||
run: bash ./scripts/ci/cleanup_cache.sh
|
run: bash ./scripts/ci/cleanup_cache.sh
|
||||||
|
|
||||||
repository_app_context_test_suites:
|
repository_app_context_test_suites:
|
||||||
name: Repository - ${{ matrix.testSuite }}
|
name: Repository - ${{ matrix.testSuite }} ${{ matrix.idp }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare]
|
needs: [prepare]
|
||||||
if: >
|
if: >
|
||||||
@@ -409,6 +412,11 @@ jobs:
|
|||||||
- testSuite: AppContext05TestSuite
|
- testSuite: AppContext05TestSuite
|
||||||
compose-profile: with-sso
|
compose-profile: with-sso
|
||||||
mvn-options: '-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm'
|
mvn-options: '-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm'
|
||||||
|
idp: Keycloak
|
||||||
|
- testSuite: AppContext05TestSuite
|
||||||
|
compose-profile: default
|
||||||
|
mvn-options: '-Didentity-service.auth-server-url=https://dev-ps-alfresco.auth0.com/ -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm -Didentity-service.audience=http://localhost:3000 -Didentity-service.resource=${AUTH0_CLIENT_ID} -Didentity-service.credentials.secret=${AUTH0_CLIENT_SECRET} -Didentity-service.public-client=false -Didentity-service.realm= -Didentity-service.client-id.validation.disabled=false -Dadmin.user=admin@alfresco.com -Dadmin.password=${AUTH0_ADMIN_PASSWORD} -Dauth0.enabled=true -Dauth0.admin.password=${AUTH0_ADMIN_PASSWORD} -Didentity-service.principal-attribute=nickname'
|
||||||
|
idp: Auth0
|
||||||
- testSuite: AppContext06TestSuite
|
- testSuite: AppContext06TestSuite
|
||||||
compose-profile: with-transform-core-aio
|
compose-profile: with-transform-core-aio
|
||||||
- testSuite: AppContextExtraTestSuite
|
- testSuite: AppContextExtraTestSuite
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -27,6 +27,7 @@ package org.alfresco.repo.security.authentication.identityservice;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,8 @@ public class IdentityServiceConfig
|
|||||||
|
|
||||||
private int clientConnectionTimeout;
|
private int clientConnectionTimeout;
|
||||||
private int clientSocketTimeout;
|
private int clientSocketTimeout;
|
||||||
|
private String issuerUrl;
|
||||||
|
private String audience;
|
||||||
// client id
|
// client id
|
||||||
private String resource;
|
private String resource;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
@@ -56,6 +59,9 @@ public class IdentityServiceConfig
|
|||||||
private String realmKey;
|
private String realmKey;
|
||||||
private int publicKeyCacheTtl;
|
private int publicKeyCacheTtl;
|
||||||
private boolean publicClient;
|
private boolean publicClient;
|
||||||
|
private String principalAttribute;
|
||||||
|
private boolean clientIdValidationDisabled;
|
||||||
|
private String adminConsoleRedirectPath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -103,9 +109,36 @@ public class IdentityServiceConfig
|
|||||||
return connectionPoolSize;
|
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()
|
public String getAuthServerUrl()
|
||||||
{
|
{
|
||||||
return authServerUrl;
|
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)
|
public void setAuthServerUrl(String authServerUrl)
|
||||||
@@ -144,14 +177,6 @@ public class IdentityServiceConfig
|
|||||||
.orElse("");
|
.orElse("");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIssuerUrl()
|
|
||||||
{
|
|
||||||
return UriComponentsBuilder.fromUriString(getAuthServerUrl())
|
|
||||||
.pathSegment(REALMS, getRealm())
|
|
||||||
.build()
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowAnyHostname(boolean allowAnyHostname)
|
public void setAllowAnyHostname(boolean allowAnyHostname)
|
||||||
{
|
{
|
||||||
this.allowAnyHostname = allowAnyHostname;
|
this.allowAnyHostname = allowAnyHostname;
|
||||||
@@ -251,4 +276,34 @@ public class IdentityServiceConfig
|
|||||||
{
|
{
|
||||||
return publicClient;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2024 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 <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
public class IdentityServiceException extends RuntimeException
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = -7541541232589648112L;
|
||||||
|
|
||||||
|
public IdentityServiceException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityServiceException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -33,7 +33,6 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to interact with the Identity Service
|
* Allows to interact with the Identity Service
|
||||||
@@ -60,9 +59,10 @@ public interface IdentityServiceFacade
|
|||||||
* Gets claims about the authenticated user,
|
* Gets claims about the authenticated user,
|
||||||
* such as name and email address, via the UserInfo endpoint of the OpenID provider.
|
* such as name and email address, via the UserInfo endpoint of the OpenID provider.
|
||||||
* @param token {@link String} with encoded access token value.
|
* @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.
|
* @return {@link OIDCUserInfo} containing user claims.
|
||||||
*/
|
*/
|
||||||
Optional<OIDCUserInfo> getUserInfo(String token);
|
Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a client registration
|
* Gets a client registration
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -29,25 +29,34 @@ import static java.util.Objects.requireNonNull;
|
|||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
import static java.util.function.Predicate.not;
|
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.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
|
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
|
||||||
@@ -57,10 +66,13 @@ import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
|||||||
import com.nimbusds.jose.proc.SecurityContext;
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
import com.nimbusds.jose.util.ResourceRetriever;
|
import com.nimbusds.jose.util.ResourceRetriever;
|
||||||
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
|
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.oauth2.sdk.id.Issuer;
|
||||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
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.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.apache.hc.client5.http.classic.HttpClient;
|
import org.apache.hc.client5.http.classic.HttpClient;
|
||||||
@@ -75,9 +87,11 @@ import org.apache.hc.client5.http.ssl.TrustAllStrategy;
|
|||||||
import org.apache.hc.core5.ssl.SSLContextBuilder;
|
import org.apache.hc.core5.ssl.SSLContextBuilder;
|
||||||
import org.apache.hc.core5.ssl.SSLContexts;
|
import org.apache.hc.core5.ssl.SSLContexts;
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.RequestEntity;
|
import org.springframework.http.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.client.ClientHttpRequest;
|
||||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||||
@@ -109,10 +123,8 @@ import org.springframework.web.client.RestTemplate;
|
|||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Creates an instance of {@link IdentityServiceFacade}. <br>
|
* Creates an instance of {@link IdentityServiceFacade}. <br>
|
||||||
* This factory can return a null if it is disabled.
|
* This factory can return a null if it is disabled.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade>
|
public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade>
|
||||||
{
|
{
|
||||||
@@ -189,9 +201,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<OIDCUserInfo> getUserInfo(String token)
|
public Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute)
|
||||||
{
|
{
|
||||||
return getTargetFacade().getUserInfo(token);
|
return getTargetFacade().getUserInfo(token, principalAttribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -247,17 +259,21 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
// * Client is authenticating itself using basic auth
|
// * Client is authenticating itself using basic auth
|
||||||
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
|
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
|
||||||
|
|
||||||
final ClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClientProvider.get());
|
final ClientHttpRequestFactory httpRequestFactory = new CustomClientHttpRequestFactory(
|
||||||
|
httpClientProvider.get());
|
||||||
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
|
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
|
||||||
final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
|
final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
|
||||||
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate, clientRegistration.getProviderDetails());
|
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate,
|
||||||
|
clientRegistration.getProviderDetails());
|
||||||
|
|
||||||
return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory), clientRegistration, jwtDecoder);
|
return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory),
|
||||||
|
clientRegistration, jwtDecoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
|
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
|
||||||
{
|
{
|
||||||
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
|
final RestTemplate restTemplate = new RestTemplate(
|
||||||
|
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
|
||||||
restTemplate.setRequestFactory(requestFactory);
|
restTemplate.setRequestFactory(requestFactory);
|
||||||
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
||||||
|
|
||||||
@@ -309,7 +325,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
|
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder) throws Exception
|
private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
|
||||||
|
throws Exception
|
||||||
{
|
{
|
||||||
SSLContextBuilder sslContextBuilder = null;
|
SSLContextBuilder sslContextBuilder = null;
|
||||||
if (config.isDisableTrustManager())
|
if (config.isDisableTrustManager())
|
||||||
@@ -351,7 +368,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
|
connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private char[] asCharArray(String value, char[] nullValue)
|
private char[] asCharArray(String value, char... nullValue)
|
||||||
{
|
{
|
||||||
return ofNullable(value)
|
return ofNullable(value)
|
||||||
.filter(not(String::isBlank))
|
.filter(not(String::isBlank))
|
||||||
@@ -360,11 +377,13 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ClientRegistrationProvider
|
static class ClientRegistrationProvider
|
||||||
{
|
{
|
||||||
private final IdentityServiceConfig config;
|
private final IdentityServiceConfig config;
|
||||||
|
|
||||||
private ClientRegistrationProvider(IdentityServiceConfig config)
|
private static final Set<String> SCOPES = Set.of("openid", "profile", "email");
|
||||||
|
|
||||||
|
ClientRegistrationProvider(IdentityServiceConfig config)
|
||||||
{
|
{
|
||||||
this.config = requireNonNull(config);
|
this.config = requireNonNull(config);
|
||||||
}
|
}
|
||||||
@@ -377,12 +396,55 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
.filter(Optional::isPresent)
|
.filter(Optional::isPresent)
|
||||||
.map(Optional::get)
|
.map(Optional::get)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
.map(this::validateDiscoveryDocument)
|
||||||
.map(this::createBuilder)
|
.map(this::createBuilder)
|
||||||
.map(this::configureClientAuthentication)
|
.map(this::configureClientAuthentication)
|
||||||
.map(Builder::build)
|
.map(Builder::build)
|
||||||
.orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
|
.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)
|
private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata)
|
||||||
{
|
{
|
||||||
final String authUri = Optional.of(metadata)
|
final String authUri = Optional.of(metadata)
|
||||||
@@ -393,7 +455,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
final String issuerUri = Optional.of(metadata)
|
final String issuerUri = Optional.of(metadata)
|
||||||
.map(OIDCProviderMetadata::getIssuer)
|
.map(OIDCProviderMetadata::getIssuer)
|
||||||
.map(Issuer::getValue)
|
.map(Issuer::getValue)
|
||||||
.orElseGet(config::getIssuerUrl);
|
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ?
|
||||||
|
config.getAuthServerUrl() :
|
||||||
|
config.getIssuerUrl());
|
||||||
|
|
||||||
return ClientRegistration
|
return ClientRegistration
|
||||||
.withRegistrationId("ids")
|
.withRegistrationId("ids")
|
||||||
@@ -402,10 +466,25 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
||||||
.issuerUri(issuerUri)
|
.issuerUri(issuerUri)
|
||||||
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
|
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
|
||||||
.scope("openid", "profile", "email")
|
.scope(getSupportedScopes(metadata.getScopes()))
|
||||||
|
.providerConfigurationMetadata(createMetadata(metadata))
|
||||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createMetadata(OIDCProviderMetadata metadata)
|
||||||
|
{
|
||||||
|
Map<String, Object> 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)
|
private Builder configureClientAuthentication(Builder builder)
|
||||||
{
|
{
|
||||||
builder.clientId(config.getResource());
|
builder.clientId(config.getResource());
|
||||||
@@ -418,6 +497,13 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> getSupportedScopes(Scope scopes)
|
||||||
|
{
|
||||||
|
return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
|
||||||
|
.map(Identifier::getValue)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
|
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
|
||||||
{
|
{
|
||||||
final String response;
|
final String response;
|
||||||
@@ -426,7 +512,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
final ResponseEntity<String> r = rest.exchange(RequestEntity.get(metadataUri).build(), String.class);
|
final ResponseEntity<String> r = rest.exchange(RequestEntity.get(metadataUri).build(), String.class);
|
||||||
if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
|
if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
|
||||||
{
|
{
|
||||||
LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode() + ", has body: " + r.hasBody() + ".");
|
LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode()
|
||||||
|
+ ", has body: " + r.hasBody() + ".");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
response = r.getBody();
|
response = r.getBody();
|
||||||
@@ -449,7 +536,17 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
private Collection<URI> possibleMetadataURIs()
|
private Collection<URI> possibleMetadataURIs()
|
||||||
{
|
{
|
||||||
return List.of(UriComponentsBuilder.fromUriString(config.getIssuerUrl())
|
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")
|
.pathSegment(".well-known", "openid-configuration")
|
||||||
.build().toUri());
|
.build().toUri());
|
||||||
}
|
}
|
||||||
@@ -472,10 +569,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
|
final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
|
||||||
|
|
||||||
decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
|
decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
|
||||||
decoder.setClaimSetConverter(new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
decoder.setClaimSetConverter(
|
||||||
|
new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
||||||
|
|
||||||
return decoder;
|
return decoder;
|
||||||
} catch (RuntimeException e)
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
{
|
{
|
||||||
LOGGER.warn("Failed to create JwtDecoder.", e);
|
LOGGER.warn("Failed to create JwtDecoder.", e);
|
||||||
throw authorizationServerCantBeUsedException(e);
|
throw authorizationServerCantBeUsedException(e);
|
||||||
@@ -504,7 +603,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
{
|
{
|
||||||
final Optional<RemoteJWKSet<SecurityContext>> jwkSource = ofNullable(jwtProcessor)
|
final Optional<RemoteJWKSet<SecurityContext>> jwkSource = ofNullable(jwtProcessor)
|
||||||
.map(ConfigurableJWTProcessor::getJWSKeySelector)
|
.map(ConfigurableJWTProcessor::getJWSKeySelector)
|
||||||
.filter(JWSVerificationKeySelector.class::isInstance).map(o -> (JWSVerificationKeySelector<SecurityContext>)o)
|
.filter(JWSVerificationKeySelector.class::isInstance)
|
||||||
|
.map(o -> (JWSVerificationKeySelector<SecurityContext>) o)
|
||||||
.map(JWSVerificationKeySelector::getJWKSource)
|
.map(JWSVerificationKeySelector::getJWKSource)
|
||||||
.filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet<SecurityContext>) o);
|
.filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet<SecurityContext>) o);
|
||||||
if (jwkSource.isEmpty())
|
if (jwkSource.isEmpty())
|
||||||
@@ -527,8 +627,10 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1, TimeUnit.SECONDS);
|
final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1,
|
||||||
final JWKSource<SecurityContext> cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(), resourceRetriever.get(), cache);
|
TimeUnit.SECONDS);
|
||||||
|
final JWKSource<SecurityContext> cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(),
|
||||||
|
resourceRetriever.get(), cache);
|
||||||
|
|
||||||
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
|
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
|
||||||
JWSAlgorithm.parse(SIGNATURE_ALGORITHM.getName()),
|
JWSAlgorithm.parse(SIGNATURE_ALGORITHM.getName()),
|
||||||
@@ -537,11 +639,18 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
|
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
|
||||||
{
|
{
|
||||||
return new DelegatingOAuth2TokenValidator<>(
|
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||||
new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)),
|
validators.add(new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)));
|
||||||
new JwtIssuerValidator(providerDetails.getIssuerUri()),
|
validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
|
||||||
new JwtClaimValidator<String>("typ", "Bearer"::equals),
|
if (!config.isClientIdValidationDisabled())
|
||||||
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull));
|
{
|
||||||
|
validators.add(new JwtClaimValidator<String>("azp", config.getResource()::equals));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(config.getAudience()))
|
||||||
|
{
|
||||||
|
validators.add(new JwtAudienceValidator(config.getAudience()));
|
||||||
|
}
|
||||||
|
return new DelegatingOAuth2TokenValidator<>(validators);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RSAPublicKey parsePublicKey(String pem)
|
private RSAPublicKey parsePublicKey(String pem)
|
||||||
@@ -575,7 +684,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
private String requireValidJwkSetUri(ProviderDetails providerDetails)
|
private String requireValidJwkSetUri(ProviderDetails providerDetails)
|
||||||
{
|
{
|
||||||
final String uri = providerDetails.getJwkSetUri();
|
final String uri = providerDetails.getJwkSetUri();
|
||||||
if (!isDefined(uri)) {
|
if (!isDefined(uri))
|
||||||
|
{
|
||||||
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
|
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
|
||||||
"Failed to find a Signature Verifier for: '"
|
"Failed to find a Signature Verifier for: '"
|
||||||
+ providerDetails.getIssuerUri()
|
+ providerDetails.getIssuerUri()
|
||||||
@@ -615,8 +725,65 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class JwtAudienceValidator implements OAuth2TokenValidator<Jwt>
|
||||||
|
{
|
||||||
|
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<String>) 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isDefined(String value)
|
private static boolean isDefined(String value)
|
||||||
{
|
{
|
||||||
return value != null && !value.isBlank();
|
return value != null && !value.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -30,7 +30,7 @@ import java.io.Serializable;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||||
@@ -38,6 +38,7 @@ import com.nimbusds.openid.connect.sdk.claims.UserInfo;
|
|||||||
|
|
||||||
import org.alfresco.model.ContentModel;
|
import org.alfresco.model.ContentModel;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
import org.alfresco.service.namespace.QName;
|
import org.alfresco.service.namespace.QName;
|
||||||
import org.alfresco.service.transaction.TransactionService;
|
import org.alfresco.service.transaction.TransactionService;
|
||||||
@@ -50,22 +51,28 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
*/
|
*/
|
||||||
public class IdentityServiceJITProvisioningHandler
|
public class IdentityServiceJITProvisioningHandler
|
||||||
{
|
{
|
||||||
|
private final IdentityServiceConfig identityServiceConfig;
|
||||||
private final IdentityServiceFacade identityServiceFacade;
|
private final IdentityServiceFacade identityServiceFacade;
|
||||||
private final PersonService personService;
|
private final PersonService personService;
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
private final Function<IdentityServiceFacade.DecodedAccessToken, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = token -> {
|
private final BiFunction<DecodedAccessToken, String, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = (token, usernameMappingClaim) -> {
|
||||||
Optional<String> firstName = Optional.ofNullable(token.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
|
Optional<String> firstName = Optional.ofNullable(token)
|
||||||
|
.map(jwtToken -> jwtToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
|
||||||
.filter(String.class::isInstance)
|
.filter(String.class::isInstance)
|
||||||
.map(String.class::cast);
|
.map(String.class::cast);
|
||||||
Optional<String> lastName = Optional.ofNullable(token.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
|
Optional<String> lastName = Optional.ofNullable(token)
|
||||||
|
.map(jwtToken -> jwtToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
|
||||||
.filter(String.class::isInstance)
|
.filter(String.class::isInstance)
|
||||||
.map(String.class::cast);
|
.map(String.class::cast);
|
||||||
Optional<String> email = Optional.ofNullable(token.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
|
Optional<String> email = Optional.ofNullable(token)
|
||||||
|
.map(jwtToken -> jwtToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
|
||||||
.filter(String.class::isInstance)
|
.filter(String.class::isInstance)
|
||||||
.map(String.class::cast);
|
.map(String.class::cast);
|
||||||
|
|
||||||
return Optional.ofNullable(token.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
|
return Optional.ofNullable(token.getClaim(Optional.ofNullable(usernameMappingClaim)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.orElse(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)))
|
||||||
.filter(String.class::isInstance)
|
.filter(String.class::isInstance)
|
||||||
.map(String.class::cast)
|
.map(String.class::cast)
|
||||||
.map(this::normalizeUserId)
|
.map(this::normalizeUserId)
|
||||||
@@ -74,11 +81,13 @@ public class IdentityServiceJITProvisioningHandler
|
|||||||
|
|
||||||
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
|
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
|
||||||
PersonService personService,
|
PersonService personService,
|
||||||
TransactionService transactionService)
|
TransactionService transactionService,
|
||||||
|
IdentityServiceConfig identityServiceConfig)
|
||||||
{
|
{
|
||||||
this.identityServiceFacade = identityServiceFacade;
|
this.identityServiceFacade = identityServiceFacade;
|
||||||
this.personService = personService;
|
this.personService = personService;
|
||||||
this.transactionService = transactionService;
|
this.transactionService = transactionService;
|
||||||
|
this.identityServiceConfig = identityServiceConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<OIDCUserInfo> extractUserInfoAndCreateUserIfNeeded(String bearerToken)
|
public Optional<OIDCUserInfo> extractUserInfoAndCreateUserIfNeeded(String bearerToken)
|
||||||
@@ -130,12 +139,15 @@ public class IdentityServiceJITProvisioningHandler
|
|||||||
{
|
{
|
||||||
return Optional.ofNullable(bearerToken)
|
return Optional.ofNullable(bearerToken)
|
||||||
.map(identityServiceFacade::decodeToken)
|
.map(identityServiceFacade::decodeToken)
|
||||||
.flatMap(mapTokenToUserInfoResponse);
|
.flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
|
||||||
|
identityServiceConfig.getPrincipalAttribute()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
|
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
|
||||||
{
|
{
|
||||||
return identityServiceFacade.getUserInfo(bearerToken)
|
return identityServiceFacade.getUserInfo(bearerToken,
|
||||||
|
StringUtils.isNotBlank(identityServiceConfig.getPrincipalAttribute()) ?
|
||||||
|
identityServiceConfig.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)
|
||||||
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty())
|
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty())
|
||||||
.map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()),
|
.map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()),
|
||||||
Optional.ofNullable(userInfo.firstName()).orElse(""),
|
Optional.ofNullable(userInfo.firstName()).orElse(""),
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2024 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 <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
public enum IdentityServiceMetadataKey
|
||||||
|
{
|
||||||
|
AUDIENCE("audience"),
|
||||||
|
SCOPES_SUPPORTED("scopes_supported");
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
IdentityServiceMetadataKey(String value)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue()
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -28,6 +28,8 @@ package org.alfresco.repo.security.authentication.identityservice;
|
|||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
@@ -44,6 +46,7 @@ import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
|
|||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
||||||
@@ -51,8 +54,10 @@ import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTo
|
|||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequestEntityConverter;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
|
||||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
@@ -65,6 +70,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
|
|||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
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.springframework.web.client.RestOperations;
|
||||||
|
|
||||||
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||||
@@ -75,7 +82,8 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
private final ClientRegistration clientRegistration;
|
private final ClientRegistration clientRegistration;
|
||||||
private final JwtDecoder jwtDecoder;
|
private final JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
SpringBasedIdentityServiceFacade(RestOperations restOperations, ClientRegistration clientRegistration, JwtDecoder jwtDecoder)
|
SpringBasedIdentityServiceFacade(RestOperations restOperations, ClientRegistration clientRegistration,
|
||||||
|
JwtDecoder jwtDecoder)
|
||||||
{
|
{
|
||||||
requireNonNull(restOperations);
|
requireNonNull(restOperations);
|
||||||
this.clientRegistration = requireNonNull(clientRegistration);
|
this.clientRegistration = requireNonNull(clientRegistration);
|
||||||
@@ -83,7 +91,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
this.clients = Map.of(
|
this.clients = Map.of(
|
||||||
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
|
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
|
||||||
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
|
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
|
||||||
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations));
|
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -112,7 +120,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter)
|
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter, String principalAttribute)
|
||||||
{
|
{
|
||||||
return Optional.ofNullable(tokenParameter)
|
return Optional.ofNullable(tokenParameter)
|
||||||
.filter(Predicate.not(String::isEmpty))
|
.filter(Predicate.not(String::isEmpty))
|
||||||
@@ -123,7 +131,8 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
.flatMap(uri -> {
|
.flatMap(uri -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Optional.of(new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send());
|
return Optional.of(
|
||||||
|
new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send());
|
||||||
}
|
}
|
||||||
catch (IOException | URISyntaxException e)
|
catch (IOException | URISyntaxException e)
|
||||||
{
|
{
|
||||||
@@ -144,7 +153,8 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
})
|
})
|
||||||
.map(UserInfoResponse::toSuccessResponse)
|
.map(UserInfoResponse::toSuccessResponse)
|
||||||
.map(UserInfoSuccessResponse::getUserInfo))
|
.map(UserInfoSuccessResponse::getUserInfo))
|
||||||
.map(userInfo -> new OIDCUserInfo(userInfo.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress()));
|
.map(userInfo -> new OIDCUserInfo(userInfo.getStringClaim(principalAttribute), userInfo.getGivenName(),
|
||||||
|
userInfo.getFamilyName(), userInfo.getEmailAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -188,7 +198,8 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
|
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
|
||||||
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
|
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
|
||||||
|
|
||||||
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken, clientRegistration.getScopes());
|
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken,
|
||||||
|
clientRegistration.getScopes());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grant.isAuthorizationCode())
|
if (grant.isAuthorizationCode())
|
||||||
@@ -221,27 +232,52 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createAuthorizationCodeClient(RestOperations rest)
|
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createAuthorizationCodeClient(
|
||||||
|
RestOperations rest)
|
||||||
{
|
{
|
||||||
final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
|
final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
|
||||||
client.setRestOperations(rest);
|
client.setRestOperations(rest);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> createRefreshTokenClient(RestOperations rest)
|
private static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> createRefreshTokenClient(
|
||||||
|
RestOperations rest)
|
||||||
{
|
{
|
||||||
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
|
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
|
||||||
client.setRestOperations(rest);
|
client.setRestOperations(rest);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest)
|
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest,
|
||||||
|
ClientRegistration clientRegistration)
|
||||||
{
|
{
|
||||||
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
||||||
client.setRestOperations(rest);
|
client.setRestOperations(rest);
|
||||||
|
Optional.of(clientRegistration)
|
||||||
|
.map(ClientRegistration::getProviderDetails)
|
||||||
|
.map(ProviderDetails::getConfigurationMetadata)
|
||||||
|
.map(metadata -> metadata.get(AUDIENCE.getValue()))
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast)
|
||||||
|
.ifPresent(audienceValue -> {
|
||||||
|
final OAuth2PasswordGrantRequestEntityConverter requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
|
||||||
|
requestEntityConverter.addParametersConverter(audienceParameterConverter(audienceValue));
|
||||||
|
client.setRequestEntityConverter(requestEntityConverter);
|
||||||
|
});
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Converter<OAuth2PasswordGrantRequest, MultiValueMap<String, String>> audienceParameterConverter(
|
||||||
|
String audienceValue)
|
||||||
|
{
|
||||||
|
return (grantRequest) -> {
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||||
|
parameters.set("audience", audienceValue);
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
|
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
|
||||||
{
|
{
|
||||||
private final OAuth2AccessTokenResponse tokenResponse;
|
private final OAuth2AccessTokenResponse tokenResponse;
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -26,11 +26,21 @@
|
|||||||
package org.alfresco.repo.security.authentication.identityservice.admin;
|
package org.alfresco.repo.security.authentication.identityservice.admin;
|
||||||
|
|
||||||
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
|
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
|
||||||
|
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.nimbusds.oauth2.sdk.Scope;
|
||||||
|
import com.nimbusds.oauth2.sdk.id.Identifier;
|
||||||
|
import com.nimbusds.oauth2.sdk.id.State;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -38,12 +48,17 @@ import org.alfresco.repo.management.subsystems.ActivateableBean;
|
|||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
|
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
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;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
|
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.AuthorizationException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID
|
||||||
@@ -56,7 +71,9 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
|
|||||||
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
|
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_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
|
||||||
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
|
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
|
||||||
|
private static final Set<String> SCOPES = Set.of("openid", "profile", "email", "offline_access");
|
||||||
|
|
||||||
|
private IdentityServiceConfig identityServiceConfig;
|
||||||
private IdentityServiceFacade identityServiceFacade;
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
private AdminConsoleAuthenticationCookiesService cookiesService;
|
private AdminConsoleAuthenticationCookiesService cookiesService;
|
||||||
private RemoteUserMapper remoteUserMapper;
|
private RemoteUserMapper remoteUserMapper;
|
||||||
@@ -177,13 +194,56 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
|
|||||||
|
|
||||||
private String getAuthenticationRequest(HttpServletRequest request)
|
private String getAuthenticationRequest(HttpServletRequest request)
|
||||||
{
|
{
|
||||||
return identityServiceFacade.getClientRegistration().getProviderDetails().getAuthorizationUri()
|
ClientRegistration clientRegistration = identityServiceFacade.getClientRegistration();
|
||||||
+ "?client_id="
|
State state = new State();
|
||||||
+ identityServiceFacade.getClientRegistration().getClientId()
|
|
||||||
+ "&redirect_uri="
|
UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||||
+ request.getRequestURL()
|
.queryParam("client_id", clientRegistration.getClientId())
|
||||||
+ "&response_type=code"
|
.queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
|
||||||
+ "&scope=openid";
|
.queryParam("response_type", "code")
|
||||||
|
.queryParam("scope", String.join("+", getScopes(clientRegistration)))
|
||||||
|
.queryParam("state", state.toString());
|
||||||
|
|
||||||
|
if(StringUtils.isNotBlank(identityServiceConfig.getAudience()))
|
||||||
|
{
|
||||||
|
authRequestBuilder.queryParam("audience", identityServiceConfig.getAudience());
|
||||||
|
}
|
||||||
|
|
||||||
|
return authRequestBuilder.build().toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getSupportedScopes(Scope scopes)
|
||||||
|
{
|
||||||
|
return scopes.stream()
|
||||||
|
.filter(scope -> SCOPES.contains(scope.getValue()))
|
||||||
|
.map(Identifier::getValue)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRedirectUri(String requestURL)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
URI originalUri = new URI(requestURL);
|
||||||
|
URI redirectUri = new URI(originalUri.getScheme(), originalUri.getAuthority(), identityServiceConfig.getAdminConsoleRedirectPath(), originalUri.getQuery(), originalUri.getFragment());
|
||||||
|
return redirectUri.toASCIIString();
|
||||||
|
}
|
||||||
|
catch (URISyntaxException e)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error while trying to get the redirect URI and respond with the authentication challenge: {}", e.getMessage(), e);
|
||||||
|
throw new AuthenticationException(e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetCookies(HttpServletResponse response)
|
private void resetCookies(HttpServletResponse response)
|
||||||
@@ -240,6 +300,12 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
|
|||||||
this.cookiesService = cookiesService;
|
this.cookiesService = cookiesService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setIdentityServiceConfig(
|
||||||
|
IdentityServiceConfig identityServiceConfig)
|
||||||
|
{
|
||||||
|
this.identityServiceConfig = identityServiceConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isActive()
|
public boolean isActive()
|
||||||
{
|
{
|
||||||
|
@@ -89,6 +89,12 @@
|
|||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="identityServiceConfig" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig">
|
<bean name="identityServiceConfig" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig">
|
||||||
|
<property name="issuerUrl">
|
||||||
|
<value>${identity-service.issuer-url:#{null}}</value>
|
||||||
|
</property>
|
||||||
|
<property name="audience">
|
||||||
|
<value>${identity-service.audience:#{null}}</value>
|
||||||
|
</property>
|
||||||
<property name="realm">
|
<property name="realm">
|
||||||
<value>${identity-service.realm}</value>
|
<value>${identity-service.realm}</value>
|
||||||
</property>
|
</property>
|
||||||
@@ -140,6 +146,15 @@
|
|||||||
<property name="publicClient">
|
<property name="publicClient">
|
||||||
<value>${identity-service.public-client:false}</value>
|
<value>${identity-service.public-client:false}</value>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="principalAttribute">
|
||||||
|
<value>${identity-service.principal-attribute:preferred_username}</value>
|
||||||
|
</property>
|
||||||
|
<property name="clientIdValidationDisabled">
|
||||||
|
<value>${identity-service.client-id.validation.disabled:true}</value>
|
||||||
|
</property>
|
||||||
|
<property name="adminConsoleRedirectPath">
|
||||||
|
<value>${identity-service.admin-console.redirect-path}</value>
|
||||||
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<!-- Enable control over mapping between request and user ID -->
|
<!-- Enable control over mapping between request and user ID -->
|
||||||
@@ -176,12 +191,16 @@
|
|||||||
<property name="remoteUserMapper">
|
<property name="remoteUserMapper">
|
||||||
<ref bean="remoteUserMapper" />
|
<ref bean="remoteUserMapper" />
|
||||||
</property>
|
</property>
|
||||||
|
<property name="identityServiceConfig">
|
||||||
|
<ref bean="identityServiceConfig" />
|
||||||
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
|
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
|
||||||
<constructor-arg ref="PersonService"/>
|
<constructor-arg ref="PersonService"/>
|
||||||
<constructor-arg ref="identityServiceFacade"/>
|
<constructor-arg ref="identityServiceFacade"/>
|
||||||
<constructor-arg ref="transactionService"/>
|
<constructor-arg ref="transactionService"/>
|
||||||
|
<constructor-arg ref="identityServiceConfig"/>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
||||||
|
@@ -11,3 +11,4 @@ identity-service.realm=alfresco
|
|||||||
identity-service.resource=alfresco
|
identity-service.resource=alfresco
|
||||||
identity-service.credentials.secret=
|
identity-service.credentials.secret=
|
||||||
identity-service.public-client=true
|
identity-service.public-client=true
|
||||||
|
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco;
|
package org.alfresco;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.ClientRegistrationProviderUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
||||||
@@ -151,6 +152,7 @@ import org.junit.runners.Suite;
|
|||||||
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
|
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
|
||||||
AdminConsoleHttpServletRequestWrapperUnitTest.class,
|
AdminConsoleHttpServletRequestWrapperUnitTest.class,
|
||||||
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
|
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
|
||||||
|
ClientRegistrationProviderUnitTest.class,
|
||||||
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
||||||
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
||||||
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
||||||
|
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2024 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 <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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 org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
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 IdentityServiceConfig config;
|
||||||
|
private RestTemplate restTemplate;
|
||||||
|
private OIDCProviderMetadata oidcResponse;
|
||||||
|
|
||||||
|
private ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws ParseException
|
||||||
|
{
|
||||||
|
config = new IdentityServiceConfig();
|
||||||
|
config.setAuthServerUrl(AUTH_SERVER);
|
||||||
|
config.setResource(CLIENT_ID);
|
||||||
|
|
||||||
|
restTemplate = mock(RestTemplate.class);
|
||||||
|
ResponseEntity responseEntity = mock(ResponseEntity.class);
|
||||||
|
when(restTemplate.exchange(requestEntityCaptor.capture(), eq(String.class))).thenReturn(responseEntity);
|
||||||
|
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
|
||||||
|
when(responseEntity.hasBody()).thenReturn(true);
|
||||||
|
when(responseEntity.getBody()).thenReturn("");
|
||||||
|
|
||||||
|
oidcResponse = spy(OIDCProviderMetadata.parse(OPENID_CONFIGURATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateClientRegistration()
|
||||||
|
{
|
||||||
|
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||||
|
restTemplate);
|
||||||
|
assertThat(clientRegistration).isNotNull();
|
||||||
|
assertThat(clientRegistration.getClientId()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getTokenUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
|
||||||
|
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
|
||||||
|
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateClientRegistrationWithoutIssuerConfigured()
|
||||||
|
{
|
||||||
|
config.setIssuerUrl(null);
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||||
|
restTemplate);
|
||||||
|
assertThat(clientRegistration).isNotNull();
|
||||||
|
assertThat(clientRegistration.getClientId()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getTokenUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
|
||||||
|
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
|
||||||
|
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfIssuerIsNotValid()
|
||||||
|
{
|
||||||
|
config.setIssuerUrl("https://invalidissuer.alfresco");
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfIssuerIsNull()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getIssuer()).thenReturn(null);
|
||||||
|
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfTokenEndpointIsNull()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getTokenEndpointURI()).thenReturn(null);
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfAuthorizationEndpointIsNull()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getAuthorizationEndpointURI()).thenReturn(null);
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfUserInfoEndpointIsNull()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getUserInfoEndpointURI()).thenReturn(null);
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowIdentityServiceExceptionIfJWKSetEndpointIsNull()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getJWKSetURI()).thenReturn(null);
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
assertThrows(IdentityServiceException.class,
|
||||||
|
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateDiscoveryEndpointWithRealm()
|
||||||
|
{
|
||||||
|
config.setRealm("alfresco");
|
||||||
|
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
|
||||||
|
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
|
||||||
|
AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSetAllSupportedScopes()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||||
|
restTemplate);
|
||||||
|
assertThat(
|
||||||
|
clientRegistration.getScopes().containsAll(
|
||||||
|
Set.of("openid", "profile", "email"))).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSetOneSupportedScope()
|
||||||
|
{
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
when(oidcResponse.getScopes()).thenReturn(new Scope("openid"));
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||||
|
restTemplate);
|
||||||
|
assertThat(clientRegistration.getScopes().size()).isEqualTo(1);
|
||||||
|
assertThat(clientRegistration.getScopes().stream().findFirst().get()).isEqualTo("openid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateDiscoveryEndpointFromIssuer()
|
||||||
|
{
|
||||||
|
config.setAuthServerUrl(null);
|
||||||
|
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
|
||||||
|
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||||
|
{
|
||||||
|
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||||
|
|
||||||
|
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
|
||||||
|
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
|
||||||
|
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -29,10 +29,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtAudienceValidator;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -45,11 +49,14 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
|
|||||||
public class IdentityServiceFacadeFactoryBeanTest
|
public class IdentityServiceFacadeFactoryBeanTest
|
||||||
{
|
{
|
||||||
private static final String EXPECTED_ISSUER = "expected-issuer";
|
private static final String EXPECTED_ISSUER = "expected-issuer";
|
||||||
|
private static final String EXPECTED_AUDIENCE = "expected-audience";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
|
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
|
||||||
{
|
{
|
||||||
final IdentityServiceConfig config = mock(IdentityServiceConfig.class);
|
final IdentityServiceConfig config = mock(IdentityServiceConfig.class);
|
||||||
when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB");
|
when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB");
|
||||||
|
when(config.isClientIdValidationDisabled()).thenReturn(true);
|
||||||
|
|
||||||
final ProviderDetails providerDetails = mock(ProviderDetails.class);
|
final ProviderDetails providerDetails = mock(ProviderDetails.class);
|
||||||
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
|
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
|
||||||
@@ -108,6 +115,64 @@ public class IdentityServiceFacadeFactoryBeanTest
|
|||||||
assertThat(validationResult.getErrors()).isEmpty();
|
assertThat(validationResult.getErrors()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailWithNotMatchingAudienceList()
|
||||||
|
{
|
||||||
|
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
|
||||||
|
|
||||||
|
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(
|
||||||
|
tokenWithAudience(List.of("different-audience")));
|
||||||
|
assertThat(validationResult).isNotNull();
|
||||||
|
assertThat(validationResult.hasErrors()).isTrue();
|
||||||
|
assertThat(validationResult.getErrors()).hasSize(1);
|
||||||
|
|
||||||
|
final OAuth2Error error = validationResult.getErrors().iterator().next();
|
||||||
|
assertThat(error).isNotNull();
|
||||||
|
assertThat(error.getDescription()).contains(EXPECTED_AUDIENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailWithNullAudience()
|
||||||
|
{
|
||||||
|
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
|
||||||
|
|
||||||
|
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(tokenWithAudience(null));
|
||||||
|
assertThat(validationResult).isNotNull();
|
||||||
|
assertThat(validationResult.hasErrors()).isTrue();
|
||||||
|
assertThat(validationResult.getErrors()).hasSize(1);
|
||||||
|
|
||||||
|
final OAuth2Error error = validationResult.getErrors().iterator().next();
|
||||||
|
assertThat(error).isNotNull();
|
||||||
|
assertThat(error.getDescription()).contains(EXPECTED_AUDIENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSucceedWithMatchingAudienceList()
|
||||||
|
{
|
||||||
|
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
|
||||||
|
|
||||||
|
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(
|
||||||
|
tokenWithAudience(List.of(EXPECTED_AUDIENCE)));
|
||||||
|
assertThat(validationResult).isNotNull();
|
||||||
|
assertThat(validationResult.hasErrors()).isFalse();
|
||||||
|
assertThat(validationResult.getErrors()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSucceedWithMatchingSingleAudience()
|
||||||
|
{
|
||||||
|
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
|
||||||
|
|
||||||
|
final Jwt token = Jwt.withTokenValue(UUID.randomUUID().toString())
|
||||||
|
.claim("aud", EXPECTED_AUDIENCE)
|
||||||
|
.header("JUST", "FOR TESTING")
|
||||||
|
.build();
|
||||||
|
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(token);
|
||||||
|
assertThat(validationResult).isNotNull();
|
||||||
|
assertThat(validationResult.hasErrors()).isFalse();
|
||||||
|
assertThat(validationResult.getErrors()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private Jwt tokenWithIssuer(String issuer)
|
private Jwt tokenWithIssuer(String issuer)
|
||||||
{
|
{
|
||||||
return Jwt.withTokenValue(UUID.randomUUID().toString())
|
return Jwt.withTokenValue(UUID.randomUUID().toString())
|
||||||
@@ -116,4 +181,12 @@ public class IdentityServiceFacadeFactoryBeanTest
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Jwt tokenWithAudience(Collection<String> audience)
|
||||||
|
{
|
||||||
|
return Jwt.withTokenValue(UUID.randomUUID().toString())
|
||||||
|
.audience(audience)
|
||||||
|
.header("JUST", "FOR TESTING")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.atLeast;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -32,6 +33,8 @@ import static org.mockito.Mockito.when;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||||
|
|
||||||
import org.alfresco.model.ContentModel;
|
import org.alfresco.model.ContentModel;
|
||||||
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
|
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
|
||||||
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
|
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
|
||||||
@@ -57,6 +60,14 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
|||||||
private IdentityServiceFacade identityServiceFacade;
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
||||||
|
|
||||||
|
private final boolean isAuth0Enabled = Optional.ofNullable(System.getProperty("auth0.enabled"))
|
||||||
|
.map(Boolean::valueOf)
|
||||||
|
.orElse(false);
|
||||||
|
|
||||||
|
private final String userPassword = Optional.ofNullable(System.getProperty("admin.password"))
|
||||||
|
.filter(password -> isAuth0Enabled)
|
||||||
|
.orElse("password");
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup()
|
public void setup()
|
||||||
{
|
{
|
||||||
@@ -85,7 +96,8 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
|||||||
assertFalse(personService.personExists(IDS_USERNAME));
|
assertFalse(personService.personExists(IDS_USERNAME));
|
||||||
|
|
||||||
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
||||||
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
|
identityServiceFacade.authorize(
|
||||||
|
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
|
||||||
|
|
||||||
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
accessTokenAuthorization.getAccessToken().getTokenValue());
|
accessTokenAuthorization.getAccessToken().getTokenValue());
|
||||||
@@ -94,13 +106,17 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
|||||||
|
|
||||||
assertTrue(userInfoOptional.isPresent());
|
assertTrue(userInfoOptional.isPresent());
|
||||||
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
||||||
|
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
|
||||||
|
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
||||||
|
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
|
||||||
|
|
||||||
|
if (!isAuth0Enabled)
|
||||||
|
{
|
||||||
assertEquals("John", userInfoOptional.get().firstName());
|
assertEquals("John", userInfoOptional.get().firstName());
|
||||||
assertEquals("Doe", userInfoOptional.get().lastName());
|
assertEquals("Doe", userInfoOptional.get().lastName());
|
||||||
assertEquals("johndoe@test.com", userInfoOptional.get().email());
|
|
||||||
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
|
||||||
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
||||||
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
||||||
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,13 +124,15 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
|||||||
{
|
{
|
||||||
assertFalse(personService.personExists(IDS_USERNAME));
|
assertFalse(personService.personExists(IDS_USERNAME));
|
||||||
|
|
||||||
|
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
|
||||||
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
||||||
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
|
identityServiceFacade.authorize(
|
||||||
|
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
|
||||||
|
|
||||||
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
|
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
|
||||||
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
|
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
|
||||||
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
|
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
|
||||||
when(idsServiceFacadeMock.getUserInfo(accessToken)).thenReturn(identityServiceFacade.getUserInfo(accessToken));
|
when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
|
||||||
|
|
||||||
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
|
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
|
||||||
Field declaredField = jitProvisioningHandler.getClass()
|
Field declaredField = jitProvisioningHandler.getClass()
|
||||||
@@ -131,15 +149,18 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
|||||||
|
|
||||||
assertTrue(userInfoOptional.isPresent());
|
assertTrue(userInfoOptional.isPresent());
|
||||||
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
||||||
|
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
||||||
|
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);
|
||||||
|
if (!isAuth0Enabled)
|
||||||
|
{
|
||||||
assertEquals("John", userInfoOptional.get().firstName());
|
assertEquals("John", userInfoOptional.get().firstName());
|
||||||
assertEquals("Doe", userInfoOptional.get().lastName());
|
assertEquals("Doe", userInfoOptional.get().lastName());
|
||||||
assertEquals("johndoe@test.com", userInfoOptional.get().email());
|
|
||||||
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
|
||||||
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
||||||
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
||||||
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
|
}
|
||||||
verify(idsServiceFacadeMock).decodeToken(accessToken);
|
|
||||||
verify(idsServiceFacadeMock).getUserInfo(accessToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -60,6 +60,9 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
@Mock
|
@Mock
|
||||||
private TransactionService transactionService;
|
private TransactionService transactionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IdentityServiceConfig identityServiceConfig;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private OIDCUserInfo userInfo;
|
private OIDCUserInfo userInfo;
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
|
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
|
||||||
when(personService.createMissingPeople()).thenReturn(true);
|
when(personService.createMissingPeople()).thenReturn(true);
|
||||||
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
|
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
|
||||||
personService, transactionService);
|
personService, transactionService, identityServiceConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -91,7 +94,23 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
assertTrue(result.isPresent());
|
assertTrue(result.isPresent());
|
||||||
assertEquals("johny123", result.get().username());
|
assertEquals("johny123", result.get().username());
|
||||||
assertFalse(result.get().allFieldsNotEmpty());
|
assertFalse(result.get().allFieldsNotEmpty());
|
||||||
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
|
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
|
||||||
|
{
|
||||||
|
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
|
||||||
|
when(personService.personExists("johny123")).thenReturn(true);
|
||||||
|
when(decodedAccessToken.getClaim("nickname")).thenReturn("johny123");
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
JWT_TOKEN);
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("johny123", result.get().username());
|
||||||
|
assertFalse(result.get().allFieldsNotEmpty());
|
||||||
|
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, "nickname");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -114,7 +133,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
assertEquals("johny123@email.com", result.get().email());
|
assertEquals("johny123@email.com", result.get().email());
|
||||||
assertTrue(result.get().allFieldsNotEmpty());
|
assertTrue(result.get().allFieldsNotEmpty());
|
||||||
verify(personService).createPerson(any());
|
verify(personService).createPerson(any());
|
||||||
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
|
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -128,7 +147,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
when(personService.personExists("johny123")).thenReturn(false);
|
when(personService.personExists("johny123")).thenReturn(false);
|
||||||
|
|
||||||
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
|
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
|
||||||
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
|
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
JWT_TOKEN);
|
JWT_TOKEN);
|
||||||
@@ -140,21 +159,21 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
assertEquals("johny123@email.com", result.get().email());
|
assertEquals("johny123@email.com", result.get().email());
|
||||||
assertTrue(result.get().allFieldsNotEmpty());
|
assertTrue(result.get().allFieldsNotEmpty());
|
||||||
verify(personService).createPerson(any());
|
verify(personService).createPerson(any());
|
||||||
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
|
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
|
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
|
||||||
{
|
{
|
||||||
|
|
||||||
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
|
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
JWT_TOKEN);
|
JWT_TOKEN);
|
||||||
|
|
||||||
assertFalse(result.isPresent());
|
assertFalse(result.isPresent());
|
||||||
verify(personService, never()).createPerson(any());
|
verify(personService, never()).createPerson(any());
|
||||||
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
|
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -165,7 +184,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
|
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
|
||||||
|
|
||||||
when(userInfo.username()).thenReturn("johny123");
|
when(userInfo.username()).thenReturn("johny123");
|
||||||
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
|
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
JWT_TOKEN);
|
JWT_TOKEN);
|
||||||
@@ -177,7 +196,31 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
assertEquals("", result.get().email());
|
assertEquals("", result.get().email());
|
||||||
assertFalse(result.get().allFieldsNotEmpty());
|
assertFalse(result.get().allFieldsNotEmpty());
|
||||||
verify(personService, never()).createPerson(any());
|
verify(personService, never()).createPerson(any());
|
||||||
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
|
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
JWT_TOKEN);
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("johny123", 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -189,8 +232,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
|
|||||||
verify(personService, never()).createPerson(any());
|
verify(personService, never()).createPerson(any());
|
||||||
verify(identityServiceFacade, never()).decodeToken(null);
|
verify(identityServiceFacade, never()).decodeToken(null);
|
||||||
verify(identityServiceFacade, never()).decodeToken("");
|
verify(identityServiceFacade, never()).decodeToken("");
|
||||||
verify(identityServiceFacade, never()).getUserInfo(null);
|
verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
verify(identityServiceFacade, never()).getUserInfo("");
|
verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -94,13 +94,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
|||||||
final TransactionService transactionService = mock(TransactionService.class);
|
final TransactionService transactionService = mock(TransactionService.class);
|
||||||
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
|
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
|
||||||
final PersonService personService = mock(PersonService.class);
|
final PersonService personService = mock(PersonService.class);
|
||||||
|
final IdentityServiceConfig identityServiceConfig = mock(IdentityServiceConfig.class);
|
||||||
when(transactionService.isReadOnly()).thenReturn(true);
|
when(transactionService.isReadOnly()).thenReturn(true);
|
||||||
when(facade.decodeToken(anyString()))
|
when(facade.decodeToken(anyString()))
|
||||||
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
|
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
|
||||||
|
|
||||||
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
||||||
|
|
||||||
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService);
|
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService, identityServiceConfig);
|
||||||
|
|
||||||
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
|
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
|
||||||
mapper.setJitProvisioningHandler(jitProvisioning);
|
mapper.setJitProvisioningHandler(jitProvisioning);
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -83,7 +83,7 @@ public class SpringBasedIdentityServiceFacadeUnitTest
|
|||||||
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
|
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
|
||||||
|
|
||||||
|
|
||||||
assertThat(facade.getUserInfo(TOKEN).isEmpty()).isTrue();
|
assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRegistration testRegistration()
|
private ClientRegistration testRegistration()
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
* Copyright (C) 2005 - 2024 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -36,10 +36,15 @@ import static org.mockito.MockitoAnnotations.initMocks;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.nimbusds.oauth2.sdk.Scope;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
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;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
|
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.AccessTokenAuthorization;
|
||||||
@@ -68,6 +73,8 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
|||||||
@Mock
|
@Mock
|
||||||
IdentityServiceFacade identityServiceFacade;
|
IdentityServiceFacade identityServiceFacade;
|
||||||
@Mock
|
@Mock
|
||||||
|
IdentityServiceConfig identityServiceConfig;
|
||||||
|
@Mock
|
||||||
AdminConsoleAuthenticationCookiesService cookiesService;
|
AdminConsoleAuthenticationCookiesService cookiesService;
|
||||||
@Mock
|
@Mock
|
||||||
RemoteUserMapper remoteUserMapper;
|
RemoteUserMapper remoteUserMapper;
|
||||||
@@ -88,9 +95,12 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
|||||||
initMocks(this);
|
initMocks(this);
|
||||||
ClientRegistration clientRegistration = mock(ClientRegistration.class);
|
ClientRegistration clientRegistration = mock(ClientRegistration.class);
|
||||||
ProviderDetails providerDetails = mock(ProviderDetails.class);
|
ProviderDetails providerDetails = mock(ProviderDetails.class);
|
||||||
|
Scope scope = Scope.parse(Arrays.asList("openid", "profile", "email", "offline_access"));
|
||||||
|
|
||||||
when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
|
when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
|
||||||
when(clientRegistration.getClientId()).thenReturn("alfresco");
|
when(clientRegistration.getClientId()).thenReturn("alfresco");
|
||||||
when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
|
when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
|
||||||
|
when(providerDetails.getConfigurationMetadata()).thenReturn(Map.of("scopes_supported", scope));
|
||||||
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
|
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
|
||||||
when(request.getRequestURL()).thenReturn(adminConsoleURL);
|
when(request.getRequestURL()).thenReturn(adminConsoleURL);
|
||||||
when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
|
when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
|
||||||
@@ -100,6 +110,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
|||||||
authenticator.setIdentityServiceFacade(identityServiceFacade);
|
authenticator.setIdentityServiceFacade(identityServiceFacade);
|
||||||
authenticator.setCookiesService(cookiesService);
|
authenticator.setCookiesService(cookiesService);
|
||||||
authenticator.setRemoteUserMapper(remoteUserMapper);
|
authenticator.setRemoteUserMapper(remoteUserMapper);
|
||||||
|
authenticator.setIdentityServiceConfig(identityServiceConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,11 +153,45 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
|||||||
@Test
|
@Test
|
||||||
public void shouldCallAuthChallenge() throws IOException
|
public void shouldCallAuthChallenge() throws IOException
|
||||||
{
|
{
|
||||||
String authenticationRequest = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=" + adminConsoleURL
|
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
|
||||||
+ "&response_type=code&scope=openid";
|
|
||||||
|
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
|
||||||
|
ArgumentCaptor<String> 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);
|
||||||
|
|
||||||
authenticator.requestAuthentication(request, response);
|
authenticator.requestAuthentication(request, response);
|
||||||
|
|
||||||
verify(response).sendRedirect(authenticationRequest);
|
verify(response).sendRedirect(authenticationRequest.capture());
|
||||||
|
assertTrue(authenticationRequest.getValue().contains(expectedUri));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("openid"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("profile"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("email"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("offline_access"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("state"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCallAuthChallengeWithAudience() throws IOException
|
||||||
|
{
|
||||||
|
String audience = "http://localhost:8082";
|
||||||
|
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
|
||||||
|
when(identityServiceConfig.getAudience()).thenReturn(audience);
|
||||||
|
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
|
||||||
|
ArgumentCaptor<String> 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);
|
||||||
|
|
||||||
|
authenticator.requestAuthentication(request, response);
|
||||||
|
|
||||||
|
verify(response).sendRedirect(authenticationRequest.capture());
|
||||||
|
assertTrue(authenticationRequest.getValue().contains(expectedUri));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("openid"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("profile"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("email"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("offline_access"));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("audience=%s".formatted(audience)));
|
||||||
|
assertTrue(authenticationRequest.getValue().contains("state"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -1889,7 +1889,7 @@
|
|||||||
"disableableCredentialTypes": [
|
"disableableCredentialTypes": [
|
||||||
"password"
|
"password"
|
||||||
],
|
],
|
||||||
"email": "johndoe@test.com",
|
"email": "johndoe123@alfresco.com",
|
||||||
"emailVerified": false,
|
"emailVerified": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"firstName": "John",
|
"firstName": "John",
|
||||||
|
Reference in New Issue
Block a user