OpenID Connect support (#865)

* Added OpenID Connect support

Utilized Spring Security's OAuth2 respectively OIDC support as another
possibility to manage users and their permissions.

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Document OpenID Connect Support

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Updated license in OidcUserManagementAutoConfiguration.java

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Revert updated license notice and add Kiwigrid license file

This reverts commit 23d36245

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Resolve SonarQube issues

- Explicitly import the needed specific classes
- Document public methods
- Add `static` to the constant `JwtAuthoritiesOidcUserService.INVALID_REQUEST`
- Remove superfluous runtime exception `OAuth2AuthenticationException`

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Add OidcUser support in SpringSecurityAuditorAware

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>

* Secure Management API using OpenID Connect, too.

Signed-off-by: Brandon Schmitt <Brandon.Schmitt@kiwigrid.com>
This commit is contained in:
Brandon Schmitt
2020-01-14 13:09:13 +01:00
committed by Dominic Schabel
parent 38017ba7bc
commit 1bcced9838
11 changed files with 492 additions and 38 deletions

View File

@@ -70,4 +70,5 @@ Authentication is provided by _RabbitMQ_ [vhost and user credentials](https://ww
## Management UI ## Management UI
- Login Dialog - Login Dialog
- OpenID Connect

View File

@@ -7,7 +7,7 @@ weight: 52
Authorization is handled separately for _Direct Device Integration (DDI) API_ and _Device Management Federation (DMF) API_ (where successful authentication includes full authorization) and _Management API_ and _UI_ which is based on Spring security [authorities](https://github.com/eclipse/hawkbit/blob/master/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java). Authorization is handled separately for _Direct Device Integration (DDI) API_ and _Device Management Federation (DMF) API_ (where successful authentication includes full authorization) and _Management API_ and _UI_ which is based on Spring security [authorities](https://github.com/eclipse/hawkbit/blob/master/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java).
<!--more--> <!--more-->
However, keep in mind that hawkBit does not offer an off the shelf authentication provider to leverage these permissions and the underlying multi user/tenant capabilities of hawkBit. Check out [Spring security documentation](http://projects.spring.io/spring-security/) for further information. In hawkBit [SecurityAutoConfiguration](https://github.com/eclipse/hawkbit/blob/master/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java) is a good starting point for integration. However, keep in mind that hawkBit does not offer an off the shelf authentication provider to leverage these permissions and the underlying multi user/tenant capabilities of hawkBit but it supports authentication providers offering an OpenID Connect interface. Check out [Spring security documentation](http://projects.spring.io/spring-security/) for further information. In hawkBit [SecurityAutoConfiguration](https://github.com/eclipse/hawkbit/blob/master/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java) is a good starting point for integration.
The default implementation is single user/tenant with basic auth and the logged in user is provided with all permissions. Additionally, the application properties may be configured for multiple static users; see [Multiple Users](#multiple-users) for details. The default implementation is single user/tenant with basic auth and the logged in user is provided with all permissions. Additionally, the application properties may be configured for multiple static users; see [Multiple Users](#multiple-users) for details.
@@ -41,6 +41,18 @@ An example configuration is given below.
A permissions value of `ALL` will provide that user with all possible permissions. Passwords need to be specified with the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it is recommended to use a hash function designed for passwords such as *bcrypt*. See this [blog post](https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-format) for more information on password encoders in Spring Security. A permissions value of `ALL` will provide that user with all possible permissions. Passwords need to be specified with the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it is recommended to use a hash function designed for passwords such as *bcrypt*. See this [blog post](https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-format) for more information on password encoders in Spring Security.
### OpenID Connect
hawkbit supports authentication providers which use the OpenID Connect standard, an authentication layer built on top of the OAuth 2.0 protocol.
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
### Delivered Permissions ### Delivered Permissions
- READ_/UPDATE_/CREATE_/DELETE_TARGETS for: - READ_/UPDATE_/CREATE_/DELETE_TARGETS for:
- Target entities including metadata (that includes also the installed and assigned distribution sets) - Target entities including metadata (that includes also the installed and assigned distribution sets)

View File

@@ -0,0 +1,326 @@
/**
* Copyright (c) 2019 Kiwigrid GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.autoconfigure.security;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
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;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
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.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.authentication.*;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Auto-configuration for OpenID Connect user management.
*
*/
@Configuration
@Conditional(value = ClientsConfiguredCondition.class)
public class OidcUserManagementAutoConfiguration {
/**
* @return the oauth2 user details service to load a user from oidc user
* manager
*/
@Bean
@ConditionalOnMissingBean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserDetailsService(JwtAuthoritiesExtractor extractor) {
return new JwtAuthoritiesOidcUserService(extractor);
}
/**
* @return the OpenID Connect authentication success handler
*/
@Bean
@ConditionalOnMissingBean
public AuthenticationSuccessHandler oidcAuthenticationSuccessHandler() {
return new OidcAuthenticationSuccessHandler();
}
/**
* @return the OpenID Connect logout handler
*/
@Bean
@ConditionalOnMissingBean
public LogoutHandler oidcLogoutHandler() {
return new OidcLogoutHandler();
}
/**
* @return a jwt authorities extractor which interprets the roles of a user as their authorities.
*/
@Bean
@ConditionalOnMissingBean
public JwtAuthoritiesExtractor jwtAuthoritiesExtractor() {
final SimpleAuthorityMapper authorityMapper = new SimpleAuthorityMapper();
authorityMapper.setPrefix("");
authorityMapper.setConvertToUpperCase(true);
return new JwtAuthoritiesExtractor(authorityMapper);
}
/**
* @return an authentication filter for using OAuth2 Bearer Tokens.
*/
@Bean
@ConditionalOnMissingBean
public OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter() {
return new OidcBearerTokenAuthenticationFilter();
}
}
/**
* Extended {@link OidcUserService} supporting JWT containing authorities
*/
class JwtAuthoritiesOidcUserService extends OidcUserService {
private final JwtAuthoritiesExtractor authoritiesExtractor;
JwtAuthoritiesOidcUserService(JwtAuthoritiesExtractor authoritiesExtractor) {
super();
this.authoritiesExtractor = authoritiesExtractor;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) {
OidcUser user = super.loadUser(userRequest);
ClientRegistration clientRegistration = userRequest.getClientRegistration();
Set<GrantedAuthority> authorities = new LinkedHashSet<>(authoritiesExtractor.extract(clientRegistration,
userRequest.getAccessToken().getTokenValue()));
if (authorities.isEmpty()) {
return user;
}
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());
}
return oidcUser;
}
}
/**
* OpenID Connect Authentication Success Handler which load tenant data
*/
class OidcAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private SystemManagement systemManagement;
@Autowired
private SystemSecurityContext systemSecurityContext;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
if (authentication instanceof AbstractAuthenticationToken) {
final String defaultTenant = "DEFAULT";
AbstractAuthenticationToken token = (AbstractAuthenticationToken) authentication;
token.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
/**
* LogoutHandler to invalidate OpenID Connect tokens
*/
class OidcLogoutHandler extends SecurityContextLogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
super.logout(request, response, authentication);
Object principal = authentication.getPrincipal();
if (principal instanceof OidcUser) {
OidcUser user = (OidcUser) authentication.getPrincipal();
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity(builder.toUriString(), String.class);
}
}
}
/**
* 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);
JwtAuthoritiesExtractor(GrantedAuthoritiesMapper authoritiesMapper) {
super();
this.authoritiesMapper = authoritiesMapper;
}
Collection<? extends GrantedAuthority> extract(ClientRegistration clientRegistration,
String tokenValue) {
Jwt token;
try {
// Token is already verified by spring security
JwtDecoder jwtDecoder = new NimbusJwtDecoderJwkSupport(
clientRegistration.getProviderDetails().getJwkSetUri());
token = jwtDecoder.decode(tokenValue);
} catch (JwtException e) {
throw new OAuth2AuthenticationException(INVALID_REQUEST, e);
}
return extract(clientRegistration.getClientId(), token.getClaims());
}
Collection<? extends GrantedAuthority> extract(String clientId, Map<String, Object> claims) {
@SuppressWarnings("unchecked")
Map<String, Object> resourceMap = (Map<String, Object>) claims.get("resource_access");
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> clientResource = (Map<String, Map<String, Object>>) resourceMap.get(clientId);
if (CollectionUtils.isEmpty(clientResource)) {
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) clientResource.get("roles");
if (CollectionUtils.isEmpty(roles)) {
return Collections.emptyList();
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(roles.toArray(new String[0]));
if (authoritiesMapper != null) {
authorities = authoritiesMapper.mapAuthorities(authorities);
}
return authorities;
}
}
class OidcBearerTokenAuthenticationFilter implements UserAuthenticationFilter, Filter {
@Autowired
private JwtAuthoritiesExtractor authoritiesExtractor;
@Autowired
private SystemManagement systemManagement;
@Autowired
private SystemSecurityContext systemSecurityContext;
private ClientRegistration clientRegistration;
void setClientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
}
@Override
public void init(final FilterConfig filterConfig) {
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken) {
final String defaultTenant = "DEFAULT";
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
Jwt jwt = jwtAuthenticationToken.getToken();
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
OidcUserInfo userInfo = new OidcUserInfo(jwt.getClaims());
Collection<? extends GrantedAuthority> authorities = authoritiesExtractor.extract(
clientRegistration.getClientId(), jwt.getClaims());
if (authorities.isEmpty()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
DefaultOidcUser user = new DefaultOidcUser(authorities, idToken, userInfo);
OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(
user, authorities, clientRegistration.getRegistrationId());
oAuth2AuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(defaultTenant, false));
systemSecurityContext.runAsSystemAsTenant(systemManagement::getTenantMetadata, defaultTenant);
SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken);
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}

View File

@@ -75,15 +75,24 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.vaadin.spring.security.VaadinSecurityContext; import org.vaadin.spring.security.VaadinSecurityContext;
import org.vaadin.spring.security.annotation.EnableVaadinSecurity; import org.vaadin.spring.security.annotation.EnableVaadinSecurity;
@@ -457,6 +466,12 @@ public class SecurityManagedConfiguration {
@Autowired @Autowired
private UserAuthenticationFilter userAuthenticationFilter; private UserAuthenticationFilter userAuthenticationFilter;
@Autowired(required = false)
private OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter;
@Autowired(required = false)
private InMemoryClientRegistrationRepository clientRegistrationRepository;
@Autowired @Autowired
private SystemManagement systemManagement; private SystemManagement systemManagement;
@@ -492,38 +507,61 @@ public class SecurityManagedConfiguration {
@Override @Override
protected void configure(final HttpSecurity http) throws Exception { protected void configure(final HttpSecurity http) throws Exception {
final BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
basicAuthEntryPoint.setRealmName(securityProperties.getBasicRealm());
HttpSecurity httpSec = http.regexMatcher("\\/rest.*|\\/system/admin.*").csrf().disable(); HttpSecurity httpSec = http.regexMatcher("\\/rest.*|\\/system/admin.*").csrf().disable();
if (securityProperties.isRequireSsl()) { if (securityProperties.isRequireSsl()) {
httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and();
} }
httpSec.addFilterBefore(new Filter() { httpSec
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
userAuthenticationFilter.init(filterConfig);
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
userAuthenticationFilter.doFilter(request, response, chain);
}
@Override
public void destroy() {
userAuthenticationFilter.destroy();
}
}, RequestHeaderAuthenticationFilter.class)
.addFilterAfter(new AuthenticationSuccessTenantMetadataCreationFilter(systemManagement,
systemSecurityContext), SessionManagementFilter.class)
.authorizeRequests().anyRequest().authenticated() .authorizeRequests().anyRequest().authenticated()
.antMatchers(MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**") .antMatchers(MgmtRestConstants.BASE_SYSTEM_MAPPING + "/admin/**")
.hasAnyAuthority(SpPermission.SYSTEM_ADMIN); .hasAnyAuthority(SpPermission.SYSTEM_ADMIN);
httpSec.httpBasic().and().exceptionHandling().authenticationEntryPoint(basicAuthEntryPoint); if (oidcBearerTokenAuthenticationFilter != null) {
// Only get the first client registration. Testing against every client could increase the
// attack vector
ClientRegistration clientRegistration = null;
for (ClientRegistration cr : clientRegistrationRepository) {
clientRegistration = cr;
break;
}
Assert.notNull(clientRegistration, "There must be a valid client registration");
httpSec.oauth2ResourceServer()
.jwt().jwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri());
oidcBearerTokenAuthenticationFilter.setClientRegistration(clientRegistration);
httpSec.addFilterAfter(oidcBearerTokenAuthenticationFilter, BearerTokenAuthenticationFilter.class);
}
else {
final BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
basicAuthEntryPoint.setRealmName(securityProperties.getBasicRealm());
httpSec.addFilterBefore(new Filter() {
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
userAuthenticationFilter.init(filterConfig);
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
userAuthenticationFilter.doFilter(request, response, chain);
}
@Override
public void destroy() {
userAuthenticationFilter.destroy();
}
}, RequestHeaderAuthenticationFilter.class);
httpSec.httpBasic().and().exceptionHandling().authenticationEntryPoint(basicAuthEntryPoint);
}
httpSec.addFilterAfter(new AuthenticationSuccessTenantMetadataCreationFilter(systemManagement,
systemSecurityContext), SessionManagementFilter.class);
httpSec.anonymous().disable(); httpSec.anonymous().disable();
httpSec.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); httpSec.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
} }
@@ -546,6 +584,15 @@ public class SecurityManagedConfiguration {
private final VaadinUrlAuthenticationSuccessHandler handler; private final VaadinUrlAuthenticationSuccessHandler handler;
@Autowired(required = false)
private AuthenticationSuccessHandler oidcAuthenticationSuccessHandler;
@Autowired(required = false)
private LogoutHandler oidcLogoutHandler;
@Autowired(required = false)
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
public UISecurityConfigurationAdapter(final VaadinRedirectStrategy redirectStrategy) { public UISecurityConfigurationAdapter(final VaadinRedirectStrategy redirectStrategy) {
handler = new TenantMetadataSavedRequestAwareVaadinAuthenticationSuccessHandler(); handler = new TenantMetadataSavedRequestAwareVaadinAuthenticationSuccessHandler();
handler.setRedirectStrategy(redirectStrategy); handler.setRedirectStrategy(redirectStrategy);
@@ -614,13 +661,21 @@ public class SecurityManagedConfiguration {
@Override @Override
protected void configure(final HttpSecurity http) throws Exception { protected void configure(final HttpSecurity http) throws Exception {
boolean enableOidc = oidcUserService != null && oidcAuthenticationSuccessHandler != null
&& oidcLogoutHandler != null;
// workaround regex: we need to exclude the URL /UI/HEARTBEAT here // workaround regex: we need to exclude the URL /UI/HEARTBEAT here
// because we bound the vaadin application to /UI and not to root, // because we bound the vaadin application to /UI and not to root,
// described in vaadin-forum: // described in vaadin-forum:
// https://vaadin.com/forum#!/thread/3200565. // https://vaadin.com/forum#!/thread/3200565.
HttpSecurity httpSec = http.regexMatcher("(?!.*HEARTBEAT)^.*\\/UI.*$") HttpSecurity httpSec = null;
// disable as CSRF is handled by Vaadin if (enableOidc) {
.csrf().disable(); httpSec = http.regexMatcher("(?!.*HEARTBEAT)^.*\\/(UI|oauth2).*$");
} else {
httpSec = http.regexMatcher("(?!.*HEARTBEAT)^.*\\/UI.*$");
}
// disable as CSRF is handled by Vaadin
httpSec.csrf().disable();
if (hawkbitSecurityProperties.isRequireSsl()) { if (hawkbitSecurityProperties.isRequireSsl()) {
httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and();
@@ -634,16 +689,27 @@ public class SecurityManagedConfiguration {
httpSec.headers().contentSecurityPolicy(hawkbitSecurityProperties.getContentSecurityPolicy()); httpSec.headers().contentSecurityPolicy(hawkbitSecurityProperties.getContentSecurityPolicy());
} }
final SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); if (enableOidc) {
simpleUrlLogoutSuccessHandler.setTargetUrlParameter("login"); httpSec.authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**")
.permitAll().anyRequest().authenticated().and()
// OIDC
.oauth2Login().userInfoEndpoint().oidcUserService(oidcUserService).and()
.successHandler(oidcAuthenticationSuccessHandler).and().oauth2Client().and()
// logout
.logout().logoutUrl("/UI/logout").addLogoutHandler(oidcLogoutHandler).logoutSuccessUrl("/");
} else {
final SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
simpleUrlLogoutSuccessHandler.setTargetUrlParameter("login");
httpSec httpSec
// UI // UI
.authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**").permitAll() .authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**")
.anyRequest().authenticated().and() .permitAll().anyRequest().authenticated().and()
// UI login / logout // UI login / logout
.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/UI/login/#/")) .exceptionHandling()
.and().logout().logoutUrl("/UI/logout").logoutSuccessHandler(simpleUrlLogoutSuccessHandler); .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/UI/login/#/")).and().logout()
.logoutUrl("/UI/logout").logoutSuccessHandler(simpleUrlLogoutSuccessHandler);
}
} }
@Override @Override

View File

@@ -13,5 +13,6 @@ org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.security.InMemoryUserManagementAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.security.InMemoryUserManagementAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.security.OidcUserManagementAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.web.WebMvcAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.web.WebMvcAutoConfiguration,\
org.eclipse.hawkbit.autoconfigure.PropertyHostnameResolverAutoConfiguration org.eclipse.hawkbit.autoconfigure.PropertyHostnameResolverAutoConfiguration

View File

@@ -43,6 +43,18 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId> <artifactId>spring-security-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</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>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId> <artifactId>spring-boot</artifactId>
@@ -66,4 +78,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -14,6 +14,7 @@ import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
/** /**
* Auditor class that allows BaseEntitys to insert current logged in user for * Auditor class that allows BaseEntitys to insert current logged in user for
@@ -38,6 +39,9 @@ public class SpringSecurityAuditorAware implements AuditorAware<String> {
if (authentication.getPrincipal() instanceof UserDetails) { if (authentication.getPrincipal() instanceof UserDetails) {
return ((UserDetails) authentication.getPrincipal()).getUsername(); return ((UserDetails) authentication.getPrincipal()).getUsername();
} }
if (authentication.getPrincipal() instanceof OidcUser) {
return ((OidcUser) authentication.getPrincipal()).getPreferredUsername();
}
return authentication.getPrincipal().toString(); return authentication.getPrincipal().toString();
} }

View File

@@ -201,6 +201,14 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId> <artifactId>spring-security-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.vaadin</groupId> <groupId>com.vaadin</groupId>

View File

@@ -12,14 +12,18 @@ import java.util.Collections;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.im.authentication.UserPrincipal; import org.eclipse.hawkbit.im.authentication.UserPrincipal;
import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.ui.utils.SpringContextHelper; import org.eclipse.hawkbit.ui.utils.SpringContextHelper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinService;
@@ -187,7 +191,20 @@ public final class UserDetailsFormatter {
public static UserDetails getCurrentUser() { public static UserDetails getCurrentUser() {
final SecurityContext context = (SecurityContext) VaadinService.getCurrentRequest().getWrappedSession() final SecurityContext context = (SecurityContext) VaadinService.getCurrentRequest().getWrappedSession()
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
return (UserDetails) context.getAuthentication().getPrincipal(); Authentication authentication = context.getAuthentication();
if (authentication instanceof OAuth2AuthenticationToken) {
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
Object details = authentication.getDetails();
String tenant = "DEFAULT";
if (details instanceof TenantAwareAuthenticationDetails) {
tenant = ((TenantAwareAuthenticationDetails) details).getTenant();
}
return new UserPrincipal(oidcUser.getPreferredUsername(), "***", oidcUser.getGivenName(),
oidcUser.getFamilyName(), oidcUser.getPreferredUsername(), oidcUser.getEmail(), tenant,
oidcUser.getAuthorities());
} else {
return (UserDetails) authentication.getPrincipal();
}
} }
private static String trimAndFormatDetail(final String formatString, final int expectedDetailLength) { private static String trimAndFormatDetail(final String formatString, final int expectedDetailLength) {

View File

@@ -0,0 +1,6 @@
Copyright (c) 2019 Kiwigrid GmbH and others.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html

View File

@@ -327,6 +327,7 @@
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_MICROSOFT_18.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_MICROSOFT_18.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_BOSCH_18.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_BOSCH_18.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_19.txt</validHeader> <validHeader>licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_19.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt</validHeader>
</validHeaders> </validHeaders>
<excludes> <excludes>
<exclude>**/banner.txt</exclude> <exclude>**/banner.txt</exclude>