OICD Pluggable permission mapper (#1469)

By default the resource_access/<client id>/roles claim is mapped to hawkBit permissions.
However, by registering a Spring bean _org.eclipse.hawkbit.autoconfigure.security.OidcUserManagementAutoConfiguration.JwtAuthoritiesExtractor_ a custom extractor permission mapper could be registered.

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2023-11-03 14:52:31 +02:00
committed by GitHub
parent 7b67de3082
commit ac946e76ef
3 changed files with 237 additions and 234 deletions

View File

@@ -50,13 +50,11 @@ hawkbit supports authentication providers which use the OpenID Connect standard,
An example configuration is given below. An example configuration is given below.
spring.security.oauth2.client.registration.oidc.client-id=clientID spring.security.oauth2.client.registration.oidc.client-id=clientID
spring.security.oauth2.client.registration.oidc.client-secret=oidc-client-secret
spring.security.oauth2.client.provider.oidc.issuer-uri=https://oidc-provider/issuer-uri spring.security.oauth2.client.provider.oidc.issuer-uri=https://oidc-provider/issuer-uri
spring.security.oauth2.client.provider.oidc.authorization-uri=https://oidc-provider/authorization-uri
spring.security.oauth2.client.provider.oidc.token-uri=https://oidc-provider/token-uri
spring.security.oauth2.client.provider.oidc.user-info-uri=https://oidc-provider/user-info-uri
spring.security.oauth2.client.provider.oidc.jwk-set-uri=https://oidc-provider/jwk-set-uri spring.security.oauth2.client.provider.oidc.jwk-set-uri=https://oidc-provider/jwk-set-uri
Note: at the moment only DEFAULT tenant is supported. By default the resource_access/<client id>/roles claim is mapped to hawkBit permissions. However, by registering a Spring bean _org.eclipse.hawkbit.autoconfigure.security.OidcUserManagementAutoConfiguration.JwtAuthoritiesExtractor_ a custom extractor permission mapper could be registered.
### Delivered Permissions ### Delivered Permissions
- READ_/UPDATE_/CREATE_/DELETE_TARGET for: - READ_/UPDATE_/CREATE_/DELETE_TARGET for:

View File

@@ -29,7 +29,6 @@ import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter; import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter;
import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -47,6 +46,7 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
@@ -80,14 +80,12 @@ import org.springframework.web.util.UriComponentsBuilder;
public class OidcUserManagementAutoConfiguration { public class OidcUserManagementAutoConfiguration {
/** /**
* @return the oauth2 user details service to load a user from oidc user * @return the OpenID Connect authentication success handler
* manager
*/ */
@Bean @Bean
@ConditionalOnMissingBean public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler(
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserDetailsService( final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
final JwtAuthoritiesExtractor extractor) { return new OidcAuthenticationSuccessHandler(systemManagement, systemSecurityContext);
return new JwtAuthoritiesOidcUserService(extractor);
} }
/** /**
@@ -98,14 +96,6 @@ public class OidcUserManagementAutoConfiguration {
return new OidcLogoutSuccessHandler(); return new OidcLogoutSuccessHandler();
} }
/**
* @return the OpenID Connect authentication success handler
*/
@Bean
public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler() {
return new OidcAuthenticationSuccessHandler();
}
/** /**
* @return the OpenID Connect logout handler * @return the OpenID Connect logout handler
*/ */
@@ -116,7 +106,7 @@ public class OidcUserManagementAutoConfiguration {
/** /**
* @return a jwt authorities extractor which interprets the roles of a user * @return a jwt authorities extractor which interprets the roles of a user
* as their authorities. * as their authorities.
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@@ -125,7 +115,17 @@ public class OidcUserManagementAutoConfiguration {
authorityMapper.setPrefix(""); authorityMapper.setPrefix("");
authorityMapper.setConvertToUpperCase(true); authorityMapper.setConvertToUpperCase(true);
return new JwtAuthoritiesExtractor(authorityMapper); return new DefaultJwtAuthoritiesExtractor(authorityMapper);
}
/**
* @return the oauth2 user details service to load a user from oidc user manager
*/
@Bean
@ConditionalOnMissingBean
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserDetailsService(
final JwtAuthoritiesExtractor extractor) {
return new JwtAuthoritiesOidcUserService(extractor);
} }
/** /**
@@ -133,236 +133,240 @@ public class OidcUserManagementAutoConfiguration {
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter() { OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter(
return new OidcBearerTokenAuthenticationFilter(); final JwtAuthoritiesExtractor authoritiesExtractor,
} final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
} return new OidcBearerTokenAuthenticationFilter(
authoritiesExtractor, systemManagement, systemSecurityContext);
/**
* Extended {@link OidcUserService} supporting JWT containing authorities
*/
class JwtAuthoritiesOidcUserService extends OidcUserService {
private final JwtAuthoritiesExtractor authoritiesExtractor;
JwtAuthoritiesOidcUserService(final JwtAuthoritiesExtractor authoritiesExtractor) {
super();
this.authoritiesExtractor = authoritiesExtractor;
} }
@Override /**
public OidcUser loadUser(final OidcUserRequest userRequest) { * By registering bean of such type hawkBit could be customized to extract authorities from the token.
final OidcUser user = super.loadUser(userRequest); */
final ClientRegistration clientRegistration = userRequest.getClientRegistration(); public interface JwtAuthoritiesExtractor {
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(clientRegistration, Set<GrantedAuthority> extract(final Jwt token, final ClientRegistration clientRegistration );
userRequest.getAccessToken().getTokenValue()); }
if (authorities.isEmpty()) {
return user; /**
* Extended {@link OidcUserService} supporting JWT containing authorities
*/
private static class JwtAuthoritiesOidcUserService extends OidcUserService {
private final JwtAuthoritiesExtractor authoritiesExtractor;
JwtAuthoritiesOidcUserService(final JwtAuthoritiesExtractor authoritiesExtractor) {
this.authoritiesExtractor = authoritiesExtractor;
} }
final String userNameAttributeName = clientRegistration.getProviderDetails().getUserInfoEndpoint() @Override
.getUserNameAttributeName(); public OidcUser loadUser(final OidcUserRequest userRequest) {
OidcUser oidcUser; final OidcUser user = super.loadUser(userRequest);
if (StringUtils.hasText(userNameAttributeName)) { final ClientRegistration clientRegistration = userRequest.getClientRegistration();
oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo(),
userNameAttributeName);
} else {
oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo());
}
return oidcUser;
}
}
/**
* OpenID Connect Authentication Success Handler which load tenant data
*/
class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private SystemManagement systemManagement;
@Autowired
private SystemSecurityContext systemSecurityContext;
@Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
if (authentication instanceof AbstractAuthenticationToken) {
final String defaultTenant = "DEFAULT";
final AbstractAuthenticationToken token = (AbstractAuthenticationToken) authentication;
token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
/**
* LogoutHandler to invalidate OpenID Connect tokens
*/
class OidcLogoutHandler extends SecurityContextLogoutHandler {
@Override
public void logout(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) {
super.logout(request, response, authentication);
final Object principal = authentication.getPrincipal();
if (principal instanceof OidcUser) {
final OidcUser user = (OidcUser) authentication.getPrincipal();
final String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
final RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity(builder.toUriString(), String.class);
}
}
}
/**
* LogoutSuccessHandler that decides where to redirect to after logout, depending on
* the previously used auth mechanism
*/
class OidcLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
this.setTargetUrlParameter("/");
} else {
this.setTargetUrlParameter("login");
}
super.onLogoutSuccess(request, response, authentication);
}
}
/**
* Utility class to extract authorities out of the jwt. It interprets the user's
* role as their authorities.
*/
class JwtAuthoritiesExtractor {
private final GrantedAuthoritiesMapper authoritiesMapper;
private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
JwtAuthoritiesExtractor(final GrantedAuthoritiesMapper authoritiesMapper) {
super();
this.authoritiesMapper = authoritiesMapper;
}
Set<GrantedAuthority> extract(final ClientRegistration clientRegistration, final String tokenValue) {
try {
// Token is already verified by spring security // Token is already verified by spring security
final NimbusJwtDecoder jwtDecoder = final NimbusJwtDecoder jwtDecoder =
NimbusJwtDecoder NimbusJwtDecoder
.withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri()) .withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.from(JwsAlgorithms.RS256)) .jwsAlgorithm(SignatureAlgorithm.from(JwsAlgorithms.RS256))
.build(); .build();
final Jwt token = jwtDecoder.decode(tokenValue); final Jwt token = jwtDecoder.decode(userRequest.getAccessToken().getTokenValue());
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(token, clientRegistration);
return extract(clientRegistration.getClientId(), token.getClaims());
} catch (final JwtException e) {
throw new OAuth2AuthenticationException(INVALID_REQUEST, e);
}
}
@SuppressWarnings("unchecked")
Set<GrantedAuthority> extract(final String clientId, final Map<String, Object> claims) {
final Map<String, Object> resourceMap = (Map<String, Object>) claims.get("resource_access");
if (CollectionUtils.isEmpty(resourceMap)) {
return Collections.emptySet();
}
final Map<String, Map<String, Object>> clientResource = (Map<String, Map<String, Object>>) resourceMap
.get(clientId);
if (CollectionUtils.isEmpty(clientResource)) {
return Collections.emptySet();
}
final List<String> roles = (List<String>) clientResource.get("roles");
if (CollectionUtils.isEmpty(roles)) {
return Collections.emptySet();
}
final List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
if (authoritiesMapper != null) {
return new LinkedHashSet<>(authoritiesMapper.mapAuthorities(authorities));
}
return new LinkedHashSet<>(authorities);
}
}
class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter {
@Autowired
private JwtAuthoritiesExtractor authoritiesExtractor;
@Autowired
private SystemManagement systemManagement;
@Autowired
private SystemSecurityContext systemSecurityContext;
private ClientRegistration clientRegistration;
void setClientRegistration(final ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken) {
final String defaultTenant = "DEFAULT";
final JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
final Jwt jwt = jwtAuthenticationToken.getToken();
final OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
final OidcUserInfo userInfo = new OidcUserInfo(jwt.getClaims());
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(clientRegistration.getClientId(),
jwt.getClaims());
if (authorities.isEmpty()) { if (authorities.isEmpty()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); return user;
return;
} }
final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo); final String userNameAttributeName = clientRegistration.getProviderDetails().getUserInfoEndpoint()
.getUserNameAttributeName();
final OidcUser oidcUser;
if (StringUtils.hasText(userNameAttributeName)) {
oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo(),
userNameAttributeName);
} else {
oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo());
}
return oidcUser;
}
}
final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(user, authorities, /**
clientRegistration.getRegistrationId()); * OpenID Connect Authentication Success Handler which load tenant data
*/
private static class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false)); private final SystemManagement systemManagement;
private final SystemSecurityContext systemSecurityContext;
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant); OidcAuthenticationSuccessHandler(
SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken); final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
this.systemManagement = systemManagement;
this.systemSecurityContext = systemSecurityContext;
} }
chain.doFilter(request, response); @Override
public void onAuthenticationSuccess(
final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
if (authentication instanceof AbstractAuthenticationToken token) {
final String defaultTenant = "DEFAULT";
token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
}
super.onAuthenticationSuccess(request, response, authentication);
}
} }
@Override /**
public void init(final FilterConfig filterConfig) { * LogoutHandler to invalidate OpenID Connect tokens
// Nothing to do */
private static class OidcLogoutHandler extends SecurityContextLogoutHandler {
@Override
public void logout(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) {
super.logout(request, response, authentication);
final Object principal = authentication.getPrincipal();
if (principal instanceof OidcUser) {
final OidcUser user = (OidcUser) authentication.getPrincipal();
final String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
final RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity(builder.toUriString(), String.class);
}
}
} }
@Override /**
public void destroy() { * LogoutSuccessHandler that decides where to redirect to after logout, depending on
// Nothing to do * the previously used auth mechanism
*/
private static class OidcLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
this.setTargetUrlParameter("/");
} else {
this.setTargetUrlParameter("login");
}
super.onLogoutSuccess(request, response, authentication);
}
}
/**
* Utility class to extract authorities out of the jwt. It interprets the user's
* role as their authorities.
*/
private record DefaultJwtAuthoritiesExtractor
(GrantedAuthoritiesMapper authoritiesMapper) implements JwtAuthoritiesExtractor {
private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
@Override
public Set<GrantedAuthority> extract(final Jwt token, final ClientRegistration clientRegistration) {
try {
return extract(clientRegistration.getClientId(), token.getClaims());
} catch (final JwtException e) {
throw new OAuth2AuthenticationException(INVALID_REQUEST, e);
}
}
@SuppressWarnings("unchecked")
private Set<GrantedAuthority> extract(final String clientId, final Map<String, Object> claims) {
final Map<String, Object> resourceMap = (Map<String, Object>) claims.get("resource_access");
if (CollectionUtils.isEmpty(resourceMap)) {
return Collections.emptySet();
}
final Map<String, Map<String, Object>> clientResource = (Map<String, Map<String, Object>>) resourceMap
.get(clientId);
if (CollectionUtils.isEmpty(clientResource)) {
return Collections.emptySet();
}
final List<String> roles = (List<String>) clientResource.get("roles");
if (CollectionUtils.isEmpty(roles)) {
return Collections.emptySet();
}
final List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
if (authoritiesMapper != null) {
return new LinkedHashSet<>(authoritiesMapper.mapAuthorities(authorities));
}
return new LinkedHashSet<>(authorities);
}
}
static class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter {
private final JwtAuthoritiesExtractor authoritiesExtractor;
private final SystemManagement systemManagement;
private final SystemSecurityContext systemSecurityContext;
private ClientRegistration clientRegistration;
OidcBearerTokenAuthenticationFilter(
final JwtAuthoritiesExtractor authoritiesExtractor,
final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) {
this.authoritiesExtractor = authoritiesExtractor;
this.systemManagement = systemManagement;
this.systemSecurityContext = systemSecurityContext;
}
void setClientRegistration(final ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken jwtAuthenticationToken) {
final String defaultTenant = "DEFAULT";
final Jwt jwt = jwtAuthenticationToken.getToken();
final OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
final OidcUserInfo userInfo = new OidcUserInfo(jwt.getClaims());
final Set<GrantedAuthority> authorities = authoritiesExtractor.extract(jwt, clientRegistration);
if (authorities.isEmpty()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo);
final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(user, authorities,
clientRegistration.getRegistrationId());
oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken);
}
chain.doFilter(request, response);
}
@Override
public void init(final FilterConfig filterConfig) {
// Nothing to do
}
@Override
public void destroy() {
// Nothing to do
}
} }
} }

View File

@@ -477,12 +477,13 @@ public class SecurityManagedConfiguration {
@Bean @Bean
@Order(350) @Order(350)
protected SecurityFilterChain filterChainREST( SecurityFilterChain filterChainREST(
final HttpSecurity http, final HttpSecurity http,
@Lazy @Lazy
final UserAuthenticationFilter userAuthenticationFilter, final UserAuthenticationFilter userAuthenticationFilter,
@Autowired(required = false) @Autowired(required = false)
final OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter, final OidcUserManagementAutoConfiguration.OidcBearerTokenAuthenticationFilter
oidcBearerTokenAuthenticationFilter,
@Autowired(required = false) @Autowired(required = false)
final InMemoryClientRegistrationRepository clientRegistrationRepository, final InMemoryClientRegistrationRepository clientRegistrationRepository,
final SystemManagement systemManagement, final SystemManagement systemManagement,