diff --git a/common/src/main/java/ba/steleks/error/RestErrorHandler.java b/common/src/main/java/ba/steleks/error/RestErrorHandler.java index c9ad181..961655b 100644 --- a/common/src/main/java/ba/steleks/error/RestErrorHandler.java +++ b/common/src/main/java/ba/steleks/error/RestErrorHandler.java @@ -1,5 +1,6 @@ package ba.steleks.error; +import ba.steleks.error.exception.CustomHttpStatusException; import ba.steleks.error.exception.ExternalServiceException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -44,6 +45,17 @@ public class RestErrorHandler { } + @ExceptionHandler(CustomHttpStatusException.class) + @ResponseBody + public ResponseEntity> handleAllCustomExceptions(CustomHttpStatusException ex) { + Map map= new HashMap(); + map.put("status", ex.getStatusCode().toString()); + map.put("error", ex.getMessage()); + + return ResponseEntity.status(ex.getStatusCode()).body(map); + + } + @ExceptionHandler(HttpStatusCodeException.class) @ResponseBody public ResponseEntity> handleAllExceptions(HttpStatusCodeException ex) { diff --git a/common/src/main/java/ba/steleks/error/exception/CustomHttpStatusException.java b/common/src/main/java/ba/steleks/error/exception/CustomHttpStatusException.java new file mode 100644 index 0000000..73c9619 --- /dev/null +++ b/common/src/main/java/ba/steleks/error/exception/CustomHttpStatusException.java @@ -0,0 +1,40 @@ +package ba.steleks.error.exception;/** + * Created by ensar on 30/05/17. + */ + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpStatusCodeException; + +import java.nio.charset.Charset; +import java.util.logging.Logger; + +public class CustomHttpStatusException extends HttpStatusCodeException { + + private String message; + + public CustomHttpStatusException(HttpStatus statusCode, String message) { + super(statusCode); + this.message = message; + } + + public CustomHttpStatusException(HttpStatus statusCode, String statusText, String message) { + super(statusCode, statusText); + this.message = message; + } + + public CustomHttpStatusException(HttpStatus statusCode, String statusText, byte[] responseBody, Charset responseCharset, String message) { + super(statusCode, statusText, responseBody, responseCharset); + this.message = message; + } + + public CustomHttpStatusException(HttpStatus statusCode, String statusText, HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset, String message) { + super(statusCode, statusText, responseHeaders, responseBody, responseCharset); + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/common/src/main/java/ba/steleks/storage/store/HashMapKeyValueStore.java b/common/src/main/java/ba/steleks/storage/store/HashMapKeyValueStore.java new file mode 100644 index 0000000..cbf0049 --- /dev/null +++ b/common/src/main/java/ba/steleks/storage/store/HashMapKeyValueStore.java @@ -0,0 +1,44 @@ +package ba.steleks.storage.store; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by ensar on 30/05/17. + */ + +@Component +public class HashMapKeyValueStore implements KeyValueStore { + + private final Map kvMap; + + public HashMapKeyValueStore() { + kvMap = new HashMap<>(); + } + + public HashMapKeyValueStore(Map kvMap) { + this.kvMap = kvMap; + } + + @Override + public void save(K key, V value) { + kvMap.put(key, value); + } + + @Override + public V get(K key) { + return kvMap.get(key); + } + + @Override + public boolean contains(K key) { + return kvMap.containsKey(key); + } + + @Override + public void remove(K key) { + kvMap.remove(key); + } +} diff --git a/common/src/main/java/ba/steleks/storage/store/KeyValueStore.java b/common/src/main/java/ba/steleks/storage/store/KeyValueStore.java new file mode 100644 index 0000000..123dfc7 --- /dev/null +++ b/common/src/main/java/ba/steleks/storage/store/KeyValueStore.java @@ -0,0 +1,11 @@ +package ba.steleks.storage.store; + +/** + * Created by ensar on 30/05/17. + */ +public interface KeyValueStore { + void save(K key, V value); + V get(K key); + boolean contains(K key); + void remove(K key); +} diff --git a/common/src/main/java/ba/steleks/util/CalendarUtils.java b/common/src/main/java/ba/steleks/util/CalendarUtils.java new file mode 100644 index 0000000..b414cbe --- /dev/null +++ b/common/src/main/java/ba/steleks/util/CalendarUtils.java @@ -0,0 +1,14 @@ +package ba.steleks.util; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Created by ensar on 30/05/17. + */ + +public class CalendarUtils { + public static Calendar getUTCCalendar() { + return Calendar.getInstance(TimeZone.getTimeZone("UTC")); + } +} diff --git a/users/src/main/java/ba/steleks/controller/AuthenticationController.java b/users/src/main/java/ba/steleks/controller/AuthenticationController.java index fdfd350..4b4d94d 100644 --- a/users/src/main/java/ba/steleks/controller/AuthenticationController.java +++ b/users/src/main/java/ba/steleks/controller/AuthenticationController.java @@ -1,14 +1,23 @@ package ba.steleks.controller; +import ba.steleks.error.exception.CustomHttpStatusException; import ba.steleks.model.AuthRequest; +import ba.steleks.model.User; import ba.steleks.repository.UsersJpaRepository; +import ba.steleks.security.SessionIdentifierGenerator; +import ba.steleks.security.token.TokenStore; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; 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 java.util.HashMap; +import java.util.Map; + /** * Created by admin on 13/05/2017. */ @@ -17,16 +26,40 @@ public class AuthenticationController { private UsersJpaRepository usersJpaRepository; private PasswordEncoder passwordEncoder; + private TokenStore tokenStore; @Autowired - public AuthenticationController(UsersJpaRepository usersJpaRepository, PasswordEncoder passwordEncoder) { - this.passwordEncoder = passwordEncoder; + public AuthenticationController(UsersJpaRepository usersJpaRepository, PasswordEncoder passwordEncoder, TokenStore tokenStore) { this.usersJpaRepository = usersJpaRepository; + this.passwordEncoder = passwordEncoder; + this.tokenStore = tokenStore; } @RequestMapping(path = "/accesstoken", method = RequestMethod.POST) - public String generateToken(@RequestBody AuthRequest body){ - return passwordEncoder.matches(body.getPassword(), usersJpaRepository.findByUsername(body.getUsername()).getPasswordHash()) ? "true" : "false"; - } + public ResponseEntity generateToken(@RequestBody AuthRequest body) { + if(body.getUsername() == null || body.getPassword() == null) { + throw new CustomHttpStatusException(HttpStatus.BAD_REQUEST, + "'username' and 'password' fields are mandatory!"); + } + User user = usersJpaRepository.findByUsername(body.getUsername()); + if(user == null) { + throw new CustomHttpStatusException(HttpStatus.NOT_FOUND, + "User with username " + body.getUsername() + " not found!"); + } + + if (passwordEncoder.matches(body.getPassword(), user.getPasswordHash())) { + String token = new SessionIdentifierGenerator().nextSessionId(); + tokenStore.saveToken(user.getId(), token); + Map response = new HashMap<>(); + response.put("token", token); + response.put("userId", String.valueOf(user.getId())); + return ResponseEntity + .ok() + .body(response); + } else { + throw new CustomHttpStatusException(HttpStatus.UNAUTHORIZED, + "Invalid password!"); + } + } } diff --git a/users/src/main/java/ba/steleks/model/User.java b/users/src/main/java/ba/steleks/model/User.java index d01db13..e008066 100644 --- a/users/src/main/java/ba/steleks/model/User.java +++ b/users/src/main/java/ba/steleks/model/User.java @@ -2,6 +2,7 @@ package ba.steleks.model;/** * Created by ensar on 22/03/17. */ +import ba.steleks.security.UserPasswordEntityListener; import com.fasterxml.jackson.annotation.JsonProperty; import javax.persistence.*; @@ -12,9 +13,8 @@ import java.util.Set; import java.util.logging.Logger; @Entity +@EntityListeners(UserPasswordEntityListener.class) public class User { - private static final Logger logger = - Logger.getLogger(User.class.getName()); @Id @GeneratedValue(strategy = GenerationType.AUTO) @@ -35,6 +35,10 @@ public class User { @NotNull @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String passwordHash; + + @Transient + private String password; + @NotNull private String username; @@ -48,7 +52,7 @@ public class User { @JoinColumn private Set membershipTypes; - @ManyToMany + @ManyToMany(fetch = FetchType.EAGER) @JoinColumn private Set userRoles; @@ -156,6 +160,14 @@ public class User { this.userRoles = userRoles; } + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + @PrePersist protected void onCreate() { this.registrationDate = new Timestamp(new Date().getTime()); diff --git a/users/src/main/java/ba/steleks/security/CustomUrlUsernamePasswordAuthenticationFilter.java b/users/src/main/java/ba/steleks/security/CustomUrlUsernamePasswordAuthenticationFilter.java new file mode 100644 index 0000000..e037d7d --- /dev/null +++ b/users/src/main/java/ba/steleks/security/CustomUrlUsernamePasswordAuthenticationFilter.java @@ -0,0 +1,16 @@ +package ba.steleks.security;/** + * Created by ensar on 30/05/17. + */ + +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +public class CustomUrlUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + public CustomUrlUsernamePasswordAuthenticationFilter() { + super(); + setRequiresAuthenticationRequestMatcher( + new AntPathRequestMatcher("/accesstoken", "POST") + ); + } +} diff --git a/users/src/main/java/ba/steleks/security/JWTLoginFilter.java b/users/src/main/java/ba/steleks/security/JWTLoginFilter.java index 72b46fe..f1981fd 100644 --- a/users/src/main/java/ba/steleks/security/JWTLoginFilter.java +++ b/users/src/main/java/ba/steleks/security/JWTLoginFilter.java @@ -14,6 +14,7 @@ import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; import java.io.IOException; import java.util.Collections; import java.util.Set; @@ -38,5 +39,6 @@ public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { Set userRoles = UserRoleFactory.fromGrantedAuthorities(authResult.getAuthorities()); TokenAuthenticationService.addAuthenticationHeader(response, authResult.getName(), userRoles); + } } diff --git a/users/src/main/java/ba/steleks/security/SecurityConfig.java b/users/src/main/java/ba/steleks/security/SecurityConfig.java index fdd00b3..282f1bf 100644 --- a/users/src/main/java/ba/steleks/security/SecurityConfig.java +++ b/users/src/main/java/ba/steleks/security/SecurityConfig.java @@ -11,7 +11,6 @@ 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.core.userdetails.UserDetailsService; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @@ -35,10 +34,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().authorizeRequests() + .antMatchers("/accesstoken").permitAll() .anyRequest().authenticated() .and() - .addFilterBefore(new JWTLoginFilter("/accesstoken", authenticationManager()), - UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); +// .addFilterBefore(new JWTLoginFilter("/accesstoken", authenticationManager()), +// CustomUrlUsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JWTAuthenticationFilter(), CustomUrlUsernamePasswordAuthenticationFilter.class); } + } \ No newline at end of file diff --git a/users/src/main/java/ba/steleks/security/SessionIdentifierGenerator.java b/users/src/main/java/ba/steleks/security/SessionIdentifierGenerator.java new file mode 100644 index 0000000..c3bb91c --- /dev/null +++ b/users/src/main/java/ba/steleks/security/SessionIdentifierGenerator.java @@ -0,0 +1,17 @@ +package ba.steleks.security; + +import java.math.BigInteger; +import java.security.SecureRandom; + +/** + * Created by ensar on 30/05/17. + */ + + +public final class SessionIdentifierGenerator { + private SecureRandom random = new SecureRandom(); + + public String nextSessionId() { + return new BigInteger(130, random).toString(32); + } +} \ No newline at end of file diff --git a/users/src/main/java/ba/steleks/security/SteleksUsersDetailsService.java b/users/src/main/java/ba/steleks/security/SteleksUsersDetailsService.java index c2629e3..95f2c40 100644 --- a/users/src/main/java/ba/steleks/security/SteleksUsersDetailsService.java +++ b/users/src/main/java/ba/steleks/security/SteleksUsersDetailsService.java @@ -3,20 +3,14 @@ package ba.steleks.security;/** */ import ba.steleks.model.User; -import ba.steleks.model.UserRole; import ba.steleks.repository.UsersJpaRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; -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 java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; public class SteleksUsersDetailsService implements UserDetailsService { diff --git a/users/src/main/java/ba/steleks/security/TokenAuthenticationService.java b/users/src/main/java/ba/steleks/security/TokenAuthenticationService.java index 5187e40..5e09332 100644 --- a/users/src/main/java/ba/steleks/security/TokenAuthenticationService.java +++ b/users/src/main/java/ba/steleks/security/TokenAuthenticationService.java @@ -10,15 +10,14 @@ import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.sql.Date; -import java.util.Arrays; -import java.util.List; import java.util.Set; /** * Created by ensar on 28/05/17. */ -class TokenAuthenticationService { +public class TokenAuthenticationService { + static final long EXPIRATION_TIME = 864_000_000; // 10 days static final String SECRET = "ASteleksSecret"; @@ -26,16 +25,17 @@ class TokenAuthenticationService { static final String HEADER_STRING = "Authorization"; static final String ROLES = "roles"; - static void addAuthenticationHeader(HttpServletResponse res, String username, Set userRoleSet) { + public static void addAuthenticationHeader(HttpServletResponse res, String username, Set userRoleSet) { String JWT = Jwts.builder() .setSubject(username) .claim(ROLES, UserRoleFactory.toString(userRoleSet)) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET).compact(); + res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT); } - static Authentication getAuthentication(HttpServletRequest request) { + public static Authentication getAuthentication(HttpServletRequest request) { String token = request.getHeader(HEADER_STRING); if (token != null) { diff --git a/users/src/main/java/ba/steleks/security/UserPasswordEntityListener.java b/users/src/main/java/ba/steleks/security/UserPasswordEntityListener.java new file mode 100644 index 0000000..8a059a8 --- /dev/null +++ b/users/src/main/java/ba/steleks/security/UserPasswordEntityListener.java @@ -0,0 +1,27 @@ +package ba.steleks.security; + +import ba.steleks.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.persistence.PrePersist; +import javax.persistence.PreUpdate; + +/** + * Created by ensar on 30/05/17. + */ + +public class UserPasswordEntityListener { + + @Autowired + private PasswordEncoder passwordEncoder; + + @PrePersist + @PreUpdate + public void onUserUpdate(User user) { + if(user.getPassword() != null) { + user.setPasswordHash(passwordEncoder.encode(user.getPassword())); + } + } + +} diff --git a/users/src/main/java/ba/steleks/security/token/BasicTokenStore.java b/users/src/main/java/ba/steleks/security/token/BasicTokenStore.java new file mode 100644 index 0000000..5608b19 --- /dev/null +++ b/users/src/main/java/ba/steleks/security/token/BasicTokenStore.java @@ -0,0 +1,70 @@ +package ba.steleks.security.token; + +import ba.steleks.storage.store.KeyValueStore; +import ba.steleks.util.CalendarUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * Created by ensar on 30/05/17. + */ + +@Component +public class BasicTokenStore implements TokenStore { + + // Default one hour ttl + public static final long DEFAULT_TTL = + TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); + + private KeyValueStore tokenStore; + private long ttl = DEFAULT_TTL; + + @Autowired + public BasicTokenStore(KeyValueStore tokenStore) { + this.tokenStore = tokenStore; + } + + @Override + public boolean isValidToken(Long id, String token) { + if (tokenStore.contains(id)) { + // Find token in store + BasicToken basicToken = tokenStore.get(id); + + // Token is invalid, there is different token saved in store + if (!basicToken.token.equals(token)) { + return false; + } + + // Token is invalid, it has expired + if(basicToken.saveTime + ttl < CalendarUtils.getUTCCalendar().getTimeInMillis()) { + return false; + } + + // Token valid! + return true; + } else { + // No id in store, there is no token + return false; + } + } + + @Override + public void saveToken(Long id, String token) { + BasicToken basicToken = new BasicToken(); + basicToken.token = token; + basicToken.saveTime = CalendarUtils.getUTCCalendar().getTimeInMillis(); + tokenStore.save(id, basicToken); + } + + @Override + public void removeToken(Long id, String token) { + tokenStore.remove(id); + } + + private static class BasicToken { + String token; + Long saveTime; + } +} diff --git a/users/src/main/java/ba/steleks/security/token/TokenStore.java b/users/src/main/java/ba/steleks/security/token/TokenStore.java new file mode 100644 index 0000000..d6e5702 --- /dev/null +++ b/users/src/main/java/ba/steleks/security/token/TokenStore.java @@ -0,0 +1,14 @@ +package ba.steleks.security.token; + +/** + * Created by ensar on 30/05/17. + */ +public interface TokenStore { + + boolean isValidToken(Long id, String token); + + void saveToken(Long id, String token); + + void removeToken(Long id, String token); + +}