Code refactoring and cleanup.

This commit is contained in:
svlada 2016-08-18 16:37:18 +02:00
parent b2c2098a09
commit 2f8988ad4f
20 changed files with 220 additions and 122 deletions

View File

@ -78,7 +78,6 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

View File

@ -2,10 +2,15 @@ package com.svlada.entity;
/**
* Enumerated {@link User} roles.
*
* @author vladimir.stankovic
*
* Aug 16, 2016
*/
public enum Role {
ADMIN, PREMIUM_MEMBER, MEMBER
ADMIN, PREMIUM_MEMBER, MEMBER;
public String authority() {
return "ROLE_" + this.name();
}
}

View File

@ -1,12 +1,14 @@
package com.svlada.entity;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@ -22,15 +24,17 @@ public class User {
@Column(name="password")
private String password;
@Column(name="role")
@Enumerated(EnumType.STRING)
private Role role;
@OneToMany
@JoinColumn(name="APP_USER_ID", referencedColumnName="ID")
private List<UserRole> roles;
public User(Long id, String username, String password, Role role) {
public User() { }
public User(Long id, String username, String password, List<UserRole> roles) {
this.id = id;
this.username = username;
this.password = password;
this.role = role;
this.roles = roles;
}
public Long getId() {
@ -45,7 +49,7 @@ public class User {
return password;
}
public Role getRole() {
return role;
public List<UserRole> getRoles() {
return roles;
}
}

View File

@ -0,0 +1,52 @@
package com.svlada.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;
/**
* UserRole
*
* @author vladimir.stankovic
*
* Aug 18, 2016
*/
@Entity
@Table(name = "USER_ROLE")
public class UserRole {
@Embeddable
public static class Id implements Serializable {
private static final long serialVersionUID = 1322120000551624359L;
@Column(name = "APP_USER_ID")
protected Long userId;
@Enumerated(EnumType.STRING)
@Column(name = "ROLE")
protected Role role;
public Id() { }
public Id(Long userId, Role role) {
this.userId = userId;
this.role = role;
}
}
@EmbeddedId
Id id = new Id();
@Enumerated(EnumType.STRING)
@Column(name = "ROLE", insertable=false, updatable=false)
protected Role role;
public Role getRole() {
return role;
}
}

View File

@ -1,6 +1,8 @@
package com.svlada.security;
import com.svlada.security.model.UserContext;
import java.util.Optional;
import com.svlada.entity.User;
/**
*
@ -9,6 +11,5 @@ import com.svlada.security.model.UserContext;
* Aug 17, 2016
*/
public interface UserService {
public UserContext getByUsername(String username);
public UserContext getByUsernameAndPassword(String username, String password);
public Optional<User> getByUsername(String username);
}

View File

@ -1,24 +0,0 @@
package com.svlada.security.auth;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.svlada.security.config.WebSecurityConfig;
/**
* Skip processing of Refresh token URL endpoint.
*
* @author vladimir.stankovic
*
* Aug 17, 2016
*/
public class RefreshTokenRequestMatcher implements RequestMatcher {
private AntPathRequestMatcher matcher = new AntPathRequestMatcher(WebSecurityConfig.TOKEN_REFRESH_ENTRY_POINT);
@Override
public boolean matches(HttpServletRequest request) {
return matcher.matches(request) ? false : true;
}
}

View File

@ -1,13 +1,23 @@
package com.svlada.security.auth.ajax;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.svlada.entity.User;
import com.svlada.security.model.UserContext;
import com.svlada.user.service.DatabaseUserService;
@ -19,23 +29,36 @@ import com.svlada.user.service.DatabaseUserService;
*/
@Component
public class AjaxAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordEncoder encoder;
private final DatabaseUserService userService;
@Autowired
public AjaxAuthenticationProvider(final DatabaseUserService userService) {
public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) {
this.userService = userService;
this.encoder = encoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided.");
Assert.notNull(authentication, "No authentication data provided");
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
UserContext userContext = userService.getByUsernameAndPassword(username, password);
User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
if (!encoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
}
if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getRole().authority()))
.collect(Collectors.toList());
UserContext userContext = UserContext.create(user.getUsername(), authorities);
return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities());
}

View File

@ -1,5 +1,7 @@
package com.svlada.security.auth.jwt;
import org.springframework.stereotype.Component;
/**
* BloomFilterTokenVerifier
*
@ -7,6 +9,7 @@ package com.svlada.security.auth.jwt;
*
* Aug 17, 2016
*/
@Component
public class BloomFilterTokenVerifier implements TokenVerifier {
@Override
public boolean verify(String jti) {

View File

@ -1,12 +1,16 @@
package com.svlada.security.auth.jwt;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import com.svlada.security.UserService;
import com.svlada.security.auth.JwtAuthenticationToken;
import com.svlada.security.config.JwtSettings;
import com.svlada.security.model.JwtToken;
@ -25,13 +29,12 @@ import io.jsonwebtoken.Jws;
* Aug 5, 2016
*/
@Component
@SuppressWarnings("unchecked")
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final JwtSettings jwtSettings;
@Autowired
public JwtAuthenticationProvider(UserService userService, JwtSettings jwtSettings) {
this.userService = userService;
public JwtAuthenticationProvider(JwtSettings jwtSettings) {
this.jwtSettings = jwtSettings;
}
@ -42,7 +45,13 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
Jws<Claims> jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey());
String subject = jwsClaims.getBody().getSubject();
UserContext context = userService.getByUsername(subject);
List<String> scopes = jwsClaims.getBody().get("scopes", List.class);
List<GrantedAuthority> authorities = scopes.stream()
.map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors.toList());
UserContext context = UserContext.create(subject, authorities);
return new JwtAuthenticationToken(context, context.getAuthorities());
}

View File

@ -38,8 +38,8 @@ public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticati
public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler,
JwtTokenFactory tokenFactory,
TokenExtractor tokenExtractor,
RequestMatcher requestMatcher) {
super(requestMatcher);
String filterProcessingUrl) {
super(filterProcessingUrl);
this.failureHandler = failureHandler;
this.tokenExtractor = tokenExtractor;
this.tokenFactory = tokenFactory;

View File

@ -16,7 +16,6 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
import com.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.security.RestAuthenticationEntryPoint;
import com.svlada.security.auth.RefreshTokenRequestMatcher;
import com.svlada.security.auth.ajax.AjaxAuthenticationProvider;
import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter;
import com.svlada.security.auth.jwt.JwtAuthenticationProvider;
@ -61,7 +60,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenFactory, tokenExtractor, new RefreshTokenRequestMatcher());
JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenFactory, tokenExtractor, TOKEN_BASED_AUTH_ENTRY_POINT);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@ -95,8 +94,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.and()
.authorizeRequests()
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers("/console").permitAll() // H2 Console Dash-board - only for testing
.and()
.authorizeRequests()
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.and()
.addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

View File

@ -1,6 +1,8 @@
package com.svlada.security.endpoint;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -8,11 +10,16 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.svlada.entity.User;
import com.svlada.security.UserService;
import com.svlada.security.auth.jwt.TokenVerifier;
import com.svlada.security.config.JwtSettings;
@ -20,6 +27,7 @@ import com.svlada.security.config.WebSecurityConfig;
import com.svlada.security.exceptions.InvalidJwtToken;
import com.svlada.security.model.JwtToken;
import com.svlada.security.model.JwtTokenFactory;
import com.svlada.security.model.Scopes;
import com.svlada.security.model.UnsafeJwtToken;
import com.svlada.security.model.UserContext;
@ -33,6 +41,7 @@ import io.jsonwebtoken.Jws;
*
* Aug 17, 2016
*/
@SuppressWarnings("unchecked")
@RestController
public class RefreshTokenEndpoint {
@Autowired private JwtTokenFactory tokenFactory;
@ -43,17 +52,29 @@ public class RefreshTokenEndpoint {
@RequestMapping(value="/api/auth/token", method=RequestMethod.GET, produces={ MediaType.APPLICATION_JSON_VALUE })
public @ResponseBody JwtToken refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
UnsafeJwtToken unsafeToken = this.tokenFactory.createUnsafeToken(request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM));
Jws<Claims> jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey());
String subject = jwsClaims.getBody().getSubject();
String jti = jwsClaims.getBody().getId();
List<String> scopes = jwsClaims.getBody().get("scopes", List.class);
if (scopes == null || scopes.isEmpty()
|| !scopes.stream().filter(scope -> Scopes.REFRESH_TOKEN.authority().equals(scope)).findFirst().isPresent()) {
throw new InvalidJwtToken();
}
String jti = jwsClaims.getBody().getId();
if (!tokenVerifier.verify(jti)) {
throw new InvalidJwtToken();
}
UserContext userContext = userService.getByUsername(subject);
String subject = jwsClaims.getBody().getSubject();
User user = userService.getByUsername(subject).orElseThrow(() -> new UsernameNotFoundException("User not found: " + subject));
if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getRole().authority()))
.collect(Collectors.toList());
UserContext userContext = UserContext.create(user.getUsername(), authorities);
return tokenFactory.createSafeToken(userContext);
}

View File

@ -1,6 +1,8 @@
package com.svlada.security.model;
import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -40,11 +42,16 @@ public class JwtTokenFactory {
if (StringUtils.isBlank(userContext.getUsername())) {
throw new IllegalArgumentException("Cannot create JWT Token without username");
}
DateTime currentTime = new DateTime();
if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) {
throw new IllegalArgumentException("User doesn't have any privileges");
}
Claims claims = Jwts.claims().setSubject(userContext.getUsername());
claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
DateTime currentTime = new DateTime();
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
@ -56,27 +63,6 @@ public class JwtTokenFactory {
return new SafeJwtToken(token, claims);
}
public SafeJwtToken refreshToken(SafeJwtToken safeJwtToken) {
return null;
}
public SafeJwtToken createSafeToken(String token, Claims claims) {
return new SafeJwtToken(token, claims);
}
/**
* Unsafe version of JWT token is created.
*
* <strong>WARNING:</strong> Token signature validation is not performed.
*
* @param tokenPayload
* @return unsafe version of JWT token.
*/
public UnsafeJwtToken createUnsafeToken(String tokenPayload) {
return new UnsafeJwtToken(tokenPayload);
}
public JwtToken createRefreshToken(UserContext userContext) {
if (StringUtils.isBlank(userContext.getUsername())) {
throw new IllegalArgumentException("Cannot create JWT Token without username");
@ -85,6 +71,7 @@ public class JwtTokenFactory {
DateTime currentTime = new DateTime();
Claims claims = Jwts.claims().setSubject(userContext.getUsername());
claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN));
String token = Jwts.builder()
.setClaims(claims)
@ -97,4 +84,16 @@ public class JwtTokenFactory {
return new SafeJwtToken(token, claims);
}
/**
* Unsafe version of JWT token is created.
*
* <strong>WARNING:</strong> Token signature validation is not performed.
*
* @param tokenPayload
* @return unsafe version of JWT token.
*/
public UnsafeJwtToken createUnsafeToken(String tokenPayload) {
return new UnsafeJwtToken(tokenPayload);
}
}

View File

@ -0,0 +1,16 @@
package com.svlada.security.model;
/**
* Scopes
*
* @author vladimir.stankovic
*
* Aug 18, 2016
*/
public enum Scopes {
REFRESH_TOKEN;
public String authority() {
return "ROLE_" + this.name();
}
}

View File

@ -1,8 +1,11 @@
package com.svlada.security.model;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
*
@ -14,10 +17,15 @@ public class UserContext {
private final String username;
private final List<GrantedAuthority> authorities;
public UserContext(String username, List<GrantedAuthority> authorities) {
private UserContext(String username, List<GrantedAuthority> authorities) {
this.username = username;
this.authorities = authorities;
}
public static UserContext create(String username, List<GrantedAuthority> authorities) {
if (StringUtils.isBlank(username)) throw new IllegalArgumentException("Username is blank: " + username);
return new UserContext(username, authorities);
}
public String getUsername() {
return username;

View File

@ -1,16 +0,0 @@
package com.svlada.security.model;
/**
* Enumeration of user Roles.
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public enum UserRole {
ADMIN, INSTRUCTOR, PARTICIPANT, SUPERADMIN;
public String authority() {
return "ROLE_" + this.name();
}
}

View File

@ -1,6 +1,10 @@
package com.svlada.user.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.svlada.entity.User;
@ -12,5 +16,6 @@ import com.svlada.entity.User;
* Aug 16, 2016
*/
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u left join fetch u.roles r where u.username=:username")
public Optional<User> findByUsername(@Param("username") String username);
}

View File

@ -1,16 +1,12 @@
package com.svlada.user.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import com.svlada.entity.User;
import com.svlada.security.UserService;
import com.svlada.security.model.UserContext;
import com.svlada.security.model.UserRole;
import com.svlada.user.repository.UserRepository;
/**
@ -29,21 +25,12 @@ public class DatabaseUserService implements UserService {
this.userRepository = userRepository;
}
@Override
public UserContext getByUsernameAndPassword(String username, String password) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.authority()));
return new UserContext(username, authorities);
}
@Override
public UserContext getByUsername(String username) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.authority()));
return new UserContext(username, authorities);
}
public UserRepository getUserRepository() {
return userRepository;
}
@Override
public Optional<User> getByUsername(String username) {
return this.userRepository.findByUsername(username);
}
}

View File

@ -1,7 +1,8 @@
server.port: 9966
spring.profiles: default
demo.security.jwt:
tokenExpirationTime: 2 # Number of minutes
tokenExpirationTime: 15 # Number of minutes
refreshTokenExpTime: 60 # Minutes
tokenIssuer: http://svlada.com
tokenSigningKey: xm8EV6Hy5RMFK4EEACIDAwQus

View File

@ -1 +1,3 @@
insert into APP_USER(ID, PASSWORD, ROLE, USERNAME) values(1, 'test', 'ADMIN', 'svlada@gmail.com');
insert into APP_USER(ID, PASSWORD, USERNAME) values(1, '$2a$10$bnC26zz//2cavYoSCrlHdecWF8tkGfPodlHcYwlACBBwJvcEf0p2G', 'svlada@gmail.com');
insert into USER_ROLE(APP_USER_ID, ROLE) values(1, 'ADMIN');
insert into USER_ROLE(APP_USER_ID, ROLE) values(1, 'PREMIUM_MEMBER');