From fbbb8046df3793eb21789a68f302637a64fde488 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Fri, 31 Mar 2023 18:26:02 -0500 Subject: [PATCH] NIFI-11370 Corrected JWK Set retrieval for NIFI Trust Strategy (#7108) - Added StandardOidcIdTokenDecoderFactory based on Spring Security OidcIdTokenDecoderFactory with custom REST Operations Merged #7108 into main. (cherry picked from commit e4f0508c90f7f354b4210dd80f3dbed5b3254bcd) --- .../OidcSecurityConfiguration.java | 32 +-- .../StandardOidcIdTokenDecoderFactory.java | 184 ++++++++++++++++++ ...StandardOidcIdTokenDecoderFactoryTest.java | 113 +++++++++++ 3 files changed, 299 insertions(+), 30 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactory.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactoryTest.java diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java index 2ced302598..14e99d50bf 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java @@ -30,12 +30,12 @@ import org.apache.nifi.security.util.TlsConfiguration; import org.apache.nifi.security.util.TlsException; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.util.StringUtils; import org.apache.nifi.web.security.StandardAuthenticationEntryPoint; import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; import org.apache.nifi.web.security.logout.LogoutRequestManager; import org.apache.nifi.web.security.oidc.OidcConfigurationException; import org.apache.nifi.web.security.oidc.OidcUrlPath; +import org.apache.nifi.web.security.oidc.authentication.StandardOidcIdTokenDecoderFactory; import org.apache.nifi.web.security.oidc.client.web.AuthorizedClientExpirationCommand; import org.apache.nifi.web.security.oidc.client.web.OidcBearerTokenRefreshFilter; import org.apache.nifi.web.security.oidc.client.web.StandardOAuth2AuthorizationRequestResolver; @@ -69,7 +69,6 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResp import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; -import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -81,9 +80,6 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequest import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; -import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; @@ -375,14 +371,8 @@ public class OidcSecurityConfiguration { */ @Bean public JwtDecoderFactory idTokenDecoderFactory() { - OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory(); - final String preferredJwdAlgorithm = properties.getOidcPreferredJwsAlgorithm(); - if (StringUtils.isNotEmpty(preferredJwdAlgorithm)) { - idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> getJwsAlgorithm(preferredJwdAlgorithm)); - } - - return idTokenDecoderFactory; + return new StandardOidcIdTokenDecoderFactory(preferredJwdAlgorithm, oidcRestOperations()); } /** @@ -497,22 +487,4 @@ public class OidcSecurityConfiguration { properties.getOidcClaimGroups() ); } - - private JwsAlgorithm getJwsAlgorithm(final String preferredJwsAlgorithm) { - final JwsAlgorithm jwsAlgorithm; - - final MacAlgorithm macAlgorithm = MacAlgorithm.from(preferredJwsAlgorithm); - if (macAlgorithm == null) { - final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(preferredJwsAlgorithm); - if (signatureAlgorithm == null) { - final String message = String.format("Preferred JWS Algorithm [%s] not supported", preferredJwsAlgorithm); - throw new OidcConfigurationException(message); - } - jwsAlgorithm = signatureAlgorithm; - } else { - jwsAlgorithm = macAlgorithm; - } - - return jwsAlgorithm; - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactory.java new file mode 100644 index 0000000000..ed8ff9ee70 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactory.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.oidc.authentication; + +import org.apache.nifi.web.security.oidc.OidcConfigurationException; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenValidator; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +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.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.web.client.RestOperations; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Objects; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * OpenID Connect ID Token Decoder Factory with configurable REST Operations for retrieval of JSON Web Keys + */ +public class StandardOidcIdTokenDecoderFactory implements JwtDecoderFactory { + private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + + private static final String UNSPECIFIED_ERROR_URI = null; + + private static final JwsAlgorithm DEFAULT_JWS_ALGORITHM = SignatureAlgorithm.RS256; + + private static final Map SECRET_KEY_ALGORITHMS; + + static { + final Map mappings = new HashMap<>(); + mappings.put(MacAlgorithm.HS256, "HmacSHA256"); + mappings.put(MacAlgorithm.HS384, "HmacSHA384"); + mappings.put(MacAlgorithm.HS512, "HmacSHA512"); + SECRET_KEY_ALGORITHMS = Collections.unmodifiableMap(mappings); + } + + private static final ClaimTypeConverter DEFAULT_CLAIM_TYPE_CONVERTER = new ClaimTypeConverter( + OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters() + ); + + private final Map jwtDecoders = new ConcurrentHashMap<>(); + + private final JwsAlgorithm configuredJwsAlgorithm; + + private final RestOperations restOperations; + + /** + * Standard constructor with optional JWS Algorithm and required REST Operations for retrieving JSON Web Keys + * + * @param preferredJwsAlgorithm Preferred JSON Web Signature Algorithm default to RS256 when not provided + * @param restOperations REST Operations required for retrieving JSON Web Key Set with Signature Algorithms + */ + public StandardOidcIdTokenDecoderFactory(final String preferredJwsAlgorithm, final RestOperations restOperations) { + this.configuredJwsAlgorithm = getJwsAlgorithm(preferredJwsAlgorithm); + this.restOperations = Objects.requireNonNull(restOperations, "REST Operations required"); + } + + /** + * Create JSON Web Token Decoder based on Client Registration + * + * @param clientRegistration Client Registration required + * @return JSON Web Token Decoder for OpenID Connect ID Tokens + */ + @Override + public JwtDecoder createDecoder(final ClientRegistration clientRegistration) { + Objects.requireNonNull(clientRegistration, "Client Registration required"); + final String registrationId = clientRegistration.getRegistrationId(); + return jwtDecoders.computeIfAbsent(registrationId, (key) -> { + final NimbusJwtDecoder decoder = buildDecoder(clientRegistration); + decoder.setClaimSetConverter(DEFAULT_CLAIM_TYPE_CONVERTER); + final OAuth2TokenValidator tokenValidator = getTokenValidator(clientRegistration); + decoder.setJwtValidator(tokenValidator); + return decoder; + }); + } + + private NimbusJwtDecoder buildDecoder(final ClientRegistration clientRegistration) { + final NimbusJwtDecoder decoder; + + final Class jwsAlgorithmClass = configuredJwsAlgorithm.getClass(); + if (SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithmClass)) { + final String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri(); + if (jwkSetUri == null || jwkSetUri.isEmpty()) { + final String message = String.format("JSON Web Key Set URI required for Signature Verifier JWS Algorithm [%s]", configuredJwsAlgorithm); + final OAuth2Error error = getVerifierError(message); + throw new OAuth2AuthenticationException(error, message); + } + + final SignatureAlgorithm signatureAlgorithm = (SignatureAlgorithm) configuredJwsAlgorithm; + decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .jwsAlgorithm(signatureAlgorithm) + .restOperations(restOperations) + .build(); + } else if (MacAlgorithm.class.isAssignableFrom(jwsAlgorithmClass)) { + final String clientSecret = clientRegistration.getClientSecret(); + if (clientSecret == null || clientSecret.isEmpty()) { + final String message = String.format("Client Secret required for MAC Verifier JWS Algorithm [%s]", configuredJwsAlgorithm); + final OAuth2Error error = getVerifierError(message); + throw new OAuth2AuthenticationException(error, message); + } + + final byte[] clientSecretKey = clientSecret.getBytes(StandardCharsets.UTF_8); + final String secretKeyAlgorithm = SECRET_KEY_ALGORITHMS.get(configuredJwsAlgorithm); + final SecretKey secretKey = new SecretKeySpec(clientSecretKey, secretKeyAlgorithm); + final MacAlgorithm macAlgorithm = (MacAlgorithm) configuredJwsAlgorithm; + decoder = NimbusJwtDecoder.withSecretKey(secretKey).macAlgorithm(macAlgorithm).build(); + } else { + final String message = String.format("Signature Verifier JWS Algorithm [%s] not supported", configuredJwsAlgorithm); + final OAuth2Error error = getVerifierError(message); + throw new OAuth2AuthenticationException(error, message); + } + + return decoder; + } + + /** + * Get OAuth2 Token Validator based on Spring Security DefaultOidcIdTokenValidatorFactory + * + * @param clientRegistration Client Registration + * @return OAuth2 Token Validator with Timestamp and OpenID Connect ID Token Validators + */ + private OAuth2TokenValidator getTokenValidator(final ClientRegistration clientRegistration) { + return new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(), + new OidcIdTokenValidator(clientRegistration) + ); + } + + private JwsAlgorithm getJwsAlgorithm(final String preferredJwsAlgorithm) { + final JwsAlgorithm jwsAlgorithm; + + if (preferredJwsAlgorithm == null || preferredJwsAlgorithm.isEmpty()) { + jwsAlgorithm = DEFAULT_JWS_ALGORITHM; + } else { + final MacAlgorithm macAlgorithm = MacAlgorithm.from(preferredJwsAlgorithm); + if (macAlgorithm == null) { + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(preferredJwsAlgorithm); + if (signatureAlgorithm == null) { + final String message = String.format("Preferred JWS Algorithm [%s] not supported", preferredJwsAlgorithm); + throw new OidcConfigurationException(message); + } + jwsAlgorithm = signatureAlgorithm; + } else { + jwsAlgorithm = macAlgorithm; + } + } + + return jwsAlgorithm; + } + + private OAuth2Error getVerifierError(final String message) { + return new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE, message, UNSPECIFIED_ERROR_URI); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactoryTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactoryTest.java new file mode 100644 index 0000000000..4bc1412f8e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/authentication/StandardOidcIdTokenDecoderFactoryTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.oidc.authentication; + +import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.web.client.RestOperations; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +class StandardOidcIdTokenDecoderFactoryTest { + private static final String REDIRECT_URI = "https://localhost:8443/nifi-api/callback"; + + private static final String AUTHORIZATION_URI = "http://localhost/authorize"; + + private static final String TOKEN_URI = "http://localhost/token"; + + private static final String JWK_SET_URI = "http://localhost/token"; + + private static final String CLIENT_ID = "client-id"; + + private static final String CLIENT_SECRET = "client-secret"; + + private static final String NULL_JWS_ALGORITHM = null; + + private static final SignatureAlgorithm DEFAULT_JWS_ALGORITHM = SignatureAlgorithm.RS256; + + @Mock + RestOperations restOperations; + + StandardOidcIdTokenDecoderFactory factory; + + @Test + void testNullJwsAlgorithmJwkSetUriRequired() { + factory = new StandardOidcIdTokenDecoderFactory(NULL_JWS_ALGORITHM, restOperations); + + final ClientRegistration clientRegistration = getClientRegistrationBuilder().jwkSetUri(JWK_SET_URI).build(); + final JwtDecoder decoder = factory.createDecoder(clientRegistration); + + assertNotNull(decoder); + } + + @Test + void testNullJwsAlgorithmJwkSetUriNotFound() { + factory = new StandardOidcIdTokenDecoderFactory(NULL_JWS_ALGORITHM, restOperations); + + final ClientRegistration clientRegistration = getClientRegistrationBuilder().build(); + final OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, () -> factory.createDecoder(clientRegistration)); + + final OAuth2Error error = exception.getError(); + final String description = error.getDescription(); + assertTrue(description.contains(DEFAULT_JWS_ALGORITHM.name())); + } + + @Test + void testMacJwsAlgorithmClientSecretRequired() { + factory = new StandardOidcIdTokenDecoderFactory(MacAlgorithm.HS256.getName(), restOperations); + + final ClientRegistration clientRegistration = getClientRegistrationBuilder().clientSecret(CLIENT_SECRET).build(); + final JwtDecoder decoder = factory.createDecoder(clientRegistration); + + assertNotNull(decoder); + } + + @Test + void testMacJwsAlgorithmClientSecretNotFound() { + final MacAlgorithm macAlgorithm = MacAlgorithm.HS256; + factory = new StandardOidcIdTokenDecoderFactory(macAlgorithm.getName(), restOperations); + + final ClientRegistration clientRegistration = getClientRegistrationBuilder().build(); + final OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, () -> factory.createDecoder(clientRegistration)); + + final OAuth2Error error = exception.getError(); + final String description = error.getDescription(); + assertTrue(description.contains(macAlgorithm.name())); + } + + ClientRegistration.Builder getClientRegistrationBuilder() { + return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty()) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI); + } +}