diff --git a/docs/content/concepts/authorization.md b/docs/content/concepts/authorization.md index cce7c214f1..e1aa7434b8 100644 --- a/docs/content/concepts/authorization.md +++ b/docs/content/concepts/authorization.md @@ -50,13 +50,11 @@ hawkbit supports authentication providers which use the OpenID Connect standard, An example configuration is given below. 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.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 +Note: at the moment only DEFAULT tenant is supported. By default the resource_access//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 - READ_/UPDATE_/CREATE_/DELETE_TARGET for: diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java index 88e2e79aa5..d532fc6a63 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java @@ -29,7 +29,6 @@ import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter; import org.eclipse.hawkbit.repository.SystemManagement; 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.security.oauth2.client.ClientsConfiguredCondition; import org.springframework.context.annotation.Bean; @@ -47,6 +46,7 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; 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.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; @@ -80,14 +80,12 @@ public class OidcUserManagementAutoConfiguration { /** - * @return the oauth2 user details service to load a user from oidc user - * manager + * @return the OpenID Connect authentication success handler */ @Bean - @ConditionalOnMissingBean - public OAuth2UserService oidcUserDetailsService( - final JwtAuthoritiesExtractor extractor) { - return new JwtAuthoritiesOidcUserService(extractor); + public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler( + final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) { + return new OidcAuthenticationSuccessHandler(systemManagement, systemSecurityContext); } /** @@ -98,14 +96,6 @@ public LogoutSuccessHandler oidcLogoutSuccessHandler() { return new OidcLogoutSuccessHandler(); } - /** - * @return the OpenID Connect authentication success handler - */ - @Bean - public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler() { - return new OidcAuthenticationSuccessHandler(); - } - /** * @return the OpenID Connect logout handler */ @@ -116,7 +106,7 @@ public LogoutHandler oidcLogoutHandler() { /** * @return a jwt authorities extractor which interprets the roles of a user - * as their authorities. + * as their authorities. */ @Bean @ConditionalOnMissingBean @@ -125,244 +115,258 @@ public JwtAuthoritiesExtractor jwtAuthoritiesExtractor() { authorityMapper.setPrefix(""); authorityMapper.setConvertToUpperCase(true); - return new JwtAuthoritiesExtractor(authorityMapper); + return new DefaultJwtAuthoritiesExtractor(authorityMapper); } /** - * @return an authentication filter for using OAuth2 Bearer Tokens. + * @return the oauth2 user details service to load a user from oidc user manager */ @Bean @ConditionalOnMissingBean - public OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter() { - return new OidcBearerTokenAuthenticationFilter(); + OAuth2UserService oidcUserDetailsService( + final JwtAuthoritiesExtractor extractor) { + return new JwtAuthoritiesOidcUserService(extractor); } -} - -/** - * Extended {@link OidcUserService} supporting JWT containing authorities - */ -class JwtAuthoritiesOidcUserService extends OidcUserService { - private final JwtAuthoritiesExtractor authoritiesExtractor; + /** + * @return an authentication filter for using OAuth2 Bearer Tokens. + */ + @Bean + @ConditionalOnMissingBean + OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter( + final JwtAuthoritiesExtractor authoritiesExtractor, + final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) { + return new OidcBearerTokenAuthenticationFilter( + authoritiesExtractor, systemManagement, systemSecurityContext); + } - JwtAuthoritiesOidcUserService(final JwtAuthoritiesExtractor authoritiesExtractor) { - super(); + /** + * By registering bean of such type hawkBit could be customized to extract authorities from the token. + */ + public interface JwtAuthoritiesExtractor { - this.authoritiesExtractor = authoritiesExtractor; + Set extract(final Jwt token, final ClientRegistration clientRegistration ); } - @Override - public OidcUser loadUser(final OidcUserRequest userRequest) { - final OidcUser user = super.loadUser(userRequest); - final ClientRegistration clientRegistration = userRequest.getClientRegistration(); + /** + * Extended {@link OidcUserService} supporting JWT containing authorities + */ + private static class JwtAuthoritiesOidcUserService extends OidcUserService { + + private final JwtAuthoritiesExtractor authoritiesExtractor; - final Set authorities = authoritiesExtractor.extract(clientRegistration, - userRequest.getAccessToken().getTokenValue()); - if (authorities.isEmpty()) { - return user; + JwtAuthoritiesOidcUserService(final JwtAuthoritiesExtractor authoritiesExtractor) { + this.authoritiesExtractor = authoritiesExtractor; } - final String userNameAttributeName = clientRegistration.getProviderDetails().getUserInfoEndpoint() - .getUserNameAttributeName(); - OidcUser oidcUser; - if (StringUtils.hasText(userNameAttributeName)) { - oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo(), - userNameAttributeName); - } else { - oidcUser = new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo()); + @Override + public OidcUser loadUser(final OidcUserRequest userRequest) { + final OidcUser user = super.loadUser(userRequest); + final ClientRegistration clientRegistration = userRequest.getClientRegistration(); + + // Token is already verified by spring security + final NimbusJwtDecoder jwtDecoder = + NimbusJwtDecoder + .withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri()) + .jwsAlgorithm(SignatureAlgorithm.from(JwsAlgorithms.RS256)) + .build(); + final Jwt token = jwtDecoder.decode(userRequest.getAccessToken().getTokenValue()); + final Set authorities = authoritiesExtractor.extract(token, clientRegistration); + if (authorities.isEmpty()) { + return user; + } + + 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; } - return oidcUser; } -} -/** - * OpenID Connect Authentication Success Handler which load tenant data - */ -class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + /** + * OpenID Connect Authentication Success Handler which load tenant data + */ + private static class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { - @Autowired - private SystemManagement systemManagement; + private final SystemManagement systemManagement; + private final SystemSecurityContext systemSecurityContext; - @Autowired - private SystemSecurityContext systemSecurityContext; + OidcAuthenticationSuccessHandler( + final SystemManagement systemManagement, final SystemSecurityContext systemSecurityContext) { + this.systemManagement = systemManagement; + this.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"; + @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"; - final AbstractAuthenticationToken token = (AbstractAuthenticationToken) authentication; - token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false)); + token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false)); - systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant); - } + systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant); + } - super.onAuthenticationSuccess(request, response, authentication); + super.onAuthenticationSuccess(request, response, authentication); + } } -} -/** - * LogoutHandler to invalidate OpenID Connect tokens - */ -class OidcLogoutHandler extends SecurityContextLogoutHandler { + /** + * LogoutHandler to invalidate OpenID Connect tokens + */ + private static class OidcLogoutHandler extends SecurityContextLogoutHandler { - @Override - public void logout(final HttpServletRequest request, final HttpServletResponse response, - final Authentication authentication) { - super.logout(request, response, authentication); + @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 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 UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endSessionEndpoint) + .queryParam("id_token_hint", user.getIdToken().getTokenValue()); - final RestTemplate restTemplate = new RestTemplate(); - restTemplate.getForEntity(builder.toUriString(), String.class); + 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"); + /** + * LogoutSuccessHandler that decides where to redirect to after logout, depending on + * 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); } - 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); + /** + * 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 { - JwtAuthoritiesExtractor(final GrantedAuthoritiesMapper authoritiesMapper) { - super(); + private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); - this.authoritiesMapper = authoritiesMapper; - } - - Set extract(final ClientRegistration clientRegistration, final String tokenValue) { - try { - // Token is already verified by spring security - final NimbusJwtDecoder jwtDecoder = - NimbusJwtDecoder - .withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri()) - .jwsAlgorithm(SignatureAlgorithm.from(JwsAlgorithms.RS256)) - .build(); - final Jwt token = jwtDecoder.decode(tokenValue); - - return extract(clientRegistration.getClientId(), token.getClaims()); - } catch (final JwtException e) { - throw new OAuth2AuthenticationException(INVALID_REQUEST, e); + @Override + public Set 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") - Set extract(final String clientId, final Map claims) { - final Map resourceMap = (Map) claims.get("resource_access"); - if (CollectionUtils.isEmpty(resourceMap)) { - return Collections.emptySet(); - } + @SuppressWarnings("unchecked") + private Set extract(final String clientId, final Map claims) { + final Map resourceMap = (Map) claims.get("resource_access"); + if (CollectionUtils.isEmpty(resourceMap)) { + return Collections.emptySet(); + } - final Map> clientResource = (Map>) resourceMap - .get(clientId); - if (CollectionUtils.isEmpty(clientResource)) { - return Collections.emptySet(); - } + final Map> clientResource = (Map>) resourceMap + .get(clientId); + if (CollectionUtils.isEmpty(clientResource)) { + return Collections.emptySet(); + } - final List roles = (List) clientResource.get("roles"); - if (CollectionUtils.isEmpty(roles)) { - return Collections.emptySet(); - } + final List roles = (List) clientResource.get("roles"); + if (CollectionUtils.isEmpty(roles)) { + return Collections.emptySet(); + } - final List authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0])); - if (authoritiesMapper != null) { - return new LinkedHashSet<>(authoritiesMapper.mapAuthorities(authorities)); - } + final List authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0])); + if (authoritiesMapper != null) { + return new LinkedHashSet<>(authoritiesMapper.mapAuthorities(authorities)); + } - return new LinkedHashSet<>(authorities); + return new LinkedHashSet<>(authorities); + } } -} - -class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter { - @Autowired - private JwtAuthoritiesExtractor authoritiesExtractor; + static class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter { - @Autowired - private SystemManagement systemManagement; + private final JwtAuthoritiesExtractor authoritiesExtractor; + private final SystemManagement systemManagement; + private final SystemSecurityContext systemSecurityContext; - @Autowired - private SystemSecurityContext systemSecurityContext; + private ClientRegistration clientRegistration; - 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; - } + void setClientRegistration(final ClientRegistration clientRegistration) { + this.clientRegistration = clientRegistration; + } - @Override - public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) - throws IOException, ServletException { + @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 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication instanceof 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 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 authorities = authoritiesExtractor.extract(jwt, clientRegistration); - final Set authorities = authoritiesExtractor.extract(clientRegistration.getClientId(), - jwt.getClaims()); + if (authorities.isEmpty()) { + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } - if (authorities.isEmpty()) { - ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); - return; - } + final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo); - final DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo); + final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(user, authorities, + clientRegistration.getRegistrationId()); - final OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(user, authorities, - clientRegistration.getRegistrationId()); + oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false)); - oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false)); + systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant); + SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken); + } - systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant); - SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken); + chain.doFilter(request, response); } - chain.doFilter(request, response); - } - - @Override - public void init(final FilterConfig filterConfig) { - // Nothing to do - } + @Override + public void init(final FilterConfig filterConfig) { + // Nothing to do + } - @Override - public void destroy() { - // Nothing to do + @Override + public void destroy() { + // Nothing to do + } } } diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index 2706972086..762ce2345c 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -477,12 +477,13 @@ public FilterRegistrationBean dosFilterREST() { @Bean @Order(350) - protected SecurityFilterChain filterChainREST( + SecurityFilterChain filterChainREST( final HttpSecurity http, @Lazy final UserAuthenticationFilter userAuthenticationFilter, @Autowired(required = false) - final OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter, + final OidcUserManagementAutoConfiguration.OidcBearerTokenAuthenticationFilter + oidcBearerTokenAuthenticationFilter, @Autowired(required = false) final InMemoryClientRegistrationRepository clientRegistrationRepository, final SystemManagement systemManagement,