OIDC Authentication/Authorization on Management API (#2386)

* Added Oidc ressource server capabilities to mgmt api to allow users to login via identity provider.

Signed-off-by: ChristianB <christian.breitwieser@blue-zone.at>

* Adress review findings:
 - Code Style fixes
 - Readability improvements

Signed-off-by: ChristianB <christian.breitwieser@blue-zone.at>

---------

Signed-off-by: ChristianB <christian.breitwieser@blue-zone.at>
This commit is contained in:
Christian Breitwieser
2025-05-08 10:19:19 +02:00
committed by GitHub
parent 7456e52095
commit acaec605bd
5 changed files with 164 additions and 1 deletions

View File

@@ -72,6 +72,14 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-aspects</artifactId> <artifactId>spring-security-aspects</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<!-- Spring - END --> <!-- Spring - END -->
</dependencies> </dependencies>
</project> </project>

View File

@@ -10,18 +10,28 @@
package org.eclipse.hawkbit.autoconfigure.mgmt; package org.eclipse.hawkbit.autoconfigure.mgmt;
import java.util.List; 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.im.authentication.SpPermission;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.oidc.OidcProperties;
import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.rest.SecurityManagedConfiguration; import org.eclipse.hawkbit.rest.SecurityManagedConfiguration;
import org.eclipse.hawkbit.rest.security.DosFilter; import org.eclipse.hawkbit.rest.security.DosFilter;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.MdcHandler; import org.eclipse.hawkbit.security.MdcHandler;
import org.eclipse.hawkbit.security.SystemSecurityContext; 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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; 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.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication; 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.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.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.session.SessionManagementFilter; 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. * Security configuration for the REST management API.
*/ */
@Configuration @Configuration
@EnableConfigurationProperties({HawkbitSecurityProperties.class, OidcProperties.class})
@EnableWebSecurity @EnableWebSecurity
public class MgmtSecurityConfiguration { public class MgmtSecurityConfiguration {
private final HawkbitSecurityProperties securityProperties; 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.securityProperties = securityProperties;
this.oidcProperties = oidcProperties;
} }
/** /**
@@ -70,6 +87,69 @@ public class MgmtSecurityConfiguration {
return filterRegBean; return filterRegBean;
} }
public class DefaultOAuth2ResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> {
@Getter
static class HawkbitJwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
private static final long serialVersionUID = 1L;
private final String name;
public HawkbitJwtAuthenticationToken(Jwt jwt, TenantAwareUser user, Collection<GrantedAuthority> authorities) {
super(jwt, user, jwt, authorities);
setDetails(new TenantAwareAuthenticationDetails(user.getTenant(), false));
this.name = jwt.getSubject();
setAuthenticated(true);
}
public Map<String, Object> 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<String, Object>) current).get(part);
} else {
break;
}
}
return current;
}
@Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity> 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<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
final Collection<String> resourceRoles = (Collection<String>) 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<OAuth2ResourceServerConfigurer<HttpSecurity>> defaultOAuth2ResourceServerCustomizer() {
return new DefaultOAuth2ResourceServerCustomizer();
}
@Bean @Bean
@Order(350) @Order(350)
SecurityFilterChain filterChainREST( SecurityFilterChain filterChainREST(

View File

@@ -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 <a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#_specifying_the_authorization_server">Spring Documentation</a>
*/
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;
}
}
}
}
}

View File

@@ -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

View File

@@ -411,6 +411,7 @@
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_20.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_20.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_ENAPTER.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_ENAPTER.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_BLUEZONE_25.txt</validHeader>
</validHeaders> </validHeaders>
<excludes> <excludes>
<exclude>.3rd-party/**</exclude> <exclude>.3rd-party/**</exclude>