diff --git a/build.gradle b/build.gradle index c65502c..ea7614f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { group = 'ch.usi.inf.sa4.sanmarinoes.' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '11' +sourceCompatibility = "11" repositories { mavenCentral() @@ -16,7 +16,11 @@ 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' + implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json' } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java index f215fe7..242f03f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java @@ -2,8 +2,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication +@EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models") public class SmarthutApplication { public static void main(String[] args) { SpringApplication.run(SmarthutApplication.class, args); 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/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java index 078a7ad..d6108e8 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java @@ -29,15 +29,15 @@ public abstract class Device { /** * The name for the category of this particular device (e.g 'dimmer'). Not stored in the - * database (thanks to 'transient') but set thanks to constructors + * database but set thanks to constructors */ - private final transient String kind; + @Transient private final String kind; /** - * The way this device behaves in the automation flow. Not stored in the database (thanks to - * 'transient') but set thanks to constructors + * The way this device behaves in the automation flow. Not stored in the database but set thanks + * to constructors */ - private final transient FlowType flowType; + @Transient private final FlowType flowType; public long getId() { return id; @@ -66,6 +66,5 @@ public abstract class Device { public Device(String kind, FlowType flowType) { this.kind = kind; this.flowType = flowType; - this.name = kind + " " + this.id; } } 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/JWTUserDetailsService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java new file mode 100644 index 0000000..b20832b --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java @@ -0,0 +1,25 @@ +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.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class JWTUserDetailsService implements UserDetailsService { + @Autowired private UserRepository repository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User toReturn = repository.findByUsername(username); + if (toReturn != null) { + return new org.springframework.security.core.userdetails.User( + toReturn.getUsername(), toReturn.getPassword(), Set.of()); + } else { + throw new UsernameNotFoundException(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 92202e2..7f9ec95 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,13 +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 @NotNull private String username; /** A properly salted way to store the password TODO: define the implementation of salt */ - @Column private String hashedPassword; + @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") @@ -37,6 +41,14 @@ public class User { return name; } + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + public void setName(String name) { this.name = name; } @@ -49,12 +61,12 @@ public class User { this.email = email; } - public String getHashedPassword() { - return hashedPassword; + public String getPassword() { + return password; } - public void setHashedPassword(String hashedPassword) { - this.hashedPassword = hashedPassword; + public void setPassword(String password) { + this.password = password; } public Set getRooms() { @@ -69,14 +81,11 @@ public class User { + ", name='" + name + '\'' - + ", hashedPassword='" - + hashedPassword + + ", password='" + + password + '\'' + ", email='" + email - + '\'' - + ", rooms=" - + rooms - + '}'; + + "\'}"; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java index 6c735ae..0b8c62a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java @@ -3,4 +3,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import java.util.*; import org.springframework.data.repository.CrudRepository; -public interface UserRepository extends CrudRepository {} +public interface UserRepository extends CrudRepository { + User findByUsername(String username); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7cadd1e..30d24cd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,7 +6,8 @@ spring.datasource.password= # Hibernate properties spring.jpa.database=POSTGRESQL spring.jpa.show-sql=true -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update 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