Code refactor.

This commit is contained in:
svlada 2016-08-19 14:18:25 +02:00
parent 2f8988ad4f
commit a3e9b93382
16 changed files with 122 additions and 82 deletions

View File

@ -482,6 +482,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
### [](http://stackoverflow.com/questions/38557379/secure-and-stateless-jwt-implementation) ### [](http://stackoverflow.com/questions/38557379/secure-and-stateless-jwt-implementation)
http://by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
http://nordicapis.com/how-to-control-user-identity-within-microservices/
http://stackoverflow.com/questions/3487991/why-does-oauth-v2-have-both-access-and-refresh-tokens/12885823 http://stackoverflow.com/questions/3487991/why-does-oauth-v2-have-both-access-and-refresh-tokens/12885823
https://tools.ietf.org/html/rfc6749#section-1.4 https://tools.ietf.org/html/rfc6749#section-1.4
@ -499,4 +503,6 @@ true statelessness and revocation are mutually exclusive
https://www.sslvpn.online/are-breaches-of-jwt-based-servers-more-damaging/ https://www.sslvpn.online/are-breaches-of-jwt-based-servers-more-damaging/
http://nordicapis.com/how-to-control-user-identity-within-microservices/ http://nordicapis.com/how-to-control-user-identity-within-microservices/
https://tools.ietf.org/html/rfc6749

View File

@ -5,8 +5,8 @@ import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import com.svlada.security.model.UnsafeJwtToken;
import com.svlada.security.model.UserContext; import com.svlada.security.model.UserContext;
import com.svlada.security.model.token.RawAccessJwtToken;
/** /**
* An {@link org.springframework.security.core.Authentication} implementation * An {@link org.springframework.security.core.Authentication} implementation
@ -19,18 +19,18 @@ import com.svlada.security.model.UserContext;
public class JwtAuthenticationToken extends AbstractAuthenticationToken { public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 2877954820905567501L; private static final long serialVersionUID = 2877954820905567501L;
private UnsafeJwtToken unsafeToken; private RawAccessJwtToken rawAccessToken;
private UserContext userContext; private UserContext userContext;
public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) { public JwtAuthenticationToken(RawAccessJwtToken unsafeToken) {
super(null); super(null);
this.unsafeToken = unsafeToken; this.rawAccessToken = unsafeToken;
this.setAuthenticated(false); this.setAuthenticated(false);
} }
public JwtAuthenticationToken(UserContext userContext, Collection<? extends GrantedAuthority> authorities) { public JwtAuthenticationToken(UserContext userContext, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.unsafeToken = null; this.eraseCredentials();
this.userContext = userContext; this.userContext = userContext;
super.setAuthenticated(true); super.setAuthenticated(true);
} }
@ -46,7 +46,7 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public Object getCredentials() { public Object getCredentials() {
return unsafeToken; return rawAccessToken;
} }
@Override @Override
@ -57,6 +57,6 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public void eraseCredentials() { public void eraseCredentials() {
super.eraseCredentials(); super.eraseCredentials();
this.unsafeToken = null; this.rawAccessToken = null;
} }
} }

View File

@ -16,9 +16,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.security.model.JwtToken;
import com.svlada.security.model.JwtTokenFactory;
import com.svlada.security.model.UserContext; import com.svlada.security.model.UserContext;
import com.svlada.security.model.token.JwtToken;
import com.svlada.security.model.token.JwtTokenFactory;
/** /**
* AjaxAwareAuthenticationSuccessHandler * AjaxAwareAuthenticationSuccessHandler
@ -43,7 +43,7 @@ public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSucc
Authentication authentication) throws IOException, ServletException { Authentication authentication) throws IOException, ServletException {
UserContext userContext = (UserContext) authentication.getPrincipal(); UserContext userContext = (UserContext) authentication.getPrincipal();
JwtToken token = tokenFactory.createSafeToken(userContext); JwtToken token = tokenFactory.createAccessJwtToken(userContext);
JwtToken refreshToken = tokenFactory.createRefreshToken(userContext); JwtToken refreshToken = tokenFactory.createRefreshToken(userContext);
response.setStatus(HttpStatus.OK.value()); response.setStatus(HttpStatus.OK.value());

View File

@ -13,9 +13,9 @@ import org.springframework.stereotype.Component;
import com.svlada.security.auth.JwtAuthenticationToken; import com.svlada.security.auth.JwtAuthenticationToken;
import com.svlada.security.config.JwtSettings; import com.svlada.security.config.JwtSettings;
import com.svlada.security.model.JwtToken;
import com.svlada.security.model.UnsafeJwtToken;
import com.svlada.security.model.UserContext; import com.svlada.security.model.UserContext;
import com.svlada.security.model.token.JwtToken;
import com.svlada.security.model.token.RawAccessJwtToken;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jws;
@ -40,13 +40,11 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UnsafeJwtToken unsafeToken = (UnsafeJwtToken) authentication.getCredentials(); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
Jws<Claims> jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey()); Jws<Claims> jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey());
String subject = jwsClaims.getBody().getSubject(); String subject = jwsClaims.getBody().getSubject();
List<String> scopes = jwsClaims.getBody().get("scopes", List.class); List<String> scopes = jwsClaims.getBody().get("scopes", List.class);
List<GrantedAuthority> authorities = scopes.stream() List<GrantedAuthority> authorities = scopes.stream()
.map(authority -> new SimpleGrantedAuthority(authority)) .map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@ -14,13 +14,11 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.svlada.security.auth.JwtAuthenticationToken; import com.svlada.security.auth.JwtAuthenticationToken;
import com.svlada.security.auth.jwt.extractor.TokenExtractor; import com.svlada.security.auth.jwt.extractor.TokenExtractor;
import com.svlada.security.config.WebSecurityConfig; import com.svlada.security.config.WebSecurityConfig;
import com.svlada.security.model.JwtTokenFactory; import com.svlada.security.model.token.RawAccessJwtToken;
import com.svlada.security.model.UnsafeJwtToken;
/** /**
* Performs validation of provided JWT Token. * Performs validation of provided JWT Token.
@ -32,24 +30,21 @@ import com.svlada.security.model.UnsafeJwtToken;
public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final AuthenticationFailureHandler failureHandler; private final AuthenticationFailureHandler failureHandler;
private final TokenExtractor tokenExtractor; private final TokenExtractor tokenExtractor;
private final JwtTokenFactory tokenFactory;
@Autowired @Autowired
public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler,
JwtTokenFactory tokenFactory,
TokenExtractor tokenExtractor, TokenExtractor tokenExtractor,
String filterProcessingUrl) { String filterProcessingUrl) {
super(filterProcessingUrl); super(filterProcessingUrl);
this.failureHandler = failureHandler; this.failureHandler = failureHandler;
this.tokenExtractor = tokenExtractor; this.tokenExtractor = tokenExtractor;
this.tokenFactory = tokenFactory;
} }
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException { throws AuthenticationException, IOException, ServletException {
String tokenPayload = request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM); String tokenPayload = request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM);
UnsafeJwtToken token = tokenFactory.createUnsafeToken(tokenExtractor.extract(tokenPayload)); RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload));
return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token)); return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
} }

View File

@ -1,4 +1,4 @@
package com.svlada.security.auth.jwt; package com.svlada.security.auth.jwt.verifier;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@ -1,4 +1,4 @@
package com.svlada.security.auth.jwt; package com.svlada.security.auth.jwt.verifier;
/** /**
* *

View File

@ -21,7 +21,7 @@ import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter;
import com.svlada.security.auth.jwt.JwtAuthenticationProvider; import com.svlada.security.auth.jwt.JwtAuthenticationProvider;
import com.svlada.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import com.svlada.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import com.svlada.security.auth.jwt.extractor.TokenExtractor; import com.svlada.security.auth.jwt.extractor.TokenExtractor;
import com.svlada.security.model.JwtTokenFactory; import com.svlada.security.model.token.JwtTokenFactory;
/** /**
* WebSecurityConfig * WebSecurityConfig

View File

@ -21,18 +21,15 @@ import org.springframework.web.bind.annotation.RestController;
import com.svlada.entity.User; import com.svlada.entity.User;
import com.svlada.security.UserService; import com.svlada.security.UserService;
import com.svlada.security.auth.jwt.TokenVerifier; import com.svlada.security.auth.jwt.verifier.TokenVerifier;
import com.svlada.security.config.JwtSettings; import com.svlada.security.config.JwtSettings;
import com.svlada.security.config.WebSecurityConfig; import com.svlada.security.config.WebSecurityConfig;
import com.svlada.security.exceptions.InvalidJwtToken; 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; import com.svlada.security.model.UserContext;
import com.svlada.security.model.token.JwtToken;
import io.jsonwebtoken.Claims; import com.svlada.security.model.token.JwtTokenFactory;
import io.jsonwebtoken.Jws; import com.svlada.security.model.token.RawAccessJwtToken;
import com.svlada.security.model.token.RefreshToken;
/** /**
* RefreshTokenEndpoint * RefreshTokenEndpoint
@ -41,7 +38,6 @@ import io.jsonwebtoken.Jws;
* *
* Aug 17, 2016 * Aug 17, 2016
*/ */
@SuppressWarnings("unchecked")
@RestController @RestController
public class RefreshTokenEndpoint { public class RefreshTokenEndpoint {
@Autowired private JwtTokenFactory tokenFactory; @Autowired private JwtTokenFactory tokenFactory;
@ -51,31 +47,24 @@ public class RefreshTokenEndpoint {
@RequestMapping(value="/api/auth/token", method=RequestMethod.GET, produces={ MediaType.APPLICATION_JSON_VALUE }) @RequestMapping(value="/api/auth/token", method=RequestMethod.GET, produces={ MediaType.APPLICATION_JSON_VALUE })
public @ResponseBody JwtToken refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { public @ResponseBody JwtToken refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
UnsafeJwtToken unsafeToken = this.tokenFactory.createUnsafeToken(request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM)); RawAccessJwtToken rawToken = new RawAccessJwtToken(request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM));
Jws<Claims> jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey()); RefreshToken refreshToken = RefreshToken.create(rawToken, jwtSettings.getTokenSigningKey()).orElseThrow(() -> new InvalidJwtToken());
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(); String jti = refreshToken.getJti();
if (!tokenVerifier.verify(jti)) { if (!tokenVerifier.verify(jti)) {
throw new InvalidJwtToken(); throw new InvalidJwtToken();
} }
String subject = jwsClaims.getBody().getSubject(); String subject = refreshToken.getSubject();
User user = userService.getByUsername(subject).orElseThrow(() -> new UsernameNotFoundException("User not found: " + subject)); User user = userService.getByUsername(subject).orElseThrow(() -> new UsernameNotFoundException("User not found: " + subject));
if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned"); if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");
List<GrantedAuthority> authorities = user.getRoles().stream() List<GrantedAuthority> authorities = user.getRoles().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getRole().authority())) .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority()))
.collect(Collectors.toList()); .collect(Collectors.toList());
UserContext userContext = UserContext.create(user.getUsername(), authorities); UserContext userContext = UserContext.create(user.getUsername(), authorities);
return tokenFactory.createSafeToken(userContext); return tokenFactory.createAccessJwtToken(userContext);
} }
} }

View File

@ -2,7 +2,7 @@ package com.svlada.security.exceptions;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import com.svlada.security.model.JwtToken; import com.svlada.security.model.token.JwtToken;
/** /**
* *

View File

@ -1,11 +1,9 @@
package com.svlada.security.model; package com.svlada.security.model;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/** /**
* *

View File

@ -1,4 +1,4 @@
package com.svlada.security.model; package com.svlada.security.model.token;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -11,11 +11,11 @@ import io.jsonwebtoken.Claims;
* *
* May 31, 2016 * May 31, 2016
*/ */
public final class SafeJwtToken implements JwtToken { public final class AccessJwtToken implements JwtToken {
private final String rawToken; private final String rawToken;
@JsonIgnore private Claims claims; @JsonIgnore private Claims claims;
protected SafeJwtToken(final String token, Claims claims) { protected AccessJwtToken(final String token, Claims claims) {
this.rawToken = token; this.rawToken = token;
this.claims = claims; this.claims = claims;
} }

View File

@ -1,4 +1,4 @@
package com.svlada.security.model; package com.svlada.security.model.token;
public interface JwtToken { public interface JwtToken {
String getToken(); String getToken();

View File

@ -1,4 +1,4 @@
package com.svlada.security.model; package com.svlada.security.model.token;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
@ -10,6 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.svlada.security.config.JwtSettings; import com.svlada.security.config.JwtSettings;
import com.svlada.security.model.Scopes;
import com.svlada.security.model.UserContext;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
@ -38,14 +40,12 @@ public class JwtTokenFactory {
* @param roles * @param roles
* @return * @return
*/ */
public SafeJwtToken createSafeToken(UserContext userContext) { public AccessJwtToken createAccessJwtToken(UserContext userContext) {
if (StringUtils.isBlank(userContext.getUsername())) { if (StringUtils.isBlank(userContext.getUsername()))
throw new IllegalArgumentException("Cannot create JWT Token without username"); throw new IllegalArgumentException("Cannot create JWT Token without username");
}
if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) { if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty())
throw new IllegalArgumentException("User doesn't have any privileges"); throw new IllegalArgumentException("User doesn't have any privileges");
}
Claims claims = Jwts.claims().setSubject(userContext.getUsername()); Claims claims = Jwts.claims().setSubject(userContext.getUsername());
claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList())); claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
@ -60,7 +60,7 @@ public class JwtTokenFactory {
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact(); .compact();
return new SafeJwtToken(token, claims); return new AccessJwtToken(token, claims);
} }
public JwtToken createRefreshToken(UserContext userContext) { public JwtToken createRefreshToken(UserContext userContext) {
@ -82,18 +82,6 @@ public class JwtTokenFactory {
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact(); .compact();
return new SafeJwtToken(token, claims); return new AccessJwtToken(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

@ -1,4 +1,4 @@
package com.svlada.security.model; package com.svlada.security.model.token;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -14,12 +14,12 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.UnsupportedJwtException;
public class UnsafeJwtToken implements JwtToken { public class RawAccessJwtToken implements JwtToken {
private static Logger logger = LoggerFactory.getLogger(UnsafeJwtToken.class); private static Logger logger = LoggerFactory.getLogger(RawAccessJwtToken.class);
private String token; private String token;
public UnsafeJwtToken(String token) { public RawAccessJwtToken(String token) {
this.token = token; this.token = token;
} }

View File

@ -0,0 +1,66 @@
package com.svlada.security.model.token;
import java.util.List;
import java.util.Optional;
import org.springframework.security.authentication.BadCredentialsException;
import com.svlada.security.exceptions.JwtExpiredTokenException;
import com.svlada.security.model.Scopes;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
/**
*
* @author vladimir.stankovic
*
* Aug 19, 2016
*/
public class RefreshToken implements JwtToken {
private Jws<Claims> claims;
private RefreshToken(Jws<Claims> claims) {
this.claims = claims;
}
/**
* Creates and validates Refresh token
*
* @param token
* @param signingKey
*
* @throws BadCredentialsException
* @throws JwtExpiredTokenException
*
* @return
*/
public static Optional<RefreshToken> create(RawAccessJwtToken token, String signingKey) {
Jws<Claims> claims = token.parseClaims(signingKey);
List<String> scopes = claims.getBody().get("scopes", List.class);
if (scopes == null || scopes.isEmpty()
|| !scopes.stream().filter(scope -> Scopes.REFRESH_TOKEN.authority().equals(scope)).findFirst().isPresent()) {
return Optional.empty();
}
return Optional.of(new RefreshToken(claims));
}
@Override
public String getToken() {
return null;
}
public Jws<Claims> getClaims() {
return claims;
}
public String getJti() {
return claims.getBody().getId();
}
public String getSubject() {
return claims.getBody().getSubject();
}
}