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 856c4c29423..716b04388e0 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 @@ -57,6 +57,7 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jose.util.ResourceRetriever; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.oauth2.sdk.id.Issuer; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException; @@ -88,7 +89,9 @@ import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -96,7 +99,6 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimValidator; import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.web.client.RestOperations; @@ -361,12 +363,18 @@ private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata) .map(OIDCProviderMetadata::getAuthorizationEndpointURI) .map(URI::toASCIIString) .orElse(null); + + final String issuerUri = Optional.of(metadata) + .map(OIDCProviderMetadata::getIssuer) + .map(Issuer::getValue) + .orElseGet(config::getIssuerUrl); + return ClientRegistration .withRegistrationId("ids") .authorizationUri(authUri) .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) - .issuerUri(config.getIssuerUrl()) + .issuerUri(issuerUri) .authorizationGrantType(AuthorizationGrantType.PASSWORD); } @@ -551,6 +559,34 @@ private String requireValidJwkSetUri(ProviderDetails providerDetails) } } + static class JwtIssuerValidator implements OAuth2TokenValidator + { + private final String requiredIssuer; + + public JwtIssuerValidator(String issuer) + { + this.requiredIssuer = requireNonNull(issuer, "issuer cannot be null"); + } + + @Override + public OAuth2TokenValidatorResult validate(Jwt token) + { + requireNonNull(token, "token cannot be null"); + final Object issuer = token.getClaim(JwtClaimNames.ISS); + if (issuer != null && requiredIssuer.equals(issuer.toString())) + { + return OAuth2TokenValidatorResult.success(); + } + + final OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_TOKEN, + "The iss claim is not valid. Expected `%s` but got `%s`.".formatted(requiredIssuer, issuer), + "https://tools.ietf.org/html/rfc6750#section-3.1"); + return OAuth2TokenValidatorResult.failure(error); + } + + } + private static boolean isDefined(String value) { return value != null && !value.isBlank(); 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 index bf107e68bcc..5b07c83db0c 100644 --- 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 @@ -31,15 +31,20 @@ import static org.mockito.Mockito.when; import java.util.Map; +import java.util.UUID; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator; import org.junit.Test; import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; public class IdentityServiceFacadeFactoryBeanTest { + private static final String EXPECTED_ISSUER = "expected-issuer"; @Test public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided() { @@ -62,4 +67,53 @@ public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided() .containsEntry(USERNAME_CLAIM, "piotrek"); } + @Test + public void shouldFailWithNotMatchingIssuerURIs() + { + final JwtIssuerValidator issuerValidator = new JwtIssuerValidator(EXPECTED_ISSUER); + + final OAuth2TokenValidatorResult validationResult = issuerValidator.validate(tokenWithIssuer("different-issuer")); + 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_ISSUER, "different-issuer"); + } + + @Test + public void shouldFailWithNullIssuerURI() + { + final JwtIssuerValidator issuerValidator = new JwtIssuerValidator(EXPECTED_ISSUER); + + final OAuth2TokenValidatorResult validationResult = issuerValidator.validate(tokenWithIssuer(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_ISSUER, "null"); + } + + @Test + public void shouldSucceedWithMatchingIssuerURI() + { + final JwtIssuerValidator issuerValidator = new JwtIssuerValidator(EXPECTED_ISSUER); + + final OAuth2TokenValidatorResult validationResult = issuerValidator.validate(tokenWithIssuer(EXPECTED_ISSUER)); + assertThat(validationResult).isNotNull(); + assertThat(validationResult.hasErrors()).isFalse(); + assertThat(validationResult.getErrors()).isEmpty(); + } + + private Jwt tokenWithIssuer(String issuer) + { + return Jwt.withTokenValue(UUID.randomUUID().toString()) + .issuer(issuer) + .header("JUST", "FOR TESTING") + .build(); + } + } \ No newline at end of file