diff --git a/etc/blog.md b/etc/blog.md new file mode 100644 index 0000000..2daa9b1 --- /dev/null +++ b/etc/blog.md @@ -0,0 +1,83 @@ +## Table of contents: +1. Introduction +2. Ajax authentication + +### Introduction + +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 +``` + +### 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. + +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 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 013d095..7a8dd1d 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ UTF-8 UTF-8 1.8 + 0.6.0 @@ -33,12 +34,44 @@ org.springframework.boot spring-boot-starter-web + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + org.apache.commons + commons-lang3 + 3.3.2 + + + joda-time + joda-time + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + org.springframework.boot spring-boot-starter-test test + + org.springframework.boot + spring-boot-configuration-processor + true + diff --git a/src/main/java/com/svlada/SpringbootSecurityJwtApplication.java b/src/main/java/com/svlada/SpringbootSecurityJwtApplication.java index a974601..e83232f 100644 --- a/src/main/java/com/svlada/SpringbootSecurityJwtApplication.java +++ b/src/main/java/com/svlada/SpringbootSecurityJwtApplication.java @@ -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); } diff --git a/src/main/java/com/svlada/common/ErrorCode.java b/src/main/java/com/svlada/common/ErrorCode.java new file mode 100644 index 0000000..995a337 --- /dev/null +++ b/src/main/java/com/svlada/common/ErrorCode.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/common/ErrorResponse.java b/src/main/java/com/svlada/common/ErrorResponse.java new file mode 100644 index 0000000..40fbb67 --- /dev/null +++ b/src/main/java/com/svlada/common/ErrorResponse.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/common/WebUtil.java b/src/main/java/com/svlada/common/WebUtil.java new file mode 100644 index 0000000..aa1aff4 --- /dev/null +++ b/src/main/java/com/svlada/common/WebUtil.java @@ -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); + } +} diff --git a/src/main/java/com/svlada/profile/endpoint/ProfileEndpoint.java b/src/main/java/com/svlada/profile/endpoint/ProfileEndpoint.java new file mode 100644 index 0000000..985d3e7 --- /dev/null +++ b/src/main/java/com/svlada/profile/endpoint/ProfileEndpoint.java @@ -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"); + } +} diff --git a/src/main/java/com/svlada/security/RestAuthenticationEntryPoint.java b/src/main/java/com/svlada/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..c13e47a --- /dev/null +++ b/src/main/java/com/svlada/security/RestAuthenticationEntryPoint.java @@ -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"); + } +} diff --git a/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java b/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java new file mode 100644 index 0000000..8ae063b --- /dev/null +++ b/src/main/java/com/svlada/security/auth/JwtAuthenticationToken.java @@ -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 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(); + } +} diff --git a/src/main/java/com/svlada/security/auth/ajax/AjaxAuthenticationProvider.java b/src/main/java/com/svlada/security/auth/ajax/AjaxAuthenticationProvider.java new file mode 100644 index 0000000..17c0755 --- /dev/null +++ b/src/main/java/com/svlada/security/auth/ajax/AjaxAuthenticationProvider.java @@ -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)); + } +} diff --git a/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationFailureHandler.java b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationFailureHandler.java new file mode 100644 index 0000000..4781b40 --- /dev/null +++ b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationFailureHandler.java @@ -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)); + } +} diff --git a/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java new file mode 100644 index 0000000..7babfbd --- /dev/null +++ b/src/main/java/com/svlada/security/auth/ajax/AjaxAwareAuthenticationSuccessHandler.java @@ -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); + } +} diff --git a/src/main/java/com/svlada/security/auth/ajax/AjaxLoginProcessingFilter.java b/src/main/java/com/svlada/security/auth/ajax/AjaxLoginProcessingFilter.java new file mode 100644 index 0000000..84cec43 --- /dev/null +++ b/src/main/java/com/svlada/security/auth/ajax/AjaxLoginProcessingFilter.java @@ -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); + } +} diff --git a/src/main/java/com/svlada/security/auth/ajax/LoginRequest.java b/src/main/java/com/svlada/security/auth/ajax/LoginRequest.java new file mode 100644 index 0000000..d6cf162 --- /dev/null +++ b/src/main/java/com/svlada/security/auth/ajax/LoginRequest.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/security/config/JwtSettings.java b/src/main/java/com/svlada/security/config/JwtSettings.java new file mode 100644 index 0000000..b617f28 --- /dev/null +++ b/src/main/java/com/svlada/security/config/JwtSettings.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/security/config/WebSecurityConfig.java b/src/main/java/com/svlada/security/config/WebSecurityConfig.java new file mode 100644 index 0000000..40fa2e4 --- /dev/null +++ b/src/main/java/com/svlada/security/config/WebSecurityConfig.java @@ -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); + } +} diff --git a/src/main/java/com/svlada/security/exceptions/AuthMethodNotSupportedException.java b/src/main/java/com/svlada/security/exceptions/AuthMethodNotSupportedException.java new file mode 100644 index 0000000..236d15a --- /dev/null +++ b/src/main/java/com/svlada/security/exceptions/AuthMethodNotSupportedException.java @@ -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); + } +} diff --git a/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java b/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java new file mode 100644 index 0000000..0b1c18c --- /dev/null +++ b/src/main/java/com/svlada/security/exceptions/JwtExpiredTokenException.java @@ -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(); + } +} diff --git a/src/main/java/com/svlada/security/model/JwtToken.java b/src/main/java/com/svlada/security/model/JwtToken.java new file mode 100644 index 0000000..d2afe7f --- /dev/null +++ b/src/main/java/com/svlada/security/model/JwtToken.java @@ -0,0 +1,5 @@ +package com.svlada.security.model; + +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/JwtTokenFactory.java new file mode 100644 index 0000000..6af378e --- /dev/null +++ b/src/main/java/com/svlada/security/model/JwtTokenFactory.java @@ -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 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. + * + * 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/SafeJwtToken.java b/src/main/java/com/svlada/security/model/SafeJwtToken.java new file mode 100644 index 0000000..6150fc9 --- /dev/null +++ b/src/main/java/com/svlada/security/model/SafeJwtToken.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/security/model/UnsafeJwtToken.java b/src/main/java/com/svlada/security/model/UnsafeJwtToken.java new file mode 100644 index 0000000..e4f2060 --- /dev/null +++ b/src/main/java/com/svlada/security/model/UnsafeJwtToken.java @@ -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; + } +} diff --git a/src/main/java/com/svlada/security/model/UserContext.java b/src/main/java/com/svlada/security/model/UserContext.java new file mode 100644 index 0000000..ee126c8 --- /dev/null +++ b/src/main/java/com/svlada/security/model/UserContext.java @@ -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 authorities; + + public UserContext(String username, String email, List authorities) { + this.username = username; + this.email = email; + this.authorities = authorities; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } + + public List getAuthorities() { + return authorities; + } +} diff --git a/src/main/java/com/svlada/security/model/UserRole.java b/src/main/java/com/svlada/security/model/UserRole.java new file mode 100644 index 0000000..ee243c9 --- /dev/null +++ b/src/main/java/com/svlada/security/model/UserRole.java @@ -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(); + } +} diff --git a/src/main/java/com/svlada/security/service/UserService.java b/src/main/java/com/svlada/security/service/UserService.java new file mode 100644 index 0000000..ac8b7b6 --- /dev/null +++ b/src/main/java/com/svlada/security/service/UserService.java @@ -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 authorities = new ArrayList(); + authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.authority())); + return new UserContext(username, "svlada@gmail.com", authorities); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 7c5b6a3..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -server.port=9966 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e4498af --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,6 @@ +server.port: 9966 +spring.profiles: default +demo.security.jwt: + tokenExpirationTime: 2 # Number of minutes + tokenIssuer: http://svlada.com + tokenSigningKey: xm8EV6Hy5RMFK4EEACIDAwQus \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..54a4b48 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file