diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/authentication/ResponseAuthenticationConverter.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/authentication/ResponseAuthenticationConverter.java index 60a204826d24..2c2b875bf959 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/authentication/ResponseAuthenticationConverter.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/service/authentication/ResponseAuthenticationConverter.java @@ -26,7 +26,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import java.util.Collection; @@ -53,7 +53,9 @@ public ResponseAuthenticationConverter(final String groupAttributeName) { } /** - * Convert SAML 2 Response Token using default Converter and process authorities based on Group Attribute Name + * Convert SAML 2 Response Token using default Converter and process authorities based on Group Attribute Name. + * The Response Assertion and Relying Party Registration from the default Converter are retained so that + * downstream handlers can read attributes from the Response Assertion. * * @param responseToken SAML 2 Response Token * @return SAML 2 Authentication @@ -63,8 +65,14 @@ public Saml2Authentication convert(final ResponseToken responseToken) { Objects.requireNonNull(responseToken, "Response Token required"); final List assertions = responseToken.getResponse().getAssertions(); final Saml2Authentication authentication = Objects.requireNonNull(defaultConverter.convert(responseToken), "Authentication required"); - final Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); - return new Saml2Authentication(principal, authentication.getSaml2Response(), getAuthorities(assertions)); + final Collection authorities = getAuthorities(assertions); + + if (authentication instanceof final Saml2AssertionAuthentication assertionAuthentication) { + final String registrationId = assertionAuthentication.getRelyingPartyRegistrationId(); + return new Saml2AssertionAuthentication(assertionAuthentication.getPrincipal(), assertionAuthentication.getCredentials(), authorities, registrationId); + } + + return new Saml2Authentication(authentication.getPrincipal(), authentication.getSaml2Response(), authorities); } private Collection getAuthorities(final List assertions) { diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandler.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandler.java index ba2b2c454694..90bd83816b73 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandler.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandler.java @@ -30,7 +30,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import java.net.URI; @@ -59,7 +60,7 @@ public class Saml2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSu private final Duration expiration; - private Converter identityConverter = Saml2AuthenticatedPrincipal::getName; + private Converter identityConverter = Saml2ResponseAssertionAccessor::getNameId; /** * SAML 2 Authentication Success Handler requires Bearer Token Provider and expiration for generated tokens @@ -82,11 +83,11 @@ public Saml2AuthenticationSuccessHandler( } /** - * Set Identity Converter for customized mapping of SAML 2 Authenticated Principal to user identity + * Set Identity Converter for customized mapping of SAML 2 Response Assertion to user identity * * @param identityConverter Identity Converter required */ - public void setIdentityConverter(final Converter identityConverter) { + public void setIdentityConverter(final Converter identityConverter) { this.identityConverter = Objects.requireNonNull(identityConverter, "Converter required"); } @@ -123,11 +124,9 @@ private String getBearerToken(final String identity, final Set groups) { } private String getIdentity(final Authentication authentication) { - final Object principal = authentication.getPrincipal(); - final String identity; - if (principal instanceof final Saml2AuthenticatedPrincipal authenticatedPrincipal) { - identity = identityConverter.convert(authenticatedPrincipal); + if (authentication instanceof final Saml2AssertionAuthentication assertionAuthentication) { + identity = identityConverter.convert(assertionAuthentication.getCredentials()); } else { identity = authentication.getName(); } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverter.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverter.java index 79cf485f13bb..d067c62cc0a2 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverter.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverter.java @@ -17,14 +17,14 @@ package org.apache.nifi.web.security.saml2.web.authentication.identity; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import java.util.Objects; /** * Converter for customized User Identity using SAML Attribute Value */ -public class AttributeNameIdentityConverter implements Converter { +public class AttributeNameIdentityConverter implements Converter { private final String attributeName; public AttributeNameIdentityConverter(final String attributeName) { @@ -32,14 +32,14 @@ public AttributeNameIdentityConverter(final String attributeName) { } /** - * Convert Principal to identity using configured attribute name when found + * Convert Response Assertion to identity using configured attribute name when found * - * @param principal SAML 2 Authenticated Principal - * @return Attribute Value or Principal Name when attribute not found + * @param assertion SAML 2 Response Assertion + * @return Attribute Value or Name Identifier when attribute not found */ @Override - public String convert(final Saml2AuthenticatedPrincipal principal) { - final Object attribute = principal.getFirstAttribute(attributeName); - return attribute == null ? principal.getName() : attribute.toString(); + public String convert(final Saml2ResponseAssertionAccessor assertion) { + final Object attribute = assertion.getFirstAttribute(attributeName); + return attribute == null ? assertion.getNameId() : attribute.toString(); } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandlerTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandlerTest.java index 370fca56c226..97bfe125fc32 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandlerTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/Saml2AuthenticationSuccessHandlerTest.java @@ -21,10 +21,13 @@ import org.apache.nifi.authorization.util.IdentityMapping; import org.apache.nifi.web.security.cookie.ApplicationCookieName; import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; +import org.apache.nifi.web.security.saml2.web.authentication.identity.AttributeNameIdentityConverter; +import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.apache.nifi.web.servlet.shared.ProxyHeader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; @@ -32,13 +35,19 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; import java.time.Duration; import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class Saml2AuthenticationSuccessHandlerTest { @@ -72,6 +81,20 @@ class Saml2AuthenticationSuccessHandlerTest { private static final String ALLOWED_CONTEXT_PATHS_PARAMETER = "allowedContextPaths"; + private static final String IDENTITY_ATTRIBUTE_NAME = "urn:oid:0.9.2342.19200300.100.1.3"; + + private static final String IDENTITY_ATTRIBUTE_VALUE = "user@localhost.localdomain"; + + private static final String MAPPED_IDENTITY_ATTRIBUTE_VALUE = "USER@LOCALHOST.LOCALDOMAIN"; + + private static final String NAME_IDENTIFIER = "name-identifier"; + + private static final String MAPPED_NAME_IDENTIFIER = "NAME-IDENTIFIER"; + + private static final String RESPONSE_VALUE = ""; + + private static final String REGISTRATION_ID = "consumer"; + private static final IdentityMapping UPPER_IDENTITY_MAPPING = new IdentityMapping( IdentityMapping.Transform.UPPER.toString(), MATCH_PATTERN, @@ -128,6 +151,29 @@ void testDetermineTargetUrlForwardedPath() { assertBearerCookieAdded(FORWARDED_COOKIE_PATH); } + @Test + void testDetermineTargetUrlAssertionAuthenticationNameIdentifier() { + httpServletRequest.setRequestURI(REQUEST_URI); + + final Authentication authentication = getAssertionAuthentication(Map.of()); + final String targetUrl = handler.determineTargetUrl(httpServletRequest, httpServletResponse, authentication); + + assertEquals(TARGET_URL, targetUrl); + assertBearerTokenIdentityEquals(MAPPED_NAME_IDENTIFIER); + } + + @Test + void testDetermineTargetUrlAssertionAuthenticationIdentityConverter() { + handler.setIdentityConverter(new AttributeNameIdentityConverter(IDENTITY_ATTRIBUTE_NAME)); + httpServletRequest.setRequestURI(REQUEST_URI); + + final Authentication authentication = getAssertionAuthentication(Map.of(IDENTITY_ATTRIBUTE_NAME, List.of(IDENTITY_ATTRIBUTE_VALUE))); + final String targetUrl = handler.determineTargetUrl(httpServletRequest, httpServletResponse, authentication); + + assertEquals(TARGET_URL, targetUrl); + assertBearerTokenIdentityEquals(MAPPED_IDENTITY_ATTRIBUTE_VALUE); + } + void assertTargetUrlEquals(final String expectedTargetUrl) { final Authentication authentication = new TestingAuthenticationToken(IDENTITY, IDENTITY, AUTHORITY); @@ -136,6 +182,22 @@ void assertTargetUrlEquals(final String expectedTargetUrl) { assertEquals(expectedTargetUrl, targetUrl); } + void assertBearerTokenIdentityEquals(final String expectedIdentity) { + final ArgumentCaptor tokenCaptor = ArgumentCaptor.forClass(LoginAuthenticationToken.class); + verify(bearerTokenProvider).getBearerToken(tokenCaptor.capture()); + + assertEquals(expectedIdentity, tokenCaptor.getValue().getName()); + } + + private Authentication getAssertionAuthentication(final Map> attributes) { + final Saml2ResponseAssertionAccessor assertion = Saml2ResponseAssertion.withResponseValue(RESPONSE_VALUE) + .nameId(NAME_IDENTIFIER) + .attributes(attributes) + .build(); + + return new Saml2AssertionAuthentication(NAME_IDENTIFIER, assertion, Collections.emptyList(), REGISTRATION_ID); + } + void assertBearerCookieAdded(final String expectedCookiePath) { final Cookie responseCookie = httpServletResponse.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName()); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverterTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverterTest.java new file mode 100644 index 000000000000..a1e017c3fa5d --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml2/web/authentication/identity/AttributeNameIdentityConverterTest.java @@ -0,0 +1,65 @@ +/* + * 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.saml2.web.authentication.identity; + +import org.junit.jupiter.api.Test; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AttributeNameIdentityConverterTest { + private static final String ATTRIBUTE_NAME = "urn:oid:0.9.2342.19200300.100.1.3"; + + private static final String ATTRIBUTE_VALUE = "user@localhost.localdomain"; + + private static final String OTHER_ATTRIBUTE_NAME = "urn:oid:2.5.4.3"; + + private static final String NAME_IDENTIFIER = "name-identifier"; + + private static final String RESPONSE_VALUE = ""; + + private final AttributeNameIdentityConverter converter = new AttributeNameIdentityConverter(ATTRIBUTE_NAME); + + @Test + void testConvertConfiguredAttributeFound() { + final Saml2ResponseAssertionAccessor assertion = getAssertion(Map.of(ATTRIBUTE_NAME, List.of(ATTRIBUTE_VALUE))); + + assertEquals(ATTRIBUTE_VALUE, converter.convert(assertion)); + } + + @Test + void testConvertConfiguredAttributeNotFoundReturnsNameIdentifier() { + final Saml2ResponseAssertionAccessor assertionWithoutAttributes = getAssertion(Map.of()); + + assertEquals(NAME_IDENTIFIER, converter.convert(assertionWithoutAttributes)); + + final Saml2ResponseAssertionAccessor assertionWithOtherAttribute = getAssertion(Map.of(OTHER_ATTRIBUTE_NAME, List.of(ATTRIBUTE_VALUE))); + + assertEquals(NAME_IDENTIFIER, converter.convert(assertionWithOtherAttribute)); + } + + private Saml2ResponseAssertionAccessor getAssertion(final Map> attributes) { + return Saml2ResponseAssertion.withResponseValue(RESPONSE_VALUE) + .nameId(NAME_IDENTIFIER) + .attributes(attributes) + .build(); + } +}