diff --git a/etc/blog.md b/etc/blog.md
index 2daa9b1..391c313 100644
--- a/etc/blog.md
+++ b/etc/blog.md
@@ -6,8 +6,7 @@
Following are three scenarios that will be implemented in this tutorial:
1. Ajax Authentication
-2. JWT Token
-3. URL Based Authentication with JWT Token
+2. JWT Token Authentication
### Prerequisites
@@ -39,9 +38,7 @@ Lets start by creating base package structure for our sample code.
### Ajax authentication
-Code for ajax authentication will reside in the following package: com/svlada/security/auth/ajax.
-
-In order to implement Ajax Login in Spring Boot we'll need to implement a couple of components.
+In order to implement Ajax Login in Spring Boot we'll need to implement a couple of components:
1. AjaxLoginProcessingFilter
2. AjaxAuthenticationProvider
@@ -50,34 +47,435 @@ In order to implement Ajax Login in Spring Boot we'll need to implement a couple
5. RestAuthenticationEntryPoint
6. WebSecurityConfig
+Authentication flow starts with AJAX authentication request as shown below:
+
+```
+POST /api/auth/login HTTP/1.1
+Host: localhost:9966
+X-Requested-With: XMLHttpRequest
+Content-Type: application/json
+Cache-Control: no-cache
+
+{
+ "username": "svlada@gmail.com",
+ "password": "test1234"
+}
+```
+
+User credentials are sent in JSON Payload of authentication request. If credentials are valid, authentication API will respond with HTTP status "200 OK". Additionaly JWT token is included in the HTTP response body. JWT token is then used to make authenticated API requests.
+
+Sample HTTP Authentication reponse with JWT Token included:
+```
+{
+ "token": "eyJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTQ3MDM4NDg4NSwiZXhwIjoxNDcwMzg1MDA1fQ.pvAyzZDd8F8snjH1a-geHGXFAnN-vnMJ5uW9TmF7nFvIGaYYUh-B5kyAr4nYioB07fwXFw6s22zPc3Ge1bekfQ"
+}
+```
+
+JWT Token consists of three parts: Header, Claims and Signature. Decoded values are presented below:
+
+Header
+```
+
+{
+ "alg": "HS512"
+}
+```
+Claims
+```
+
+{
+ iss: "http://svlada.com",
+ sub: "svlada@gmail.com",
+ "roles": [
+ "ROLE_ADMIN"
+ ],
+ "iat": 1470384885,
+ "exp": 1470385005
+}
+```
+Signature (encoded)
+```
+pvAyzZDd8F8snjH1a-geHGXFAnN-vnMJ5uW9TmF7nFvIGaYYUh-B5kyAr4nYioB07fwXFw6s22zPc3Ge1bekfQ
+```
+
Let's dive in the implementation details.
#### AjaxLoginProcessingFilter
-#### Security Config
+AbstractAuthenticationProcessingFilter class is responsible for processing of HTTP-based authentication requests. Please note that AuthenticationManager must be set for this class.
-Create WebSecurityConfig class and put it in the com.svlada.security.config package.
+AjaxLoginProcessingFilter is overriding AbstractAuthenticationProcessingFilter to provide implementation for AJAX based authentication.
-WebSecurityConfig class needs to extend org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.
+Parsing and basic validation of incoming JSON payload is done in the AjaxLoginProcessingFilter#attemptAuthentication method. If authentication JSON payload is valid, actual authentication logic is delegated to AjaxAuthenticationProvider class.
-#### Un-successufull access to protected resource
+In case of successuful authentication AjaxLoginProcessingFilter#successfulAuthentication is called.
+In case of application failure AjaxLoginProcessingFilter#unsuccessfulAuthentication is called.
-Request
-```
-GET /api/me HTTP/1.1
-Host: localhost:9966
-Cache-Control: no-cache
-```
+```language-java
+public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class);
-Response
-```
-{
- "timestamp": 1470301809962,
- "status": 401,
- "error": "Unauthorized",
- "message": "Full authentication is required to access this resource",
- "path": "/api/me"
+ private final AuthenticationSuccessHandler successHandler;
+ private final AuthenticationFailureHandler failureHandler;
+
+ private final ObjectMapper objectMapper;
+
+ public AjaxLoginProcessingFilter(String defaultFilterProcessesUrl,
+ AuthenticationSuccessHandler successHandler,
+ AuthenticationFailureHandler failureHandler,
+ ObjectMapper mapper) {
+ super(defaultFilterProcessesUrl);
+ this.successHandler = successHandler;
+ this.failureHandler = failureHandler;
+ this.objectMapper = mapper;
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+ throws AuthenticationException, IOException, ServletException {
+ if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) {
+ throw new AuthMethodNotSupportedException("Authentication method not supported");
+ }
+
+ LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
+
+ if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {
+ throw new AuthenticationServiceException("Username or Password not provided");
+ }
+
+ UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
+
+ return this.getAuthenticationManager().authenticate(token);
+ }
+
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+ Authentication authResult) throws IOException, ServletException {
+ successHandler.onAuthenticationSuccess(request, response, authResult);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ failureHandler.onAuthenticationFailure(request, response, failed);
+ }
}
```
-#### Successufull ajax authentication
\ No newline at end of file
+#### AjaxAuthenticationProvider
+
+AjaxAuthenticationProvider class responsiblity is to:
+
+1. Verify user credentials against database, ldap or some other system which holds user data.
+2. Throw authentication exception in case of that username and password doesn't match record in the database, username doesnt exists, etc.
+3. Create UserContext and populate it with information you need.
+4. Create JWT Token and sign it with the private key (JwtTokenFactory).
+
+```language-java
+@Component
+public class AjaxAuthenticationProvider implements AuthenticationProvider {
+ private final JwtTokenFactory tokenFactory;
+ private final UserService userService;
+
+ @Autowired
+ public AjaxAuthenticationProvider(final JwtTokenFactory tokenFactory, final UserService userService) {
+ this.tokenFactory = tokenFactory;
+ this.userService = userService;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ Assert.notNull(authentication, "No authentication data provided.");
+
+ String username = (String) authentication.getPrincipal();
+ String password = (String) authentication.getCredentials();
+
+ UserContext userContext = userService.loadUser(username, password);
+
+ SafeJwtToken safeJwtToken = tokenFactory.createSafeToken(userContext, userContext.getAuthorities());
+
+ return new JwtAuthenticationToken(userContext, safeJwtToken, userContext.getAuthorities());
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
+ }
+}
+```
+
+Let's focus for a moment on how JWT token is created. In this tutorial we are using [Java JWT](https://github.com/jwtk/jjwt) library created by [Stormpath](https://stormpath.com/).
+
+Make sure that this JJWT dependency is included in your pom.xml.
+
+```language-xml
+
+ io.jsonwebtoken
+ jjwt
+ ${jjwt.version}
+
+```
+
+JwtTokenFactory#createSafeToken method will create signed Jwt Token.
+
+Please note that if you are instantiating Claims object outside of Jwts.builder() make sure to first invoke Jwts.builder()#setClaims(claims). Why? Well, if you don't do that, Jwts.builder will, by default, create empty Claims object. What that means? Well if you call Jwts.builder()#setClaims() after you have set subject with Jwts.builder()#setSubject() your subject will be lost. Simply new instance of Claims class will overwrite default one created by Jwts.builder().
+
+```
+@Component
+public class JwtTokenFactory {
+ @Autowired
+ private JwtSettings settings;
+
+ /**
+ * Factory method for issuing new JWT Tokens.
+ *
+ * @param username
+ * @param roles
+ * @return
+ */
+ public SafeJwtToken createSafeToken(UserContext userContext, final Collection roles) {
+ if (StringUtils.isBlank(userContext.getUsername())) {
+ throw new IllegalArgumentException("Cannot create JWT Token without username");
+ }
+
+ if (Collections.isEmpty(roles)) {
+ throw new IllegalArgumentException("Cannot create JWT Token without roles");
+ }
+
+ DateTime currentTime = new DateTime();
+
+ Claims claims = Jwts.claims();
+ claims.put("roles", AuthorityUtils.authorityListToSet(roles));
+
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .setIssuer(settings.getTokenIssuer())
+ .setSubject(userContext.getUsername())
+ .setIssuedAt(currentTime.toDate())
+ .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
+ .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);
+ }
+}
+```
+
+We have extended AbstractAuthenticationToken and implemented JwtAuthenticationToken that will be passed through application as an authentication object.
+
+```
+public class JwtAuthenticationToken extends AbstractAuthenticationToken {
+ private static final long serialVersionUID = 2877954820905567501L;
+
+ private JwtToken safeToken;
+ private UnsafeJwtToken unsafeToken;
+
+ private UserContext userContext;
+
+ public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) {
+ super(null);
+ this.unsafeToken = unsafeToken;
+ this.setAuthenticated(false);
+ }
+
+ public JwtAuthenticationToken(UserContext userContext, SafeJwtToken token,
+ Collection extends GrantedAuthority> authorities) {
+ super(authorities);
+ this.safeToken = token;
+ this.userContext = userContext;
+ super.setAuthenticated(true);
+ }
+
+ @Override
+ public void setAuthenticated(boolean authenticated) {
+ if (authenticated) {
+ throw new IllegalArgumentException(
+ "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+ }
+ super.setAuthenticated(false);
+ }
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return this.userContext;
+ }
+
+ public JwtToken getSafeToken() {
+ return this.safeToken;
+ }
+
+ public UnsafeJwtToken getUnsafeToken() {
+ return unsafeToken;
+ }
+
+ @Override
+ public void eraseCredentials() {
+ super.eraseCredentials();
+ }
+}
+```
+
+#### AjaxAwareAuthenticationSuccessHandler
+
+AjaxAwareAuthenticationSuccessHandler is simple class and it's used by Spring to actually send HTTP response upon successuful authentication.
+
+
+```
+@Component
+public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+ private final ObjectMapper mapper;
+
+ @Autowired
+ public AjaxAwareAuthenticationSuccessHandler(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+ JwtToken token = ((JwtAuthenticationToken) authentication).getSafeToken();
+
+ response.setStatus(HttpStatus.OK.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ mapper.writeValue(response.getWriter(), token);
+
+ clearAuthenticationAttributes(request);
+ }
+
+ /**
+ * Removes temporary authentication-related data which may have been stored
+ * in the session during the authentication process..
+ *
+ */
+ protected final void clearAuthenticationAttributes(HttpServletRequest request) {
+ HttpSession session = request.getSession(false);
+
+ if (session == null) {
+ return;
+ }
+
+ session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
+ }
+}
+```
+
+#### AjaxAwareAuthenticationFailureHandler
+
+AjaxAwareAuthenticationFailureHandler is invoked by Spring in case of authentication failure. You can create specific error message based on exception type that have occured during the authentication process.
+
+```
+@Component
+public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {
+ private final ObjectMapper mapper;
+
+ @Autowired
+ public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException e) throws IOException, ServletException {
+
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ if (e instanceof BadCredentialsException) {
+ mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ } else if (e instanceof JwtExpiredTokenException) {
+ mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
+ } else if (e instanceof AuthMethodNotSupportedException) {
+ mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ }
+
+ mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ }
+}
+```
+
+#### WebSecurityConfig - Initial version to support AJAX based login
+
+This is first version of WebSecurityConfig. We will add more configuration to it once we start with showcase of JWT Authentication flow.
+
+```
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+ public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
+ public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
+
+ @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint;
+ @Autowired private AuthenticationSuccessHandler successHandler;
+ @Autowired private AuthenticationFailureHandler failureHandler;
+ @Autowired private AjaxAuthenticationProvider ajaxAuthenticationProvider;
+
+ @Autowired private AuthenticationManager authenticationManager;
+
+ @Autowired private ObjectMapper objectMapper;
+
+ @Bean
+ protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception {
+ AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
+ filter.setAuthenticationManager(this.authenticationManager);
+ return filter;
+ }
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+ protected void configure(AuthenticationManagerBuilder auth) {
+ auth.authenticationProvider(ajaxAuthenticationProvider);
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .csrf().disable() // We don't need CSRF for JWT based authentication
+ .exceptionHandling()
+ .authenticationEntryPoint(this.authenticationEntryPoint)
+
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+
+ .and()
+ .authorizeRequests()
+ .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
+ .and()
+ .addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
+ }
+}
+```
+
+
+### Jwt token authentication
+
+
+
+
+
+
+## References
+
+[Spring Security Architecture - Dave Syer](https://github.com/dsyer/spring-security-architecture)
\ 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 8ae063b..0ef2d7f 100644
--- a/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java
+++ b/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java
@@ -11,64 +11,64 @@ import com.svlada.security.model.UnsafeJwtToken;
import com.svlada.security.model.UserContext;
/**
- * An {@link org.springframework.security.core.Authentication} implementation that is designed for simple presentation
- * of JwtToken.
+ * An {@link org.springframework.security.core.Authentication} implementation
+ * that is designed for simple presentation of JwtToken.
*
* @author vladimir.stankovic
*
- * May 23, 2016
+ * May 23, 2016
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
- private static final long serialVersionUID = 2877954820905567501L;
-
- private JwtToken safeToken;
- private UnsafeJwtToken unsafeToken;
-
- private UserContext userContext;
-
- public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) {
- super(null);
- this.unsafeToken = unsafeToken;
- this.
- setAuthenticated(false);
- }
+ private static final long serialVersionUID = 2877954820905567501L;
- public JwtAuthenticationToken(UserContext userContext, SafeJwtToken token, Collection extends GrantedAuthority> authorities) {
- super(authorities);
- this.safeToken = token;
- this.userContext = userContext;
- super.setAuthenticated(true);
- }
+ private JwtToken safeToken;
+ private UnsafeJwtToken unsafeToken;
- @Override
- public void setAuthenticated(boolean authenticated) {
- if (authenticated) {
- throw new IllegalArgumentException(
- "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
- }
- super.setAuthenticated(false);
- }
-
- @Override
- public Object getCredentials() {
- return null;
- }
+ private UserContext userContext;
+
+ public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) {
+ super(null);
+ this.unsafeToken = unsafeToken;
+ this.setAuthenticated(false);
+ }
+
+ public JwtAuthenticationToken(UserContext userContext, SafeJwtToken token,
+ Collection extends GrantedAuthority> authorities) {
+ super(authorities);
+ this.safeToken = token;
+ this.userContext = userContext;
+ super.setAuthenticated(true);
+ }
+
+ @Override
+ public void setAuthenticated(boolean authenticated) {
+ if (authenticated) {
+ throw new IllegalArgumentException(
+ "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+ }
+ super.setAuthenticated(false);
+ }
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return this.userContext;
+ }
+
+ public JwtToken getSafeToken() {
+ return this.safeToken;
+ }
- @Override
- public Object getPrincipal() {
- return this.userContext;
- }
-
- public JwtToken getSafeToken() {
- return this.safeToken;
- }
-
public UnsafeJwtToken getUnsafeToken() {
return unsafeToken;
}
@Override
public void eraseCredentials() {
- super.eraseCredentials();
+ super.eraseCredentials();
}
}
diff --git a/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java b/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java
new file mode 100644
index 0000000..fc3062d
--- /dev/null
+++ b/src/main/java/com/svlada/security/auth/jwt/JwtAuthenticationProvider.java
@@ -0,0 +1,76 @@
+package com.svlada.security.auth.jwt;
+
+import java.util.ArrayList;
+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.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.auth.JwtAuthenticationToken;
+import com.svlada.security.config.JwtSettings;
+import com.svlada.security.exceptions.JwtExpiredTokenException;
+import com.svlada.security.model.JwtToken;
+import com.svlada.security.model.UnsafeJwtToken;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.UnsupportedJwtException;
+
+/**
+ * An {@link AuthenticationProvider} implementation that will use provided
+ * instance of {@link JwtToken} to perform authentication.
+ *
+ * @author vladimir.stankovic
+ *
+ * Aug 5, 2016
+ */
+@Component
+public class JwtAuthenticationProvider implements AuthenticationProvider {
+ private final JwtSettings jwtSettings;
+ private final TokenAuthStrategy tokenAuthStrategy;
+
+ @Autowired
+ public JwtAuthenticationProvider(JwtSettings jwtSettings, TokenAuthStrategy tokenAuthStrategy) {
+ this.jwtSettings = jwtSettings;
+ this.tokenAuthStrategy = tokenAuthStrategy;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ UnsafeJwtToken token = ((JwtAuthenticationToken) authentication).getUnsafeToken();
+
+ SafeToken safeToken = token.authenticate(tokenAuthStrategy);
+
+ try {
+ token.validateToken(jwtSettings.getTokenSigningKey());
+ } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
+ throw new BadCredentialsException("Invalid JWT token: ", ex);
+ } catch (ExpiredJwtException expiredEx) {
+ throw new JwtExpiredTokenException(token, "Token expired.", expiredEx);
+ }
+
+ Claims claims = token.claims(jwtSettings.getTokenSigningKey());
+ ArrayList rawAuthorities = claims.get("roles", ArrayList.class);
+
+ List authorities = rawAuthorities.stream()
+ .map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
+
+ JwtAuthenticationToken authToken = new JwtAuthenticationToken(token, authorities, claims.getSubject());
+
+ return authToken;
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
+ }
+}
diff --git a/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
new file mode 100644
index 0000000..dfaeefc
--- /dev/null
+++ b/src/main/java/com/svlada/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
@@ -0,0 +1,71 @@
+package com.svlada.security.auth.jwt;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+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.authentication.AuthenticationSuccessHandler;
+
+import com.svlada.security.auth.JwtAuthenticationToken;
+import com.svlada.security.auth.jwt.extractor.TokenExtractor;
+import com.svlada.security.model.JwtTokenFactory;
+import com.svlada.security.model.UnsafeJwtToken;
+
+/**
+ * Performs validation of provided JWT Token.
+ *
+ * @author vladimir.stankovic
+ *
+ * Aug 5, 2016
+ */
+public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ private final AuthenticationFailureHandler failureHandler;
+ private final TokenExtractor tokenExtractor;
+ private final JwtTokenFactory tokenFactory;
+
+ @Autowired
+ public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler,
+ JwtTokenFactory tokenFactory,
+ @Qualifier("jwtHeaderTokenExtractor") TokenExtractor tokenExtractor,
+ String defaultFilterProcessesUrl) {
+ super(defaultFilterProcessesUrl);
+ 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("X-Authorization");
+ UnsafeJwtToken token = tokenFactory.createUnsafeToken(tokenExtractor.extract(tokenPayload));
+ return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
+ }
+
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+ Authentication authResult) throws IOException, ServletException {
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(authResult);
+ SecurityContextHolder.setContext(context);
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ failureHandler.onAuthenticationFailure(request, response, failed);
+ }
+}
diff --git a/src/main/java/com/svlada/security/auth/jwt/TokenAuthStrategy.java b/src/main/java/com/svlada/security/auth/jwt/TokenAuthStrategy.java
new file mode 100644
index 0000000..ca466cf
--- /dev/null
+++ b/src/main/java/com/svlada/security/auth/jwt/TokenAuthStrategy.java
@@ -0,0 +1,13 @@
+package com.svlada.security.auth.jwt;
+
+import com.svlada.security.model.SafeJwtToken;
+
+/**
+ *
+ * @author vladimir.stankovic
+ *
+ * Aug 5, 2016
+ */
+public interface TokenAuthStrategy {
+ public SafeJwtToken authenticate(String token);
+}
diff --git a/src/main/java/com/svlada/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/src/main/java/com/svlada/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java
new file mode 100644
index 0000000..93fa2f9
--- /dev/null
+++ b/src/main/java/com/svlada/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java
@@ -0,0 +1,31 @@
+package com.svlada.security.auth.jwt.extractor;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.stereotype.Component;
+
+/**
+ * An implementation of {@link TokenExtractor} extracts token from
+ * Authorization: Bearer scheme.
+ *
+ * @author vladimir.stankovic
+ *
+ * Aug 5, 2016
+ */
+@Component
+public class JwtHeaderTokenExtractor implements TokenExtractor {
+ public static String HEADER_PREFIX = "Bearer ";
+
+ @Override
+ public String extract(String header) {
+ if (StringUtils.isBlank(header)) {
+ throw new AuthenticationServiceException("Authorization header cannot be blank!");
+ }
+
+ if (header.length() < HEADER_PREFIX.length()) {
+ throw new AuthenticationServiceException("Invalid authorization header size.");
+ }
+
+ return header.substring(HEADER_PREFIX.length(), header.length());
+ }
+}
diff --git a/src/main/java/com/svlada/security/auth/jwt/extractor/TokenExtractor.java b/src/main/java/com/svlada/security/auth/jwt/extractor/TokenExtractor.java
new file mode 100644
index 0000000..d63eab7
--- /dev/null
+++ b/src/main/java/com/svlada/security/auth/jwt/extractor/TokenExtractor.java
@@ -0,0 +1,13 @@
+package com.svlada.security.auth.jwt.extractor;
+
+/**
+ * Implementations of this interface should always return raw base-64 encoded
+ * representation of JWT Token.
+ *
+ * @author vladimir.stankovic
+ *
+ * Aug 5, 2016
+ */
+public interface TokenExtractor {
+ public String extract(String payload);
+}
diff --git a/src/main/java/com/svlada/security/model/JwtTokenFactory.java b/src/main/java/com/svlada/security/model/JwtTokenFactory.java
index 6af378e..bbf9f4c 100644
--- a/src/main/java/com/svlada/security/model/JwtTokenFactory.java
+++ b/src/main/java/com/svlada/security/model/JwtTokenFactory.java
@@ -18,57 +18,62 @@ import io.jsonwebtoken.lang.Collections;
/**
* Factory class that should be always used to create {@link JwtToken}.
- *
+ *
* @author vladimir.stankovic
*
- * May 31, 2016
+ * May 31, 2016
*/
@Component
public class JwtTokenFactory {
- @Autowired private JwtSettings settings;
+ private final JwtSettings settings;
+
+ @Autowired
+ public JwtTokenFactory(JwtSettings settings) {
+ this.settings = settings;
+ }
- /**
- * Factory method for issuing new JWT Tokens.
- *
- * @param username
- * @param roles
- * @return
- */
- public SafeJwtToken createSafeToken(UserContext userContext, final Collection roles) {
- if (StringUtils.isBlank(userContext.getUsername())) {
- throw new IllegalArgumentException("Cannot create JWT Token without username");
- }
-
- if (Collections.isEmpty(roles)) {
- throw new IllegalArgumentException("Cannot create JWT Token without roles");
- }
-
- DateTime currentTime = new DateTime();
-
- Claims claims = Jwts.claims();
- claims.put("roles", AuthorityUtils.authorityListToSet(roles));
-
- String token = Jwts.builder()
- .setIssuer(settings.getTokenIssuer())
- .setSubject(userContext.getUsername())
- .setClaims(claims)
- .setIssuedAt(currentTime.toDate())
- .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
- .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
- .compact();
-
- return new SafeJwtToken(token, claims);
- }
+ /**
+ * Factory method for issuing new JWT Tokens.
+ *
+ * @param username
+ * @param roles
+ * @return
+ */
+ public SafeJwtToken createSafeToken(UserContext userContext, final Collection roles) {
+ if (StringUtils.isBlank(userContext.getUsername())) {
+ throw new IllegalArgumentException("Cannot create JWT Token without username");
+ }
- /**
- * 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);
- }
+ if (Collections.isEmpty(roles)) {
+ throw new IllegalArgumentException("Cannot create JWT Token without roles");
+ }
+
+ DateTime currentTime = new DateTime();
+
+ Claims claims = Jwts.claims();
+ claims.put("roles", AuthorityUtils.authorityListToSet(roles));
+
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .setIssuer(settings.getTokenIssuer())
+ .setSubject(userContext.getUsername())
+ .setIssuedAt(currentTime.toDate())
+ .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
+ .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);
+ }
}
diff --git a/src/main/java/com/svlada/security/model/UnsafeJwtToken.java b/src/main/java/com/svlada/security/model/UnsafeJwtToken.java
index e4f2060..38eab9a 100644
--- a/src/main/java/com/svlada/security/model/UnsafeJwtToken.java
+++ b/src/main/java/com/svlada/security/model/UnsafeJwtToken.java
@@ -1,5 +1,7 @@
package com.svlada.security.model;
+import com.svlada.security.auth.jwt.TokenAuthStrategy;
+
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@@ -28,6 +30,10 @@ public class UnsafeJwtToken implements JwtToken {
return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token).getBody();
}
+ public SafeJwtToken authenticate(TokenAuthStrategy strategy) {
+ return strategy.authenticate(this.token);
+ }
+
@Override
public String getToken() {
return token;