mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-31 17:39:05 +00:00
ACS-4895 Restore support for providing IDS public key (#1856)
This commit is contained in:
@@ -58,6 +58,7 @@ public class IdentityServiceConfig implements InitializingBean
|
||||
private String clientKeystore;
|
||||
private String clientKeystorePassword;
|
||||
private String clientKeyPassword;
|
||||
private String realmKey;
|
||||
|
||||
public void setGlobalProperties(Properties globalProperties)
|
||||
{
|
||||
@@ -229,4 +230,14 @@ public class IdentityServiceConfig implements InitializingBean
|
||||
{
|
||||
return clientKeyPassword;
|
||||
}
|
||||
|
||||
public void setRealmKey(String realmKey)
|
||||
{
|
||||
this.realmKey = realmKey;
|
||||
}
|
||||
|
||||
public String getRealmKey()
|
||||
{
|
||||
return realmKey;
|
||||
}
|
||||
}
|
||||
|
@@ -29,8 +29,12 @@ import static java.util.Objects.requireNonNull;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
@@ -63,10 +67,12 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.security.converter.RsaKeyConverters;
|
||||
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
@@ -109,7 +115,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
factory = new SpringBasedIdentityServiceFacadeFactory(
|
||||
new HttpClientProvider(identityServiceConfig)::createHttpClient,
|
||||
new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
|
||||
new JwtDecoderProvider()::createJwtDecoder
|
||||
new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,12 +202,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
{
|
||||
private final Supplier<HttpClient> httpClientProvider;
|
||||
private final Function<RestOperations, ClientRegistration> clientRegistrationProvider;
|
||||
private final BiFunction<RestOperations, ClientRegistration, JwtDecoder> jwtDecoderProvider;
|
||||
private final BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider;
|
||||
|
||||
SpringBasedIdentityServiceFacadeFactory(
|
||||
Supplier<HttpClient> httpClientProvider,
|
||||
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
|
||||
BiFunction<RestOperations, ClientRegistration, JwtDecoder> jwtDecoderProvider)
|
||||
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
|
||||
{
|
||||
this.httpClientProvider = Objects.requireNonNull(httpClientProvider);
|
||||
this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider);
|
||||
@@ -217,7 +223,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
final ClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClientProvider.get());
|
||||
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
|
||||
final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
|
||||
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate, clientRegistration);
|
||||
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate, clientRegistration.getProviderDetails());
|
||||
|
||||
return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory), clientRegistration, jwtDecoder);
|
||||
}
|
||||
@@ -246,6 +252,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
try
|
||||
{
|
||||
HttpClientBuilder clientBuilder = HttpClients.custom();
|
||||
|
||||
clientBuilder = applyConnectionConfiguration(clientBuilder);
|
||||
clientBuilder = applySSLConfiguration(clientBuilder);
|
||||
|
||||
@@ -389,54 +396,100 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
}
|
||||
}
|
||||
|
||||
private static class JwtDecoderProvider
|
||||
static class JwtDecoderProvider
|
||||
{
|
||||
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
|
||||
private final IdentityServiceConfig config;
|
||||
|
||||
public JwtDecoder createJwtDecoder(RestOperations rest, ClientRegistration clientRegistration)
|
||||
JwtDecoderProvider(IdentityServiceConfig config)
|
||||
{
|
||||
this.config = Objects.requireNonNull(config);
|
||||
}
|
||||
|
||||
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
|
||||
{
|
||||
try
|
||||
{
|
||||
final String jwkSetUri = requireValidJwkSetUri(clientRegistration);
|
||||
final NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
|
||||
.jwsAlgorithm(SIGNATURE_ALGORITHM)
|
||||
.restOperations(rest)
|
||||
.build();
|
||||
final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
|
||||
|
||||
decoder.setJwtValidator(createJwtTokenValidator(clientRegistration));
|
||||
decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
|
||||
decoder.setClaimSetConverter(new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
||||
|
||||
return decoder;
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
} catch (RuntimeException e)
|
||||
{
|
||||
LOGGER.warn("Failed to create JwtDecoder.", e);
|
||||
throw authorizationServerCantBeUsedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String requireValidJwkSetUri(ClientRegistration clientRegistration)
|
||||
private NimbusJwtDecoder buildJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
|
||||
{
|
||||
final String uri = clientRegistration.getProviderDetails().getJwkSetUri();
|
||||
if (isDefined(config.getRealmKey()))
|
||||
{
|
||||
final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
|
||||
return NimbusJwtDecoder.withPublicKey(publicKey)
|
||||
.signatureAlgorithm(SIGNATURE_ALGORITHM)
|
||||
.build();
|
||||
}
|
||||
|
||||
final String jwkSetUri = requireValidJwkSetUri(providerDetails);
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
|
||||
.jwsAlgorithm(SIGNATURE_ALGORITHM)
|
||||
.restOperations(rest)
|
||||
.build();
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
|
||||
{
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)),
|
||||
new JwtIssuerValidator(providerDetails.getIssuerUri()),
|
||||
new JwtClaimValidator<String>("typ", "Bearer"::equals),
|
||||
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull));
|
||||
}
|
||||
|
||||
private RSAPublicKey parsePublicKey(String pem)
|
||||
{
|
||||
try
|
||||
{
|
||||
return tryToParsePublicKey(pem);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isPemFormatException(e))
|
||||
{
|
||||
//For backward compatibility with Keycloak adapter
|
||||
return tryToParsePublicKey("-----BEGIN PUBLIC KEY-----\n" + pem + "\n-----END PUBLIC KEY-----");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private RSAPublicKey tryToParsePublicKey(String pem)
|
||||
{
|
||||
final InputStream pemStream = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8));
|
||||
return RsaKeyConverters.x509().convert(pemStream);
|
||||
}
|
||||
|
||||
private boolean isPemFormatException(Exception e)
|
||||
{
|
||||
return e.getMessage() != null && e.getMessage().contains("-----BEGIN PUBLIC KEY-----");
|
||||
}
|
||||
|
||||
private String requireValidJwkSetUri(ProviderDetails providerDetails)
|
||||
{
|
||||
final String uri = providerDetails.getJwkSetUri();
|
||||
if (!isDefined(uri)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
|
||||
"Failed to find a Signature Verifier for Client Registration: '"
|
||||
+ clientRegistration.getRegistrationId()
|
||||
"Failed to find a Signature Verifier for: '"
|
||||
+ providerDetails.getIssuerUri()
|
||||
+ "'. Check to ensure you have configured the JwkSet URI.",
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ClientRegistration clientRegistration)
|
||||
{
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)),
|
||||
new JwtIssuerValidator(clientRegistration.getProviderDetails().getIssuerUri()),
|
||||
new JwtClaimValidator<String>("typ", "Bearer"::equals),
|
||||
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDefined(String value)
|
||||
|
@@ -128,6 +128,9 @@
|
||||
<property name="clientSocketTimeout">
|
||||
<value>${identity-service.client-socket-timeout:2000}</value>
|
||||
</property>
|
||||
<property name="realmKey">
|
||||
<value>${identity-service.realm-public-key:#{null}}</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!-- Enable control over mapping between request and user ID -->
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2022 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
package org.alfresco;
|
||||
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
|
||||
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
||||
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
||||
import org.alfresco.util.testing.category.DBTests;
|
||||
@@ -138,6 +139,7 @@ import org.junit.runners.Suite;
|
||||
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
|
||||
org.alfresco.util.BeanExtenderUnitTest.class,
|
||||
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
|
||||
IdentityServiceFacadeFactoryBeanTest.class,
|
||||
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
||||
SpringBasedIdentityServiceFacadeUnitTest.class,
|
||||
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
||||
|
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.repo.security.authentication.identityservice;
|
||||
|
||||
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
|
||||
public class IdentityServiceFacadeFactoryBeanTest
|
||||
{
|
||||
@Test
|
||||
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
|
||||
{
|
||||
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");
|
||||
|
||||
final ProviderDetails providerDetails = mock(ProviderDetails.class);
|
||||
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
|
||||
|
||||
final JwtDecoderProvider provider = new JwtDecoderProvider(config);
|
||||
|
||||
final JwtDecoder decoder = provider.createJwtDecoder(null, providerDetails);
|
||||
|
||||
final Jwt decodedToken = decoder.decode("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxNDc0ODM2NDcsImp0aSI6IjEyMzQiLCJpc3MiOiJodHRwczovL215Lmlzc3VlciIsInN1YiI6ImFiYzEyMyIsInR5cCI6IkJlYXJlciIsInByZWZlcnJlZF91c2VybmFtZSI6InBpb3RyZWsifQ.k_KaOrLLh3QsT8mKphkcz2vKpulgxp92UoEDccpHJ1mxE3Pa3gFXPKTj4goUBKXieGPZRMvBDhfWNxMvRYZPiQr2NXJKapkh0bTd0qoaSWz9ICe9Nu3eg7_VA_nwUVPz_35wwmrxgVk0_kpUYQN_VtaO7ZgFE2sJzFjbkVls5aqfAMnEjEgQl837hqZvmlW2ZRWebtxXfQxAjtp0gcTg-xtAHKIINYo_1_uAtt_H9L8KqFaioxrVAEDDIlcKnb-Ks3Y62CrZauaGUJeN_aNj2gdOpdkhvCw79yJyZSGZ7okjGbidCNSAf7Bo2Y6h3dP1Gga7kRmD648ftZESrNvbyg");
|
||||
assertThat(decodedToken).isNotNull();
|
||||
|
||||
final Map<String, Object> claims = decodedToken.getClaims();
|
||||
assertThat(claims).isNotNull()
|
||||
.isNotEmpty()
|
||||
.containsEntry(USERNAME_CLAIM, "piotrek");
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user