Add login endpoint

master
esensar 2017-05-30 21:06:30 +02:00
parent 5c3a592a55
commit 18684754ac
16 changed files with 330 additions and 23 deletions

View File

@ -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<Map<String, String>> handleAllCustomExceptions(CustomHttpStatusException ex) {
Map<String, String> map= new HashMap<String, String>();
map.put("status", ex.getStatusCode().toString());
map.put("error", ex.getMessage());
return ResponseEntity.status(ex.getStatusCode()).body(map);
}
@ExceptionHandler(HttpStatusCodeException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleAllExceptions(HttpStatusCodeException ex) {

View File

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

View File

@ -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<K, V> implements KeyValueStore<K, V> {
private final Map<K, V> kvMap;
public HashMapKeyValueStore() {
kvMap = new HashMap<>();
}
public HashMapKeyValueStore(Map<K, V> 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);
}
}

View File

@ -0,0 +1,11 @@
package ba.steleks.storage.store;
/**
* Created by ensar on 30/05/17.
*/
public interface KeyValueStore<K, V> {
void save(K key, V value);
V get(K key);
boolean contains(K key);
void remove(K key);
}

View File

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

View File

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

View File

@ -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<MembershipType> membershipTypes;
@ManyToMany
@ManyToMany(fetch = FetchType.EAGER)
@JoinColumn
private Set<UserRole> 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());

View File

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

View File

@ -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<UserRole> userRoles = UserRoleFactory.fromGrantedAuthorities(authResult.getAuthorities());
TokenAuthenticationService.addAuthenticationHeader(response, authResult.getName(), userRoles);
}
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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<UserRole> userRoleSet) {
public static void addAuthenticationHeader(HttpServletResponse res, String username, Set<UserRole> 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) {

View File

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

View File

@ -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<Long, BasicToken> tokenStore;
private long ttl = DEFAULT_TTL;
@Autowired
public BasicTokenStore(KeyValueStore<Long, BasicToken> 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;
}
}

View File

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