ACS-4895 Restore support for providing IDS public key (#1856)

This commit is contained in:
Piotr Żurek
2023-04-03 17:35:14 +02:00
committed by GitHub
parent 1c45748f9a
commit 106e398393
5 changed files with 162 additions and 28 deletions

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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 -->

View File

@@ -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,

View File

@@ -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");
}
}