From 2b7084920c5e49cd35b37adf1b3c619e10db30d6 Mon Sep 17 00:00:00 2001 From: jerrypan44 <jerrypan44@gmail.com> Date: Fri, 22 Nov 2019 15:35:02 +0000 Subject: [PATCH] WIP committed working features roles and keycloak integration --- docker/config/application.tmpl | 5 +- pom.xml | 6 - .../configuration/KeycloakConfiguration.java | 14 - .../configuration/SecurityConfiguration.java | 417 +++++++++--------- .../eu/hbp/mip/controllers/SecurityApi.java | 2 + 5 files changed, 213 insertions(+), 231 deletions(-) delete mode 100644 src/main/java/eu/hbp/mip/configuration/KeycloakConfiguration.java diff --git a/docker/config/application.tmpl b/docker/config/application.tmpl index e3a624a13..c05c497c1 100644 --- a/docker/config/application.tmpl +++ b/docker/config/application.tmpl @@ -30,11 +30,11 @@ hbp: clientSecret: {{ .Env.CLIENT_SECRET }} accessTokenUri: {{ default .Env.TOKEN_URI "https://services.humanbrainproject.eu/oidc/token" }} userAuthorizationUri: {{ default .Env.AUTH_URI "https://services.humanbrainproject.eu/oidc/authorize" }} - tokenName: oauth_token + tokenName: access_token authenticationScheme: query clientAuthenticationScheme: form useCurrentUri: false - preEstablishedRedirectUri: {{ default .Env.FRONTEND_LOGIN_URL "http://frontend/services/login/hbp" }} + preEstablishedRedirectUri: {{ default .Env.FRONTEND_LOGIN_URL "http://localhost:8080/services/login/hbp" }} resource: userInfoUri: {{ default .Env.USER_INFO_URI "https://services.humanbrainproject.eu/oidc/userinfo" }} revokeTokenUri: {{ default .Env.REVOKE_TOKEN_URI "https://services.humanbrainproject.eu/oidc/slo" }} @@ -52,6 +52,7 @@ logging: root: {{ default .Env.LOG_LEVEL "INFO" }} org: springframework: + security: DEBUG web: {{ default .Env.LOGGING_LEVEL_WEB "WARN" }} web.servlet.handler.BeanNameUrlHandlerMapping: WARN hibernate: {{ default .Env.LOGGING_LEVEL_HIBERNATE "WARN" }} diff --git a/pom.xml b/pom.xml index d206d2ad2..f0fbbe9dc 100644 --- a/pom.xml +++ b/pom.xml @@ -255,12 +255,6 @@ <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency> - <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-spring-boot-starter</artifactId> - </dependency> - - </dependencies> <build> diff --git a/src/main/java/eu/hbp/mip/configuration/KeycloakConfiguration.java b/src/main/java/eu/hbp/mip/configuration/KeycloakConfiguration.java deleted file mode 100644 index 45c598924..000000000 --- a/src/main/java/eu/hbp/mip/configuration/KeycloakConfiguration.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.hbp.mip.configuration; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import org.keycloak.KeycloakSecurityContext; - -public class KeycloakConfiguration { - - @Autowired - private HttpServletRequest request; - public KeycloakSecurityContext getKeycloakSecurityContext() { - return (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java b/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java index ba6a380de..b8259bca6 100644 --- a/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java +++ b/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; import org.springframework.boot.context.embedded.FilterRegistrationBean; @@ -34,6 +35,10 @@ import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.WebUtils; +import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -42,223 +47,217 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; // See https://spring.io/guides/tutorials/spring-boot-oauth2/ for reference about configuring OAuth2 login // also http://cscarioni.blogspot.ch/2013/04/pro-spring-security-and-oauth-2.html -/** - * Configuration for security. - */ -@KeycloakConfiguration -public class SecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter { - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(keycloakAuthenticationProvider()); - } +@Configuration +@EnableOAuth2Client +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - /** - * Defines the session authentication strategy. - */ - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new NullAuthenticatedSessionStrategy(); - } + private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfiguration.class); - @Override - protected void configure(HttpSecurity http) throws Exception - { - super.configure(http); - http - .authorizeRequests() - .antMatchers("/user*").authenticated() - .antMatchers("/public1*").hasAuthority("public13") - .antMatchers("/public2*").hasAuthority("public2") - .antMatchers("/public3*").hasAuthority("public3") - .antMatchers("/admin*").authenticated() - .anyRequest().permitAll() - .and() - .logout() - .addLogoutHandler(keycloakLogoutHandler())//.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) - .logoutUrl("/logout").permitAll() - .logoutSuccessUrl("/"); -// .anyRequest().permitAll().and().logout().addLogoutHandler(new KeycloakLogoutHandler(new RestTemplate())).logoutRequestMatcher(new AntPathRequestMatcher("/logout")); - ; - } + @Autowired + private OAuth2ClientContext oauth2ClientContext; -} + /** + * Enable HBP collab authentication (1) or disable it (0). Default is 1 + */ + @Value("#{'${hbp.authentication.enabled:1}'}") + private boolean authentication; + + /** + * Absolute URL to redirect to when login is required + */ + @Value("#{'${frontend.loginUrl:/login/hbp}'}") + private String loginUrl; + + /** + * Absolute URL to redirect to after successful login + */ + @Value("#{'${frontend.redirectAfterLoginUrl:http://frontend/home}'}") + private String frontendRedirectAfterLogin; + + /** + * Absolute URL to redirect to after logout has occurred + */ + @Value("#{'${frontend.redirectAfterLogoutUrl:/login/hbp}'}") + private String redirectAfterLogoutUrl; + + /** + * URL to revoke auth token + */ + @Value("#{'${hbp.resource.revokeTokenUri:https://services.humanbrainproject.eu/oidc/revoke}'}") + private String revokeTokenURI; -//@Configuration -//@EnableOAuth2Client -//public class SecurityConfiguration extends WebSecurityConfigurerAdapter { -// -// private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfiguration.class); -// // @Autowired -// private OAuth2ClientContext oauth2ClientContext; -// -// /** -// * Enable HBP collab authentication (1) or disable it (0). Default is 1 -// */ -// @Value("#{'${hbp.authentication.enabled:1}'}") -// private boolean authentication; -// -// /** -// * Absolute URL to redirect to when login is required -// */ -// @Value("#{'${frontend.loginUrl:/login/hbp}'}") -// private String loginUrl; -// -// /** -// * Absolute URL to redirect to after successful login -// */ -// @Value("#{'${frontend.redirectAfterLoginUrl:http://frontend/home}'}") -// private String frontendRedirectAfterLogin; -// -// /** -// * Absolute URL to redirect to after logout has occurred -// */ -// @Value("#{'${frontend.redirectAfterLogoutUrl:/login/hbp}'}") -// private String redirectAfterLogoutUrl; -// -// /** -// * URL to revoke auth token -// */ -// @Value("#{'${hbp.resource.revokeTokenUri:https://services.humanbrainproject.eu/oidc/revoke}'}") -// private String revokeTokenURI; -// -//// @Autowired -//// private HttpServletRequest request; -// -// @Override -// protected void configure(HttpSecurity http) throws Exception { -// // @formatter:off -// http.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class); -// -// if (authentication) { +// private HttpServletRequest request; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class); + + if (authentication) { + http.antMatcher("/**") + .authorizeRequests() + .antMatchers( + "/", "/login/**", "/health/**", "/info/**", "/metrics/**", "/trace/**", "/frontend/**", "/webjars/**", "/v2/api-docs", "/swagger-ui.html", "/swagger-resources/**" + ) + .permitAll() + .antMatchers("/galaxy*","/galaxy/*").hasRole("Data Manager") + //.anyRequest().authenticated() + .anyRequest().hasRole("Researcher") + .and().exceptionHandling().authenticationEntryPoint(new CustomLoginUrlAuthenticationEntryPoint(loginUrl)) + .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessUrl(redirectAfterLogoutUrl) + .and().logout().permitAll() + .and().csrf().ignoringAntMatchers("/logout").csrfTokenRepository(csrfTokenRepository()) + .and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class) + .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); + } + else { + //keycloak + //KeycloakConfiguration.getKeycloakSecurityContext(); // http.antMatcher("/**") // .authorizeRequests() -// .antMatchers( -// "/", "/login/**", "/health/**", "/info/**", "/metrics/**", "/trace/**", "/frontend/**", "/webjars/**", "/v2/api-docs", "/swagger-ui.html", "/swagger-resources/**" -// ).permitAll() -// .anyRequest().authenticated() -// .and().exceptionHandling().authenticationEntryPoint(new CustomLoginUrlAuthenticationEntryPoint(loginUrl)) -// .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessUrl(redirectAfterLogoutUrl) -// .and().logout().permitAll() -// .and().csrf().ignoringAntMatchers("/logout").csrfTokenRepository(csrfTokenRepository()) -// .and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class) -// .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); -// } -// else { -// //keycloak -// //KeycloakConfiguration.getKeycloakSecurityContext(); -//// http.antMatcher("/**") -//// .authorizeRequests() -//// .antMatchers("/**").permitAll().and().csrf().disable(); -// } -// } -// -// private Filter ssoFilter() { -// OAuth2ClientAuthenticationProcessingFilter hbpFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/hbp"); -// OAuth2RestTemplate hbpTemplate = new OAuth2RestTemplate(hbp(), oauth2ClientContext); -// hbpFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(frontendRedirectAfterLogin)); -// hbpFilter.setRestTemplate(hbpTemplate); -// hbpFilter.setTokenServices(new UserInfoTokenServices(hbpResource().getUserInfoUri(), hbp().getClientId())); -// return hbpFilter; -// } -// -// @Bean -// public FilterRegistrationBean oauth2ClientFilterRegistration( -// OAuth2ClientContextFilter filter) { -// FilterRegistrationBean registration = new FilterRegistrationBean(); -// registration.setFilter(filter); -// registration.setOrder(-100); -// return registration; -// } -// -// @Bean(name="hbp") -// @ConfigurationProperties("hbp.client") -// public OAuth2ProtectedResourceDetails hbp() { -// return new AuthorizationCodeResourceDetails(); -// } -// -// @Bean(name="hbpResource") -// @ConfigurationProperties("hbp.resource") -// public ResourceServerProperties hbpResource() { -// return new ResourceServerProperties(); -// } -// -// public boolean isAuthentication() { -// return authentication; -// } -// -// public String getFrontendRedirectAfterLogin() { -// return frontendRedirectAfterLogin; -// } -// -// private Filter csrfHeaderFilter() { -// return new OncePerRequestFilter() { -// @Override -// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, -// FilterChain filterChain) throws ServletException, IOException { -// CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); -// if (csrf != null) { -// Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN"); -// String token = csrf.getToken(); -// if (cookie == null || token != null && !token.equals(cookie.getValue())) { -// cookie = new Cookie("XSRF-TOKEN", token); -// cookie.setPath("/"); -// response.addCookie(cookie); -// } -// } -// filterChain.doFilter(request, response); -// } -// }; -// } -// -// private CsrfTokenRepository csrfTokenRepository() { -// HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); -// repository.setHeaderName("X-XSRF-TOKEN"); -// return repository; -// } -// -// private class CustomLogoutHandler implements LogoutHandler { -// @Override -// public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { -// -// // Hackish way of accessing to this information... -// final UserInfo userInfo = (UserInfo) httpServletRequest.getSession().getAttribute("userInfo"); -// if (userInfo != null) { -// userInfo.setFakeAuth(false); -// } -// -// if (oauth2ClientContext == null || oauth2ClientContext.getAccessToken() == null) -// { -// return; -// } -// -// String idToken = oauth2ClientContext.getAccessToken().getAdditionalInformation().get("id_token").toString(); -// -// StringBuilder query = new StringBuilder(); -// query.append("{"); -// query.append("\"token\":"); -// query.append("\"").append(idToken).append("\""); -// query.append("}"); -// -// try { -// int responseCode = HTTPUtil.sendPost(revokeTokenURI, query.toString(), new StringBuilder()); -// if (responseCode != 200) -// { -// LOGGER.warn("Cannot send request to OIDC server for revocation ! "); -// } -// else{ -// LOGGER.info("Should be logged out"); -// } -// } catch (IOException e) { -// LOGGER.warn("Cannot notify logout to OIDC server !"); -// LOGGER.trace("Cannot notify logout", e); -// } -// -// } -// } -//} +// .antMatchers("/**").permitAll().and().csrf().disable(); + } + } + + private Filter ssoFilter() { + OAuth2ClientAuthenticationProcessingFilter hbpFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/hbp"); + OAuth2RestTemplate hbpTemplate = new OAuth2RestTemplate(hbp(), oauth2ClientContext); + hbpFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(frontendRedirectAfterLogin)); + hbpFilter.setRestTemplate(hbpTemplate); + hbpFilter.setTokenServices(new UserInfoTokenServices(hbpResource().getUserInfoUri(), hbp().getClientId())); + return hbpFilter; + } + + @Bean + public FilterRegistrationBean oauth2ClientFilterRegistration( + OAuth2ClientContextFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(filter); + registration.setOrder(-100); + return registration; + } + + @Bean(name="hbp") + @ConfigurationProperties("hbp.client") + public BaseOAuth2ProtectedResourceDetails hbp() { + return new AuthorizationCodeResourceDetails(); + } + + @Bean(name="hbpResource") + @ConfigurationProperties("hbp.resource") + public ResourceServerProperties hbpResource() { + return new ResourceServerProperties(); + } + + public boolean isAuthentication() { + return authentication; + } + + public String getFrontendRedirectAfterLogin() { + return frontendRedirectAfterLogin; + } + + private Filter csrfHeaderFilter() { + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + if (csrf != null) { + Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN"); + String token = csrf.getToken(); + if (cookie == null || token != null && !token.equals(cookie.getValue())) { + cookie = new Cookie("XSRF-TOKEN", token); + cookie.setPath("/"); + response.addCookie(cookie); + } + } + filterChain.doFilter(request, response); + } + }; + } + + private CsrfTokenRepository csrfTokenRepository() { + HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); + repository.setHeaderName("X-XSRF-TOKEN"); + return repository; + } + + private class CustomLogoutHandler implements LogoutHandler { + @Override + public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { + + // Hackish way of accessing to this information... + final UserInfo userInfo = (UserInfo) httpServletRequest.getSession().getAttribute("userInfo"); + if (userInfo != null) { + userInfo.setFakeAuth(false); + } + + if (oauth2ClientContext == null || oauth2ClientContext.getAccessToken() == null) + { + return; + } + + String idToken = oauth2ClientContext.getAccessToken().getAdditionalInformation().get("id_token").toString(); + + StringBuilder query = new StringBuilder(); + query.append("{"); + query.append("\"token\":"); + query.append("\"").append(idToken).append("\""); + query.append("}"); + + try { + int responseCode = HTTPUtil.sendPost(revokeTokenURI, query.toString(), new StringBuilder()); + if (responseCode != 200) + { + LOGGER.warn("Cannot send request to OIDC server for revocation ! "); + } + else{ + LOGGER.info("Should be logged out"); + } + } catch (IOException e) { + LOGGER.warn("Cannot notify logout to OIDC server !"); + LOGGER.trace("Cannot notify logout", e); + } + + } + } + + @Bean + public AuthoritiesExtractor keycloakAuthoritiesExtractor() { + return new KeycloakAuthoritiesExtractor(); + } + + + public class KeycloakAuthoritiesExtractor + implements AuthoritiesExtractor { + + @Override + public List<GrantedAuthority> extractAuthorities + (Map<String, Object> map) { + return AuthorityUtils + .commaSeparatedStringToAuthorityList(asAuthorities(map)); + } + + private String asAuthorities(Map<String, Object> map) { + List<String> authorities = new ArrayList<>(); +// authorities.add("BAELDUNG_USER"); + List<LinkedHashMap<String, String>> authz; + authz = (List<LinkedHashMap<String, String>>) map.get("authorities"); + for (LinkedHashMap<String, String> entry : authz) { + authorities.add(entry.get("authority")); + } + return String.join(",", authorities); + } + } + +} diff --git a/src/main/java/eu/hbp/mip/controllers/SecurityApi.java b/src/main/java/eu/hbp/mip/controllers/SecurityApi.java index cfddbd334..08c6f0410 100644 --- a/src/main/java/eu/hbp/mip/controllers/SecurityApi.java +++ b/src/main/java/eu/hbp/mip/controllers/SecurityApi.java @@ -17,6 +17,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.security.access.prepost.PreAuthorize; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @@ -102,6 +103,7 @@ public class SecurityApi { */ @RequestMapping(path = "/galaxy", method = RequestMethod.GET, produces = "application/json") + @PreAuthorize("hasRole('Data Manager')") @ResponseStatus(value = HttpStatus.OK) public ResponseEntity getGalaxyConfiguration(){ String stringEncoded = Base64.getEncoder().encodeToString((galaxyUsername + ":" + galaxyPassword).getBytes()); -- GitLab