From 20eb68b44fd81caae5b947ffd0a80d221cee5179 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 1 May 2020 23:10:54 +0200 Subject: [PATCH] Implement JWT Security Manager Filter Rename User to TimetrackUser Add default data Add BCrypter as password encryptor Add security constants Enable websecurity on all endpoints except on SIGN_UP_URL Implement UserDetailsService Update properties files --- .../de/hft/geotime/GeotimeApplication.java | 7 +++ .../security/JWTAuthenticationFilter.java | 60 +++++++++++++++++++ .../security/JWTAuthorizationFilter.java | 56 +++++++++++++++++ .../geotime/security/SecurityConstants.java | 9 +++ .../de/hft/geotime/security/WebSecurity.java | 51 ++++++++++++++++ .../timetrackaccount/TimetrackAccount.java | 4 +- .../user/{User.java => TimetrackUser.java} | 9 ++- .../geotime/user/TimetrackUserRepository.java | 11 ++++ .../geotime/user/UserDetailsServiceImpl.java | 29 +++++++++ .../de/hft/geotime/user/UserRepository.java | 12 ---- .../main/resources/application-dev.properties | 1 - .../resources/application-prod.properties | 1 + .../src/main/resources/application.properties | 1 + backend/src/main/resources/data.sql | 12 ++++ 14 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java create mode 100644 backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java create mode 100644 backend/src/main/java/de/hft/geotime/security/SecurityConstants.java create mode 100644 backend/src/main/java/de/hft/geotime/security/WebSecurity.java rename backend/src/main/java/de/hft/geotime/user/{User.java => TimetrackUser.java} (76%) create mode 100644 backend/src/main/java/de/hft/geotime/user/TimetrackUserRepository.java create mode 100644 backend/src/main/java/de/hft/geotime/user/UserDetailsServiceImpl.java delete mode 100644 backend/src/main/java/de/hft/geotime/user/UserRepository.java create mode 100644 backend/src/main/resources/data.sql diff --git a/backend/src/main/java/de/hft/geotime/GeotimeApplication.java b/backend/src/main/java/de/hft/geotime/GeotimeApplication.java index f2563ba..859b276 100644 --- a/backend/src/main/java/de/hft/geotime/GeotimeApplication.java +++ b/backend/src/main/java/de/hft/geotime/GeotimeApplication.java @@ -2,7 +2,9 @@ package de.hft.geotime; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @SpringBootApplication @ComponentScan(basePackages = "de.hft") @@ -12,4 +14,9 @@ public class GeotimeApplication { SpringApplication.run(GeotimeApplication.class, args); } + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + } diff --git a/backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java b/backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java new file mode 100644 index 0000000..f111c1f --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java @@ -0,0 +1,60 @@ +package de.hft.geotime.security; + +import com.auth0.jwt.JWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.hft.geotime.user.TimetrackUser; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; + +import static com.auth0.jwt.algorithms.Algorithm.HMAC512; +import static de.hft.geotime.security.SecurityConstants.*; + +public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + + public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public Authentication attemptAuthentication( + HttpServletRequest req, + HttpServletResponse res) throws AuthenticationException { + try { + TimetrackUser creds = new ObjectMapper().readValue(req.getInputStream(), TimetrackUser.class); + return authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + creds.getUsername(), + creds.getPassword(), + new ArrayList<>() + ) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void successfulAuthentication( + HttpServletRequest req, + HttpServletResponse res, + FilterChain chain, + Authentication auth) { + String token = JWT.create() + .withSubject(((User) auth.getPrincipal()).getUsername()) + .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .sign(HMAC512(SECRET.getBytes())); + res.addHeader(HEADER_STRING, TOKEN_PREFIX + token); + } +} \ No newline at end of file diff --git a/backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java b/backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java new file mode 100644 index 0000000..7a9525f --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java @@ -0,0 +1,56 @@ +package de.hft.geotime.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; + +import static de.hft.geotime.security.SecurityConstants.*; + +public class JWTAuthorizationFilter extends BasicAuthenticationFilter { + + public JWTAuthorizationFilter(AuthenticationManager authManager) { + super(authManager); + } + + @Override + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + String header = req.getHeader(HEADER_STRING); + + if (header == null || !header.startsWith(TOKEN_PREFIX)) { + chain.doFilter(req, res); + return; + } + + UsernamePasswordAuthenticationToken authentication = getAuthentication(req); + + SecurityContextHolder.getContext().setAuthentication(authentication); + chain.doFilter(req, res); + } + + private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { + String token = request.getHeader(HEADER_STRING); + if (token != null) { + // parse the token. + String user = JWT.require(Algorithm.HMAC512(SECRET.getBytes())) + .build() + .verify(token.replace(TOKEN_PREFIX, "")) + .getSubject(); + + if (user != null) { + return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); + } + return null; + } + return null; + } +} \ No newline at end of file diff --git a/backend/src/main/java/de/hft/geotime/security/SecurityConstants.java b/backend/src/main/java/de/hft/geotime/security/SecurityConstants.java new file mode 100644 index 0000000..27ac9e8 --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/security/SecurityConstants.java @@ -0,0 +1,9 @@ +package de.hft.geotime.security; + +public class SecurityConstants { + public static final String SECRET = "SecretKeyToGenJWTs"; + public static final long EXPIRATION_TIME = 864_000_000; // 10 days + public static final String TOKEN_PREFIX = "Bearer "; + public static final String HEADER_STRING = "Authorization"; + public static final String SIGN_UP_URL = "/users/sign-up"; +} \ No newline at end of file diff --git a/backend/src/main/java/de/hft/geotime/security/WebSecurity.java b/backend/src/main/java/de/hft/geotime/security/WebSecurity.java new file mode 100644 index 0000000..63c3213 --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/security/WebSecurity.java @@ -0,0 +1,51 @@ +package de.hft.geotime.security; + +import de.hft.geotime.user.UserDetailsServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import static de.hft.geotime.security.SecurityConstants.SIGN_UP_URL; + +@EnableWebSecurity +public class WebSecurity extends WebSecurityConfigurerAdapter { + private final UserDetailsServiceImpl userDetailsService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + public WebSecurity(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userDetailsService = userDetailsService; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable().authorizeRequests() + .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll() + .anyRequest().authenticated() + .and() + .addFilter(new JWTAuthenticationFilter(authenticationManager())) + .addFilter(new JWTAuthorizationFilter(authenticationManager())) + // this disables session creation on Spring Security + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + + @Override + public void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); + return source; + } +} \ No newline at end of file diff --git a/backend/src/main/java/de/hft/geotime/timetrackaccount/TimetrackAccount.java b/backend/src/main/java/de/hft/geotime/timetrackaccount/TimetrackAccount.java index f44e6c8..772e6ec 100644 --- a/backend/src/main/java/de/hft/geotime/timetrackaccount/TimetrackAccount.java +++ b/backend/src/main/java/de/hft/geotime/timetrackaccount/TimetrackAccount.java @@ -1,6 +1,6 @@ package de.hft.geotime.timetrackaccount; -import de.hft.geotime.user.User; +import de.hft.geotime.user.TimetrackUser; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -17,7 +17,7 @@ public class TimetrackAccount { @GeneratedValue(strategy = GenerationType.AUTO) private long id; @OneToOne - private User user; + private TimetrackUser timetrackUser; private double revenue; private String name; private String description; diff --git a/backend/src/main/java/de/hft/geotime/user/User.java b/backend/src/main/java/de/hft/geotime/user/TimetrackUser.java similarity index 76% rename from backend/src/main/java/de/hft/geotime/user/User.java rename to backend/src/main/java/de/hft/geotime/user/TimetrackUser.java index 70c1b6e..aebd202 100644 --- a/backend/src/main/java/de/hft/geotime/user/User.java +++ b/backend/src/main/java/de/hft/geotime/user/TimetrackUser.java @@ -5,6 +5,7 @@ import de.hft.geotime.timetrackaccount.TimetrackAccount; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.UniqueElements; import javax.persistence.*; import java.util.List; @@ -13,16 +14,18 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Entity -public class User { +public class TimetrackUser { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; + @UniqueElements private String username; + private String password; private String firstname; private String lastname; - @OneToMany - private List roles; + @OneToOne + private Role role; @OneToMany private List timetrackAccounts; diff --git a/backend/src/main/java/de/hft/geotime/user/TimetrackUserRepository.java b/backend/src/main/java/de/hft/geotime/user/TimetrackUserRepository.java new file mode 100644 index 0000000..5eb7367 --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/user/TimetrackUserRepository.java @@ -0,0 +1,11 @@ +package de.hft.geotime.user; + +import org.springframework.data.repository.CrudRepository; + +import javax.websocket.server.PathParam; + +public interface TimetrackUserRepository extends CrudRepository { + + TimetrackUser findFirstByUsername(@PathParam("username") String username); + +} diff --git a/backend/src/main/java/de/hft/geotime/user/UserDetailsServiceImpl.java b/backend/src/main/java/de/hft/geotime/user/UserDetailsServiceImpl.java new file mode 100644 index 0000000..365b6f8 --- /dev/null +++ b/backend/src/main/java/de/hft/geotime/user/UserDetailsServiceImpl.java @@ -0,0 +1,29 @@ +package de.hft.geotime.user; + +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private final TimetrackUserRepository userRepository; + + public UserDetailsServiceImpl(TimetrackUserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + TimetrackUser timetrackUser = userRepository.findFirstByUsername(username); + if (timetrackUser == null) { + throw new UsernameNotFoundException(username); + } + System.out.println("Loaded user " + timetrackUser.getFirstname() + " " + timetrackUser.getLastname()); + return new User(timetrackUser.getUsername(), timetrackUser.getPassword(), Collections.emptyList()); + } +} diff --git a/backend/src/main/java/de/hft/geotime/user/UserRepository.java b/backend/src/main/java/de/hft/geotime/user/UserRepository.java deleted file mode 100644 index 2755da1..0000000 --- a/backend/src/main/java/de/hft/geotime/user/UserRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.hft.geotime.user; - -import org.springframework.data.repository.CrudRepository; - -import javax.websocket.server.PathParam; -import java.util.List; - -public interface UserRepository extends CrudRepository { - - List findByUsername(@PathParam("username") String username); - -} diff --git a/backend/src/main/resources/application-dev.properties b/backend/src/main/resources/application-dev.properties index 63999d4..4497ed1 100644 --- a/backend/src/main/resources/application-dev.properties +++ b/backend/src/main/resources/application-dev.properties @@ -3,6 +3,5 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.show-sql=true spring.h2.console.enabled=true spring.h2.console.path=/h2-console \ No newline at end of file diff --git a/backend/src/main/resources/application-prod.properties b/backend/src/main/resources/application-prod.properties index 63f56f2..2ed9234 100644 --- a/backend/src/main/resources/application-prod.properties +++ b/backend/src/main/resources/application-prod.properties @@ -2,4 +2,5 @@ spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mariadb://db:3306/geotime spring.datasource.username=root spring.datasource.password=supersecure +spring.datasource.initialization-mode=always spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index cae4ac3..fb71f70 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,4 +1,5 @@ server.port=80 spring.datasource.hikari.initialization-fail-timeout=0 spring.datasource.hikari.max-lifetime=300000 +spring.jpa.show-sql=true spring.profiles.active=prod \ No newline at end of file diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql new file mode 100644 index 0000000..a9a33fb --- /dev/null +++ b/backend/src/main/resources/data.sql @@ -0,0 +1,12 @@ +INSERT INTO role (id, `name`) VALUES + (1, 'Admin'); + + /* password is the username in lowercase e.g. marcel or tobias + https://bcrypt-generator.com/ with 10 rounds + */ +INSERT INTO timetrack_user (id, firstname, lastname, password, username, role_id) VALUES + (1, 'Marcel', 'Schwarz' ,'$2y$10$pDBv7dEaAiNs5Kr1.8g4XuTFx48zGxJu77rei4TlO.sDOF2yHWxo.', 'scma', 1), + (2, 'Tobias', 'Wieck' ,'$2y$10$Fxj5cGrZblGKjIExvS/MquEE0lgyYo1ILxPgPR2vSiaaLKkqJ.C.u', 'wito', 1), + (3, 'Tim', 'Zieger' ,'$2y$10$pYGHZhoaelceImO7aIN4nOkWJBp.oqNGFYaRAonHkYF4u9ljqPelC', 'ziti', 1), + (4, 'Simon', 'Kellner' ,'$2y$10$Puzm/Nr/Dyq3nQxlkXGIfubS5JPtXJSOf2e6mrQ6HhVYQN9YiQQsC', 'kesi', 1); +