Add login endpoint
parent
5c3a592a55
commit
18684754ac
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
Reference in New Issue