diff --git a/hawkbit-mgmt/hawkbit-mgmt-starter/pom.xml b/hawkbit-mgmt/hawkbit-mgmt-starter/pom.xml index e4702d81f..8ce183e46 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-starter/pom.xml +++ b/hawkbit-mgmt/hawkbit-mgmt-starter/pom.xml @@ -72,6 +72,14 @@ org.springframework.security spring-security-aspects + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/autoconfigure/mgmt/MgmtSecurityConfiguration.java b/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/autoconfigure/mgmt/MgmtSecurityConfiguration.java index 3156a4fad..e0e62b71a 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/autoconfigure/mgmt/MgmtSecurityConfiguration.java +++ b/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/autoconfigure/mgmt/MgmtSecurityConfiguration.java @@ -10,18 +10,28 @@ package org.eclipse.hawkbit.autoconfigure.mgmt; import java.util.List; +import java.util.Collection; +import java.util.Map; +import java.util.HashSet; +import java.util.stream.Collectors; +import lombok.Getter; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.oidc.OidcProperties; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.rest.SecurityManagedConfiguration; import org.eclipse.hawkbit.rest.security.DosFilter; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.MdcHandler; import org.eclipse.hawkbit.security.SystemSecurityContext; +import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.tenancy.TenantAwareUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,7 +43,11 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.session.SessionManagementFilter; @@ -42,13 +56,16 @@ import org.springframework.security.web.session.SessionManagementFilter; * Security configuration for the REST management API. */ @Configuration +@EnableConfigurationProperties({HawkbitSecurityProperties.class, OidcProperties.class}) @EnableWebSecurity public class MgmtSecurityConfiguration { private final HawkbitSecurityProperties securityProperties; + private final OidcProperties oidcProperties; - public MgmtSecurityConfiguration(final HawkbitSecurityProperties securityProperties) { + public MgmtSecurityConfiguration(final HawkbitSecurityProperties securityProperties, final OidcProperties oidcProperties) { this.securityProperties = securityProperties; + this.oidcProperties = oidcProperties; } /** @@ -70,6 +87,69 @@ public class MgmtSecurityConfiguration { return filterRegBean; } + public class DefaultOAuth2ResourceServerCustomizer implements Customizer> { + + @Getter + static class HawkbitJwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { + private static final long serialVersionUID = 1L; + + private final String name; + + public HawkbitJwtAuthenticationToken(Jwt jwt, TenantAwareUser user, Collection authorities) { + super(jwt, user, jwt, authorities); + setDetails(new TenantAwareAuthenticationDetails(user.getTenant(), false)); + this.name = jwt.getSubject(); + setAuthenticated(true); + } + + public Map getTokenAttributes() { + return (this.getToken()).getClaims(); + } + + } + + static Object followPathInJWTClaims(Jwt jwt, String path) { + final String[] parts = path.split("\\."); + Object current = jwt.getClaims(); + for (final String part : parts) { + if (current instanceof Map) { + current = ((Map) current).get(part); + } else { + break; + } + } + return current; + } + + @Override + public void customize(OAuth2ResourceServerConfigurer oauth2ResourceServerConfigurer) { + final String usernameClaim = oidcProperties.getOauth2().getResourceserver().getJwt().getClaim().getUsername(); + final String rolesClaim = oidcProperties.getOauth2().getResourceserver().getJwt().getClaim().getRoles(); + final String tenantClaim = oidcProperties.getOauth2().getResourceserver().getJwt().getClaim().getTenant(); + oauth2ResourceServerConfigurer.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> { + + final String username = (String) followPathInJWTClaims(jwt, usernameClaim); + final String tenantName = tenantClaim == null ? "DEFAULT" : (String) followPathInJWTClaims(jwt, tenantClaim); + final Collection authorities = new HashSet(); + final Collection resourceRoles = (Collection) followPathInJWTClaims(jwt, rolesClaim); + if (resourceRoles != null) { + authorities.addAll(resourceRoles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet())); + } + final TenantAwareUser user = new TenantAwareUser(username, username, authorities, tenantName); + return new HawkbitJwtAuthenticationToken(jwt, user, authorities); + })); + } + } + + @Bean(name = "hawkbitOAuth2ResourceServerCustomizer") + @ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.resourceserver", name = "enabled", matchIfMissing = false) + @ConditionalOnMissingBean(name = "hawkbitOAuth2ResourceServerCustomizer") + Customizer> defaultOAuth2ResourceServerCustomizer() { + return new DefaultOAuth2ResourceServerCustomizer(); + } + @Bean @Order(350) SecurityFilterChain filterChainREST( diff --git a/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/oidc/OidcProperties.java b/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/oidc/OidcProperties.java new file mode 100644 index 000000000..c081032a1 --- /dev/null +++ b/hawkbit-mgmt/hawkbit-mgmt-starter/src/main/java/org/eclipse/hawkbit/oidc/OidcProperties.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2025 blue-zone GmbH and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.oidc; + +import lombok.Data; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for hawkBit oidc resource server + */ +@Data +@ToString +@ConfigurationProperties("hawkbit.server.security") +public class OidcProperties { + + private final OidcProperties.Oauth2 oauth2 = new OidcProperties.Oauth2(); + + @Data + public static class Oauth2 { + private final OidcProperties.Oauth2.ResourceServer resourceserver = new OidcProperties.Oauth2.ResourceServer(); + + @Data + public static class ResourceServer { + private final OidcProperties.Oauth2.ResourceServer.Jwt jwt = new OidcProperties.Oauth2.ResourceServer.Jwt(); + + /** + * Indicates whether the default OAuth2 resource server configuration is enabled. + * Defaults to false. If false either no Oauth2 resource server is active or a hawkbitOAuth2ResourceServerCustomizer component can be used to define custom OAuth2 resource server behaviour. + * If true, the default spring OAuth2 resource server configuration is activated. + * @see Spring Documentation + */ + private boolean enabled = false; + + @Data + public static class Jwt { + private final OidcProperties.Oauth2.ResourceServer.Jwt.Claim claim = new OidcProperties.Oauth2.ResourceServer.Jwt.Claim(); + + @Data + public static class Claim { + /** + * Defines the claim within the JWT token that supplies the hawkbit username. + */ + private String username = "preferred_username"; + + /** + * Defines the claim within the JWT token that supplies the hawkbit authorities. + */ + private String roles = "roles"; + + /** + * Defines the claim within the JWT token that supplies the hawkbit tenant. + * If null, the DEFAULT tenant is used for every user. + */ + private String tenant = null; + } + } + } + } +} \ No newline at end of file diff --git a/licenses/LICENSE_HEADER_TEMPLATE_BLUEZONE_25.txt b/licenses/LICENSE_HEADER_TEMPLATE_BLUEZONE_25.txt new file mode 100644 index 000000000..e1b295339 --- /dev/null +++ b/licenses/LICENSE_HEADER_TEMPLATE_BLUEZONE_25.txt @@ -0,0 +1,7 @@ +Copyright (c) 2025 blue-zone GmbH and others + +This program and the accompanying materials are made +available under the terms of the Eclipse Public License 2.0 +which is available at https://www.eclipse.org/legal/epl-2.0/ + +SPDX-License-Identifier: EPL-2.0 diff --git a/pom.xml b/pom.xml index 44a610870..818f3d564 100644 --- a/pom.xml +++ b/pom.xml @@ -411,6 +411,7 @@ licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_20.txt licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt licenses/LICENSE_HEADER_TEMPLATE_ENAPTER.txt + licenses/LICENSE_HEADER_TEMPLATE_BLUEZONE_25.txt .3rd-party/**