diff --git a/hawkbit-simple-ui/pom.xml b/hawkbit-simple-ui/pom.xml
index 50244850c..0d6f337d9 100644
--- a/hawkbit-simple-ui/pom.xml
+++ b/hawkbit-simple-ui/pom.xml
@@ -81,6 +81,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java
index 4c9fb81ac..7abb6092f 100644
--- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java
+++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java
@@ -9,13 +9,6 @@
*/
package org.eclipse.hawkbit.ui.simple;
-import static feign.Util.ISO_8859_1;
-
-import java.util.Base64;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Objects;
-
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.PWA;
import com.vaadin.flow.theme.Theme;
@@ -28,7 +21,9 @@ import feign.codec.ErrorDecoder;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
+import org.eclipse.hawkbit.ui.simple.security.OAuth2TokenManager;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
@@ -40,6 +35,21 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+import java.util.Base64;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+import static feign.Util.ISO_8859_1;
+import static java.util.Collections.emptyList;
@Theme(themeClass = Lumo.class)
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
@@ -47,11 +57,18 @@ import org.springframework.security.core.context.SecurityContextHolder;
@Import(FeignClientsConfiguration.class)
public class SimpleUIApp implements AppShellConfigurator {
- private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
+ private static final Function AUTHORIZATION = (oAuth2TokenManager) -> requestTemplate -> {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- requestTemplate.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(
- (Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
- authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1)));
+ if (oAuth2TokenManager != null && authentication instanceof OAuth2AuthenticationToken oAuth2AuthenticationToken) {
+ String bearerToken = oAuth2TokenManager.getToken(oAuth2AuthenticationToken);
+ requestTemplate.header("Authorization", "Bearer " + bearerToken);
+ } else {
+ requestTemplate.header(
+ "Authorization", "Basic " + Base64.getEncoder().encodeToString(
+ (Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
+ authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1))
+ );
+ }
};
private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
@@ -67,13 +84,21 @@ public class SimpleUIApp implements AppShellConfigurator {
@Bean
HawkbitClient hawkbitClient(
- final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
+ final HawkbitServer hawkBitServer,
+ final Encoder encoder,
+ final Decoder decoder,
+ final Contract contract,
+ @Autowired(required = false)
+ final OAuth2TokenManager oAuth2TokenManager
+ ) {
return new HawkbitClient(
hawkBitServer, encoder, decoder, contract,
ERROR_DECODER,
(tenant, controller) ->
- controller == null ?
- AUTHORIZATION : HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller));
+ controller == null
+ ? AUTHORIZATION.apply(oAuth2TokenManager)
+ : HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller)
+ );
}
@Bean
@@ -81,6 +106,27 @@ public class SimpleUIApp implements AppShellConfigurator {
return new HawkbitMgmtClient(tenant, hawkbitClient);
}
+ @Bean
+ OAuth2UserService oidcUserService(final HawkbitMgmtClient hawkbitClient) {
+ final OidcUserService delegate = new OidcUserService();
+ return (userRequest) -> {
+ OidcUser oidcUser = delegate.loadUser(userRequest);
+
+ final OAuth2AuthenticationToken tempToken = new OAuth2AuthenticationToken(
+ oidcUser,
+ emptyList(),
+ userRequest.getClientRegistration().getRegistrationId()
+ );
+ final List grantedAuthorities =
+ getGrantedAuthorities(hawkbitClient, tempToken);
+ return new DefaultOidcUser(
+ grantedAuthorities,
+ oidcUser.getIdToken(),
+ oidcUser.getUserInfo()
+ );
+ };
+ }
+
// accepts all user / pass, just delegating them to the feign client
@Bean
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient) {
@@ -88,35 +134,9 @@ public class SimpleUIApp implements AppShellConfigurator {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
- final List roles = new LinkedList<>();
- roles.add("ANONYMOUS");
- final SecurityContext unauthorizedContext = SecurityContextHolder.createEmptyContext();
- unauthorizedContext.setAuthentication(
- new UsernamePasswordAuthenticationToken(username, password));
- final SecurityContext currentContext = SecurityContextHolder.getContext();
- try {
- SecurityContextHolder.setContext(unauthorizedContext);
- if (hawkbitClient.hasSoftwareModulesRead()) {
- roles.add("SOFTWARE_MODULE_READ");
- }
- if (hawkbitClient.hasRolloutRead()) {
- roles.add("ROLLOUT_READ");
- }
- if (hawkbitClient.hasDistributionSetRead()) {
- roles.add("DISTRIBUTION_SET_READ");
- }
- if (hawkbitClient.hasTargetRead()) {
- roles.add("TARGET_READ");
- }
- if (hawkbitClient.hasConfigRead()) {
- roles.add("CONFIG_READ");
- }
- } finally {
- SecurityContextHolder.setContext(currentContext);
- }
- return new UsernamePasswordAuthenticationToken(
- username, password,
- roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()) {
+ final List grantedAuthorities = getGrantedAuthorities(
+ hawkbitClient, new UsernamePasswordAuthenticationToken(username, password));
+ return new UsernamePasswordAuthenticationToken(username, password, grantedAuthorities) {
@Override
public void eraseCredentials() {
@@ -126,4 +146,33 @@ public class SimpleUIApp implements AppShellConfigurator {
};
};
}
+
+ private List getGrantedAuthorities(final HawkbitMgmtClient hawkbitClient, Authentication authentication) {
+ final List roles = new LinkedList<>();
+ roles.add("ANONYMOUS");
+ final SecurityContext unauthorizedContext = SecurityContextHolder.createEmptyContext();
+ unauthorizedContext.setAuthentication(authentication);
+ final SecurityContext currentContext = SecurityContextHolder.getContext();
+ try {
+ SecurityContextHolder.setContext(unauthorizedContext);
+ if (hawkbitClient.hasSoftwareModulesRead()) {
+ roles.add("SOFTWARE_MODULE_READ");
+ }
+ if (hawkbitClient.hasRolloutRead()) {
+ roles.add("ROLLOUT_READ");
+ }
+ if (hawkbitClient.hasDistributionSetRead()) {
+ roles.add("DISTRIBUTION_SET_READ");
+ }
+ if (hawkbitClient.hasTargetRead()) {
+ roles.add("TARGET_READ");
+ }
+ if (hawkbitClient.hasConfigRead()) {
+ roles.add("CONFIG_READ");
+ }
+ } finally {
+ SecurityContextHolder.setContext(currentContext);
+ }
+ return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
+ }
}
diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java
new file mode 100644
index 000000000..e9c2924ad
--- /dev/null
+++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * 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.ui.simple.security;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
+import org.springframework.security.oauth2.client.endpoint.RestClientRefreshTokenTokenResponseClient;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@Component
+@ConditionalOnBean(name = "hawkbitOAuth2ClientCustomizer")
+public class OAuth2TokenManager {
+
+ private final OAuth2AuthorizedClientService clientService;
+ private final OAuth2AuthorizedClientManager clientManager;
+ private final OAuth2AccessTokenResponseClient tokenResponseClient;
+
+ OAuth2TokenManager(
+ final OAuth2AuthorizedClientService clientService,
+ final OAuth2AuthorizedClientManager clientManager
+ ) {
+ this.clientService = clientService;
+ this.clientManager = clientManager;
+ this.tokenResponseClient = new RestClientRefreshTokenTokenResponseClient();
+ }
+
+ public String getToken(final OAuth2AuthenticationToken authentication) {
+ return Optional.ofNullable(authorizedToken(authentication)).orElse(
+ ((DefaultOidcUser) authentication.getPrincipal()).getIdToken().getTokenValue()
+ );
+ }
+
+ /**
+ * Tries to refresh the id token if it is expired and adds it to the request.
+ */
+ private String authorizedToken(final OAuth2AuthenticationToken authentication) {
+ String registrationId = authentication.getAuthorizedClientRegistrationId();
+ OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId).principal(authentication).build();
+
+ // This ensures that there is a client already, otherwise we won't be able to call the manager for authorization
+ OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(registrationId, authentication.getName());
+ if (authorizedClient == null) return null;
+
+ // Will ensure that the token is refreshed if needed; do not rely on it being not null as it won't be available
+ // during the first calls made to get the rights and generate the authorities
+ OAuth2AuthorizedClient refreshClient = clientManager.authorize(request);
+ if (refreshClient == null) return null;
+
+ // A small trick to refresh the token if it is expired; the current spring version does not refresh the ID Token when the Access Token is refreshed
+ // This won't be necessary after Spring Security 6.5; cf. https://github.com/spring-projects/spring-security/pull/16589
+ OAuth2AccessToken accessToken = refreshClient.getAccessToken();
+ OAuth2RefreshToken refreshToken = refreshClient.getRefreshToken();
+ ClientRegistration clientRegistration = refreshClient.getClientRegistration();
+ // if this is null, please request it via the scopes
+ if (refreshToken == null) return null;
+
+ OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
+ clientRegistration, accessToken, refreshToken);
+ OAuth2AccessTokenResponse response = tokenResponseClient.getTokenResponse(refreshTokenGrantRequest);
+ return (String) response.getAdditionalParameters().get("id_token");
+ }
+}
diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/Oauth2ClientConfig.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/Oauth2ClientConfig.java
new file mode 100644
index 000000000..37792cd79
--- /dev/null
+++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/Oauth2ClientConfig.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * 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.ui.simple.security;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
+
+@Configuration
+public class Oauth2ClientConfig {
+ @Bean(name = "hawkbitOAuth2ClientCustomizer")
+ @ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.client", name = "enabled")
+ @ConditionalOnMissingBean(name = "hawkbitOAuth2ClientCustomizer")
+ Customizer> defaultOAuth2ClientCustomizer() {
+ return Customizer.withDefaults();
+ }
+}
diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OidcClientProperties.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OidcClientProperties.java
new file mode 100644
index 000000000..8a47d1ad0
--- /dev/null
+++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OidcClientProperties.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * 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.ui.simple.security;
+
+import lombok.Data;
+import lombok.ToString;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Data
+@ToString
+@ConfigurationProperties("hawkbit.server.security")
+public class OidcClientProperties {
+
+ private final OidcClientProperties.Oauth2 oauth2 = new OidcClientProperties.Oauth2();
+
+ @Data
+ public static class Oauth2 {
+
+ private final OidcClientProperties.Oauth2.Client client = new OidcClientProperties.Oauth2.Client();
+
+ @Data
+ public static class Client {
+
+ private boolean enabled = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/SecurityConfiguration.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/SecurityConfiguration.java
index 12563a915..19696dd62 100644
--- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/SecurityConfiguration.java
+++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/SecurityConfiguration.java
@@ -11,18 +11,33 @@ package org.eclipse.hawkbit.ui.simple.security;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.eclipse.hawkbit.ui.simple.view.LoginView;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@Configuration
+@EnableConfigurationProperties({ OidcClientProperties.class })
public class SecurityConfiguration extends VaadinWebSecurity {
+ private Customizer> oAuth2LoginConfigurerCustomizer;
+
+ @Autowired(required = false)
+ public void setOAuth2LoginConfigurerCustomizer(
+ @Qualifier("hawkbitOAuth2ClientCustomizer") final Customizer> oauth2LoginConfigurerCustomizer
+ ) {
+ this.oAuth2LoginConfigurerCustomizer = oauth2LoginConfigurerCustomizer;
+ }
+
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -34,6 +49,11 @@ public class SecurityConfiguration extends VaadinWebSecurity {
authorize -> authorize.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());
super.configure(http);
- setLoginView(http, LoginView.class);
+
+ if (oAuth2LoginConfigurerCustomizer != null) {
+ http.oauth2Login(oAuth2LoginConfigurerCustomizer);
+ } else {
+ setLoginView(http, LoginView.class);
+ }
}
}