diff --git a/pom.xml b/pom.xml index 2978103..e715c42 100644 --- a/pom.xml +++ b/pom.xml @@ -4,8 +4,8 @@ 4.0.0 com.revature - interview-evaluations - 0.0.1-SNAPSHOT + interview-evaluations + 0.0.1-SNAPSHOT jar interview-evaluations @@ -75,10 +75,6 @@ - - org.springframework.boot - spring-boot-starter-web - org.springframework.boot spring-boot-starter-test @@ -113,7 +109,44 @@ spring-boot-devtools true - + + org.springframework.mobile + spring-mobile-device + 1.1.5.RELEASE + jar + + + org.springframework.security + spring-security-core + 4.2.1.RELEASE + jar + + + org.springframework.security + spring-security-web + 4.2.1.RELEASE + jar + + + io.jsonwebtoken + jjwt + 0.7.0 + jar + + + org.springframework.security + spring-security-config + 4.2.1.RELEASE + jar + + @@ -138,6 +171,10 @@ JBoss Maven Release Repository https://repository.jboss.org/nexus/content/repositories/releases + + + + diff --git a/src/main/java/com/revature/InterviewEvaluationsApplication.java b/src/main/java/com/revature/InterviewEvaluationsApplication.java index 406a464..08801d1 100644 --- a/src/main/java/com/revature/InterviewEvaluationsApplication.java +++ b/src/main/java/com/revature/InterviewEvaluationsApplication.java @@ -10,9 +10,7 @@ @SpringBootApplication public class InterviewEvaluationsApplication { - public static void main(String[] args) { - SpringApplication.run(InterviewEvaluationsApplication.class, args); - - } - + public static void main(String[] args) { + SpringApplication.run(InterviewEvaluationsApplication.class, args); + } } diff --git a/src/main/java/com/revature/DataSourceConfig.java b/src/main/java/com/revature/config/DataSourceConfig.java similarity index 96% rename from src/main/java/com/revature/DataSourceConfig.java rename to src/main/java/com/revature/config/DataSourceConfig.java index 79bfa5a..dd5925c 100644 --- a/src/main/java/com/revature/DataSourceConfig.java +++ b/src/main/java/com/revature/config/DataSourceConfig.java @@ -1,4 +1,4 @@ -package com.revature; +package com.revature.config; import javax.activation.DataSource; diff --git a/src/main/java/com/revature/WebConfig.java b/src/main/java/com/revature/config/WebConfig.java similarity index 97% rename from src/main/java/com/revature/WebConfig.java rename to src/main/java/com/revature/config/WebConfig.java index 4ecc141..4f1a47b 100644 --- a/src/main/java/com/revature/WebConfig.java +++ b/src/main/java/com/revature/config/WebConfig.java @@ -1,4 +1,4 @@ -package com.revature; +package com.revature.config; import java.time.LocalDate; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/revature/config/WebSecurityConfig.java b/src/main/java/com/revature/config/WebSecurityConfig.java new file mode 100644 index 0000000..46824c3 --- /dev/null +++ b/src/main/java/com/revature/config/WebSecurityConfig.java @@ -0,0 +1,86 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.config; + +import com.revature.security.JwtAuthenticationEntryPoint; +import com.revature.security.extra.JwtAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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; + +/** + * + * @author FayeRedd + */ + +@SuppressWarnings("SpringJavaAutowiringInspection") +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ + + @Autowired + private JwtAuthenticationEntryPoint unauthorizedHandler; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{ + authenticationManagerBuilder + .userDetailsService(this.userDetailsService) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public JwtAuthenticationFilter authenticationFilterBean(){ + return new JwtAuthenticationFilter(); + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception{ + httpSecurity + .csrf().disable() + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers( + HttpMethod.GET, + "/", + "/*.html", + "/favicon.ico", + "/**/*.html", + "/**/*.css", + "/**/*.js" + ).permitAll() + .antMatchers("/auth/**").permitAll() + .anyRequest().authenticated(); + + httpSecurity + .addFilterBefore(authenticationFilterBean(), UsernamePasswordAuthenticationFilter.class); + + httpSecurity.headers().cacheControl(); + + } +} diff --git a/src/main/java/com/revature/controllers/BatchController.java b/src/main/java/com/revature/controllers/BatchController.java index dc26831..692d52c 100644 --- a/src/main/java/com/revature/controllers/BatchController.java +++ b/src/main/java/com/revature/controllers/BatchController.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -16,6 +17,7 @@ @CrossOrigin @RestController @RequestMapping(value = "/api/v1/") +@PreAuthorize("hasRole('ADMIN')") public class BatchController { @Autowired diff --git a/src/main/java/com/revature/repositories/extra/UserRepository.java b/src/main/java/com/revature/repositories/extra/UserRepository.java new file mode 100644 index 0000000..886c40c --- /dev/null +++ b/src/main/java/com/revature/repositories/extra/UserRepository.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.repositories.extra; + +import com.revature.security.extra.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * + * @author FayeRedd + */ + +@Repository +public interface UserRepository extends JpaRepository { + + User findByUsername(String username); + +} diff --git a/src/main/java/com/revature/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/revature/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..f9a171b --- /dev/null +++ b/src/main/java/com/revature/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security; + +import java.io.IOException; +import java.io.Serializable; +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; + +/** + * + * @author FayeRedd + */ +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable{ + + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } +} diff --git a/src/main/java/com/revature/security/JwtAuthenticationRequest.java b/src/main/java/com/revature/security/JwtAuthenticationRequest.java new file mode 100644 index 0000000..c6555c9 --- /dev/null +++ b/src/main/java/com/revature/security/JwtAuthenticationRequest.java @@ -0,0 +1,45 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security; + +import java.io.Serializable; + +/** + * + * @author FayeRedd + */ +public class JwtAuthenticationRequest implements Serializable { + + private static final long serialVersionUID = -8445943548965154778L; + + private String username; + private String password; + + public JwtAuthenticationRequest() { + super(); + } + + public JwtAuthenticationRequest(String username, String password) { + this.setUsername(username); + this.setPassword(password); + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/revature/security/exceptions/JwtTokenMissingException.java b/src/main/java/com/revature/security/exceptions/JwtTokenMissingException.java new file mode 100644 index 0000000..6c2a485 --- /dev/null +++ b/src/main/java/com/revature/security/exceptions/JwtTokenMissingException.java @@ -0,0 +1,19 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.exceptions; + +/** + * + * @author FayeRedd + */ +public class JwtTokenMissingException extends RuntimeException{ + + public JwtTokenMissingException(){} + + public JwtTokenMissingException(String message){ + super(message); + } +} diff --git a/src/main/java/com/revature/security/extra/JwtAuthenticationFilter.java b/src/main/java/com/revature/security/extra/JwtAuthenticationFilter.java new file mode 100644 index 0000000..22add8c --- /dev/null +++ b/src/main/java/com/revature/security/extra/JwtAuthenticationFilter.java @@ -0,0 +1,59 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra; + +import com.revature.security.extra.JwtTokenUtil; +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.beans.factory.annotation.Value; +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.web.filter.OncePerRequestFilter; + +/** + * + * @author FayeRedd + */ +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private JwtTokenUtil jwtTokenUtil; + + @Value("${jwt.header}") + private String tokenHeader; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + String authToken = request.getHeader(this.tokenHeader); + + String username = jwtTokenUtil.getUsernameFromToken(authToken); + + if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){ + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if(jwtTokenUtil.validateToken(authToken, userDetails)){ + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/revature/security/extra/JwtAuthenticationResponse.java b/src/main/java/com/revature/security/extra/JwtAuthenticationResponse.java new file mode 100644 index 0000000..6c65e7c --- /dev/null +++ b/src/main/java/com/revature/security/extra/JwtAuthenticationResponse.java @@ -0,0 +1,27 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra; + +import java.io.Serializable; + +/** + * + * @author FayeRedd + */ +public class JwtAuthenticationResponse implements Serializable { + + private static final long serialVersionUID = 1250166508152483573L; + + private final String token; + + public JwtAuthenticationResponse(String token) { + this.token = token; + } + + public String getToken() { + return this.token; + } +} diff --git a/src/main/java/com/revature/security/extra/JwtTokenUtil.java b/src/main/java/com/revature/security/extra/JwtTokenUtil.java new file mode 100644 index 0000000..338459a --- /dev/null +++ b/src/main/java/com/revature/security/extra/JwtTokenUtil.java @@ -0,0 +1,186 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra; + +/** + * + * @author FayeRedd + */ + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mobile.device.Device; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/* +* This class is the demo token generator/validator. It can be replaced in the future with +* a microservice providing these same features +*/ + +@Component +public class JwtTokenUtil implements Serializable { + + private static final long serialVersionUID = -3301605591108950415L; + + static final String CLAIM_KEY_USERNAME = "sub"; + static final String CLAIM_KEY_AUDIENCE = "audience"; + static final String CLAIM_KEY_CREATED = "created"; + + private static final String AUDIENCE_UNKNOWN = "unknown"; + private static final String AUDIENCE_WEB = "web"; + private static final String AUDIENCE_MOBILE = "mobile"; + private static final String AUDIENCE_TABLET = "tablet"; + + //This value is stored in the application.yml file + @Value("${jwt.secret}") + private String secret; + + //This value is stored in the application.yml file + @Value("${jwt.expiration}") + private Long expiration; + + public String getUsernameFromToken(String token) { + String username; + try { + final Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + public Date getCreatedDateFromToken(String token) { + Date created; + try { + final Claims claims = getClaimsFromToken(token); + created = new Date((Long) claims.get(CLAIM_KEY_CREATED)); + } catch (Exception e) { + created = null; + } + return created; + } + + public Date getExpirationDateFromToken(String token) { + Date expires; + try { + final Claims claims = getClaimsFromToken(token); + expires = claims.getExpiration(); + } catch (Exception e) { + expires = null; + } + return expires; + } + + public String getAudienceFromToken(String token) { + String audience; + try { + final Claims claims = getClaimsFromToken(token); + audience = (String) claims.get(CLAIM_KEY_AUDIENCE); + } catch (Exception e) { + audience = null; + } + return audience; + } + + private Claims getClaimsFromToken(String token) { + Claims claims; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException | MalformedJwtException | SignatureException | UnsupportedJwtException | IllegalArgumentException e) { + claims = null; + } + return claims; + } + + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + private Boolean isTokenExpired(String token) { + final Date expires = getExpirationDateFromToken(token); + return expires.before(new Date()); + } + + private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) { + return lastPasswordReset != null && created.before(lastPasswordReset); + } + + private String generateAudience(Device device) { + String audience = AUDIENCE_UNKNOWN; + if (device.isNormal()) { + audience = AUDIENCE_WEB; + } else if (device.isTablet()) { + audience = AUDIENCE_TABLET; + } else if (device.isMobile()) { + audience = AUDIENCE_MOBILE; + } + return audience; + } + + private Boolean ignoreTokenExpiration(String token) { + String audience = getAudienceFromToken(token); + return AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience); + } + + public String generateToken(UserDetails userDetails, Device device) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_AUDIENCE, generateAudience(device)); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) { + final Date created = getCreatedDateFromToken(token); + return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset) + && (!isTokenExpired(token) || ignoreTokenExpiration(token)); + } + + public String refreshToken(String token) { + String refreshedToken; + try { + final Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + refreshedToken = generateToken(claims); + } catch (Exception e) { + refreshedToken = null; + } + return refreshedToken; + } + + public Boolean validateToken(String token, UserDetails userDetails) { + JwtUser user = (JwtUser) userDetails; + final String username = getUsernameFromToken(token); + final Date created = getCreatedDateFromToken(token); + return username.equals(user.getUsername()) + && !isTokenExpired(token) + && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate()); + } +} \ No newline at end of file diff --git a/src/main/java/com/revature/security/extra/JwtUser.java b/src/main/java/com/revature/security/extra/JwtUser.java new file mode 100644 index 0000000..36c5e62 --- /dev/null +++ b/src/main/java/com/revature/security/extra/JwtUser.java @@ -0,0 +1,113 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra; + +/** + * + * @author FayeRedd + */ +import java.util.Collection; +import java.util.Date; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class JwtUser implements UserDetails { + + private final Long id; + private final String username; + private final String firstname; + private final String lastname; + private final String password; + private final String email; + private final Collection authorities; + private final boolean enabled; + private final Date lastPasswordResetDate; + + public JwtUser( + Long id, + String username, + String firstname, + String lastname, + String email, + String password, Collection authorities, + boolean enabled, + Date lastPasswordResetDate + ) { + this.id = id; + this.username = username; + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + this.password = password; + this.authorities = authorities; + this.enabled = enabled; + this.lastPasswordResetDate = lastPasswordResetDate; + } + + @JsonIgnore + public Long getId() { + return id; + } + + @Override + public String getUsername() { + return username; + } + + @JsonIgnore + @Override + public boolean isAccountNonExpired() { + return true; + } + + @JsonIgnore + @Override + public boolean isAccountNonLocked() { + return true; + } + + @JsonIgnore + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public String getEmail() { + return email; + } + + @JsonIgnore + @Override + public String getPassword() { + return password; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @JsonIgnore + public Date getLastPasswordResetDate() { + return lastPasswordResetDate; + } +} diff --git a/src/main/java/com/revature/security/extra/JwtUserFactory.java b/src/main/java/com/revature/security/extra/JwtUserFactory.java new file mode 100644 index 0000000..805def8 --- /dev/null +++ b/src/main/java/com/revature/security/extra/JwtUserFactory.java @@ -0,0 +1,45 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra; + +import com.revature.security.extra.model.Authority; +import com.revature.security.extra.model.User; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + + + +/** + * + * @author FayeRedd + */ +public class JwtUserFactory { + + private JwtUserFactory(){} + + public static JwtUser create(User user){ + return new JwtUser( + user.getId(), + user.getUsername(), + user.getFirstname(), + user.getLastname(), + user.getEmail(), + user.getPassword(), + mapToGrantedAuthorities(user.getAuthorities()), + user.getEnabled(), + user.getLastPasswordResetDate() + ); + } + + private static List mapToGrantedAuthorities(List authorities) { + return authorities.stream() + .map(authority -> new SimpleGrantedAuthority(authority.getName().name())) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/revature/security/extra/controllers/AuthenticationRestController.java b/src/main/java/com/revature/security/extra/controllers/AuthenticationRestController.java new file mode 100644 index 0000000..2361c27 --- /dev/null +++ b/src/main/java/com/revature/security/extra/controllers/AuthenticationRestController.java @@ -0,0 +1,83 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.controllers; + +import com.revature.security.JwtAuthenticationRequest; +import com.revature.security.extra.JwtTokenUtil; +import com.revature.security.extra.JwtUser; +import com.revature.security.extra.JwtAuthenticationResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.mobile.device.Device; +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.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * + * @author FayeRedd + */ + +@RestController +public class AuthenticationRestController { + + //This value can be found in the application.yml file + @Value("${jwt.header}") + private String tokenHeader; + + @Autowired + private AuthenticationManager authMan; + + //This is the token generator/validator. Can be replaced with future microservice providing/varifying tokens + @Autowired + private JwtTokenUtil tokenUtil; + + @Autowired + private UserDetailsService userDetServ; + + //This value can be found in the application.yml file + @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) + public ResponseEntity createAuthenticationToken(@RequestBody JwtAuthenticationRequest authReq, Device dev) throws AuthenticationException{ + + final Authentication authentication = authMan.authenticate( + new UsernamePasswordAuthenticationToken( + authReq.getUsername(), + authReq.getPassword()) + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + final UserDetails userDetails = userDetServ.loadUserByUsername(authReq.getUsername()); + final String token = tokenUtil.generateToken(userDetails, dev); + + return ResponseEntity.ok(new JwtAuthenticationResponse(token)); + } + + //This value can be found in the application.yml file + @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET) + public ResponseEntity refreshAndGetAuthenticationToken(HttpServletRequest req){ + String token = req.getHeader(tokenHeader); + String username = tokenUtil.getUsernameFromToken(token); + JwtUser user = (JwtUser) userDetServ.loadUserByUsername(username); + + if(tokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){ + String refreshedToken = tokenUtil.refreshToken(token); + return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken)); + } else { + return ResponseEntity.badRequest().body(null); + } + } +} diff --git a/src/main/java/com/revature/security/extra/controllers/UserRestController.java b/src/main/java/com/revature/security/extra/controllers/UserRestController.java new file mode 100644 index 0000000..f1a4a95 --- /dev/null +++ b/src/main/java/com/revature/security/extra/controllers/UserRestController.java @@ -0,0 +1,48 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.controllers; + +import com.revature.security.extra.JwtTokenUtil; +import com.revature.security.extra.JwtUser; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * + * @author FayeRedd + */ + +@RestController +public class UserRestController { + + //This value is found in the application.yml file + @Value("${jwt.header}") + private String tokenHeader; + + //This is the token generator/validator. Can be replaced with future microservice providing/varifying tokens + @Autowired + private JwtTokenUtil jwtTokenUtil; + + @Autowired + private UserDetailsService userDetailsService; + + @RequestMapping(value="user", method = RequestMethod.GET) + public JwtUser getAuthenticatedUser(HttpServletRequest req){ + + String token = req.getHeader(tokenHeader); + String username = jwtTokenUtil.getUsernameFromToken(token); + JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username); + + return user; + } +} \ No newline at end of file diff --git a/src/main/java/com/revature/security/extra/model/Authority.java b/src/main/java/com/revature/security/extra/model/Authority.java new file mode 100644 index 0000000..b3166fb --- /dev/null +++ b/src/main/java/com/revature/security/extra/model/Authority.java @@ -0,0 +1,62 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.model; + +/** + * + * @author FayeRedd + */ +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.util.List; + +/* +This is a placeholder Entity for an "Authority" POJO. Feel free to modify/edit as needed +for compatibility with actual database Table +*/ + +@Entity +@Table(name = "ie_authority") +public class Authority { + + @Id + @Column(name = "a_authid") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "authority_seq") + @SequenceGenerator(name = "authority_seq", sequenceName = "authority_seq", allocationSize = 1) + private Long id; + + @Column(name = "a_authorityname", length = 50) + @NotNull + @Enumerated(EnumType.STRING) + private AuthorityName name; + + @ManyToMany(mappedBy = "authorities", fetch = FetchType.LAZY) + private List users; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AuthorityName getName() { + return name; + } + + public void setName(AuthorityName name) { + this.name = name; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } +} \ No newline at end of file diff --git a/src/main/java/com/revature/security/extra/model/AuthorityName.java b/src/main/java/com/revature/security/extra/model/AuthorityName.java new file mode 100644 index 0000000..c802732 --- /dev/null +++ b/src/main/java/com/revature/security/extra/model/AuthorityName.java @@ -0,0 +1,14 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.model; + +/** + * + * @author FayeRedd + */ +public enum AuthorityName { + ROLE_USER, ROLE_ADMIN +} diff --git a/src/main/java/com/revature/security/extra/model/User.java b/src/main/java/com/revature/security/extra/model/User.java new file mode 100644 index 0000000..53af8eb --- /dev/null +++ b/src/main/java/com/revature/security/extra/model/User.java @@ -0,0 +1,158 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.model; + +/** + * + * @author FayeRedd + */ +import java.util.Date; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/* +This is a placeholder Entity for the User POJO. Feel free to modify/edit as needed +for compatibility with actual database Table +*/ + +@Entity +@Table(name = "ie_user") +public class User { + + @Id + @Column(name = "u_userid") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq") + @SequenceGenerator(name = "user_seq", sequenceName = "user_seq", allocationSize = 1) + private Long id; + + @Column(name = "u_username", length = 50, unique = true) + @NotNull + @Size(min = 4, max = 50) + private String username; + + @Column(name = "u_password", length = 100) + @NotNull + @Size(min = 4, max = 100) + private String password; + + @Column(name = "u_firstname", length = 50) + @NotNull + @Size(min = 4, max = 50) + private String firstname; + + @Column(name = "u_lastname", length = 50) + @NotNull + @Size(min = 4, max = 50) + private String lastname; + + @Column(name = "u_email", length = 50) + @NotNull + @Size(min = 4, max = 50) + private String email; + + @Column(name = "u_enabled") + @NotNull + private Boolean enabled; + + @Column(name = "u_lastpasswordresetdate") + @Temporal(TemporalType.TIMESTAMP) + @NotNull + private Date lastPasswordResetDate; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "ie_user_authority", + joinColumns = {@JoinColumn(name = "ua_user_id", referencedColumnName = "u_userid")}, + inverseJoinColumns = {@JoinColumn(name = "ua_authority_id", referencedColumnName = "a_authid")}) + private List authorities; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public List getAuthorities() { + return authorities; + } + + public void setAuthorities(List authorities) { + this.authorities = authorities; + } + + public Date getLastPasswordResetDate() { + return lastPasswordResetDate; + } + + public void setLastPasswordResetDate(Date lastPasswordResetDate) { + this.lastPasswordResetDate = lastPasswordResetDate; + } +} diff --git a/src/main/java/com/revature/security/extra/service/JwtUserDetailsServiceImpl.java b/src/main/java/com/revature/security/extra/service/JwtUserDetailsServiceImpl.java new file mode 100644 index 0000000..647f785 --- /dev/null +++ b/src/main/java/com/revature/security/extra/service/JwtUserDetailsServiceImpl.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.revature.security.extra.service; + +import com.revature.security.extra.JwtUserFactory; +import com.revature.security.extra.model.User; +import com.revature.repositories.extra.UserRepository; +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; + +/** + * + * @author FayeRedd + */ +@Service +public class JwtUserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username)); + } else { + return JwtUserFactory.create(user); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..9b24ad4 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,23 @@ +# config context path to "/" by setting an empty string +server: + contextPath: + +# JACKSON +spring: + jackson: + serialization: + INDENT_OUTPUT: true + +jwt: + header: Authorization + secret: mySecret + expiration: 604800 + route: + authentication: + path: auth + refresh: refresh + +#logging: +# level: +# org.springframework: +# security: DEBUG \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..60e270d --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,117 @@ + + + + + JWT Spring Security Demo + + + + + + + + + + +
+

JWT Spring Security Demo

+ +
Not logged in!
+ +
+ +
+
+
+
+

Login

+
+
+
+
+ +
+
+ +
+
+ Try one of the following logins +
    +
  • admin & admin
  • +
  • user & password
  • +
  • disabled & password
  • +
+
+ +
+
+
+ +
+
+
+

Authenticated user

+
+
+
+ +
+
+
+
+ +
+
+ + +
+
+
+

Response:

+
+
+

+                
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/js/client.js b/src/main/resources/static/js/client.js new file mode 100644 index 0000000..2c0fb4d --- /dev/null +++ b/src/main/resources/static/js/client.js @@ -0,0 +1,175 @@ +/** + * Created by stephan on 20.03.16. + */ + +$(function () { + // VARIABLES ============================================================= + var TOKEN_KEY = "jwtToken" + var $notLoggedIn = $("#notLoggedIn"); + var $loggedIn = $("#loggedIn").hide(); + var $response = $("#response"); + var $login = $("#login"); + var $userInfo = $("#userInfo").hide(); + + // FUNCTIONS ============================================================= + function getJwtToken() { + return localStorage.getItem(TOKEN_KEY); + } + + function setJwtToken(token) { + localStorage.setItem(TOKEN_KEY, token); + } + + function removeJwtToken() { + localStorage.removeItem(TOKEN_KEY); + } + + function doLogin(loginData) { + $.ajax({ + url: "/auth", + type: "POST", + data: JSON.stringify(loginData), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function (data, textStatus, jqXHR) { + setJwtToken(data.token); + $login.hide(); + $notLoggedIn.hide(); + showTokenInformation() + showUserInformation(); + }, + error: function (jqXHR, textStatus, errorThrown) { + if (jqXHR.status === 401) { + $('#loginErrorModal') + .modal("show") + .find(".modal-body") + .empty() + .html("

Spring exception:
" + jqXHR.responseJSON.exception + "

"); + } else { + throw new Error("an unexpected error occured: " + errorThrown); + } + } + }); + } + + function doLogout() { + removeJwtToken(); + $login.show(); + $userInfo + .hide() + .find("#userInfoBody").empty(); + $loggedIn + .hide() + .attr("title", "") + .empty(); + $notLoggedIn.show(); + } + + function createAuthorizationTokenHeader() { + var token = getJwtToken(); + if (token) { + return {"Authorization": token}; + } else { + return {}; + } + } + + function showUserInformation() { + $.ajax({ + url: "/user", + type: "GET", + contentType: "application/json; charset=utf-8", + dataType: "json", + headers: createAuthorizationTokenHeader(), + success: function (data, textStatus, jqXHR) { + var $userInfoBody = $userInfo.find("#userInfoBody"); + + $userInfoBody.append($("
").text("Username: " + data.username)); + $userInfoBody.append($("
").text("Email: " + data.email)); + + var $authorityList = $("
    "); + data.authorities.forEach(function (authorityItem) { + $authorityList.append($("
  • ").text(authorityItem.authority)); + }); + var $authorities = $("
    ").text("Authorities:"); + $authorities.append($authorityList); + + $userInfoBody.append($authorities); + $userInfo.show(); + } + }); + } + + function showTokenInformation() { + $loggedIn + .text("Token: " + getJwtToken()) + .attr("title", "Token: " + getJwtToken()) + .show(); + } + + function showResponse(statusCode, message) { + $response + .empty() + .text("status code: " + statusCode + "\n-------------------------\n" + message); + } + + // REGISTER EVENT LISTENERS ============================================================= + $("#loginForm").submit(function (event) { + event.preventDefault(); + + var $form = $(this); + var formData = { + username: $form.find('input[name="username"]').val(), + password: $form.find('input[name="password"]').val() + }; + + doLogin(formData); + }); + + $("#logoutButton").click(doLogout); + + $("#exampleServiceBtn").click(function () { + $.ajax({ + url: "/api/v1/evaluations/154", + type: "GET", + contentType: "application/json; charset=utf-8", + dataType: "json", + headers: createAuthorizationTokenHeader(), + success: function (data, textStatus, jqXHR) { + showResponse(jqXHR.status, JSON.stringify(data)); + }, + error: function (jqXHR, textStatus, errorThrown) { + showResponse(jqXHR.status, errorThrown); + } + }); + }); + + $("#adminServiceBtn").click(function () { + $.ajax({ + url: "/api/v1/batches/171", + type: "GET", + contentType: "application/json; charset=utf-8", + headers: createAuthorizationTokenHeader(), + success: function (data, textStatus, jqXHR) { + showResponse(jqXHR.status, data); + }, + error: function (jqXHR, textStatus, errorThrown) { + showResponse(jqXHR.status, errorThrown); + } + }); + }); + + $loggedIn.click(function () { + $loggedIn + .toggleClass("text-hidden") + .toggleClass("text-shown"); + }); + + // INITIAL CALLS ============================================================= + if (getJwtToken()) { + $login.hide(); + $notLoggedIn.hide(); + showTokenInformation(); + showUserInformation(); + } +}); \ No newline at end of file diff --git a/src/test/java/com/revature/InterviewEvaluationsApplicationTests.java b/src/test/java/com/revature/InterviewEvaluationsApplicationTests.java deleted file mode 100644 index e2e449e..0000000 --- a/src/test/java/com/revature/InterviewEvaluationsApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.revature; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class InterviewEvaluationsApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/src/test/java/com/revature/spring/test/PersonEntityTest.java b/src/test/java/com/revature/spring/test/PersonEntityTest.java deleted file mode 100644 index e69de29..0000000