diff --git a/docs/content/concepts/authentication.md b/docs/content/concepts/authentication.md index d9d8d756c9..8f618696ae 100644 --- a/docs/content/concepts/authentication.md +++ b/docs/content/concepts/authentication.md @@ -70,4 +70,5 @@ Authentication is provided by _RabbitMQ_ [vhost and user credentials](https://ww ## Management UI - Login Dialog +- OpenID Connect diff --git a/docs/content/concepts/authorization.md b/docs/content/concepts/authorization.md index fa1e802d60..fd5df23c0e 100644 --- a/docs/content/concepts/authorization.md +++ b/docs/content/concepts/authorization.md @@ -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). -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. @@ -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. +### 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 - READ_/UPDATE_/CREATE_/DELETE_TARGETS for: - Target entities including metadata (that includes also the installed and assigned distribution sets) diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java new file mode 100644 index 0000000000..389879ab2e --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java @@ -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 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 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 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 extract(String clientId, Map claims) { + @SuppressWarnings("unchecked") + Map resourceMap = (Map) claims.get("resource_access"); + + @SuppressWarnings("unchecked") + Map> clientResource = (Map>) resourceMap.get(clientId); + if (CollectionUtils.isEmpty(clientResource)) { + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + List roles = (List) clientResource.get("roles"); + if (CollectionUtils.isEmpty(roles)) { + return Collections.emptyList(); + } + + Collection 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 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() { + } +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index ef0d4ba417..c5d897c36d 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -75,15 +75,24 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 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.preauth.RequestHeaderAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.vaadin.spring.security.VaadinSecurityContext; import org.vaadin.spring.security.annotation.EnableVaadinSecurity; @@ -457,6 +466,12 @@ public static class RestSecurityConfigurationAdapter extends WebSecurityConfigur @Autowired private UserAuthenticationFilter userAuthenticationFilter; + @Autowired(required = false) + private OidcBearerTokenAuthenticationFilter oidcBearerTokenAuthenticationFilter; + + @Autowired(required = false) + private InMemoryClientRegistrationRepository clientRegistrationRepository; + @Autowired private SystemManagement systemManagement; @@ -492,38 +507,61 @@ public FilterRegistrationBean dosMgmtFilter(final HawkbitSecurityProp @Override 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(); if (securityProperties.isRequireSsl()) { httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); } - 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) - .addFilterAfter(new AuthenticationSuccessTenantMetadataCreationFilter(systemManagement, - systemSecurityContext), SessionManagementFilter.class) + httpSec .authorizeRequests().anyRequest().authenticated() .antMatchers(MgmtRestConstants.BASE_SYSTEM_MAPPING + "/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.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @@ -546,6 +584,15 @@ public static class UISecurityConfigurationAdapter extends WebSecurityConfigurer private final VaadinUrlAuthenticationSuccessHandler handler; + @Autowired(required = false) + private AuthenticationSuccessHandler oidcAuthenticationSuccessHandler; + + @Autowired(required = false) + private LogoutHandler oidcLogoutHandler; + + @Autowired(required = false) + private OAuth2UserService oidcUserService; + public UISecurityConfigurationAdapter(final VaadinRedirectStrategy redirectStrategy) { handler = new TenantMetadataSavedRequestAwareVaadinAuthenticationSuccessHandler(); handler.setRedirectStrategy(redirectStrategy); @@ -614,13 +661,21 @@ public ServletListenerRegistrationBean httpSessionEve @Override 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 // because we bound the vaadin application to /UI and not to root, // described in vaadin-forum: // https://vaadin.com/forum#!/thread/3200565. - HttpSecurity httpSec = http.regexMatcher("(?!.*HEARTBEAT)^.*\\/UI.*$") - // disable as CSRF is handled by Vaadin - .csrf().disable(); + HttpSecurity httpSec = null; + if (enableOidc) { + 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()) { httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); @@ -634,16 +689,27 @@ protected void configure(final HttpSecurity http) throws Exception { httpSec.headers().contentSecurityPolicy(hawkbitSecurityProperties.getContentSecurityPolicy()); } - final SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); - simpleUrlLogoutSuccessHandler.setTargetUrlParameter("login"); - - httpSec - // UI - .authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**").permitAll() - .anyRequest().authenticated().and() - // UI login / logout - .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/UI/login/#/")) - .and().logout().logoutUrl("/UI/logout").logoutSuccessHandler(simpleUrlLogoutSuccessHandler); + if (enableOidc) { + 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 + // UI + .authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**") + .permitAll().anyRequest().authenticated().and() + // UI login / logout + .exceptionHandling() + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/UI/login/#/")).and().logout() + .logoutUrl("/UI/logout").logoutSuccessHandler(simpleUrlLogoutSuccessHandler); + } } @Override diff --git a/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories index 716504d52d..5716eb54a7 100644 --- a/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories @@ -13,5 +13,6 @@ org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.security.InMemoryUserManagementAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.security.OidcUserManagementAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.web.WebMvcAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.PropertyHostnameResolverAutoConfiguration diff --git a/hawkbit-security-core/pom.xml b/hawkbit-security-core/pom.xml index 642993edb8..b90c59d05f 100644 --- a/hawkbit-security-core/pom.xml +++ b/hawkbit-security-core/pom.xml @@ -43,6 +43,18 @@ org.springframework.security spring-security-core + + org.springframework.security + spring-security-oauth2-client + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + org.springframework.boot spring-boot @@ -66,4 +78,4 @@ - \ No newline at end of file + diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java index 0cbb351186..204cdaa57f 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java @@ -14,6 +14,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; 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 @@ -38,6 +39,9 @@ private static String getCurrentAuditor(final Authentication authentication) { if (authentication.getPrincipal() instanceof UserDetails) { return ((UserDetails) authentication.getPrincipal()).getUsername(); } + if (authentication.getPrincipal() instanceof OidcUser) { + return ((OidcUser) authentication.getPrincipal()).getPreferredUsername(); + } return authentication.getPrincipal().toString(); } diff --git a/hawkbit-ui/pom.xml b/hawkbit-ui/pom.xml index 790188f269..a9e4181745 100644 --- a/hawkbit-ui/pom.xml +++ b/hawkbit-ui/pom.xml @@ -201,6 +201,14 @@ org.springframework.security spring-security-web + + org.springframework.security + spring-security-oauth2-client + + + org.springframework.security + spring-security-oauth2-jose + com.vaadin diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/UserDetailsFormatter.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/UserDetailsFormatter.java index e24da63b8b..4be8966acc 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/UserDetailsFormatter.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/UserDetailsFormatter.java @@ -12,14 +12,18 @@ import java.util.Optional; import org.apache.commons.lang3.StringUtils; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.im.authentication.UserPrincipal; import org.eclipse.hawkbit.repository.model.BaseEntity; 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.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; 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 com.vaadin.server.VaadinService; @@ -187,7 +191,20 @@ public static Optional getCurrentUserEmail() { public static UserDetails getCurrentUser() { final SecurityContext context = (SecurityContext) VaadinService.getCurrentRequest().getWrappedSession() .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) { diff --git a/licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt b/licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt new file mode 100644 index 0000000000..4957bc6ea0 --- /dev/null +++ b/licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt @@ -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 diff --git a/pom.xml b/pom.xml index fe79ba4d50..1338ec4576 100644 --- a/pom.xml +++ b/pom.xml @@ -327,6 +327,7 @@ licenses/LICENSE_HEADER_TEMPLATE_MICROSOFT_18.txt licenses/LICENSE_HEADER_TEMPLATE_BOSCH_18.txt licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_19.txt + licenses/LICENSE_HEADER_TEMPLATE_KIWIGRID_19.txt **/banner.txt