Refacto oauth2 granted authorities (#2684)
* refacto: oauth2 * feat: clear user from cache on logout
This commit is contained in:
committed by
GitHub
parent
a3cf835ae2
commit
4306548b4a
@@ -48,7 +48,7 @@ public class HawkbitMgmtClient {
|
|||||||
private final MgmtTenantManagementRestApi tenantManagementRestApi;
|
private final MgmtTenantManagementRestApi tenantManagementRestApi;
|
||||||
private final MgmtActionRestApi actionRestApi;
|
private final MgmtActionRestApi actionRestApi;
|
||||||
|
|
||||||
HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
public HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
||||||
this.tenant = tenant;
|
this.tenant = tenant;
|
||||||
this.hawkbitClient = hawkbitClient;
|
this.hawkbitClient = hawkbitClient;
|
||||||
|
|
||||||
@@ -66,23 +66,23 @@ public class HawkbitMgmtClient {
|
|||||||
actionRestApi = service(MgmtActionRestApi.class);
|
actionRestApi = service(MgmtActionRestApi.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasSoftwareModulesRead() {
|
public boolean hasSoftwareModulesRead() {
|
||||||
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
|
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasRolloutRead() {
|
public boolean hasRolloutRead() {
|
||||||
return hasRead(() -> rolloutRestApi.getRollout(-1L));
|
return hasRead(() -> rolloutRestApi.getRollout(-1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasDistributionSetRead() {
|
public boolean hasDistributionSetRead() {
|
||||||
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
|
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasTargetRead() {
|
public boolean hasTargetRead() {
|
||||||
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
|
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasConfigRead() {
|
public boolean hasConfigRead() {
|
||||||
return hasRead(() -> tenantManagementRestApi.getTenantConfigurationValue("_#ETE$ER"));
|
return hasRead(() -> tenantManagementRestApi.getTenantConfigurationValue("_#ETE$ER"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ public class MainLayout extends AppLayout {
|
|||||||
protected void afterNavigation() {
|
protected void afterNavigation() {
|
||||||
super.afterNavigation();
|
super.afterNavigation();
|
||||||
viewTitle.setText(
|
viewTitle.setText(
|
||||||
Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class))
|
Optional.ofNullable(getContent())
|
||||||
|
.map(c -> c.getClass().getAnnotation(PageTitle.class))
|
||||||
.map(PageTitle::value)
|
.map(PageTitle::value)
|
||||||
.orElse(""));
|
.orElse(""));
|
||||||
if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) {
|
if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) {
|
||||||
|
|||||||
@@ -21,52 +21,43 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||||
import org.eclipse.hawkbit.sdk.HawkbitServer;
|
import org.eclipse.hawkbit.sdk.HawkbitServer;
|
||||||
import org.eclipse.hawkbit.sdk.Tenant;
|
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.eclipse.hawkbit.ui.simple.view.util.Utils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
|
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
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.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 org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.LinkedList;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import static feign.Util.ISO_8859_1;
|
import static feign.Util.ISO_8859_1;
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Theme("hawkbit")
|
@Theme("hawkbit")
|
||||||
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
|
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
|
||||||
|
@EnableCaching
|
||||||
|
@EnableScheduling
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@Import(FeignClientsConfiguration.class)
|
@Import(FeignClientsConfiguration.class)
|
||||||
public class SimpleUIApp implements AppShellConfigurator {
|
public class SimpleUIApp implements AppShellConfigurator {
|
||||||
|
|
||||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
private static final Function<OAuth2TokenManager, RequestInterceptor> AUTHORIZATION = oAuth2TokenManager -> requestTemplate -> {
|
private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
|
||||||
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication instanceof OAuth2AuthenticationToken authenticationToken) {
|
if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
|
||||||
String bearerToken = oAuth2TokenManager.getToken(authenticationToken);
|
requestTemplate.header(AUTHORIZATION_HEADER, "Bearer " + oidcUser.getIdToken().getTokenValue());
|
||||||
requestTemplate.header(AUTHORIZATION_HEADER, "Bearer " + bearerToken);
|
|
||||||
} else {
|
} else {
|
||||||
requestTemplate.header(
|
requestTemplate.header(
|
||||||
AUTHORIZATION_HEADER, "Basic " + Base64.getEncoder().encodeToString(
|
AUTHORIZATION_HEADER, "Basic " + Base64.getEncoder().encodeToString(
|
||||||
@@ -92,17 +83,14 @@ public class SimpleUIApp implements AppShellConfigurator {
|
|||||||
final HawkbitServer hawkBitServer,
|
final HawkbitServer hawkBitServer,
|
||||||
final Encoder encoder,
|
final Encoder encoder,
|
||||||
final Decoder decoder,
|
final Decoder decoder,
|
||||||
final Contract contract,
|
final Contract contract
|
||||||
@Autowired(required = false)
|
|
||||||
final OAuth2TokenManager oAuth2TokenManager
|
|
||||||
) {
|
) {
|
||||||
return new HawkbitClient(
|
return new HawkbitClient(
|
||||||
hawkBitServer, encoder, decoder, contract,
|
hawkBitServer, encoder, decoder, contract,
|
||||||
ERROR_DECODER,
|
ERROR_DECODER,
|
||||||
(tenant, controller) ->
|
(tenant, controller) -> controller == null
|
||||||
controller == null
|
? AUTHORIZATION
|
||||||
? AUTHORIZATION.apply(oAuth2TokenManager)
|
: HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller)
|
||||||
: HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,21 +99,6 @@ public class SimpleUIApp implements AppShellConfigurator {
|
|||||||
return new HawkbitMgmtClient(tenant, hawkbitClient);
|
return new HawkbitMgmtClient(tenant, hawkbitClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
OAuth2UserService<OidcUserRequest, OidcUser> 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<SimpleGrantedAuthority> grantedAuthorities = getGrantedAuthorities(hawkbitClient, tempToken);
|
|
||||||
return new DefaultOidcUser(grantedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// accepts all user / pass, just delegating them to the feign client
|
// accepts all user / pass, just delegating them to the feign client
|
||||||
@Bean
|
@Bean
|
||||||
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient, final HawkbitServer server) {
|
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient, final HawkbitServer server) {
|
||||||
@@ -138,9 +111,7 @@ public class SimpleUIApp implements AppShellConfigurator {
|
|||||||
throw new BadCredentialsException("Incorrect username or password!");
|
throw new BadCredentialsException("Incorrect username or password!");
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SimpleGrantedAuthority> grantedAuthorities = getGrantedAuthorities(
|
return new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()) {
|
||||||
hawkbitClient, new UsernamePasswordAuthenticationToken(username, password));
|
|
||||||
return new UsernamePasswordAuthenticationToken(username, password, grantedAuthorities) {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void eraseCredentials() {
|
public void eraseCredentials() {
|
||||||
@@ -151,35 +122,6 @@ public class SimpleUIApp implements AppShellConfigurator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SimpleGrantedAuthority> getGrantedAuthorities(final HawkbitMgmtClient hawkbitClient, Authentication authentication) {
|
|
||||||
final List<String> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isAuthenticated(String username, String password, String mgmtUrl) {
|
public static boolean isAuthenticated(String username, String password, String mgmtUrl) {
|
||||||
try {
|
try {
|
||||||
final URL url = new URL(mgmtUrl + "/rest/v1/rollouts");
|
final URL url = new URL(mgmtUrl + "/rest/v1/rollouts");
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import org.springframework.stereotype.Component;
|
|||||||
public class AuthenticatedUser {
|
public class AuthenticatedUser {
|
||||||
|
|
||||||
private final AuthenticationContext authenticationContext;
|
private final AuthenticationContext authenticationContext;
|
||||||
|
private final GrantedAuthoritiesService grantedAuthoritiesService;
|
||||||
|
|
||||||
public AuthenticatedUser(final AuthenticationContext authenticationContext) {
|
public AuthenticatedUser(final AuthenticationContext authenticationContext, GrantedAuthoritiesService grantedAuthoritiesService) {
|
||||||
this.authenticationContext = authenticationContext;
|
this.authenticationContext = authenticationContext;
|
||||||
|
this.grantedAuthoritiesService = grantedAuthoritiesService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> getName() {
|
public Optional<String> getName() {
|
||||||
@@ -28,6 +30,7 @@ public class AuthenticatedUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void logout() {
|
public void logout() {
|
||||||
|
this.getName().ifPresent(grantedAuthoritiesService::evictUserFromCache);
|
||||||
authenticationContext.logout();
|
authenticationContext.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class GrantedAuthoritiesService {
|
||||||
|
|
||||||
|
public static final String USER_CACHE = "userDetails";
|
||||||
|
private HawkbitMgmtClient hawkbitClient;
|
||||||
|
|
||||||
|
@Cacheable(cacheNames = "userDetails", key = "#authentication.getName()")
|
||||||
|
public List<SimpleGrantedAuthority> getGrantedAuthorities(Authentication authentication) {
|
||||||
|
final List<String> roles = new LinkedList<>();
|
||||||
|
roles.add("ANONYMOUS");
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRateString = "${caching.spring.userDetailsTTL:1h}")
|
||||||
|
@CacheEvict(cacheNames = USER_CACHE, allEntries = true)
|
||||||
|
public void emptyCache() {
|
||||||
|
log.debug("emptying userDetails cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(cacheNames = "userDetails")
|
||||||
|
public void evictUserFromCache(String principalName) {
|
||||||
|
log.debug("remove user from {} cache", principalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.ConditionalOnProperty;
|
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
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.core.oidc.user.DefaultOidcUser;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.client", name = "enabled")
|
|
||||||
public class OAuth2TokenManager {
|
|
||||||
|
|
||||||
private final OAuth2AuthorizedClientService clientService;
|
|
||||||
private final OAuth2AuthorizedClientManager clientManager;
|
|
||||||
|
|
||||||
OAuth2TokenManager(
|
|
||||||
final OAuth2AuthorizedClientService clientService,
|
|
||||||
final OAuth2AuthorizedClientManager clientManager
|
|
||||||
) {
|
|
||||||
this.clientService = clientService;
|
|
||||||
this.clientManager = clientManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getToken(final OAuth2AuthenticationToken authentication) {
|
|
||||||
final String currentToken = ((DefaultOidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
|
|
||||||
String registrationId = authentication.getAuthorizedClientRegistrationId();
|
|
||||||
|
|
||||||
// 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 currentToken;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId).principal(authentication).build();
|
|
||||||
// since Spring Security 6.5 this will trigger a refresh of the id token
|
|
||||||
authorizedClient = clientManager.authorize(request);
|
|
||||||
if (authorizedClient == null) return currentToken;
|
|
||||||
|
|
||||||
// we need to fetch the newly created context containing the matching token
|
|
||||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
|
||||||
return ((DefaultOidcUser) securityContext.getAuthentication().getPrincipal()).getIdToken().getTokenValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.ui.simple.security;
|
|||||||
|
|
||||||
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration;
|
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration;
|
||||||
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
|
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.hawkbit.ui.simple.view.LoginView;
|
import org.eclipse.hawkbit.ui.simple.view.LoginView;
|
||||||
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;
|
||||||
@@ -24,19 +25,27 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
|||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(OidcClientProperties.class)
|
@EnableConfigurationProperties(OidcClientProperties.class)
|
||||||
|
@Slf4j
|
||||||
@Import(VaadinAwareSecurityContextHolderStrategyConfiguration.class)
|
@Import(VaadinAwareSecurityContextHolderStrategyConfiguration.class)
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
private Customizer<OAuth2LoginConfigurer<HttpSecurity>> oAuth2LoginConfigurerCustomizer;
|
private Customizer<OAuth2LoginConfigurer<HttpSecurity>> oAuth2LoginConfigurerCustomizer;
|
||||||
|
@Autowired
|
||||||
|
private UserDetailsSetter userDetailsSetter;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private InMemoryClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
public void setOAuth2LoginConfigurerCustomizer(
|
public void setOAuth2LoginConfigurerCustomizer(
|
||||||
@@ -52,23 +61,24 @@ public class SecurityConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
public AuthenticationFailureHandler customFailureHandler() {
|
public AuthenticationFailureHandler customFailureHandler() {
|
||||||
// Redirect back to login with your message
|
// Redirect back to login with your message
|
||||||
return (request, response, exception) ->
|
return (request, response, exception) -> response.sendRedirect("/login?error=" + URLEncoder.encode(exception.getMessage(),
|
||||||
response.sendRedirect("/login?error=" + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8));
|
StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/images/*.png").permitAll());
|
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/images/*.png").permitAll());
|
||||||
if (oAuth2LoginConfigurerCustomizer != null) {
|
http.addFilterAfter(userDetailsSetter, AuthorizationFilter.class);
|
||||||
http.oauth2Login(oAuth2LoginConfigurerCustomizer);
|
|
||||||
} else {
|
|
||||||
http.formLogin(form -> form
|
|
||||||
.loginPage("/login")
|
|
||||||
.failureHandler(customFailureHandler()));
|
|
||||||
}
|
|
||||||
return http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
|
return http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
|
||||||
if (oAuth2LoginConfigurerCustomizer == null) {
|
if (oAuth2LoginConfigurerCustomizer == null) {
|
||||||
configurer.loginView(LoginView.class);
|
configurer.loginView(LoginView.class);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var defaultClientRegistration = clientRegistrationRepository.iterator().next();
|
||||||
|
configurer.oauth2LoginPage(
|
||||||
|
"/oauth2/authorization/" + defaultClientRegistration.getRegistrationId(),
|
||||||
|
"{baseUrl}/"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).build();
|
}).build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.security.authentication.AccountExpiredException;
|
||||||
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
class UserDetailsSetter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final SecurityContextHolderStrategy securityContextHolderStrategy;
|
||||||
|
private final GrantedAuthoritiesService grantedAuthoritiesService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response,
|
||||||
|
@NotNull final FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
Authentication authentication = securityContextHolderStrategy.getContext().getAuthentication();
|
||||||
|
Authentication newAuthentication;
|
||||||
|
|
||||||
|
if (!(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {
|
||||||
|
Collection<? extends GrantedAuthority> grantedAuthorities = grantedAuthoritiesService.getGrantedAuthorities(authentication);
|
||||||
|
|
||||||
|
if (authentication instanceof OAuth2AuthenticationToken oAuth2AuthenticationToken) {
|
||||||
|
newAuthentication = new OAuth2AuthenticationToken(oAuth2AuthenticationToken.getPrincipal(), grantedAuthorities,
|
||||||
|
oAuth2AuthenticationToken.getAuthorizedClientRegistrationId());
|
||||||
|
if (authentication.getPrincipal() instanceof OidcUser user) {
|
||||||
|
// if there is no refresh token and the access token is expired then relogin is required
|
||||||
|
if (user.getIdToken().getExpiresAt() != null && Instant.now().isAfter(user.getIdToken().getExpiresAt())) {
|
||||||
|
throw new AccountExpiredException("Token expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newAuthentication = new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
|
||||||
|
grantedAuthorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
securityContextHolderStrategy.getContext().setAuthentication(newAuthentication);
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user