diff --git a/pom.xml b/pom.xml index 0f8072e7..f339649a 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-devtools @@ -133,6 +137,15 @@ org.thymeleaf.extras thymeleaf-extras-springsecurity5 + + io.jsonwebtoken + jjwt + 0.9.1 + + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtApiController.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtApiController.java new file mode 100644 index 00000000..f540dc9b --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtApiController.java @@ -0,0 +1,56 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestBody; +// import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PostMapping; +// import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.nighthawk.spring_portfolio.mvc.person.*; + +@RestController +@CrossOrigin +public class JwtApiController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private JwtTokenUtil jwtTokenUtil; + + @Autowired + private JwtUserDetailsService jwtUserDetailsService; + + @PostMapping("/authenticate") + public ResponseEntity createAuthenticationToken(@RequestBody Person authenticationRequest) throws Exception { + + authenticate(authenticationRequest.getEmail(), authenticationRequest.getPassword()); + System.out.println("good email and password"); + final UserDetails userDetails = jwtUserDetailsService + .loadUserByUsername(authenticationRequest.getEmail()); + + final String token = jwtTokenUtil.generateToken(userDetails); + + return ResponseEntity.ok(new JwtResponse(token)); + } + + private void authenticate(String username, String password) throws Exception { + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + } catch (DisabledException e) { + System.out.println("bad email and password"); + throw new Exception("USER_DISABLED", e); + } catch (BadCredentialsException e) { + System.out.println("bad email and password"); + throw new Exception("INVALID_CREDENTIALS", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..393cf961 --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtRequestFilter.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtRequestFilter.java new file mode 100644 index 00000000..e4cca140 --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtRequestFilter.java @@ -0,0 +1,79 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; + +import java.io.IOException; + +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.authentication.UsernamePasswordAuthenticationToken; +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.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.util.AntPathMatcher; + + +import io.jsonwebtoken.ExpiredJwtException; + +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + @Autowired + private JwtUserDetailsService jwtUserDetailsService; + + @Autowired + private JwtTokenUtil jwtTokenUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + + final String requestTokenHeader = request.getHeader("Authorization"); + + String username = null; + String jwtToken = null; + // JWT Token is in the form "Bearer token". Remove Bearer word and get + // only the Token + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { + jwtToken = requestTokenHeader.substring(7); + try { + username = jwtTokenUtil.getUsernameFromToken(jwtToken); + } catch (IllegalArgumentException e) { + System.out.println("Unable to get JWT Token"); + } catch (ExpiredJwtException e) { + System.out.println("JWT Token has expired"); + } + } else { + logger.warn("JWT Token does not begin with Bearer String"); + } + + // Once we get the token validate it. + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username); + + // if token is valid configure Spring Security to manually set + // authentication + if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // After setting the Authentication in the context, we specify + // that the current user is authenticated. So it passes the + // Spring Security Configurations successfully. + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + chain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + return new AntPathMatcher().match("/authenticate", request.getServletPath()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtResponse.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtResponse.java new file mode 100644 index 00000000..65ba2258 --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtResponse.java @@ -0,0 +1,13 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; + +public class JwtResponse { + private final String jwttoken; + + public JwtResponse(String jwttoken) { + this.jwttoken = jwttoken; + } + + public String getToken() { + return this.jwttoken; + } +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtTokenUtil.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtTokenUtil.java new file mode 100644 index 00000000..b223b125 --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtTokenUtil.java @@ -0,0 +1,86 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; +// import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +// @Service +// public class JwtGenerator{ +// public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; +// // @Value("${jwt.secret}") +// private final String secret = "nighthawk"; +// public Map generateJwt(Person user) { +// String jwtToken = Jwts.builder().setSubject(user.getEmail()).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact(); +// Map jwtTokenGen = new HashMap<>(); +// jwtTokenGen.put("token", jwtToken); +// return jwtTokenGen; +// } +// static public void main(String[] args) { +// // create and display LightBoard +// JwtGenerator jwtgen = new JwtGenerator(); +// System.out.println(jwtgen.generateJwt(new Person("aidanywu@gmail.com", "password", "Aidan Wu", new java.util.GregorianCalendar(2006,4,6).getTime()))); +// } +// } +@Component +public class JwtTokenUtil { + + public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; + + // @Value("${jwt.secret}") + private String secret = "nighthawk"; + + //retrieve username from jwt token + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + //retrieve expiration date from jwt token + public Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + //for retrieveing any information from token we will need the secret key + private Claims getAllClaimsFromToken(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } + + //check if the token has expired + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + //generate token for user + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return doGenerateToken(claims, userDetails.getUsername()); + } + + //while creating the token - + //1. Define claims of the token, like Issuer, Expiration, Subject, and the ID + //2. Sign the JWT using the HS512 algorithm and secret key. + //3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) + // compaction of the JWT to a URL-safe string + private String doGenerateToken(Map claims, String subject) { + + return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + } + + //validate token + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = getUsernameFromToken(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtUserDetailsService.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtUserDetailsService.java new file mode 100644 index 00000000..27fb2913 --- /dev/null +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/jwt/JwtUserDetailsService.java @@ -0,0 +1,26 @@ +package com.nighthawk.spring_portfolio.mvc.jwt; + +import java.util.ArrayList; + +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 com.nighthawk.spring_portfolio.mvc.person.*; + +@Service +public class JwtUserDetailsService implements UserDetailsService { + private PersonJpaRepository repository; + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Person person = repository.findByEmail(email); + if (person != null) { + return new User(person.getEmail(), person.getPassword(), + new ArrayList<>()); + } else { + throw new UsernameNotFoundException("User not found with username: " + email); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/Person.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/Person.java index d0cde22e..4d8c803b 100644 --- a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/Person.java +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/Person.java @@ -40,7 +40,7 @@ @Entity @TypeDef(name="json", typeClass = JsonType.class) public class Person { - + // automatic unique identifier for Person record @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonApiController.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonApiController.java index 1dd9943e..4dd40b09 100644 --- a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonApiController.java +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonApiController.java @@ -1,5 +1,6 @@ package com.nighthawk.spring_portfolio.mvc.person; +// import com.nighthawk.spring_portfolio.mvc.jwt.JwtTokenUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -12,6 +13,8 @@ @RestController @RequestMapping("/api/person") public class PersonApiController { + // @Autowired + // private JwtTokenUtil jwtGen; /* #### RESTful API #### Resource: https://spring.io/guides/gs/rest-service/ @@ -126,4 +129,22 @@ public ResponseEntity personStats(@RequestBody final Map } + // @PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE) + // public ResponseEntity createToken(@RequestBody final Map stat_map) { + // // find ID + // // System.out.println(stat_map); + // String email=((String)stat_map.get("email")); + // // System.out.println('a'); + // String password=((String)stat_map.get("password")); + // // System.out.println('b'); + // try { + // Person person = repository.findByEmailAndPassword(email, password); + // return new ResponseEntity<>(jwtGen.generateToken(person), HttpStatus.OK); + // } catch(Exception e) { + // // return Bad ID + // return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + // } + + // } + } diff --git a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonJpaRepository.java b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonJpaRepository.java index a4aaadb2..650119a7 100644 --- a/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonJpaRepository.java +++ b/src/main/java/com/nighthawk/spring_portfolio/mvc/person/PersonJpaRepository.java @@ -22,7 +22,7 @@ public interface PersonJpaRepository extends JpaRepository { https://springframework.guru/spring-data-jpa-query/ https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods */ - + Person findByEmailAndPassword(String email, String password); // Custom JPA query @Query( value = "SELECT * FROM Person p WHERE p.name LIKE ?1 or p.email LIKE ?1", diff --git a/src/main/java/com/nighthawk/spring_portfolio/security/SecurityConfig.java b/src/main/java/com/nighthawk/spring_portfolio/security/SecurityConfig.java index 8c521aa8..96e1ed65 100644 --- a/src/main/java/com/nighthawk/spring_portfolio/security/SecurityConfig.java +++ b/src/main/java/com/nighthawk/spring_portfolio/security/SecurityConfig.java @@ -1,18 +1,78 @@ package com.nighthawk.spring_portfolio.security; +import com.nighthawk.spring_portfolio.mvc.jwt.*; + 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.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.authentication.builders.AuthenticationManagerBuilder; +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.configuration.EnableWebSecurity; +// import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +// import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /* * To enable HTTP Security in Spring, extend the WebSecurityConfigurerAdapter. */ +@Configuration @EnableWebSecurity // Beans to enable basic Web security +@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Autowired + private JwtUserDetailsService jwtUserDetailsService; + + @Autowired + private JwtRequestFilter jwtRequestFilter; + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // configure AuthenticationManager so that it knows from where to load + // user for matching credentials + // Use BCryptPasswordEncoder + auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + // Provide a default configuration using configure(HttpSecurity http) - @Override - protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable(); // Cross-Site Request Forgery disable for JS Fetch URIs - } + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf().disable(); + // httpSecurity + // // We don't need CSRF for this example + // .csrf().disable() + // // don't authenticate this particular request + // .authorizeRequests().antMatchers("/authenticate").permitAll().and() + // // all other requests need to be authenticated + // .authorizeRequests().anyRequest().authenticated().and(). + // // make sure we use stateless session; session won't be used to + // // store user's state. + // exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement() + // .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // // Add a filter to validate the tokens with every request + // httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 343418c2..ebc9d9bf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,4 +12,6 @@ spring.datasource.driver-class-name = org.sqlite.JDBC spring.datasource.username = admin spring.datasource.password = admin -server.port=8085 \ No newline at end of file +server.port=8085 + +jwt.secret=nighthawk diff --git a/volumes/sqlite.db b/volumes/sqlite.db index c32659c8..4af2bf38 100644 Binary files a/volumes/sqlite.db and b/volumes/sqlite.db differ