diff --git a/etc/blog.md b/etc/blog.md index 03a50b5..eeaf0f8 100644 --- a/etc/blog.md +++ b/etc/blog.md @@ -482,6 +482,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ### [](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 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/ -http://nordicapis.com/how-to-control-user-identity-within-microservices/ \ No newline at end of file +http://nordicapis.com/how-to-control-user-identity-within-microservices/ + +https://tools.ietf.org/html/rfc6749 \ No newline at end of file diff --git a/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java b/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java index 7aec56c..8d7e7a7 100644 --- a/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java +++ b/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java @@ -5,8 +5,8 @@ import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; -import com.svlada.security.model.UnsafeJwtToken; import com.svlada.security.model.UserContext; +import com.svlada.security.model.token.RawAccessJwtToken; /** * An {@link org.springframework.security.core.Authentication} implementation @@ -19,18 +19,18 @@ import com.svlada.security.model.UserContext; public class JwtAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 2877954820905567501L; - private UnsafeJwtToken unsafeToken; + private RawAccessJwtToken rawAccessToken; private UserContext userContext; - public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) { + public JwtAuthenticationToken(RawAccessJwtToken unsafeToken) { super(null); - this.unsafeToken = unsafeToken; + this.rawAccessToken = unsafeToken; this.setAuthenticated(false); } public JwtAuthenticationToken(UserContext userContext, Collection authorities) { super(authorities); - this.unsafeToken = null; + this.eraseCredentials(); this.userContext = userContext; super.setAuthenticated(true); } @@ -46,7 +46,7 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken { @Override public Object getCredentials() { - return unsafeToken; + return rawAccessToken; } @Override @@ -57,6 +57,6 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken { @Override public void eraseCredentials() { super.eraseCredentials(); - this.unsafeToken = null; + this.rawAccessToken = null; } } diff --git a/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java index af5e530..77dd408 100644 --- a/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java +++ b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java @@ -16,9 +16,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand import org.springframework.stereotype.Component; 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.token.JwtToken; +import com.svlada.security.model.token.JwtTokenFactory; /** * AjaxAwareAuthenticationSuccessHandler @@ -43,7 +43,7 @@ public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSucc Authentication authentication) throws IOException, ServletException { UserContext userContext = (UserContext) authentication.getPrincipal(); - JwtToken token = tokenFactory.createSafeToken(userContext); + JwtToken token = tokenFactory.createAccessJwtToken(userContext); JwtToken refreshToken = tokenFactory.createRefreshToken(userContext); response.setStatus(HttpStatus.OK.value()); diff --git a/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java b/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java index 6c8f21a..ed85761 100644 --- a/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java +++ b/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java @@ -13,9 +13,9 @@ import org.springframework.stereotype.Component; import com.svlada.security.auth.JwtAuthenticationToken; 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.token.JwtToken; +import com.svlada.security.model.token.RawAccessJwtToken; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; @@ -40,13 +40,11 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - UnsafeJwtToken unsafeToken = (UnsafeJwtToken) authentication.getCredentials(); + RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); - Jws jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey()); + Jws jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey()); String subject = jwsClaims.getBody().getSubject(); - List scopes = jwsClaims.getBody().get("scopes", List.class); - List authorities = scopes.stream() .map(authority -> new SimpleGrantedAuthority(authority)) .collect(Collectors.toList()); diff --git a/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java index edd2f38..6763c9e 100644 --- a/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java +++ b/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java @@ -14,13 +14,11 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 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.jwt.extractor.TokenExtractor; import com.svlada.security.config.WebSecurityConfig; -import com.svlada.security.model.JwtTokenFactory; -import com.svlada.security.model.UnsafeJwtToken; +import com.svlada.security.model.token.RawAccessJwtToken; /** * Performs validation of provided JWT Token. @@ -32,24 +30,21 @@ import com.svlada.security.model.UnsafeJwtToken; public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { private final AuthenticationFailureHandler failureHandler; private final TokenExtractor tokenExtractor; - private final JwtTokenFactory tokenFactory; @Autowired public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, - JwtTokenFactory tokenFactory, TokenExtractor tokenExtractor, String filterProcessingUrl) { super(filterProcessingUrl); this.failureHandler = failureHandler; this.tokenExtractor = tokenExtractor; - this.tokenFactory = tokenFactory; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { 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)); } diff --git a/src/main/java/com/svlada/security/auth/jwt/BloomFilterTokenVerifier.java b/src/main/java/com/svlada/security/auth/jwt/verifier/BloomFilterTokenVerifier.java similarity index 86% rename from src/main/java/com/svlada/security/auth/jwt/BloomFilterTokenVerifier.java rename to src/main/java/com/svlada/security/auth/jwt/verifier/BloomFilterTokenVerifier.java index 3f5e4be..823a691 100644 --- a/src/main/java/com/svlada/security/auth/jwt/BloomFilterTokenVerifier.java +++ b/src/main/java/com/svlada/security/auth/jwt/verifier/BloomFilterTokenVerifier.java @@ -1,4 +1,4 @@ -package com.svlada.security.auth.jwt; +package com.svlada.security.auth.jwt.verifier; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/svlada/security/auth/jwt/TokenVerifier.java b/src/main/java/com/svlada/security/auth/jwt/verifier/TokenVerifier.java similarity index 74% rename from src/main/java/com/svlada/security/auth/jwt/TokenVerifier.java rename to src/main/java/com/svlada/security/auth/jwt/verifier/TokenVerifier.java index 1f8052f..6dac81d 100644 --- a/src/main/java/com/svlada/security/auth/jwt/TokenVerifier.java +++ b/src/main/java/com/svlada/security/auth/jwt/verifier/TokenVerifier.java @@ -1,4 +1,4 @@ -package com.svlada.security.auth.jwt; +package com.svlada.security.auth.jwt.verifier; /** * diff --git a/src/main/java/com/svlada/security/config/WebSecurityConfig.java b/src/main/java/com/svlada/security/config/WebSecurityConfig.java index dfee99c..341b7a6 100644 --- a/src/main/java/com/svlada/security/config/WebSecurityConfig.java +++ b/src/main/java/com/svlada/security/config/WebSecurityConfig.java @@ -21,7 +21,7 @@ import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter; import com.svlada.security.auth.jwt.JwtAuthenticationProvider; import com.svlada.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import com.svlada.security.auth.jwt.extractor.TokenExtractor; -import com.svlada.security.model.JwtTokenFactory; +import com.svlada.security.model.token.JwtTokenFactory; /** * WebSecurityConfig diff --git a/src/main/java/com/svlada/security/endpoint/RefreshTokenEndpoint.java b/src/main/java/com/svlada/security/endpoint/RefreshTokenEndpoint.java index 983c04c..47acfbe 100644 --- a/src/main/java/com/svlada/security/endpoint/RefreshTokenEndpoint.java +++ b/src/main/java/com/svlada/security/endpoint/RefreshTokenEndpoint.java @@ -21,18 +21,15 @@ 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.auth.jwt.verifier.TokenVerifier; import com.svlada.security.config.JwtSettings; 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; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; +import com.svlada.security.model.token.JwtToken; +import com.svlada.security.model.token.JwtTokenFactory; +import com.svlada.security.model.token.RawAccessJwtToken; +import com.svlada.security.model.token.RefreshToken; /** * RefreshTokenEndpoint @@ -41,7 +38,6 @@ import io.jsonwebtoken.Jws; * * Aug 17, 2016 */ -@SuppressWarnings("unchecked") @RestController public class RefreshTokenEndpoint { @Autowired private JwtTokenFactory tokenFactory; @@ -51,31 +47,24 @@ 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 jwsClaims = unsafeToken.parseClaims(jwtSettings.getTokenSigningKey()); - - List 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(); - } + RawAccessJwtToken rawToken = new RawAccessJwtToken(request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM)); + RefreshToken refreshToken = RefreshToken.create(rawToken, jwtSettings.getTokenSigningKey()).orElseThrow(() -> new InvalidJwtToken()); - String jti = jwsClaims.getBody().getId(); + String jti = refreshToken.getJti(); if (!tokenVerifier.verify(jti)) { throw new InvalidJwtToken(); } - String subject = jwsClaims.getBody().getSubject(); - + String subject = refreshToken.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 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); + + return tokenFactory.createAccessJwtToken(userContext); } } diff --git a/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java b/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java index 0b1c18c..91c0304 100644 --- a/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java +++ b/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java @@ -2,7 +2,7 @@ package com.svlada.security.exceptions; import org.springframework.security.core.AuthenticationException; -import com.svlada.security.model.JwtToken; +import com.svlada.security.model.token.JwtToken; /** * diff --git a/src/main/java/com/svlada/security/model/UserContext.java b/src/main/java/com/svlada/security/model/UserContext.java index 4b9930f..19bd4fd 100644 --- a/src/main/java/com/svlada/security/model/UserContext.java +++ b/src/main/java/com/svlada/security/model/UserContext.java @@ -1,11 +1,9 @@ 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; /** * diff --git a/src/main/java/com/svlada/security/model/SafeJwtToken.java b/src/main/java/com/svlada/security/model/token/AccessJwtToken.java similarity index 74% rename from src/main/java/com/svlada/security/model/SafeJwtToken.java rename to src/main/java/com/svlada/security/model/token/AccessJwtToken.java index 6150fc9..fb90ba2 100644 --- a/src/main/java/com/svlada/security/model/SafeJwtToken.java +++ b/src/main/java/com/svlada/security/model/token/AccessJwtToken.java @@ -1,4 +1,4 @@ -package com.svlada.security.model; +package com.svlada.security.model.token; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -11,11 +11,11 @@ import io.jsonwebtoken.Claims; * * May 31, 2016 */ -public final class SafeJwtToken implements JwtToken { +public final class AccessJwtToken implements JwtToken { private final String rawToken; @JsonIgnore private Claims claims; - protected SafeJwtToken(final String token, Claims claims) { + protected AccessJwtToken(final String token, Claims claims) { this.rawToken = token; this.claims = claims; } diff --git a/src/main/java/com/svlada/security/model/JwtToken.java b/src/main/java/com/svlada/security/model/token/JwtToken.java similarity index 56% rename from src/main/java/com/svlada/security/model/JwtToken.java rename to src/main/java/com/svlada/security/model/token/JwtToken.java index d2afe7f..351be66 100644 --- a/src/main/java/com/svlada/security/model/JwtToken.java +++ b/src/main/java/com/svlada/security/model/token/JwtToken.java @@ -1,4 +1,4 @@ -package com.svlada.security.model; +package com.svlada.security.model.token; public interface JwtToken { String getToken(); diff --git a/src/main/java/com/svlada/security/model/JwtTokenFactory.java b/src/main/java/com/svlada/security/model/token/JwtTokenFactory.java similarity index 79% rename from src/main/java/com/svlada/security/model/JwtTokenFactory.java rename to src/main/java/com/svlada/security/model/token/JwtTokenFactory.java index e9a393a..d263366 100644 --- a/src/main/java/com/svlada/security/model/JwtTokenFactory.java +++ b/src/main/java/com/svlada/security/model/token/JwtTokenFactory.java @@ -1,4 +1,4 @@ -package com.svlada.security.model; +package com.svlada.security.model.token; import java.util.Arrays; import java.util.UUID; @@ -10,6 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; 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.Jwts; @@ -38,14 +40,12 @@ public class JwtTokenFactory { * @param roles * @return */ - public SafeJwtToken createSafeToken(UserContext userContext) { - if (StringUtils.isBlank(userContext.getUsername())) { + public AccessJwtToken createAccessJwtToken(UserContext userContext) { + if (StringUtils.isBlank(userContext.getUsername())) 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"); - } Claims claims = Jwts.claims().setSubject(userContext.getUsername()); 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()) .compact(); - return new SafeJwtToken(token, claims); + return new AccessJwtToken(token, claims); } public JwtToken createRefreshToken(UserContext userContext) { @@ -82,18 +82,6 @@ public class JwtTokenFactory { .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) .compact(); - return new SafeJwtToken(token, claims); - } - - /** - * Unsafe version of JWT token is created. - * - * WARNING: Token signature validation is not performed. - * - * @param tokenPayload - * @return unsafe version of JWT token. - */ - public UnsafeJwtToken createUnsafeToken(String tokenPayload) { - return new UnsafeJwtToken(tokenPayload); + return new AccessJwtToken(token, claims); } } diff --git a/src/main/java/com/svlada/security/model/UnsafeJwtToken.java b/src/main/java/com/svlada/security/model/token/RawAccessJwtToken.java similarity index 86% rename from src/main/java/com/svlada/security/model/UnsafeJwtToken.java rename to src/main/java/com/svlada/security/model/token/RawAccessJwtToken.java index 9ee446c..b0c46e2 100644 --- a/src/main/java/com/svlada/security/model/UnsafeJwtToken.java +++ b/src/main/java/com/svlada/security/model/token/RawAccessJwtToken.java @@ -1,4 +1,4 @@ -package com.svlada.security.model; +package com.svlada.security.model.token; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,12 +14,12 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; -public class UnsafeJwtToken implements JwtToken { - private static Logger logger = LoggerFactory.getLogger(UnsafeJwtToken.class); +public class RawAccessJwtToken implements JwtToken { + private static Logger logger = LoggerFactory.getLogger(RawAccessJwtToken.class); private String token; - public UnsafeJwtToken(String token) { + public RawAccessJwtToken(String token) { this.token = token; } diff --git a/src/main/java/com/svlada/security/model/token/RefreshToken.java b/src/main/java/com/svlada/security/model/token/RefreshToken.java new file mode 100644 index 0000000..e730e5e --- /dev/null +++ b/src/main/java/com/svlada/security/model/token/RefreshToken.java @@ -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; + + private RefreshToken(Jws claims) { + this.claims = claims; + } + + /** + * Creates and validates Refresh token + * + * @param token + * @param signingKey + * + * @throws BadCredentialsException + * @throws JwtExpiredTokenException + * + * @return + */ + public static Optional create(RawAccessJwtToken token, String signingKey) { + Jws claims = token.parseClaims(signingKey); + + List 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 getClaims() { + return claims; + } + + public String getJti() { + return claims.getBody().getId(); + } + + public String getSubject() { + return claims.getBody().getSubject(); + } +}