diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java index cb3d8878e5..f0b618869b 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java @@ -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; + } } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java index 2d1b2fb721..b1fbca113b 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java @@ -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 httpClientProvider; private final Function clientRegistrationProvider; - private final BiFunction jwtDecoderProvider; + private final BiFunction jwtDecoderProvider; SpringBasedIdentityServiceFacadeFactory( Supplier httpClientProvider, Function clientRegistrationProvider, - BiFunction jwtDecoderProvider) + BiFunction jwtDecoderProvider) { this.httpClientProvider = Objects.requireNonNull(httpClientProvider); this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider); @@ -217,7 +223,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean createJwtTokenValidator(ProviderDetails providerDetails) + { + return new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)), + new JwtIssuerValidator(providerDetails.getIssuerUri()), + new JwtClaimValidator("typ", "Bearer"::equals), + new JwtClaimValidator(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 createJwtTokenValidator(ClientRegistration clientRegistration) - { - return new DelegatingOAuth2TokenValidator<>( - new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)), - new JwtIssuerValidator(clientRegistration.getProviderDetails().getIssuerUri()), - new JwtClaimValidator("typ", "Bearer"::equals), - new JwtClaimValidator(JwtClaimNames.SUB, Objects::nonNull)); - } } private static boolean isDefined(String value) diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml index 21a3092ad7..d60420109f 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml @@ -128,6 +128,9 @@ ${identity-service.client-socket-timeout:2000} + + ${identity-service.realm-public-key:#{null}} + diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 3b2441f36e..999f2e16ca 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -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, diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java new file mode 100644 index 0000000000..bf107e68bc --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java @@ -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 . + * #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 claims = decodedToken.getClaims(); + assertThat(claims).isNotNull() + .isNotEmpty() + .containsEntry(USERNAME_CLAIM, "piotrek"); + } + +} \ No newline at end of file