diff --git a/.idea/misc.xml b/.idea/misc.xml index 8f27022..241f098 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index e6c5a36..ea7614f 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,8 @@ dependencies { compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'org.springframework.security:spring-security-web' implementation 'org.postgresql:postgresql' diff --git a/git-hooks/pre-commit.sh b/git-hooks/pre-commit.sh index 68ab401..760f9a7 100755 --- a/git-hooks/pre-commit.sh +++ b/git-hooks/pre-commit.sh @@ -13,6 +13,9 @@ if [ "$NO_VERIFY" ]; then fi # list all added/copied/modified/renamed java files -git diff --staged --name-only --diff-filter=ACMR | egrep -a '.java$' | tr "\n" "\0" | +files="$(git diff --staged --name-only --diff-filter=ACMR | egrep -a '.java$' | tr '\n' ' ')" +for f in $files; do # run google-java-format on each file and re-stage any new changes - xargs -0 -I % echo "$format_cmd --aosp -i '%'; git add -f '%'" | sh + $format_cmd --aosp -i "$f" + git add -f "$f" +done diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java new file mode 100644 index 0000000..7dfc16d --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java @@ -0,0 +1,21 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +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"); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java new file mode 100644 index 0000000..853083b --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java @@ -0,0 +1,69 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.JWTUserDetailsService; +import io.jsonwebtoken.ExpiredJwtException; +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.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@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 th + // e 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); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java new file mode 100644 index 0000000..40d369f --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java @@ -0,0 +1,72 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +public class JWTTokenUtil { + public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; + + @Value("${jwt.secret}") + private String secret; + + // 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)); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java new file mode 100644 index 0000000..a8bcb1d --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java @@ -0,0 +1,75 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.JWTUserDetailsService; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig 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(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // We don't need CSRF for this example + httpSecurity + .csrf() + .disable() + // dont authenticate this particular request + .authorizeRequests() + .antMatchers("/auth/login") + .permitAll() + .antMatchers("/auth/register") + .permitAll() + . + // all other requests need to be authenticated + 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/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/JWTAuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/JWTAuthenticationController.java new file mode 100644 index 0000000..6e3d01b --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/JWTAuthenticationController.java @@ -0,0 +1,57 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import org.springframework.beans.factory.annotation.Autowired; +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.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.*; + +@RestController +@CrossOrigin +@RequestMapping("/auth") +public class JWTAuthenticationController { + + @Autowired private AuthenticationManager authenticationManager; + + @Autowired private JWTTokenUtil jwtTokenUtil; + + @Autowired private JWTUserDetailsService userDetailsService; + + @Autowired private UserRepository users; + + private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + @PostMapping("/login") + public JWTResponse login(@RequestBody JWTRequest authenticationRequest) throws Exception { + authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); + final UserDetails userDetails = + userDetailsService.loadUserByUsername(authenticationRequest.getUsername()); + final String token = jwtTokenUtil.generateToken(userDetails); + return new JWTResponse(token); + } + + @PostMapping("/register") + public User register(@RequestBody User user) { + user.setPassword(encoder.encode(user.getPassword())); + users.save(user); + return user; + } + + private void authenticate(String username, String password) throws Exception { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password)); + } catch (DisabledException e) { + e.printStackTrace(); + throw new Exception("USER_DISABLED", e); + } catch (BadCredentialsException e) { + e.printStackTrace(); + throw new Exception("INVALID_CREDENTIALS", e); + } + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserController.java deleted file mode 100644 index 55e9139..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserController.java +++ /dev/null @@ -1,30 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; - -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SecurityService; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; - -@Controller -public class UserController { - @Autowired private UserService userService; - - @Autowired private SecurityService securityService; - - /* - @PostMapping("/registration") - public String registration(@ModelAttribute("userForm") User userForm, BindingResult bindingResult) { - userValidator.validate(userForm, bindingResult); - - if (bindingResult.hasErrors()) { - return "registration"; - } - - userService.save(userForm); - - securityService.autoLogin(userForm.getUsername(), userForm.getPasswordConfirm()); - - return "redirect:/welcome"; - } - */ -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTRequest.java new file mode 100644 index 0000000..66d0c75 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTRequest.java @@ -0,0 +1,30 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +public class JWTRequest { + private String username; + private String password; + + // need default constructor for JSON Parsing + public JWTRequest() {} + + public JWTRequest(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/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTResponse.java new file mode 100644 index 0000000..9eb1092 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTResponse.java @@ -0,0 +1,13 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +public class JWTResponse { + private final String jwttoken; + + public JWTResponse(String jwttoken) { + this.jwttoken = jwttoken; + } + + public String getToken() { + return this.jwttoken; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserDetailsServiceImpl.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java similarity index 77% rename from src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserDetailsServiceImpl.java rename to src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java index c1d9ff1..53341d5 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserDetailsServiceImpl.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java @@ -3,21 +3,23 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.*; -import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Component; -public class UserDetailsServiceImpl implements UserDetailsService { +@Component +public class JWTUserDetailsService implements UserDetailsService { @Autowired private UserRepository repository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User toReturn = repository.findByUsername(username); + System.out.println("malusa: " + username); + System.out.println(toReturn); if (toReturn != null) { - Set authoritySet = Set.of(new SimpleGrantedAuthority("user")); return new org.springframework.security.core.userdetails.User( - toReturn.getUsername(), toReturn.getPassword(), authoritySet); + toReturn.getUsername(), toReturn.getPassword(), Set.of()); } else { throw new UsernameNotFoundException(username); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityService.java deleted file mode 100644 index 5ff2e3b..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityService.java +++ /dev/null @@ -1,42 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.models; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationManager; -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; - -public class SecurityService { - @Autowired private AuthenticationManager manager; - - @Autowired private UserDetailsService service; - - private Logger logger = LoggerFactory.getLogger(SecurityService.class); - - public String loggedUser() { - Object details = SecurityContextHolder.getContext().getAuthentication().getDetails(); - if (details instanceof UserDetails) { - return ((UserDetails) details).getUsername(); - } else { - return null; - } - } - - public void autoLogin(String username, String password) { - UserDetails userDetails = service.loadUserByUsername(username); - UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = - new UsernamePasswordAuthenticationToken( - userDetails, password, userDetails.getAuthorities()); - - manager.authenticate(usernamePasswordAuthenticationToken); - - if (usernamePasswordAuthenticationToken.isAuthenticated()) { - SecurityContextHolder.getContext() - .setAuthentication(usernamePasswordAuthenticationToken); - logger.debug(String.format("Auto login %s successfully!", username)); - } - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java index 7378306..fcc9b05 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java @@ -2,6 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import java.util.Set; import javax.persistence.*; +import javax.validation.constraints.NotNull; /** A user of the Smarthut application */ @Entity(name = "smarthutuser") @@ -13,16 +14,16 @@ public class User { private Long id; /** The full name of the user */ - @Column private String name; + @Column @NotNull private String name; /** The full name of the user */ - @Column private String username; + @Column @NotNull private String username; /** A properly salted way to store the password TODO: define the implementation of salt */ - @Column private String password; + @Column @NotNull private String password; /** The user's email TODO: validate email in setters */ - @Column private String email; + @Column @NotNull private String email; /** All rooms in the user's house */ @OneToMany(mappedBy = "user") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserService.java deleted file mode 100644 index 3ae7b93..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserService.java +++ /dev/null @@ -1,22 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.models; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class UserService { - - @Autowired private UserRepository userRepository; - - @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; - - public void save(User user) { - user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); - userRepository.save(user); - } - - public User findByUsername(String username) { - return userRepository.findByUsername(username); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7cadd1e..bbbffc7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,5 @@ spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl spring.jpa.properties.hibernate.format_sql=true -, \ No newline at end of file + +jwt.secret=thiskeymustbeverylongorthethingcomplainssoiamjustgoingtowritehereabunchofgarbageciaomamma \ No newline at end of file