mirror of https://github.com/apache/nifi.git
NIFI-11781 Corrected OIDC Claim Identity Processing
- Added StandardOidcUserService supporting fallback claim names - Updated StandardClientRegistrationProvider to use standard Subject claim - Updated OIDC Security Configuration to use customized OidcUserService for claim handling Signed-off-by: Joe Gresock <jgresock@gmail.com> This closes #7468.
This commit is contained in:
parent
437995b75a
commit
95bb23d403
|
@ -50,6 +50,7 @@ import org.apache.nifi.web.security.oidc.registration.DisabledClientRegistration
|
||||||
import org.apache.nifi.web.security.oidc.registration.StandardClientRegistrationProvider;
|
import org.apache.nifi.web.security.oidc.registration.StandardClientRegistrationProvider;
|
||||||
import org.apache.nifi.web.security.oidc.revocation.StandardTokenRevocationResponseClient;
|
import org.apache.nifi.web.security.oidc.revocation.StandardTokenRevocationResponseClient;
|
||||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
||||||
|
import org.apache.nifi.web.security.oidc.userinfo.StandardOidcUserService;
|
||||||
import org.apache.nifi.web.security.oidc.web.authentication.OidcAuthenticationSuccessHandler;
|
import org.apache.nifi.web.security.oidc.web.authentication.OidcAuthenticationSuccessHandler;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cache.caffeine.CaffeineCache;
|
import org.springframework.cache.caffeine.CaffeineCache;
|
||||||
|
@ -296,7 +297,10 @@ public class OidcSecurityConfiguration {
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public OidcUserService oidcUserService() {
|
public OidcUserService oidcUserService() {
|
||||||
final OidcUserService oidcUserService = new OidcUserService();
|
final StandardOidcUserService oidcUserService = new StandardOidcUserService(
|
||||||
|
getUserClaimNames(),
|
||||||
|
IdentityMappingUtil.getIdentityMappings(properties)
|
||||||
|
);
|
||||||
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
|
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
|
||||||
userService.setRestOperations(oidcRestOperations());
|
userService.setRestOperations(oidcRestOperations());
|
||||||
oidcUserService.setOauth2UserService(userService);
|
oidcUserService.setOauth2UserService(userService);
|
||||||
|
@ -468,9 +472,7 @@ public class OidcSecurityConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
private OidcAuthenticationSuccessHandler getAuthenticationSuccessHandler() {
|
private OidcAuthenticationSuccessHandler getAuthenticationSuccessHandler() {
|
||||||
final List<String> userClaimNames = new ArrayList<>();
|
final List<String> userClaimNames = getUserClaimNames();
|
||||||
userClaimNames.add(properties.getOidcClaimIdentifyingUser());
|
|
||||||
userClaimNames.addAll(properties.getOidcFallbackClaimsIdentifyingUser());
|
|
||||||
|
|
||||||
return new OidcAuthenticationSuccessHandler(
|
return new OidcAuthenticationSuccessHandler(
|
||||||
bearerTokenProvider,
|
bearerTokenProvider,
|
||||||
|
@ -480,4 +482,11 @@ public class OidcSecurityConfiguration {
|
||||||
properties.getOidcClaimGroups()
|
properties.getOidcClaimGroups()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> getUserClaimNames() {
|
||||||
|
final List<String> userClaimNames = new ArrayList<>();
|
||||||
|
userClaimNames.add(properties.getOidcClaimIdentifyingUser());
|
||||||
|
userClaimNames.addAll(properties.getOidcFallbackClaimsIdentifyingUser());
|
||||||
|
return userClaimNames;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||||
import org.springframework.web.client.RestOperations;
|
import org.springframework.web.client.RestOperations;
|
||||||
|
|
||||||
|
@ -85,8 +86,6 @@ public class StandardClientRegistrationProvider implements ClientRegistrationPro
|
||||||
final List<String> additionalScopes = properties.getOidcAdditionalScopes();
|
final List<String> additionalScopes = properties.getOidcAdditionalScopes();
|
||||||
scope.addAll(additionalScopes);
|
scope.addAll(additionalScopes);
|
||||||
|
|
||||||
final String userNameAttributeName = properties.getOidcClaimIdentifyingUser();
|
|
||||||
|
|
||||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||||
.clientId(clientId)
|
.clientId(clientId)
|
||||||
.clientSecret(clientSecret)
|
.clientSecret(clientSecret)
|
||||||
|
@ -99,7 +98,8 @@ public class StandardClientRegistrationProvider implements ClientRegistrationPro
|
||||||
.providerConfigurationMetadata(configurationMetadata)
|
.providerConfigurationMetadata(configurationMetadata)
|
||||||
.redirectUri(REGISTRATION_REDIRECT_URI)
|
.redirectUri(REGISTRATION_REDIRECT_URI)
|
||||||
.scope(scope)
|
.scope(scope)
|
||||||
.userNameAttributeName(userNameAttributeName)
|
// OpenID Connect 1.0 requires the sub claim and other components handle application username mapping
|
||||||
|
.userNameAttributeName(IdTokenClaimNames.SUB)
|
||||||
.clientAuthenticationMethod(clientAuthenticationMethod)
|
.clientAuthenticationMethod(clientAuthenticationMethod)
|
||||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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.userinfo;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard extension of Spring Security OIDC User supporting customized name configuration
|
||||||
|
*/
|
||||||
|
class StandardOidcUser extends DefaultOidcUser {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard OIDC User constructor with required parameters and customized name value for identification
|
||||||
|
*
|
||||||
|
* @param authorities Granted Authorities
|
||||||
|
* @param idToken OIDC ID Token
|
||||||
|
* @param userInfo OIDC User Information
|
||||||
|
* @param nameAttributeKey Claim name that parent class uses to determine username for identification
|
||||||
|
* @param name Customized name identifying the user
|
||||||
|
*/
|
||||||
|
public StandardOidcUser(
|
||||||
|
final Collection<? extends GrantedAuthority> authorities,
|
||||||
|
final OidcIdToken idToken,
|
||||||
|
final OidcUserInfo userInfo,
|
||||||
|
final String nameAttributeKey,
|
||||||
|
final String name
|
||||||
|
) {
|
||||||
|
super(authorities, idToken, userInfo, nameAttributeKey);
|
||||||
|
this.name = Objects.requireNonNull(name, "Name required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.userinfo;
|
||||||
|
|
||||||
|
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||||
|
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||||
|
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard extension of Spring Security OIDC User Service supporting customized identity claim mapping
|
||||||
|
*/
|
||||||
|
public class StandardOidcUserService extends OidcUserService {
|
||||||
|
private final List<String> userClaimNames;
|
||||||
|
|
||||||
|
private final List<IdentityMapping> userIdentityMappings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard OIDC User Service constructor with arguments derived from application properties for mapping usernames
|
||||||
|
*
|
||||||
|
* @param userClaimNames Ordered list of Token Claim names from which to determine the user identity
|
||||||
|
* @param userIdentityMappings List of Identity Mapping rules for optional transformation of a user identity
|
||||||
|
*/
|
||||||
|
public StandardOidcUserService(final List<String> userClaimNames, final List<IdentityMapping> userIdentityMappings) {
|
||||||
|
this.userClaimNames = Objects.requireNonNull(userClaimNames, "User Claim Names required");
|
||||||
|
this.userIdentityMappings = Objects.requireNonNull(userIdentityMappings, "User Identity Mappings required");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load User with user identity based on first available Token Claim found
|
||||||
|
*
|
||||||
|
* @param userRequest OIDC User Request information
|
||||||
|
* @return Standard OIDC User
|
||||||
|
* @throws OAuth2AuthenticationException Thrown on failures loading user information from Identity Provider
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OidcUser loadUser(final OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
final OidcUser oidcUser = super.loadUser(userRequest);
|
||||||
|
final String userClaimName = getUserClaimName(oidcUser);
|
||||||
|
final String claim = oidcUser.getClaimAsString(userClaimName);
|
||||||
|
final String name = IdentityMappingUtil.mapIdentity(claim, userIdentityMappings);
|
||||||
|
return new StandardOidcUser(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo(), userClaimName, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserClaimName(final OidcUser oidcUser) {
|
||||||
|
final Optional<String> userClaimNameFound = userClaimNames.stream()
|
||||||
|
.filter(oidcUser::hasClaim)
|
||||||
|
.findFirst();
|
||||||
|
return userClaimNameFound.orElseThrow(() -> {
|
||||||
|
final String message = String.format("User Claim Name not found in configured Token Claims %s", userClaimNames);
|
||||||
|
return new OidcConfigurationException(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ class StandardClientRegistrationProviderTest {
|
||||||
|
|
||||||
private static final String CLIENT_SECRET = "client-secret";
|
private static final String CLIENT_SECRET = "client-secret";
|
||||||
|
|
||||||
private static final String USER_NAME_ATTRIBUTE_NAME = "email";
|
private static final String USER_NAME_ATTRIBUTE_NAME = "sub";
|
||||||
|
|
||||||
private static final Set<String> EXPECTED_SCOPES = new LinkedHashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.EMAIL, OidcScopes.PROFILE));
|
private static final Set<String> EXPECTED_SCOPES = new LinkedHashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.EMAIL, OidcScopes.PROFILE));
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* 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.userinfo;
|
||||||
|
|
||||||
|
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||||
|
import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
|
||||||
|
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||||
|
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
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.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class StandardOidcUserServiceTest {
|
||||||
|
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 CLIENT_ID = "client-id";
|
||||||
|
|
||||||
|
private static final String ACCESS_TOKEN = "access";
|
||||||
|
|
||||||
|
private static final String ID_TOKEN = "id";
|
||||||
|
|
||||||
|
private static final String USER_NAME_CLAIM = "email";
|
||||||
|
|
||||||
|
private static final String FALLBACK_CLAIM = "preferred_username";
|
||||||
|
|
||||||
|
private static final String MISSING_CLAIM = "missing";
|
||||||
|
|
||||||
|
private static final String SUBJECT = String.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final String IDENTITY = Authentication.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final String FIRST_GROUP = "$1";
|
||||||
|
|
||||||
|
private static final Pattern MATCH_PATTERN = Pattern.compile("(.*)");
|
||||||
|
|
||||||
|
private static final IdentityMapping UPPER_IDENTITY_MAPPING = new IdentityMapping(
|
||||||
|
IdentityMapping.Transform.UPPER.toString(),
|
||||||
|
MATCH_PATTERN,
|
||||||
|
FIRST_GROUP,
|
||||||
|
IdentityMapping.Transform.UPPER
|
||||||
|
);
|
||||||
|
|
||||||
|
private StandardOidcUserService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setService() {
|
||||||
|
service = new StandardOidcUserService(
|
||||||
|
Arrays.asList(USER_NAME_CLAIM, FALLBACK_CLAIM),
|
||||||
|
Collections.singletonList(UPPER_IDENTITY_MAPPING)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLoadUser() {
|
||||||
|
final OidcUserRequest userRequest = getUserRequest(USER_NAME_CLAIM);
|
||||||
|
final OidcUser oidcUser = service.loadUser(userRequest);
|
||||||
|
|
||||||
|
assertNotNull(oidcUser);
|
||||||
|
assertEquals(IDENTITY.toUpperCase(), oidcUser.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLoadUserFallbackClaim() {
|
||||||
|
final OidcUserRequest userRequest = getUserRequest(FALLBACK_CLAIM);
|
||||||
|
final OidcUser oidcUser = service.loadUser(userRequest);
|
||||||
|
|
||||||
|
assertNotNull(oidcUser);
|
||||||
|
assertEquals(IDENTITY.toUpperCase(), oidcUser.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLoadUserClaimNotFound() {
|
||||||
|
final OidcUserRequest userRequest = getUserRequest(MISSING_CLAIM);
|
||||||
|
|
||||||
|
assertThrows(OidcConfigurationException.class, () -> service.loadUser(userRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
OidcUserRequest getUserRequest(final String userNameClaim) {
|
||||||
|
final ClientRegistration clientRegistration = getClientRegistrationBuilder().build();
|
||||||
|
|
||||||
|
final Instant issuedAt = Instant.now();
|
||||||
|
final Instant expiresAt = Instant.MAX;
|
||||||
|
final OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN, issuedAt, expiresAt);
|
||||||
|
|
||||||
|
final Map<String, Object> claims = getClaims(userNameClaim);
|
||||||
|
|
||||||
|
final OidcIdToken idToken = new OidcIdToken(ID_TOKEN, issuedAt, expiresAt, claims);
|
||||||
|
return new OidcUserRequest(clientRegistration, accessToken, idToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> getClaims(final String userNameClaim) {
|
||||||
|
final Map<String, Object> claims = new LinkedHashMap<>();
|
||||||
|
claims.put(SupportedClaim.SUBJECT.getClaim(), SUBJECT);
|
||||||
|
claims.put(SupportedClaim.ISSUED_AT.getClaim(), Instant.now());
|
||||||
|
claims.put(SupportedClaim.EXPIRATION.getClaim(), Instant.MAX);
|
||||||
|
claims.put(userNameClaim, IDENTITY);
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue