diff --git a/quartz-manager-api/.gitignore b/quartz-manager-api/.gitignore index 233a70f..b889765 100644 --- a/quartz-manager-api/.gitignore +++ b/quartz-manager-api/.gitignore @@ -7,3 +7,5 @@ /mvnw /mvnw.cmd /.classpath +/.idea/ +/quartz-manager.iml diff --git a/quartz-manager-api/pom.xml b/quartz-manager-api/pom.xml index 72afb43..caccc3c 100644 --- a/quartz-manager-api/pom.xml +++ b/quartz-manager-api/pom.xml @@ -26,6 +26,7 @@ + org.springframework.boot spring-boot-starter-web @@ -58,6 +59,11 @@ org.springframework spring-tx + + org.springframework.boot + spring-boot-configuration-processor + true + org.springframework.boot spring-boot-starter-tomcat @@ -69,6 +75,7 @@ test + io.jsonwebtoken jjwt @@ -91,23 +98,31 @@ h2 runtime - org.codehaus.groovy groovy - net.sourceforge.nekohtml nekohtml - io.rest-assured spring-mock-mvc test + + org.projectlombok + lombok + provided + + + org.apache.commons + commons-lang3 + 3.10 + + org.quartz-scheduler quartz @@ -158,13 +173,6 @@ ${springfox.version} - - org.projectlombok - lombok - provided - - - diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java deleted file mode 100644 index b0b9f6f..0000000 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java +++ /dev/null @@ -1,88 +0,0 @@ -package it.fabioformosa.quartzmanager.configuration; - -import javax.annotation.Resource; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.crypto.factory.PasswordEncoderFactories; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -import it.fabioformosa.quartzmanager.security.ComboEntryPoint; -import it.fabioformosa.quartzmanager.security.auth.AuthenticationFailureHandler; -import it.fabioformosa.quartzmanager.security.auth.AuthenticationSuccessHandler; - -//@Configuration -//@EnableWebSecurity -//@EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - - // @Configuration - @Order(1) - public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter { - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/","/v2/api-docs", - "/swagger-resources/**", - "/swagger-ui.html", - "/webjars/**", - "/csrf"); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable() // - .antMatcher("/notifications").authorizeRequests().anyRequest().hasAnyRole("ADMIN").and() - .httpBasic(); - } - } - - // @Configuration - @Order(2) - public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter { - - @Resource - private ComboEntryPoint comboEntryPoint; - - @Autowired - private AuthenticationSuccessHandler authenticationSuccessHandler; - - @Autowired - private AuthenticationFailureHandler authenticationFailureHandler; - - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/webjars/**"); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - // http.csrf().ignoringAntMatchers("/api/login", "/api/signup").and() // - http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(comboEntryPoint) - .and()// - .authorizeRequests().anyRequest().authenticated().and()// - .formLogin().loginPage("/api/login").successHandler(authenticationSuccessHandler) - .failureHandler(authenticationFailureHandler).and().logout() - .logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")) - .logoutSuccessUrl("/manager"); - } - } - - @Value("${quartz-manager.account.user}") - private String adminUser; - - @Value("${quartz-manager.account.pwd}") - private String adminPwd; - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - auth.inMemoryAuthentication().withUser(adminUser).password(encoder.encode(adminPwd)).roles("ADMIN"); - } - -} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfigJWT.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfigJWT.java index 77bb44f..d264134 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfigJWT.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfigJWT.java @@ -1,10 +1,14 @@ package it.fabioformosa.quartzmanager.configuration; +import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; @@ -14,114 +18,175 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import it.fabioformosa.quartzmanager.security.auth.AuthenticationFailureHandler; -import it.fabioformosa.quartzmanager.security.auth.AuthenticationSuccessHandler; -import it.fabioformosa.quartzmanager.security.auth.LogoutSuccess; -import it.fabioformosa.quartzmanager.security.auth.RestAuthenticationEntryPoint; -import it.fabioformosa.quartzmanager.security.auth.TokenAuthenticationFilter; +import com.fasterxml.jackson.databind.ObjectMapper; + +import it.fabioformosa.quartzmanager.configuration.properties.InMemoryAccountProperties; +import it.fabioformosa.quartzmanager.configuration.properties.JwtSecurityProperties; +import it.fabioformosa.quartzmanager.security.helpers.LoginConfigurer; +import it.fabioformosa.quartzmanager.security.helpers.impl.AuthenticationFailureHandler; +import it.fabioformosa.quartzmanager.security.helpers.impl.AuthenticationSuccessHandler; +import it.fabioformosa.quartzmanager.security.helpers.impl.FormLoginConfig; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtAuthenticationSuccessHandler; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtAuthenticationSuccessHandlerImpl; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtTokenAuthenticationFilter; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtTokenHelper; +import it.fabioformosa.quartzmanager.security.helpers.impl.QuartzManagerHttpSecurity; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtUsernamePasswordFiterLoginConfig; +import it.fabioformosa.quartzmanager.security.helpers.impl.LogoutSuccess; /** * * @author Fabio.Formosa * */ - @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfigJWT extends WebSecurityConfigurerAdapter { - @Value("${jwt.cookie}") - private String TOKEN_COOKIE; + private static final String[] PATTERNS_SWAGGER_UI = {"/swagger-ui.html", "/v2/api-docs", "/swagger-resources/**", "/webjars/**"}; - // @Autowired - // private CustomUserDetailsService jwtUserDetailsService; + private static final String LOGIN_PATH = "/api/login"; + private static final String LOGOUT_PATH = "/api/logout"; - @Autowired - private RestAuthenticationEntryPoint restAuthenticationEntryPoint; + @Value("${server.servlet.context-path}") + private String contextPath; - @Autowired - private LogoutSuccess logoutSuccess; + @Value("${app.name}") + private String APP_NAME; - @Autowired - private AuthenticationSuccessHandler authenticationSuccessHandler; + @Value("${quartz-manager.security.login-model.form-login-enabled}") + private Boolean formLoginEnabled; + @Value("${quartz-manager.security.login-model.userpwd-filter-enabled}") + private Boolean userpwdFilterEnabled; - @Autowired - private AuthenticationFailureHandler authenticationFailureHandler; + @Autowired + private JwtSecurityProperties jwtSecurityProps; - @Value("${quartz-manager.account.user}") - private String adminUser; + @Autowired + private ObjectMapper objectMapper; - @Value("${quartz-manager.account.pwd}") - private String adminPwd; + @Autowired + private UserDetailsService userDetailsService; - // @Bean - // @Override - // public AuthenticationManager authenticationManagerBean() throws Exception { - // return super.authenticationManagerBean(); - // } + @Autowired + private InMemoryAccountProperties inMemoryAccountProps; - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/css/**", // - "/js/**", // - "/img/**", // - "/lib/**", // - "/swagger-resources/**", "/swagger-ui.html","/v2/api-docs", // - "/webjars/**"); - } - @Autowired - public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) - throws Exception { - // authenticationManagerBuilder.userDetailsService(jwtUserDetailsService) - // .passwordEncoder(passwordEncoder()); - PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - authenticationManagerBuilder.inMemoryAuthentication().withUser(adminUser).password(encoder.encode(adminPwd)).roles("ADMIN"); - } + @Override + public void configure(AuthenticationManagerBuilder authenticationManagerBuilder)throws Exception { + configureInMemoryAuthentication(authenticationManagerBuilder); + } - @Bean - public TokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception { - return new TokenAuthenticationFilter(); - } + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() // + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // + .exceptionHandling().authenticationEntryPoint(restAuthEntryPoint()).and() // + .addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class) // + .authorizeRequests().anyRequest().authenticated(); - @Bean - @Override - public UserDetailsService userDetailsServiceBean() throws Exception { - return super.userDetailsServiceBean(); - } + QuartzManagerHttpSecurity.from(http).withLoginConfigurer(loginConfigurer(), logoutConfigurer()) // + .login(LOGIN_PATH, authenticationManager()).logout(LOGOUT_PATH); - // @Bean - // public PasswordEncoder passwordEncoder() { - // return new BCryptPasswordEncoder(); - // } + // temporary disabled csfr + // http.csrf().ignoringAntMatchers("/api/login", "/api/signup") // + // .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // + } - @Override - protected void configure(HttpSecurity http) throws Exception { - // http.csrf().ignoringAntMatchers("/api/login", "/api/signup") // - // .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // - http.csrf().disable() // - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // - .exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).and() - .addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class) - .authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/api/login") - .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler) - .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")) - .logoutSuccessHandler(logoutSuccess).deleteCookies(TOKEN_COOKIE); + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring()// + .antMatchers(HttpMethod.GET, PATTERNS_SWAGGER_UI) // + .antMatchers(HttpMethod.GET,"/css/**", "/js/**", "/img/**", "/lib/**"); + } - } + private void configureInMemoryAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { + PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + if(inMemoryAccountProps.isEnabled() && inMemoryAccountProps.getUsers() != null && !inMemoryAccountProps.getUsers().isEmpty()) { + InMemoryUserDetailsManagerConfigurer inMemoryAuth = authenticationManagerBuilder.inMemoryAuthentication(); + inMemoryAccountProps.getUsers() + .forEach(u -> inMemoryAuth + .withUser(u.getName()) + .password(encoder.encode(u.getPassword())) + .roles(u.getRoles().toArray(new String[0]))); + } + } - @Bean - CorsConfigurationSource corsConfigurationSource() { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); - return source; - } + @Bean + CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); + return source; + } + + @Bean + public LoginConfigurer formLoginConfigurer() { + JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler = jwtAuthenticationSuccessHandler(); + AuthenticationSuccessHandler authenticationSuccessHandler = new AuthenticationSuccessHandler(jwtAuthenticationSuccessHandler); + AuthenticationFailureHandler authenticationFailureHandler = new AuthenticationFailureHandler(); + LoginConfigurer loginConfigurer = new FormLoginConfig(authenticationSuccessHandler, authenticationFailureHandler); + return loginConfigurer; + } + + @Bean + public JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler() { + JwtTokenHelper jwtTokenHelper = jwtTokenHelper(); + JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler = new JwtAuthenticationSuccessHandlerImpl(contextPath, jwtSecurityProps, jwtTokenHelper, objectMapper); + return jwtAuthenticationSuccessHandler; + } + + @Bean + public JwtTokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception { + return new JwtTokenAuthenticationFilter(jwtTokenHelper(), userDetailsService); + } + + @Bean + public JwtTokenHelper jwtTokenHelper() { + return new JwtTokenHelper(APP_NAME, jwtSecurityProps); + } + + @Bean + public LoginConfigurer loginConfigurer() { + if(BooleanUtils.isTrue(userpwdFilterEnabled)) + return userpwdFilterLoginConfigurer(); + if(BooleanUtils.isNotFalse(formLoginEnabled)) + return formLoginConfigurer(); + throw new RuntimeException("No login configurer enabled!"); + } + + @Bean + public LogoutSuccess logoutConfigurer() { + return new LogoutSuccess(objectMapper); + } + + @Bean + public AuthenticationEntryPoint restAuthEntryPoint() { + return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); + } + + @Bean + @Override + public UserDetailsService userDetailsServiceBean() throws Exception { + return super.userDetailsServiceBean(); + } + + @Bean + public LoginConfigurer userpwdFilterLoginConfigurer() { + LoginConfigurer loginConfigurer = new JwtUsernamePasswordFiterLoginConfig(jwtAuthenticationSuccessHandler()); + return loginConfigurer; + } + + // @Bean + // public PasswordEncoder passwordEncoder() { + // return new BCryptPasswordEncoder(); + // } } diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/InMemoryAccountProperties.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/InMemoryAccountProperties.java new file mode 100644 index 0000000..a6905ba --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/InMemoryAccountProperties.java @@ -0,0 +1,24 @@ +package it.fabioformosa.quartzmanager.configuration.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@ConfigurationProperties(prefix = "quartz-manager.accounts.in-memory") +@Getter @Setter +public class InMemoryAccountProperties { + private boolean enabled; + private List users; + + @Getter @Setter + public static class User { + private String name; + private String password; + private List roles = new ArrayList<>(); + } +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/JwtSecurityProperties.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/JwtSecurityProperties.java new file mode 100644 index 0000000..e33baba --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/configuration/properties/JwtSecurityProperties.java @@ -0,0 +1,33 @@ +package it.fabioformosa.quartzmanager.configuration.properties; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + + +@Configuration +@ConfigurationProperties(prefix = "quartz-manager.security.jwt") +@Getter @Setter +public class JwtSecurityProperties { + private boolean enabled; + private String secret; + private long expirationInSec; + + private CookieStrategy cookieStrategy; + private HeaderStrategy headerStrategy; + + @Data + public static class CookieStrategy { + private boolean enabled; + private String cookie; + } + + @Data + public static class HeaderStrategy { + private boolean enabled; + private String header; + } + +} \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/controllers/AuthenticationController.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/controllers/AuthenticationController.java index 61f64a4..4b0d704 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/controllers/AuthenticationController.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/controllers/AuthenticationController.java @@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import it.fabioformosa.quartzmanager.security.TokenHelper; +import it.fabioformosa.quartzmanager.security.helpers.impl.JwtTokenHelper; import it.fabioformosa.quartzmanager.security.model.UserTokenState; import it.fabioformosa.quartzmanager.security.service.impl.CustomUserDetailsService; @@ -39,12 +39,12 @@ public class AuthenticationController { private CustomUserDetailsService userDetailsService; @Autowired - TokenHelper tokenHelper; + JwtTokenHelper tokenHelper; - @Value("${jwt.expires_in_sec}") + @Value("${quartz-manager.security.jwt.expiration-in-sec}") private int EXPIRES_IN_SEC; - @Value("${jwt.cookie}") + @Value("${quartz-manager.security.jwt.cookie-strategy-cookie}") private String TOKEN_COOKIE; @RequestMapping(value = "/changePassword", method = RequestMethod.POST) @@ -59,7 +59,7 @@ public class AuthenticationController { @RequestMapping(value = "/refresh", method = RequestMethod.GET) public ResponseEntity refreshAuthenticationToken(HttpServletRequest request, HttpServletResponse response) { - String authToken = tokenHelper.getToken( request ); + String authToken = tokenHelper.retrieveToken( request ); if (authToken != null && tokenHelper.canTokenBeRefreshed(authToken)) { // TODO check user password last update String refreshedToken = tokenHelper.refreshToken(authToken); diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationSuccessHandler.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationSuccessHandler.java deleted file mode 100644 index 9f3bd77..0000000 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationSuccessHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -package it.fabioformosa.quartzmanager.security.auth; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import it.fabioformosa.quartzmanager.security.TokenHelper; -import it.fabioformosa.quartzmanager.security.model.UserTokenState; - -@Component -public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - - @Value("${jwt.expires_in_sec}") - private int EXPIRES_IN_SEC; - - @Value("${jwt.cookie}") - private String TOKEN_COOKIE; - - @Autowired - TokenHelper tokenHelper; - // - @Autowired - ObjectMapper objectMapper; - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication ) throws IOException, ServletException { - clearAuthenticationAttributes(request); - User user = (User)authentication.getPrincipal(); - - String jws = tokenHelper.generateToken( user.getUsername() ); - - Cookie authCookie = new Cookie( TOKEN_COOKIE, jws ); - - authCookie.setHttpOnly( true ); - - authCookie.setMaxAge( EXPIRES_IN_SEC ); - - authCookie.setPath( "/quartz-manager" ); - response.addCookie( authCookie ); - - // JWT is also in the response - UserTokenState userTokenState = new UserTokenState(jws, EXPIRES_IN_SEC); - String jwtResponse = objectMapper.writeValueAsString( userTokenState ); - response.setContentType("application/json"); - response.getWriter().write( jwtResponse ); - - } - - // @Override - // public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - // Authentication authentication ) throws IOException, ServletException { - // // clearAuthenticationAttributes(request); - // response.setContentType("application/json"); - // response.getWriter().write( objectMapper.writeValueAsString("OK")); - // } -} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/LogoutSuccess.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/LogoutSuccess.java deleted file mode 100644 index 82d2f17..0000000 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/LogoutSuccess.java +++ /dev/null @@ -1,35 +0,0 @@ -package it.fabioformosa.quartzmanager.security.auth; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@Component -public class LogoutSuccess implements LogoutSuccessHandler { - - @Autowired - ObjectMapper objectMapper; - - @Override - public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) - throws IOException, ServletException { - Map result = new HashMap<>(); - result.put( "result", "success" ); - response.setContentType("application/json"); - response.getWriter().write( objectMapper.writeValueAsString( result ) ); - response.setStatus(HttpServletResponse.SC_OK); - - } - -} \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenAuthenticationFilter.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenAuthenticationFilter.java deleted file mode 100644 index b0856ad..0000000 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenAuthenticationFilter.java +++ /dev/null @@ -1,87 +0,0 @@ -package it.fabioformosa.quartzmanager.security.auth; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; -import org.springframework.web.filter.OncePerRequestFilter; - -import it.fabioformosa.quartzmanager.security.TokenHelper; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class TokenAuthenticationFilter extends OncePerRequestFilter { - - public static final String ROOT_MATCHER = "/"; - public static final String FAVICON_MATCHER = "/favicon.ico"; - public static final String HTML_MATCHER = "/**/*.html"; - public static final String CSS_MATCHER = "/**/*.css"; - public static final String JS_MATCHER = "/**/*.js"; - public static final String IMG_MATCHER = "/images/*"; - public static final String LOGIN_MATCHER = "/auth/login"; - public static final String LOGOUT_MATCHER = "/auth/logout"; - - // private final Log logger = LogFactory.getLog(this.getClass()); - - @Autowired - private TokenHelper tokenHelper; - - @Autowired - private UserDetailsService userDetailsService; - - private List pathsToSkip = Arrays.asList( - ROOT_MATCHER, - HTML_MATCHER, - FAVICON_MATCHER, - CSS_MATCHER, - JS_MATCHER, - IMG_MATCHER, - LOGIN_MATCHER, - LOGOUT_MATCHER - ); - - @Override - public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - - String authToken = tokenHelper.getToken(request); - if (authToken != null && !skipPathRequest(request, pathsToSkip)) - try { - String username = tokenHelper.getUsernameFromToken(authToken); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - // create authentication - TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails); - authentication.setToken(authToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - } catch (Exception e) { - SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); - log.error("Switched to Anonimous Authentication, " - + "because an error occurred setting authentication in security context holder due to " + e.getMessage(), e); - } - else - SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); - - chain.doFilter(request, response); - } - - private boolean skipPathRequest(HttpServletRequest request, List pathsToSkip ) { - Assert.notNull(pathsToSkip, "path cannot be null."); - List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); - OrRequestMatcher matchers = new OrRequestMatcher(m); - return matchers.matches(request); - } - -} \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenBasedAuthentication.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenBasedAuthentication.java deleted file mode 100644 index dcf3d18..0000000 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/TokenBasedAuthentication.java +++ /dev/null @@ -1,42 +0,0 @@ -package it.fabioformosa.quartzmanager.security.auth; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.userdetails.UserDetails; - - -public class TokenBasedAuthentication extends AbstractAuthenticationToken { - - private static final long serialVersionUID = 1L; - - private String token; - private final UserDetails principle; - - public TokenBasedAuthentication( UserDetails principle ) { - super( principle.getAuthorities() ); - this.principle = principle; - } - - @Override - public Object getCredentials() { - return token; - } - - @Override - public UserDetails getPrincipal() { - return principle; - } - - public String getToken() { - return token; - } - - @Override - public boolean isAuthenticated() { - return true; - } - - public void setToken( String token ) { - this.token = token; - } - -} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/LoginConfigurer.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/LoginConfigurer.java new file mode 100644 index 0000000..4446087 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/LoginConfigurer.java @@ -0,0 +1,21 @@ +package it.fabioformosa.quartzmanager.security.helpers; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +/** + * It configures filters to authenticate credentials sent by client or to set authenticationSuccessHandler + * + * Implement this interface for a login strategy + * + */ +public interface LoginConfigurer { + + /** + * If the authentication is based on cookie, it returns the name of cookie to be erased at the logout + */ + String cookieMustBeDeletedAtLogout(); + + HttpSecurity login(String loginPath, HttpSecurity http, AuthenticationManager authenticationManager) throws Exception; + +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AjaxAuthenticationFilter.java similarity index 96% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AjaxAuthenticationFilter.java index 1cd4bd2..0c4e0b3 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AjaxAuthenticationFilter.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security; +package it.fabioformosa.quartzmanager.security.helpers.impl; import java.io.IOException; diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AnonAuthentication.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AnonAuthentication.java similarity index 87% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AnonAuthentication.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AnonAuthentication.java index 19796d3..f1c266f 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AnonAuthentication.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AnonAuthentication.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security.auth; +package it.fabioformosa.quartzmanager.security.helpers.impl; import org.springframework.security.authentication.AbstractAuthenticationToken; diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationFailureHandler.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationFailureHandler.java similarity index 52% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationFailureHandler.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationFailureHandler.java index a57f6e1..08bb8b4 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/AuthenticationFailureHandler.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationFailureHandler.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security.auth; +package it.fabioformosa.quartzmanager.security.helpers.impl; import java.io.IOException; @@ -8,15 +8,13 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.stereotype.Component; -@Component public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { - super.onAuthenticationFailure(request, response, exception); - } + super.onAuthenticationFailure(request, response, exception); + } } \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationSuccessHandler.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationSuccessHandler.java new file mode 100644 index 0000000..6634870 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/AuthenticationSuccessHandler.java @@ -0,0 +1,35 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; + +//@Component +//@ConditionalOnProperty(prefix = "quartz-manager.security.login-model", name = "form-login-enabled", havingValue = "true", matchIfMissing = true) +public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler; + + // @Autowired + public AuthenticationSuccessHandler(JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler) { + super(); + this.jwtAuthenticationSuccessHandler = jwtAuthenticationSuccessHandler; + } + + public String cookieMustBeDeletedAtLogout() { + return jwtAuthenticationSuccessHandler.cookieMustBeDeletedAtLogout(); + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication ) throws IOException, ServletException { + clearAuthenticationAttributes(request); + jwtAuthenticationSuccessHandler.onLoginSuccess(authentication, response); + } + +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/ComboEntryPoint.java similarity index 94% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/ComboEntryPoint.java index 2c05117..7ab933d 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/ComboEntryPoint.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security; +package it.fabioformosa.quartzmanager.security.helpers.impl; import java.io.IOException; diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/FormLoginConfig.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/FormLoginConfig.java new file mode 100644 index 0000000..206f969 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/FormLoginConfig.java @@ -0,0 +1,75 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; + +import it.fabioformosa.quartzmanager.security.helpers.LoginConfigurer; + +/** + * It delegates form to @FormLoginConfigurer of the httpSecurity. + * + */ +public class FormLoginConfig implements LoginConfigurer { + + private static final Logger log = LoggerFactory.getLogger(FormLoginConfig.class); + + private final AuthenticationSuccessHandler authenticationSuccessHandler; + + private final AuthenticationFailureHandler authenticationFailureHandler; + + + public FormLoginConfig() { + super(); + authenticationSuccessHandler = null; + authenticationFailureHandler = null; + } + + public FormLoginConfig(AuthenticationFailureHandler authenticationFailureHandler) { + super(); + authenticationSuccessHandler = null; + this.authenticationFailureHandler = authenticationFailureHandler; + } + + public FormLoginConfig(AuthenticationSuccessHandler authenticationSuccessHandler) { + super(); + this.authenticationSuccessHandler = authenticationSuccessHandler; + authenticationFailureHandler = null; + } + + public FormLoginConfig(AuthenticationSuccessHandler authenticationSuccessHandler, + AuthenticationFailureHandler authenticationFailureHandler) { + super(); + this.authenticationSuccessHandler = authenticationSuccessHandler; + this.authenticationFailureHandler = authenticationFailureHandler; + } + + @Override + public String cookieMustBeDeletedAtLogout() { + return authenticationSuccessHandler.cookieMustBeDeletedAtLogout(); + } + + @Override + public HttpSecurity login(String loginPath, + HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { + log.debug("Configuring login through FormLoginConfigurer..."); + + FormLoginConfigurer login = http.formLogin().loginPage(loginPath); + + if(authenticationSuccessHandler != null) { + log.debug("Setting an authenticationSuccessHandler"); + login = login.successHandler(authenticationSuccessHandler); + } + + if(authenticationFailureHandler != null) { + log.debug("Setting an authenticationFailureHandler"); + login = login.failureHandler(authenticationFailureHandler); + } + + HttpSecurity httpSecurity = login.and(); + return httpSecurity; + } + +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationFilter.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationFilter.java new file mode 100644 index 0000000..c15c0ac --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationFilter.java @@ -0,0 +1,34 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import lombok.SneakyThrows; + +/** + * It extends the @UsernamePasswordAuthenticationFilter and it overrides the successfulAuthentication method to put jwtToken in the response + * + */ +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler; + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler) { + this.jwtAuthenticationSuccessHandler = jwtAuthenticationSuccessHandler; + setAuthenticationManager(authenticationManager); + } + + @SneakyThrows + @Override + protected void successfulAuthentication(HttpServletRequest req, + HttpServletResponse res, + FilterChain chain, + Authentication auth) { + jwtAuthenticationSuccessHandler.onLoginSuccess(auth, res); + } +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandler.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandler.java new file mode 100644 index 0000000..6a956c9 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandler.java @@ -0,0 +1,14 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; + +public interface JwtAuthenticationSuccessHandler { + + String cookieMustBeDeletedAtLogout(); + + void onLoginSuccess(Authentication authentication, HttpServletResponse response) throws IOException; +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandlerImpl.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandlerImpl.java new file mode 100644 index 0000000..fee0ede --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtAuthenticationSuccessHandlerImpl.java @@ -0,0 +1,78 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import java.io.IOException; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import it.fabioformosa.quartzmanager.configuration.properties.JwtSecurityProperties; +import it.fabioformosa.quartzmanager.security.model.UserTokenState; + +/** + * It depends on @JwtTokenHelper to generate the jwtToken. + * On login success, it generates the jwtToken and it returns it to the login according to possible strategies: cookie, response header. + * You can choice the strategy through @JwtSecurityProperties + * + */ +public class JwtAuthenticationSuccessHandlerImpl implements JwtAuthenticationSuccessHandler { + + private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationSuccessHandlerImpl.class); + + private final JwtSecurityProperties jwtSecurityProps; + + private final JwtTokenHelper jwtTokenHelper; + + private final ObjectMapper objectMapper; + + private final String contextPath; + + @Autowired + public JwtAuthenticationSuccessHandlerImpl(String contextPath, JwtSecurityProperties jwtSecurityProps, JwtTokenHelper jwtTokenHelper, ObjectMapper objectMapper) { + this.contextPath = contextPath; + this.jwtSecurityProps = jwtSecurityProps; + this.jwtTokenHelper = jwtTokenHelper; + this.objectMapper = objectMapper; + } + + @Override + public String cookieMustBeDeletedAtLogout() { + if(!jwtSecurityProps.getCookieStrategy().isEnabled()) + return null; + return jwtSecurityProps.getCookieStrategy().getCookie(); + } + + @Override + public void onLoginSuccess(Authentication authentication, HttpServletResponse response) throws IOException { + log.debug("Login successed, generating jwtToken..."); + + User user = (User) authentication.getPrincipal(); + String jwtToken = jwtTokenHelper.generateToken(user.getUsername()); + + if(jwtSecurityProps.getCookieStrategy().isEnabled()) { + Cookie authCookie = new Cookie(jwtSecurityProps.getCookieStrategy().getCookie(), jwtToken); + authCookie.setHttpOnly(true); + authCookie.setMaxAge((int) jwtSecurityProps.getExpirationInSec()); + authCookie.setPath(contextPath); + response.addCookie(authCookie); + log.debug("Set jwtToken into the cookie {}", jwtSecurityProps.getCookieStrategy().getCookie()); + } + + if(jwtSecurityProps.getHeaderStrategy().isEnabled()) { + jwtTokenHelper.setHeader(response, jwtToken); + log.debug("Set jwtToken into the response header {}", jwtSecurityProps.getHeaderStrategy().getHeader()); + } + + UserTokenState userTokenState = new UserTokenState(jwtToken, jwtSecurityProps.getExpirationInSec()); + String jwtResponse = objectMapper.writeValueAsString(userTokenState); + response.setContentType("application/json"); + response.getWriter().write(jwtResponse); + } +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenAuthenticationFilter.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenAuthenticationFilter.java new file mode 100644 index 0000000..346bb2f --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenAuthenticationFilter.java @@ -0,0 +1,105 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + + +/** + * It finds the jwtToken into the request, it validates it and sets an @Authentication into the @SecurityContextHolder. + * If the request has a path included into the paths that must be skipped, it sets an anonymous authentication + * + * It delegates the jwtToken retrieve to the @JwtTokenHelper that applies several strategies. + * + */ +public class JwtTokenAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(JwtTokenAuthenticationFilter.class); + + private static final String ROOT_MATCHER = "/"; + private static final String FAVICON_MATCHER = "/favicon.ico"; + private static final String HTML_MATCHER = "/**/*.html"; + private static final String CSS_MATCHER = "/**/*.css"; + private static final String JS_MATCHER = "/**/*.js"; + private static final String IMG_MATCHER = "/images/*"; + private static final String LOGIN_MATCHER = "/api/login"; + private static final String LOGOUT_MATCHER = "/api/logout"; + + private static List PATH_TO_SKIP = Arrays.asList( + ROOT_MATCHER, + HTML_MATCHER, + FAVICON_MATCHER, + CSS_MATCHER, + JS_MATCHER, + IMG_MATCHER, + LOGIN_MATCHER, + LOGOUT_MATCHER + ); + + private final JwtTokenHelper jwtTokenHelper; + private final UserDetailsService userDetailsService; + + + public JwtTokenAuthenticationFilter(JwtTokenHelper jwtTokenHelper, UserDetailsService userDetailsService) { + super(); + this.jwtTokenHelper = jwtTokenHelper; + this.userDetailsService = userDetailsService; + } + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + + String jwtToken = jwtTokenHelper.retrieveToken(request); + if (jwtToken != null) { + log.debug("Found a jwtToken into the request {}", request.getPathInfo()); + try { + String username = jwtTokenHelper.getUsernameFromToken(jwtToken); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + JwtTokenBasedAuthentication authentication = new JwtTokenBasedAuthentication(userDetails); + authentication.setToken(jwtToken); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (Exception e) { + log.error("Authentication failed! an expected error occurred authenticating the request {}", request.getRequestURL()); + // SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); + // log.error("Switched to Anonymous Authentication, " + // + "because an error occurred setting authentication in security context holder due to " + e.getMessage(), e); + } + } + else if(skipPathRequest(request, PATH_TO_SKIP)) { + log.debug("Detected a path to be skipped from authentication, so activated anonymous auth for {}", request.getRequestURL()); + SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); + } + else + log.debug("Not found any jwtToken and the request hasn't a path to be skipped from auth. Path: {}", request.getRequestURL()); + + chain.doFilter(request, response); + } + + private boolean skipPathRequest(HttpServletRequest request, List pathsToSkip ) { + if(pathsToSkip == null) + pathsToSkip = new ArrayList(); + List matchers = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); + OrRequestMatcher compositeMatchers = new OrRequestMatcher(matchers); + return compositeMatchers.matches(request); + } + +} \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenBasedAuthentication.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenBasedAuthentication.java new file mode 100644 index 0000000..904ed07 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenBasedAuthentication.java @@ -0,0 +1,42 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + + +public class JwtTokenBasedAuthentication extends AbstractAuthenticationToken { + + private static final long serialVersionUID = 1L; + + private String token; + private final UserDetails principle; + + public JwtTokenBasedAuthentication(UserDetails principle) { + super(principle.getAuthorities()); + this.principle = principle; + } + + @Override + public Object getCredentials() { + return token; + } + + @Override + public UserDetails getPrincipal() { + return principle; + } + + public String getToken() { + return token; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + public void setToken( String token ) { + this.token = token; + } + +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/TokenHelper.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenHelper.java similarity index 50% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/TokenHelper.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenHelper.java index 6576838..b7f24d7 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/TokenHelper.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtTokenHelper.java @@ -1,51 +1,52 @@ -package it.fabioformosa.quartzmanager.security; +package it.fabioformosa.quartzmanager.security.helpers.impl; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Date; import java.util.Map; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.joda.time.DateTime; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import lombok.extern.slf4j.Slf4j; +import it.fabioformosa.quartzmanager.configuration.properties.JwtSecurityProperties; /** - * JWT Temporary disabled * * @author Fabio.Formosa * */ -@Slf4j -@Component -public class TokenHelper { +public class JwtTokenHelper { - @Value("${app.name}") - private String APP_NAME; + private static final Logger log = LoggerFactory.getLogger(JwtTokenHelper.class); - @Value("${jwt.secret}") - private String SECRET; + private static String base64EncodeSecretKey(String secretKey) { + return Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); + } - @Value("${jwt.expires_in_sec}") - private int EXPIRES_IN_SEC; + // @Value("${app.name}") + private final String appName; - @Value("${jwt.header}") - private String AUTH_HEADER; - - @Value("${jwt.cookie}") - private String AUTH_COOKIE; - - // @Autowired - // UserDetailsService userDetailsService; + // @Autowired + private final JwtSecurityProperties jwtSecurityProps; private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512; + // @Autowired + public JwtTokenHelper(String appName, JwtSecurityProperties jwtSecurityProps) { + super(); + this.appName = appName; + this.jwtSecurityProps = jwtSecurityProps; + } + public Boolean canTokenBeRefreshed(String token) { try { final Date expirationDate = getClaimsFromToken(token).getExpiration(); @@ -62,34 +63,25 @@ public class TokenHelper { } private Date generateExpirationDate() { - return new Date(getCurrentTimeMillis() + EXPIRES_IN_SEC * 1000); + return new Date(getCurrentTimeMillis() + jwtSecurityProps.getExpirationInSec() * 1000); } - String generateToken(Map claims) { - return Jwts.builder() - .setClaims(claims) - .setExpiration(generateExpirationDate()) - .signWith( SIGNATURE_ALGORITHM, SECRET ) - .compact(); + private String generateToken(Map claims) { + return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()) + .signWith(SIGNATURE_ALGORITHM, base64EncodeSecretKey(jwtSecurityProps.getSecret())).compact(); } public String generateToken(String username) { - return Jwts.builder() - .setIssuer(APP_NAME) - .setSubject(username) - .setIssuedAt(generateCurrentDate()) + return Jwts.builder().setIssuer(appName).setSubject(username).setIssuedAt(generateCurrentDate()) .setExpiration(generateExpirationDate()) - .signWith(SIGNATURE_ALGORITHM, SECRET) - .compact(); + .signWith(SIGNATURE_ALGORITHM, base64EncodeSecretKey(jwtSecurityProps.getSecret())).compact(); } private Claims getClaimsFromToken(String token) { Claims claims; try { - claims = Jwts.parser() - .setSigningKey(SECRET) - .parseClaimsJws(token) - .getBody(); + claims = Jwts.parser().setSigningKey(base64EncodeSecretKey(jwtSecurityProps.getSecret())) + .parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; log.error("Error getting claims from jwt token due to " + e.getMessage(), e); @@ -101,9 +93,9 @@ public class TokenHelper { * Find a specific HTTP cookie in a request. * * @param request - * The HTTP request object. + * The HTTP request object. * @param name - * The cookie name to look for. + * The cookie name to look for. * @return The cookie, or null if not found. */ public Cookie getCookieValueByName(HttpServletRequest request, String name) { @@ -119,18 +111,6 @@ public class TokenHelper { return DateTime.now().getMillis(); } - public String getToken( HttpServletRequest request ) { - Cookie authCookie = getCookieValueByName( request, AUTH_COOKIE ); - if ( authCookie != null ) - return authCookie.getValue(); - - String authHeader = request.getHeader(AUTH_HEADER); - if ( authHeader != null && authHeader.startsWith("Bearer ")) - return authHeader.substring(7); - - return null; - } - public String getUsernameFromToken(String token) { String username; try { @@ -139,6 +119,7 @@ public class TokenHelper { } catch (Exception e) { username = null; log.error("Error getting claims from jwt token due to " + e.getMessage(), e); + throw e; } return username; } @@ -155,4 +136,27 @@ public class TokenHelper { } return refreshedToken; } + + public String retrieveToken(HttpServletRequest request) { + if (jwtSecurityProps.getCookieStrategy().isEnabled() == true) { + Cookie authCookie = getCookieValueByName(request, jwtSecurityProps.getCookieStrategy().getCookie()); + if (authCookie != null) + return authCookie.getValue(); + } + + if (jwtSecurityProps.getHeaderStrategy().isEnabled()) { + String authHeader = request.getHeader(jwtSecurityProps.getHeaderStrategy().getHeader()); + if (authHeader != null && authHeader.startsWith("Bearer ")) + return authHeader.substring(7); + } + + if(request.getParameter("access_token") != null) + return request.getParameter("access_token"); + + return null; + } + + public void setHeader(HttpServletResponse response, String token) { + response.addHeader(jwtSecurityProps.getHeaderStrategy().getHeader(), "Bearer " + token); + } } diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtUsernamePasswordFiterLoginConfig.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtUsernamePasswordFiterLoginConfig.java new file mode 100644 index 0000000..6f6991c --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/JwtUsernamePasswordFiterLoginConfig.java @@ -0,0 +1,46 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.util.matcher.RegexRequestMatcher; +import org.springframework.web.filter.GenericFilterBean; + +import it.fabioformosa.quartzmanager.security.helpers.LoginConfigurer; + +/** + * It adds a new filter @JwtAuthenticationFilter after @AbstractPreAuthenticatedProcessingFilter that match login path + * + */ +public class JwtUsernamePasswordFiterLoginConfig implements LoginConfigurer { + + private static final Logger log = LoggerFactory.getLogger(JwtUsernamePasswordFiterLoginConfig.class); + + private final JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler; + + public JwtUsernamePasswordFiterLoginConfig(JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler) { + super(); + this.jwtAuthenticationSuccessHandler = jwtAuthenticationSuccessHandler; + } + + public GenericFilterBean authenticationProcessingFilter(String loginPath, AuthenticationManager authenticationManager) throws Exception { + JwtAuthenticationFilter authenticationProcessingFilter = new JwtAuthenticationFilter(authenticationManager, jwtAuthenticationSuccessHandler); + authenticationProcessingFilter.setRequiresAuthenticationRequestMatcher(new RegexRequestMatcher(loginPath, HttpMethod.POST.name(), false)); + return authenticationProcessingFilter; + } + + @Override + public String cookieMustBeDeletedAtLogout() { + return jwtAuthenticationSuccessHandler.cookieMustBeDeletedAtLogout(); + } + + @Override + public HttpSecurity login(String loginPath, HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { + log.debug("Configuring login through JwtAuthenticationFilter..."); + return http.addFilterAfter(authenticationProcessingFilter(loginPath, authenticationManager), AbstractPreAuthenticatedProcessingFilter.class); + } + +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/LogoutSuccess.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/LogoutSuccess.java new file mode 100644 index 0000000..5f0eaa0 --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/LogoutSuccess.java @@ -0,0 +1,36 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class LogoutSuccess implements LogoutSuccessHandler { + + private final ObjectMapper objectMapper; + + public LogoutSuccess(ObjectMapper objectMapper) { + super(); + this.objectMapper = objectMapper; + } + + @Override + public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + Map result = new HashMap<>(); + result.put( "result", "success" ); + response.setContentType("application/json"); + response.getWriter().write(objectMapper.writeValueAsString(result)); + response.setStatus(HttpServletResponse.SC_OK); + + } + +} \ No newline at end of file diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/QuartzManagerHttpSecurity.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/QuartzManagerHttpSecurity.java new file mode 100644 index 0000000..55b9edb --- /dev/null +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/QuartzManagerHttpSecurity.java @@ -0,0 +1,57 @@ +package it.fabioformosa.quartzmanager.security.helpers.impl; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import it.fabioformosa.quartzmanager.security.helpers.LoginConfigurer; + +/** + * It wraps the httpSecurity to provide new function as login and logout + * + */ +public class QuartzManagerHttpSecurity extends SecurityConfigurerAdapter { + + public static QuartzManagerHttpSecurity from(HttpSecurity httpSecurity){ + QuartzManagerHttpSecurity newInstance = new QuartzManagerHttpSecurity(httpSecurity); + newInstance.setBuilder(httpSecurity); + return newInstance; + } + + private HttpSecurity httpSecurity; + + private LoginConfigurer loginConfigurer; + + private LogoutSuccess logoutSuccess; + + public QuartzManagerHttpSecurity(HttpSecurity httpSecurity) { + this.httpSecurity = httpSecurity; + // applicationContext = httpSecurity.getSharedObject(ApplicationContext.class); + } + + public QuartzManagerHttpSecurity login(String loginPath, AuthenticationManager authenticationManager) throws Exception { + if(loginConfigurer == null || logoutSuccess == null) + throw new IllegalStateException("QuartzManagerHttpSecurity requires to be set loginConfigurer and logoutSuccess!"); + httpSecurity = loginConfigurer.login(loginPath, httpSecurity, authenticationManager); + return this; + } + + + public LogoutConfigurer logout(String logoutPath) throws Exception { + LogoutConfigurer logoutConfigurer = httpSecurity.logout().logoutRequestMatcher(new AntPathRequestMatcher(logoutPath)) + .logoutSuccessHandler(logoutSuccess); + String cookie = loginConfigurer.cookieMustBeDeletedAtLogout(); + if(cookie != null) + logoutConfigurer.deleteCookies(cookie); + return logoutConfigurer; + } + + public QuartzManagerHttpSecurity withLoginConfigurer(LoginConfigurer loginConfigurer, LogoutSuccess logoutSuccess) { + this.loginConfigurer = loginConfigurer; + this.logoutSuccess = logoutSuccess; + return this; + } +} diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RESTRequestMatcher.java similarity index 94% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RESTRequestMatcher.java index 899d5ad..1fb2d2b 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RESTRequestMatcher.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security; +package it.fabioformosa.quartzmanager.security.helpers.impl; import javax.servlet.http.HttpServletRequest; diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/RestAuthenticationEntryPoint.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RestAuthenticationEntryPoint.java similarity index 88% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/RestAuthenticationEntryPoint.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RestAuthenticationEntryPoint.java index 1d1c4be..3615a2e 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/auth/RestAuthenticationEntryPoint.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/RestAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security.auth; +package it.fabioformosa.quartzmanager.security.helpers.impl; import java.io.IOException; diff --git a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/WebsocketRequestMatcher.java similarity index 89% rename from quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java rename to quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/WebsocketRequestMatcher.java index 28f64db..5b42424 100644 --- a/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java +++ b/quartz-manager-api/src/main/java/it/fabioformosa/quartzmanager/security/helpers/impl/WebsocketRequestMatcher.java @@ -1,4 +1,4 @@ -package it.fabioformosa.quartzmanager.security; +package it.fabioformosa.quartzmanager.security.helpers.impl; import javax.servlet.http.HttpServletRequest; diff --git a/quartz-manager-api/src/main/resources/application.yml b/quartz-manager-api/src/main/resources/application.yml index c4c71ce..f69a691 100644 --- a/quartz-manager-api/src/main/resources/application.yml +++ b/quartz-manager-api/src/main/resources/application.yml @@ -11,6 +11,7 @@ spring: thymeleaf: cache: false mode: LEGACYHTML5 + jpa.open-in-view: false quartz: enabled: true @@ -19,12 +20,6 @@ job: frequency: 4000 repeatCount: 19 -jwt: - header: Authorization - expires_in_sec: 600 # 10 minutes - secret: queenvictoria - cookie: AUTH-TOKEN - logging: level: org.springframework.web: WARN @@ -33,8 +28,26 @@ logging: it.fabioformosa: DEBUG quartz-manager: + security: + login-model: + form-login-enabled: true + userpwd-filter-enabled : false + jwt: + enabled: true + secret: "bibidibobidiboo" + expiration-in-sec: 28800 # 8 hours + header-strategy: + enabled: false + header: "Authorization" + cookie-strategy: + enabled: true + cookie: AUTH-TOKEN jobClass: it.fabioformosa.quartzmanager.jobs.myjobs.SampleJob - account: - user: admin - pwd: admin - \ No newline at end of file + accounts: + in-memory: + enabled: true + users: + - name: admin + password: admin + roles: + - ADMIN diff --git a/quartz-manager-api/src/main/resources/logback.xml b/quartz-manager-api/src/main/resources/logback.xml index 9977078..90577b1 100644 --- a/quartz-manager-api/src/main/resources/logback.xml +++ b/quartz-manager-api/src/main/resources/logback.xml @@ -4,7 +4,7 @@ - %d{yyyy-MM-dd HH:mm:ss.SSS} [%.10thread] %-5level [%-40.40logger{49}:%-3L] --- %m%n + %d{yyyy-MM-dd HH:mm:ss.SSS} [%.11thread] %-5level [%-40.40logger{49}:%-3L] --- %m%n diff --git a/quartz-manager-frontend/angular.json b/quartz-manager-frontend/angular.json index 7cd6f33..563894f 100644 --- a/quartz-manager-frontend/angular.json +++ b/quartz-manager-frontend/angular.json @@ -11,6 +11,7 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { + "aot": true, "outputPath": "../server/src/main/resources/static", "index": "src/index.html", "main": "src/main.ts", @@ -27,6 +28,12 @@ }, "configurations": { "production": { + "budgets": [ + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ], "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -119,7 +126,7 @@ "schematics": { "@schematics/angular:component": { "prefix": "app", - "styleext": "css" + "style": "css" }, "@schematics/angular:directive": { "prefix": "app" diff --git a/quartz-manager-frontend/browserslist b/quartz-manager-frontend/browserslist new file mode 100644 index 0000000..8084853 --- /dev/null +++ b/quartz-manager-frontend/browserslist @@ -0,0 +1,12 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/quartz-manager-frontend/package.json b/quartz-manager-frontend/package.json index 47d93ab..d442d69 100644 --- a/quartz-manager-frontend/package.json +++ b/quartz-manager-frontend/package.json @@ -12,19 +12,19 @@ }, "private": true, "dependencies": { - "@angular/animations": "7.2.13", - "@angular/cdk": "7.3.7", - "@angular/common": "7.2.13", - "@angular/compiler": "7.2.13", - "@angular/core": "7.2.13", - "@angular/flex-layout": "7.0.0-beta.24", - "@angular/forms": "7.2.13", - "@angular/http": "7.2.13", - "@angular/material": "7.3.7", - "@angular/platform-browser": "7.2.13", - "@angular/platform-browser-dynamic": "7.2.13", - "@angular/platform-server": "7.2.13", - "@angular/router": "7.2.13", + "@angular/animations": "9.1.4", + "@angular/cdk": "9.2.1", + "@angular/common": "9.1.4", + "@angular/compiler": "9.1.4", + "@angular/core": "9.1.4", + "@angular/flex-layout": "9.0.0-beta.29", + "@angular/forms": "9.1.4", + "@angular/material": "9.2.1", + "@angular/platform-browser": "9.1.4", + "@angular/platform-browser-dynamic": "9.1.4", + "@angular/platform-server": "9.1.4", + "@angular/router": "9.1.4", + "@auth0/angular-jwt": "^4.0.0", "@fortawesome/fontawesome": "^1.1.4", "@fortawesome/fontawesome-free-regular": "^5.0.8", "@fortawesome/fontawesome-free-solid": "^5.0.8", @@ -32,22 +32,22 @@ "core-js": "2.5.1", "hammerjs": "2.0.8", "net": "^1.0.2", - "rxjs": "6.4.0", + "rxjs": "6.5.5", "stompjs": "^2.3.3", - "tslib": "^1.9.0", - "zone.js": "0.8.18" + "tslib": "^1.10.0", + "zone.js": "~0.10.2" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.13.0", - "@angular-devkit/core": "^0.2.0", - "@angular/cli": "7.3.7", - "@angular/compiler-cli": "7.2.13", - "@angular/language-service": "7.2.13", + "@angular-devkit/build-angular": "~0.901.4", + "@angular-devkit/core": "^9.1.4", + "@angular/cli": "9.1.4", + "@angular/compiler-cli": "9.1.4", + "@angular/language-service": "9.1.4", "@types/hammerjs": "2.0.34", "@types/jasmine": "2.5.54", "@types/jasminewd2": "2.0.3", - "@types/node": "6.0.90", - "codelyzer": "4.2.1", + "@types/node": "^12.11.1", + "codelyzer": "^5.1.2", "jasmine-core": "2.6.4", "jasmine-spec-reporter": "4.1.1", "karma": "1.7.1", @@ -59,6 +59,6 @@ "protractor": "5.1.2", "ts-node": "3.0.6", "tslint": "5.7.0", - "typescript": "3.2.4" + "typescript": "3.8.3" } } diff --git a/quartz-manager-frontend/proxy.conf.json b/quartz-manager-frontend/proxy.conf.json index df22791..5a75f53 100644 --- a/quartz-manager-frontend/proxy.conf.json +++ b/quartz-manager-frontend/proxy.conf.json @@ -2,6 +2,7 @@ "/quartz-manager": { "target": "http://localhost:8080", "secure": false, + "cookiePathRewrite": "/", "ws":true } } diff --git a/quartz-manager-frontend/src/app/app.module.ts b/quartz-manager-frontend/src/app/app.module.ts index 72f2901..90e0d34 100644 --- a/quartz-manager-frontend/src/app/app.module.ts +++ b/quartz-manager-frontend/src/app/app.module.ts @@ -1,22 +1,23 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule, APP_INITIALIZER} from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; import { HttpClientModule } from '@angular/common/http'; + +import {JWT_OPTIONS, JwtModule} from "@auth0/angular-jwt"; + // material -import { - MatButtonModule, - MatMenuModule, - MatIconModule, - MatToolbarModule, - MatTooltipModule, - MatCardModule, - MatChipsModule, - MatInputModule, - MatIconRegistry, - MatProgressSpinnerModule, - MatProgressBarModule, -} from '@angular/material'; +import {MatIconRegistry} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatChipsModule} from '@angular/material/chips'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {MatIconModule} from '@angular/material/icon'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCardModule} from '@angular/material/card'; + import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FlexLayoutModule } from '@angular/flex-layout'; import { AppComponent } from './app.component'; @@ -77,6 +78,15 @@ export function initUserFactory(userService: UserService) { // debug: true // }; +export function jwtOptionsFactory(apiService: ApiService) { + return { + tokenGetter: () => { + return apiService.getToken(); + }, + whitelistedDomains: ['localhost:8080', 'localhost:4200'] + } +} + @NgModule({ declarations: [ AppComponent, @@ -99,9 +109,15 @@ export function initUserFactory(userService: UserService) { BrowserModule, FormsModule, ReactiveFormsModule, - HttpModule, HttpClientModule, AppRoutingModule, + JwtModule.forRoot({ + jwtOptionsProvider: { + provide: JWT_OPTIONS, + useFactory: jwtOptionsFactory, + deps: [ApiService] + } + }), MatMenuModule, MatTooltipModule, MatButtonModule, diff --git a/quartz-manager-frontend/src/app/components/header/header.component.scss b/quartz-manager-frontend/src/app/components/header/header.component.scss index 122b0bd..eb2bb4c 100644 --- a/quartz-manager-frontend/src/app/components/header/header.component.scss +++ b/quartz-manager-frontend/src/app/components/header/header.component.scss @@ -32,7 +32,7 @@ } -/deep/ { +::ng-deep { .app-header-accountMenu.mat-menu-panel { border-radius: 3px; max-width: initial; diff --git a/quartz-manager-frontend/src/app/model/SocketOption.model.ts b/quartz-manager-frontend/src/app/model/SocketOption.model.ts index 20d8fe3..529da9d 100644 --- a/quartz-manager-frontend/src/app/model/SocketOption.model.ts +++ b/quartz-manager-frontend/src/app/model/SocketOption.model.ts @@ -4,11 +4,14 @@ export class SocketOption{ brokerName : string; reconnectionTimeout : number = 30000 - constructor(socketUrl : string, topicName : string, brokerName : string = null, reconnectionTimeout : number = 30000){ + getAccessToken: Function = () => null; + + constructor(socketUrl : string, topicName : string, getAccessToken?: Function, brokerName : string = null, reconnectionTimeout : number = 30000){ this.socketUrl = socketUrl; this.topicName = topicName; this.brokerName = brokerName; this.reconnectionTimeout = reconnectionTimeout; + this.getAccessToken = getAccessToken || (() => null); } } \ No newline at end of file diff --git a/quartz-manager-frontend/src/app/services/api.service.ts b/quartz-manager-frontend/src/app/services/api.service.ts index 0fc7d7b..bf84c6f 100644 --- a/quartz-manager-frontend/src/app/services/api.service.ts +++ b/quartz-manager-frontend/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders, HttpResponse, HttpRequest, HttpEventType, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { catchError, map, filter } from 'rxjs/operators' +import { catchError, map, filter, tap } from 'rxjs/operators' import { serialize } from 'app/shared/utilities/serialize'; export enum RequestMethod { @@ -17,22 +17,43 @@ export enum RequestMethod { @Injectable() export class ApiService { + private static extractTokenFromHttpResponse(res: HttpResponse): string { + let authorization: string = null; + let headers: HttpHeaders = res.headers; + if (headers && headers.has('Authorization')){ + authorization = headers.get('Authorization'); + if(authorization.startsWith('Bearer ')) + authorization = authorization.substring(7); + } + return authorization; + } + headers = new HttpHeaders({ 'Accept': 'application/json', 'Content-Type': 'application/json' }); + private jwtToken: string; + constructor( private http: HttpClient) { } + setToken(token: string) { + this.jwtToken = token; + } + + getToken = () => this.jwtToken; + get(path: string, args?: any): Observable { const options = { headers: this.headers, withCredentials: true }; - if (args) { + if (args) options['params'] = serialize(args); - } + + // if(this.jwtToken) + // options.headers = options.headers.set('Authorization', `Bearer ${this.jwtToken}`); return this.http.get(path, options) .pipe(catchError(this.checkError.bind(this))); @@ -51,14 +72,24 @@ export class ApiService { } private request(path: string, body: any, method = RequestMethod.Post, customHeaders?: HttpHeaders): Observable { - const req = new HttpRequest(method, path, body, { + const options = { headers: customHeaders || this.headers, withCredentials: true - }); + } + + // if(this.jwtToken) + // options.headers = options.headers.append('Authorization', `Bearer ${this.jwtToken}`); + + const req = new HttpRequest(method, path, body, options); return this.http.request(req) .pipe( filter(response => response instanceof HttpResponse), + tap((resp: HttpResponse) => { + let jwtToken = ApiService.extractTokenFromHttpResponse(resp); + if(jwtToken) + this.setToken(jwtToken); + }), map((response: HttpResponse) => response.body), catchError(error => this.checkError(error)) ) @@ -74,4 +105,6 @@ export class ApiService { throw error; } + + } diff --git a/quartz-manager-frontend/src/app/services/auth.service.ts b/quartz-manager-frontend/src/app/services/auth.service.ts index 3af4dda..c39a395 100644 --- a/quartz-manager-frontend/src/app/services/auth.service.ts +++ b/quartz-manager-frontend/src/app/services/auth.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { ApiService } from './api.service'; import { UserService } from './user.service'; import { ConfigService } from './config.service'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; @Injectable() export class AuthService { @@ -21,10 +21,13 @@ export class AuthService { 'Content-Type': 'application/x-www-form-urlencoded' }); const body = `username=${user.username}&password=${user.password}`; - return this.apiService.post(this.config.login_url, body, loginHeaders).pipe(map(() => { - console.log("Login success"); - this.userService.getMyInfo().subscribe(); - })); + return this.apiService.post(this.config.login_url, body, loginHeaders) + .pipe( + map(() => { + console.log("Login success"); + this.userService.getMyInfo().subscribe(); + }) + ); } signup(user){ @@ -40,6 +43,7 @@ export class AuthService { logout() { return this.apiService.post(this.config.logout_url, {}) .pipe(map(() => { + this.apiService.setToken(null); this.userService.currentUser = null; })); } @@ -48,4 +52,5 @@ export class AuthService { return this.apiService.post(this.config.change_password_url, passwordChanger); } + } diff --git a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts b/quartz-manager-frontend/src/app/services/logs.websocket.service.ts index 76664cc..fe2cd06 100644 --- a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts +++ b/quartz-manager-frontend/src/app/services/logs.websocket.service.ts @@ -1,12 +1,12 @@ import { Injectable, OnInit } from '@angular/core'; -import { WebsocketService } from '.'; +import { WebsocketService, ApiService } from '.'; import { SocketOption } from '../model/SocketOption.model'; -Injectable() +@Injectable() export class LogsWebsocketService extends WebsocketService { - constructor(){ - super(new SocketOption('/quartz-manager/logs', '/topic/logs')) + constructor(private apiService: ApiService){ + super(new SocketOption('/quartz-manager/logs', '/topic/logs', apiService.getToken)) } } \ No newline at end of file diff --git a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts b/quartz-manager-frontend/src/app/services/progress.websocket.service.ts index 378b7d2..173428b 100644 --- a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts +++ b/quartz-manager-frontend/src/app/services/progress.websocket.service.ts @@ -1,12 +1,12 @@ import { Injectable, OnInit } from '@angular/core'; -import { WebsocketService } from '.'; +import { WebsocketService, ApiService } from '.'; import { SocketOption } from '../model/SocketOption.model'; -Injectable() +@Injectable() export class ProgressWebsocketService extends WebsocketService { - constructor(){ - super(new SocketOption('/quartz-manager/progress', '/topic/progress')) + constructor(private apiService: ApiService){ + super(new SocketOption('/quartz-manager/progress', '/topic/progress', apiService.getToken)) } } \ No newline at end of file diff --git a/quartz-manager-frontend/src/app/services/scheduler.service.ts b/quartz-manager-frontend/src/app/services/scheduler.service.ts index 2da0a1f..f2b351b 100644 --- a/quartz-manager-frontend/src/app/services/scheduler.service.ts +++ b/quartz-manager-frontend/src/app/services/scheduler.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { Headers } from '@angular/http'; import { ApiService } from './api.service'; @Injectable() diff --git a/quartz-manager-frontend/src/app/services/user.service.ts b/quartz-manager-frontend/src/app/services/user.service.ts index 310eb49..351653a 100644 --- a/quartz-manager-frontend/src/app/services/user.service.ts +++ b/quartz-manager-frontend/src/app/services/user.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { Headers } from '@angular/http'; import { ApiService } from './api.service'; import { ConfigService } from './config.service'; diff --git a/quartz-manager-frontend/src/app/services/websocket.service.ts b/quartz-manager-frontend/src/app/services/websocket.service.ts index a9fa640..e1eda64 100644 --- a/quartz-manager-frontend/src/app/services/websocket.service.ts +++ b/quartz-manager-frontend/src/app/services/websocket.service.ts @@ -1,10 +1,4 @@ -import { Injectable, OnInit } from '@angular/core'; -import { Headers } from '@angular/http'; - import { Observable } from 'rxjs'; - -import { ApiService } from './api.service'; - import { SocketEndpoint } from '../model/SocketEndpoint.model' @@ -39,10 +33,7 @@ export class WebsocketService { this.observableStompConnection = new Observable((observer) => { const subscriberIndex = this.subscriberIndex++; this.addToSubscribers({ index: subscriberIndex, observer }); - return () => { - const index = subscriberIndex; - this.removeFromSubscribers(index); - }; + return () => this.removeFromSubscribers(subscriberIndex); }); } @@ -51,13 +42,9 @@ export class WebsocketService { } removeFromSubscribers = (index) => { - let subscribeFromIndex; - for (let i=0 ; i < this.subscribers.length; i++) - if(i === index){ - subscribeFromIndex = this.subscribers[i]; - this.subscribers.splice(i, 1); - break; - } + if(index > this.subscribers.length) + throw new Error(`Unexpected error removing subscriber from websocket, because index ${index} is greater than subscriber length ${this.subscribers.length}`); + this.subscribers.splice(index, 1); } getObservable = () => { @@ -71,7 +58,7 @@ export class WebsocketService { out.headers = {}; out.headers.messageId = data.headers["message-id"]; - let messageIdIndex = this._messageIds.indexOf( out.headers.messageId); + let messageIdIndex = this._messageIds.indexOf(out.headers.messageId); if ( messageIdIndex > -1) { out.self = true; this._messageIds = this._messageIds.splice(messageIdIndex, 1); @@ -81,24 +68,20 @@ export class WebsocketService { _socketListener = (frame) => { console.log('Connected: ' + frame); - this._socket.stomp.subscribe(this._options.topicName, (data) => { - this.subscribers.forEach(subscriber => { - subscriber.observer.next(this.getMessage(data)); - }) - }) + this._socket.stomp.subscribe( + this._options.topicName, + data => this.subscribers.forEach(subscriber => subscriber.observer.next(this.getMessage(data))) + ); } _onSocketError = (errorMsg) => { let out: any = {}; out.type = 'ERROR'; out.message = errorMsg; - this.subscribers.forEach(subscriber => { - subscriber.observer.error(out); - }) + this.subscribers.forEach(subscriber => subscriber.observer.error(out)); this.scheduleReconnection(); } - scheduleReconnection = () => { this.reconnectionPromise = setTimeout(() => { console.log("Socket reconnecting... (if it fails, next attempt in " + this._options.reconnectionTimeout + " msec)"); @@ -126,7 +109,12 @@ export class WebsocketService { connect = () => { const headers = {}; - this._socket.client = new SockJS(this._options.socketUrl); + + let socketUrl = this._options.socketUrl; + if(this._options.getAccessToken()) + socketUrl += `?access_token=${this._options.getAccessToken()}` + + this._socket.client = new SockJS(socketUrl); this._socket.stomp = Stomp.over(this._socket.client); this._socket.stomp.connect(headers, this._socketListener, this._onSocketError); this._socket.stomp.onclose = this.scheduleReconnection; diff --git a/quartz-manager-frontend/src/app/views/login/login.component.ts b/quartz-manager-frontend/src/app/views/login/login.component.ts index 5962dd3..201b259 100644 --- a/quartz-manager-frontend/src/app/views/login/login.component.ts +++ b/quartz-manager-frontend/src/app/views/login/login.component.ts @@ -82,7 +82,6 @@ export class LoginComponent implements OnInit, OnDestroy { this.authService.login(this.form.value) .pipe(delay(1000)) .subscribe(data => { - this.userService.getMyInfo().subscribe(); this.router.navigate([this.returnUrl]); }, error => { diff --git a/quartz-manager-frontend/src/tsconfig.app.json b/quartz-manager-frontend/src/tsconfig.app.json index 5e2507d..26227d8 100644 --- a/quartz-manager-frontend/src/tsconfig.app.json +++ b/quartz-manager-frontend/src/tsconfig.app.json @@ -2,12 +2,14 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", - "module": "es2015", "baseUrl": "", "types": [] }, - "exclude": [ - "test.ts", - "**/*.spec.ts" + "files": [ + "main.ts", + "polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" ] } diff --git a/quartz-manager-frontend/src/tsconfig.spec.json b/quartz-manager-frontend/src/tsconfig.spec.json index 15458ed..9c56a51 100644 --- a/quartz-manager-frontend/src/tsconfig.spec.json +++ b/quartz-manager-frontend/src/tsconfig.spec.json @@ -2,8 +2,6 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", - "module": "commonjs", - "target": "es5", "baseUrl": "", "types": [ "jasmine", diff --git a/quartz-manager-frontend/tsconfig.json b/quartz-manager-frontend/tsconfig.json index ab228cc..3a709a3 100644 --- a/quartz-manager-frontend/tsconfig.json +++ b/quartz-manager-frontend/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "downlevelIteration": true, "importHelpers": true, "outDir": "./dist/out-tsc", "baseUrl": "src", @@ -9,7 +10,7 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es5", + "target": "es2015", "typeRoots": [ "node_modules/@types" ], @@ -17,6 +18,6 @@ "es2016", "dom" ], - "module": "es2015" + "module": "esnext" } } \ No newline at end of file diff --git a/quartz-manager-frontend/tslint.json b/quartz-manager-frontend/tslint.json index bb84fcf..1701630 100644 --- a/quartz-manager-frontend/tslint.json +++ b/quartz-manager-frontend/tslint.json @@ -100,12 +100,12 @@ "directive-selector": [true, "attribute", "app", "camelCase"], "component-selector": [true, "element", "app", "kebab-case"], - "use-input-property-decorator": true, - "use-output-property-decorator": true, - "use-host-property-decorator": true, + "no-inputs-metadata-property": true, + "no-outputs-metadata-property": true, + "no-host-metadata-property": true, "no-input-rename": true, "no-output-rename": true, - "use-life-cycle-interface": true, + "use-lifecycle-interface": true, "use-pipe-transform-interface": true, "component-class-suffix": true, "directive-class-suffix": true,