diff --git a/src/main/java/org/waterproofingdata/wpdauth/repository/UsersRepository.java b/src/main/java/org/waterproofingdata/wpdauth/repository/UsersRepository.java new file mode 100644 index 0000000..d4721b0 --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/repository/UsersRepository.java @@ -0,0 +1,16 @@ +package org.waterproofingdata.wpdauth.repository; + +import org.waterproofingdata.wpdauth.model.Users; +import javax.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface UsersRepository extends JpaRepository { + boolean existsByUsername(String username); + + Users findByUsername(String username); + + //@Transactional + //void activateByUsername(String username); + +} diff --git a/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilter.java b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilter.java new file mode 100644 index 0000000..bedd0ab --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilter.java @@ -0,0 +1,40 @@ +package org.waterproofingdata.wpdauth.security; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.waterproofingdata.wpdauth.security.JwtTokenProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.waterproofingdata.wpdauth.exception.CustomException; +import org.springframework.web.filter.OncePerRequestFilter; + +public class JwtTokenFilter extends OncePerRequestFilter { + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (CustomException ex) { + //this is very important, since it guarantees the user is not authenticated at all + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage()); + return; + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilterConfigurer.java b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilterConfigurer.java new file mode 100644 index 0000000..0dd0534 --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenFilterConfigurer.java @@ -0,0 +1,22 @@ +package org.waterproofingdata.wpdauth.security; + +import org.waterproofingdata.wpdauth.security.JwtTokenFilter; +import org.waterproofingdata.wpdauth.security.JwtTokenProvider; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter { + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenProvider.java b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenProvider.java new file mode 100644 index 0000000..598a030 --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/security/JwtTokenProvider.java @@ -0,0 +1,91 @@ +package org.waterproofingdata.wpdauth.security; + +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import org.waterproofingdata.wpdauth.model.Roles; +import org.waterproofingdata.wpdauth.security.MyUserDetails; +import org.waterproofingdata.wpdauth.exception.CustomException; + +@Component +public class JwtTokenProvider { + /** + * THIS IS NOT A SECURE PRACTICE! For simplicity, we are storing a static key here. Ideally, in a + * microservices environment, this key would be kept on a config-server. + */ + @Value("${security.jwt.token.secret-key:secret-key}") + private String secretKey; + + @Value("${security.jwt.token.expire-length:3600000}") + private long validityInMilliseconds = 3600000; // 1h + + @Autowired + private MyUserDetails myUserDetails; + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + public String createToken(String username, List roles) { + + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder()// + .setClaims(claims)// + .setIssuedAt(now)// + .setExpiration(validity)// + .signWith(SignatureAlgorithm.HS256, secretKey)// + .compact(); + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/org/waterproofingdata/wpdauth/security/MyUserDetails.java b/src/main/java/org/waterproofingdata/wpdauth/security/MyUserDetails.java new file mode 100644 index 0000000..0b16837 --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/security/MyUserDetails.java @@ -0,0 +1,35 @@ +package org.waterproofingdata.wpdauth.security; + +import org.springframework.beans.factory.annotation.Autowired; +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 org.waterproofingdata.wpdauth.model.Users; +import org.waterproofingdata.wpdauth.repository.UsersRepository; + +@Service +public class MyUserDetails implements UserDetailsService { + @Autowired + private UsersRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final Users user = userRepository.findByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException("User '" + username + "' not found"); + } + + return org.springframework.security.core.userdetails.User// + .withUsername(username)// + .password(user.getPassword())// + .authorities(user.getRoles())// + .accountExpired(false)// + .accountLocked(false)// + .credentialsExpired(false)// + .disabled(false)// + .build(); + } +} diff --git a/src/main/java/org/waterproofingdata/wpdauth/security/WebSecurityConfig.java b/src/main/java/org/waterproofingdata/wpdauth/security/WebSecurityConfig.java new file mode 100644 index 0000000..459cb21 --- /dev/null +++ b/src/main/java/org/waterproofingdata/wpdauth/security/WebSecurityConfig.java @@ -0,0 +1,78 @@ +package org.waterproofingdata.wpdauth.security; + +import org.waterproofingdata.wpdauth.security.JwtTokenFilterConfigurer; +import org.waterproofingdata.wpdauth.security.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +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; +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.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Override + protected void configure(HttpSecurity http) throws Exception { + + // Disable CSRF (cross site request forgery) + http.csrf().disable(); + + // No session will be created or used by spring security + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // Entry points + http.authorizeRequests()// + .antMatchers("/users/signin").permitAll()// + .antMatchers("/users/signup").permitAll()// + .antMatchers("/h2-console/**/**").permitAll() + // Disallow everything else.. + .anyRequest().authenticated(); + + // If a user try to access a resource without having enough permissions + http.exceptionHandling().accessDeniedPage("/login"); + + // Apply JWT + http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); + + // Optional, if you want to test the API from a browser + // http.httpBasic(); + } + + @Override + public void configure(WebSecurity web) throws Exception { + // Allow swagger to be accessed without authentication + web.ignoring().antMatchers("/v2/api-docs")// + .antMatchers("/swagger-resources/**")// + .antMatchers("/swagger-ui.html")// + .antMatchers("/configuration/**")// + .antMatchers("/webjars/**")// + .antMatchers("/public") + + // Un-secure H2 Database (for testing purposes, H2 console shouldn't be unprotected in production) + .and() + .ignoring() + .antMatchers("/h2-console/**/**");; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } +}