Implemented AJAX based authentication

This commit is contained in:
svlada 2016-08-04 16:47:31 +02:00
parent 5bacd10112
commit b12f1a1137
28 changed files with 1048 additions and 2 deletions

83
etc/blog.md Normal file
View File

@ -0,0 +1,83 @@
## Table of contents:
1. <a title="Introduction: JWT Token" href="#introduction">Introduction</a>
2. <a title="Ajax authentication" id="ajax-authentication">Ajax authentication</a>
### <a name="introduction" id="introduction">Introduction</a>
Following are three scenarios that will be implemented in this tutorial:
1. Ajax Authentication
2. JWT Token
3. URL Based Authentication with JWT Token
### Prerequisites
First step is to create empty Spring Boot project. Visit spring initializr website(https://start.spring.io/) to generate boilerplate.
Lets start by creating base package structure for our sample code.
```
+---main
| +---java
| | +---com
| | | \---svlada
| | | +---common
| | | \---security
| | | +---auth
| | | | +---ajax
| | | | \---jwt
| | | +---config
| | | +---exceptions
| | | \---model
| \---resources
| +---static
| \---templates
\---test
\---java
\---com
\---svlada
```
### <a name="ajax-authentication" id="ajax-authentication">Ajax authentication</a>
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.
1. AjaxLoginProcessingFilter
2. AjaxAuthenticationProvider
3. AjaxAwareAuthenticationSuccessHandler
4. AjaxAwareAuthenticationFailureHandler
5. RestAuthenticationEntryPoint
6. WebSecurityConfig
Let's dive in the implementation details.
#### AjaxLoginProcessingFilter
#### Security Config
Create WebSecurityConfig class and put it in the com.svlada.security.config package.
WebSecurityConfig class needs to extend org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.
#### Un-successufull access to protected resource
Request
```
GET /api/me HTTP/1.1
Host: localhost:9966
Cache-Control: no-cache
```
Response
```
{
"timestamp": 1470301809962,
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/api/me"
}
```
#### Successufull ajax authentication

33
pom.xml
View File

@ -22,6 +22,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<jjwt.version>0.6.0</jjwt.version>
</properties>
<dependencies>
@ -33,12 +34,44 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>

View File

@ -2,10 +2,18 @@ package com.svlada;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* Sample application for demonstrating security with JWT Tokens
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
@SpringBootApplication
@EnableConfigurationProperties
public class SpringbootSecurityJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSecurityJwtApplication.class, args);
}

View File

@ -0,0 +1,27 @@
package com.svlada.common;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Enumeration of REST Error types.
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public enum ErrorCode {
GLOBAL(2),
AUTHENTICATION(10), JWT_TOKEN_EXPIRED(11);
private int errorCode;
private ErrorCode(int errorCode) {
this.errorCode = errorCode;
}
@JsonValue
public int getErrorCode() {
return errorCode;
}
}

View File

@ -0,0 +1,52 @@
package com.svlada.common;
import java.util.Date;
import org.springframework.http.HttpStatus;
/**
* Error model for interacting with client.
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public class ErrorResponse {
// HTTP Response Status Code
private final HttpStatus status;
// General Error message
private final String message;
// Error code
private final ErrorCode errorCode;
private final Date timestamp;
protected ErrorResponse(final String message, final ErrorCode errorCode, HttpStatus status) {
this.message = message;
this.errorCode = errorCode;
this.status = status;
this.timestamp = new java.util.Date();
}
public static ErrorResponse of(final String message, final ErrorCode errorCode, HttpStatus status) {
return new ErrorResponse(message, errorCode, status);
}
public Integer getStatus() {
return status.value();
}
public String getMessage() {
return message;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public Date getTimestamp() {
return timestamp;
}
}

View File

@ -0,0 +1,31 @@
package com.svlada.common;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.savedrequest.SavedRequest;
/**
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public class WebUtil {
private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
private static final String X_REQUESTED_WITH = "X-Requested-With";
private static final String CONTENT_TYPE = "Content-type";
private static final String CONTENT_TYPE_JSON = "application/json";
public static boolean isAjax(HttpServletRequest request) {
return XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH));
}
public static boolean isAjax(SavedRequest request) {
return request.getHeaderValues(X_REQUESTED_WITH).contains(XML_HTTP_REQUEST);
}
public static boolean isContentTypeJson(SavedRequest request) {
return request.getHeaderValues(CONTENT_TYPE).contains(CONTENT_TYPE_JSON);
}
}

View File

@ -0,0 +1,24 @@
package com.svlada.profile.endpoint;
import org.apache.commons.lang3.NotImplementedException;
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.security.model.UserContext;
/**
* End-point for retrieving logged-in user details.
*
* @author vladimir.stankovic
*
* Aug 4, 2016
*/
@RestController
public class ProfileEndpoint {
@RequestMapping(value="/api/me", method=RequestMethod.GET)
public @ResponseBody UserContext get() {
throw new NotImplementedException("Not implemented");
}
}

View File

@ -0,0 +1,27 @@
package com.svlada.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
/**
*
* @author vladimir.stankovic
*
* Aug 4, 2016
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
throws IOException, ServletException {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
}

View File

@ -0,0 +1,74 @@
package com.svlada.security.auth;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import com.svlada.security.model.JwtToken;
import com.svlada.security.model.SafeJwtToken;
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.
*
* @author vladimir.stankovic
*
* 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);
}
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();
}
}

View File

@ -0,0 +1,52 @@
package com.svlada.security.auth.ajax;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.svlada.security.auth.JwtAuthenticationToken;
import com.svlada.security.model.JwtTokenFactory;
import com.svlada.security.model.SafeJwtToken;
import com.svlada.security.model.UserContext;
import com.svlada.security.service.UserService;
/**
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
@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));
}
}

View File

@ -0,0 +1,55 @@
package com.svlada.security.auth.ajax;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.common.ErrorCode;
import com.svlada.common.ErrorResponse;
import com.svlada.security.exceptions.AuthMethodNotSupportedException;
import com.svlada.security.exceptions.JwtExpiredTokenException;
/**
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
@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));
}
}

View File

@ -0,0 +1,63 @@
package com.svlada.security.auth.ajax;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.security.auth.JwtAuthenticationToken;
import com.svlada.security.model.JwtToken;
/**
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
@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);
}
}

View File

@ -0,0 +1,82 @@
package com.svlada.security.auth.ajax;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.common.WebUtil;
import com.svlada.security.exceptions.AuthMethodNotSupportedException;
/**
* AjaxLoginProcessingFilter
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class);
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);
}
}

View File

@ -0,0 +1,31 @@
package com.svlada.security.auth.ajax;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Model intended to be used for AJAX based authentication.
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public class LoginRequest {
private String username;
private String password;
@JsonCreator
public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@ -0,0 +1,46 @@
package com.svlada.security.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "demo.security.jwt")
public class JwtSettings {
/**
* {@link JwtToken} will expire after this time.
*/
private Integer tokenExpirationTime;
/**
* Token issuer.
*/
private String tokenIssuer;
/**
* Key is used to sign {@link JwtToken}.
*/
private String tokenSigningKey;
public Integer getTokenExpirationTime() {
return tokenExpirationTime;
}
public void setTokenExpirationTime(Integer tokenExpirationTime) {
this.tokenExpirationTime = tokenExpirationTime;
}
public String getTokenIssuer() {
return tokenIssuer;
}
public void setTokenIssuer(String tokenIssuer) {
this.tokenIssuer = tokenIssuer;
}
public String getTokenSigningKey() {
return tokenSigningKey;
}
public void setTokenSigningKey(String tokenSigningKey) {
this.tokenSigningKey = tokenSigningKey;
}
}

View File

@ -0,0 +1,77 @@
package com.svlada.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.svlada.security.RestAuthenticationEntryPoint;
import com.svlada.security.auth.ajax.AjaxAuthenticationProvider;
import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter;
/**
* WebSecurityConfig
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
@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);
}
}

View File

@ -0,0 +1,17 @@
package com.svlada.security.exceptions;
import org.springframework.security.authentication.AuthenticationServiceException;
/**
*
* @author vladimir.stankovic
*
* Aug 4, 2016
*/
public class AuthMethodNotSupportedException extends AuthenticationServiceException {
private static final long serialVersionUID = 3705043083010304496L;
public AuthMethodNotSupportedException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,30 @@
package com.svlada.security.exceptions;
import org.springframework.security.core.AuthenticationException;
import com.svlada.security.model.JwtToken;
/**
*
* @author vladimir.stankovic
*
* Aug 3, 2016
*/
public class JwtExpiredTokenException extends AuthenticationException {
private static final long serialVersionUID = -5959543783324224864L;
private JwtToken token;
public JwtExpiredTokenException(String msg) {
super(msg);
}
public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) {
super(msg, t);
this.token = token;
}
public String token() {
return this.token.getToken();
}
}

View File

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

View File

@ -0,0 +1,74 @@
package com.svlada.security.model;
import java.util.Collection;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.stereotype.Component;
import com.svlada.security.config.JwtSettings;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Collections;
/**
* Factory class that should be always used to create {@link JwtToken}.
*
* @author vladimir.stankovic
*
* May 31, 2016
*/
@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<GrantedAuthority> 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);
}
/**
* 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,30 @@
package com.svlada.security.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.jsonwebtoken.Claims;
/**
* Raw representation of JWT Token.
*
* @author vladimir.stankovic
*
* May 31, 2016
*/
public final class SafeJwtToken implements JwtToken {
private final String rawToken;
@JsonIgnore private Claims claims;
protected SafeJwtToken(final String token, Claims claims) {
this.rawToken = token;
this.claims = claims;
}
public String getToken() {
return this.rawToken;
}
public Claims getClaims() {
return claims;
}
}

View File

@ -0,0 +1,35 @@
package com.svlada.security.model;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
public class UnsafeJwtToken implements JwtToken {
private String token;
public UnsafeJwtToken(String token) {
this.token = token;
}
/**
* Validates JWT Token signature.
*
*/
public void validateToken(String signingKey) {
Jwts.parser().setSigningKey(signingKey).parseClaimsJws(this.token);
}
/**
* Extract Claims object from the rawToken.
*
* @param signingKey
* @return
*/
public Claims parseClaims(String signingKey) {
return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token).getBody();
}
@Override
public String getToken() {
return token;
}
}

View File

@ -0,0 +1,35 @@
package com.svlada.security.model;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
/**
*
* @author vladimir.stankovic
*
* Aug 4, 2016
*/
public class UserContext {
private final String username;
private final String email;
private final List<GrantedAuthority> authorities;
public UserContext(String username, String email, List<GrantedAuthority> authorities) {
this.username = username;
this.email = email;
this.authorities = authorities;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public List<GrantedAuthority> getAuthorities() {
return authorities;
}
}

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,27 @@
package com.svlada.security.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import com.svlada.security.model.UserContext;
import com.svlada.security.model.UserRole;
/**
* Mock implementation.
*
* @author vladimir.stankovic
*
* Aug 4, 2016
*/
@Service
public class UserService {
public UserContext loadUser(String username, String password) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.authority()));
return new UserContext(username, "svlada@gmail.com", authorities);
}
}

View File

@ -1 +0,0 @@
server.port=9966

View File

@ -0,0 +1,6 @@
server.port: 9966
spring.profiles: default
demo.security.jwt:
tokenExpirationTime: 2 # Number of minutes
tokenIssuer: http://svlada.com
tokenSigningKey: xm8EV6Hy5RMFK4EEACIDAwQus

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.springframework" level="ERROR"/>
<logger name="com.svlada" level="ALL"/>
</configuration>