Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -63,8 +65,14 @@ public Saml2Authentication convert(final ResponseToken responseToken) {
Objects.requireNonNull(responseToken, "Response Token required");
final List<Assertion> 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<? extends GrantedAuthority> 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<? extends GrantedAuthority> getAuthorities(final List<Assertion> assertions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +60,7 @@ public class Saml2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSu

private final Duration expiration;

private Converter<Saml2AuthenticatedPrincipal, String> identityConverter = Saml2AuthenticatedPrincipal::getName;
private Converter<Saml2ResponseAssertionAccessor, String> identityConverter = Saml2ResponseAssertionAccessor::getNameId;

/**
* SAML 2 Authentication Success Handler requires Bearer Token Provider and expiration for generated tokens
Expand All @@ -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<Saml2AuthenticatedPrincipal, String> identityConverter) {
public void setIdentityConverter(final Converter<Saml2ResponseAssertionAccessor, String> identityConverter) {
this.identityConverter = Objects.requireNonNull(identityConverter, "Converter required");
}

Expand Down Expand Up @@ -123,11 +124,9 @@ private String getBearerToken(final String identity, final Set<String> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,29 @@
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<Saml2AuthenticatedPrincipal, String> {
public class AttributeNameIdentityConverter implements Converter<Saml2ResponseAssertionAccessor, String> {
private final String attributeName;

public AttributeNameIdentityConverter(final String attributeName) {
this.attributeName = Objects.requireNonNull(attributeName, "Attribute Name required");
}

/**
* 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,33 @@
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;
import org.springframework.mock.web.MockHttpServletResponse;
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 {
Expand Down Expand Up @@ -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 = "<saml2p:Response/>";

private static final String REGISTRATION_ID = "consumer";

private static final IdentityMapping UPPER_IDENTITY_MAPPING = new IdentityMapping(
IdentityMapping.Transform.UPPER.toString(),
MATCH_PATTERN,
Expand Down Expand Up @@ -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);

Expand All @@ -136,6 +182,22 @@ void assertTargetUrlEquals(final String expectedTargetUrl) {
assertEquals(expectedTargetUrl, targetUrl);
}

void assertBearerTokenIdentityEquals(final String expectedIdentity) {
final ArgumentCaptor<LoginAuthenticationToken> tokenCaptor = ArgumentCaptor.forClass(LoginAuthenticationToken.class);
verify(bearerTokenProvider).getBearerToken(tokenCaptor.capture());

assertEquals(expectedIdentity, tokenCaptor.getValue().getName());
}

private Authentication getAssertionAuthentication(final Map<String, List<Object>> 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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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 = "<saml2p:Response/>";

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<String, List<Object>> attributes) {
return Saml2ResponseAssertion.withResponseValue(RESPONSE_VALUE)
.nameId(NAME_IDENTIFIER)
.attributes(attributes)
.build();
}
}
Loading