Merge branch 'login-feature' into 'dev'

Login feature

See merge request sa4-2020/the-sanmarinoes/backend!5
This commit is contained in:
Claudio Maggioni 2020-02-26 14:43:39 +01:00
commit df3f4de271
16 changed files with 408 additions and 26 deletions

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="12" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="12.0.1" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View file

@ -6,7 +6,7 @@ plugins {
group = 'ch.usi.inf.sa4.sanmarinoes.' group = 'ch.usi.inf.sa4.sanmarinoes.'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11' sourceCompatibility = "11"
repositories { repositories {
mavenCentral() mavenCentral()
@ -16,7 +16,11 @@ dependencies {
compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' 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'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 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.postgresql:postgresql'
implementation('org.springframework.boot:spring-boot-starter-web') { implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json' exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json'
} }

View file

@ -13,6 +13,9 @@ if [ "$NO_VERIFY" ]; then
fi fi
# list all added/copied/modified/renamed java files # 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 # 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

View file

@ -2,8 +2,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication @SpringBootApplication
@EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models")
public class SmarthutApplication { public class SmarthutApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SmarthutApplication.class, args); SpringApplication.run(SmarthutApplication.class, args);

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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> T getClaimFromToken(String token, Function<Claims, T> 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<String, Object> 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<String, Object> 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));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -29,15 +29,15 @@ public abstract class Device {
/** /**
* The name for the category of this particular device (e.g 'dimmer'). Not stored in the * 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 * The way this device behaves in the automation flow. Not stored in the database but set thanks
* 'transient') but set thanks to constructors * to constructors
*/ */
private final transient FlowType flowType; @Transient private final FlowType flowType;
public long getId() { public long getId() {
return id; return id;
@ -66,6 +66,5 @@ public abstract class Device {
public Device(String kind, FlowType flowType) { public Device(String kind, FlowType flowType) {
this.kind = kind; this.kind = kind;
this.flowType = flowType; this.flowType = flowType;
this.name = kind + " " + this.id;
} }
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -2,6 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set; import java.util.Set;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotNull;
/** A user of the Smarthut application */ /** A user of the Smarthut application */
@Entity(name = "smarthutuser") @Entity(name = "smarthutuser")
@ -13,13 +14,16 @@ public class User {
private Long id; private Long id;
/** The full name of the user */ /** 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 */ /** 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 */ /** 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 */ /** All rooms in the user's house */
@OneToMany(mappedBy = "user") @OneToMany(mappedBy = "user")
@ -37,6 +41,14 @@ public class User {
return name; return name;
} }
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
@ -49,12 +61,12 @@ public class User {
this.email = email; this.email = email;
} }
public String getHashedPassword() { public String getPassword() {
return hashedPassword; return password;
} }
public void setHashedPassword(String hashedPassword) { public void setPassword(String password) {
this.hashedPassword = hashedPassword; this.password = password;
} }
public Set<Room> getRooms() { public Set<Room> getRooms() {
@ -69,14 +81,11 @@ public class User {
+ ", name='" + ", name='"
+ name + name
+ '\'' + '\''
+ ", hashedPassword='" + ", password='"
+ hashedPassword + password
+ '\'' + '\''
+ ", email='" + ", email='"
+ email + email
+ '\'' + "\'}";
+ ", rooms="
+ rooms
+ '}';
} }
} }

View file

@ -2,4 +2,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {} public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}

View file

@ -6,7 +6,8 @@ spring.datasource.password=
# Hibernate properties # Hibernate properties
spring.jpa.database=POSTGRESQL spring.jpa.database=POSTGRESQL
spring.jpa.show-sql=true 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.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.format_sql=true
,
jwt.secret=thiskeymustbeverylongorthethingcomplainssoiamjustgoingtowritehereabunchofgarbageciaomamma