Showing posts with label Spring Security. Show all posts
Showing posts with label Spring Security. Show all posts

Friday, January 4, 2019

6. Introducing Spring Security

If the web services that we are building require any level of authentication and authorization, it is better to understand Spring security context. Services that are properly built with security context can implement a better level of security at each method and endpoint level.
We want to handle all the security, authentication and, authorization related code in a single block so that managing it becomes easier.
The first step that we need to do is to add security dependencies in the pom.xml.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>

For authentication and authorization to make sense, we need to have a concept of a Role within the system. The roles are required to authorize the users for specific purposes. We define a set of roles in the form of a bitmask defined as an enum.
package in.springframework.blog.tutorials;
import java.util.HashSet;
import java.util.Set;
public enum Role {
USER(0x01), // 0 User
TESTER(0x01 << 1), // 1 Tester
ADMIN(0x01 << 2), // 2 Admin
REFRESH(0x01 << 3),
ANONYMOUS(0x01 << 4); // 5 Only for internal use for refresh token.
private final int mask;
Role(int mask) {
this.mask = mask;
}
public static Set<Role> createFromMask(long mask) {
Set<Role> output = new HashSet<>();
for (Role r : values()) {
if ((r.mask & mask) != 0) {
output.add(r);
}
}
return output;
}
}
view raw Role.java hosted with ❤ by GitHub

Now that we have defined Roles, we need to add few fields in our User class. We add a field mask for holding allowed roles and two different fields for authToken and expiry. We also add an index in the User entity.
package in.springframework.blog.tutorials;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "user",
uniqueConstraints =
{
@UniqueConstraint(name = "uq_email", columnNames = {"email"}),
@UniqueConstraint(name = "uq_authToken", columnNames = {"authToken"}),
@UniqueConstraint(name = "uq_username", columnNames = {"username"})
})
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String fullname;
private String username;
private String password;
private String email;
private long mask;
private String authToken;
private Date expiry;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFullname() {
return fullname;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public long getMask() {
return mask;
}
public void setMask(long mask) {
this.mask = mask;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public Date getExpiry() {
return expiry;
}
public void setExpiry(Date expiry) {
this.expiry = expiry;
}
}
view raw User.java hosted with ❤ by GitHub

We also need to add a class that implements GrantedAuthority interface. An authority in the spring security system is represented by a string and we use the Role enum name as the authority in the system.
package in.springframework.blog.tutorials;
import org.springframework.security.core.GrantedAuthority;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class UserAuthority implements GrantedAuthority {
private final String authority;
public UserAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
public static Set<UserAuthority> getAuthoritiesFromRoles(List<Role> roles) {
return roles.stream().map(r -> new UserAuthority(r.name())).collect(Collectors.toSet());
}
public static Set<UserAuthority> getAuthoritiesFromRoles(long mask) {
return Role.createFromMask(mask).stream().map(r -> new UserAuthority(r.name())).collect(Collectors.toSet());
}
}

We probably need to have two different types of authentication and authorization within the server. The first authentication and authorization will work with username and password and the second one will work with the token. Spring security requires two different entities to be defined for authentication and authorization. We need a Principal and a Provider class each for username and token authentication and authorization. Let's first look at the class required for authentication using username and password.  A principal is nothing but the abstraction of credentials that is flowing through spring security. We define it as below.
package in.springframework.blog.tutorials;
import java.security.Principal;
import java.util.Optional;
/**
* Created by vinay on 2/3/16.
*/
public class UsernamePasswordPrincipal implements Principal {
public UsernamePasswordPrincipal(Optional<String> username, Optional<String> password) {
this.username = username;
this.password = password;
}
@Override
public String getName() {
return username.get();
}
public Optional<String> getUsername() {
return username;
}
public Optional<String> getPassword() {
return password;
}
private final Optional<String> username;
private final Optional<String> password;
}

A provider is a class that provides the authentication and authorization functionality for a particular type. The username and password provider is defined as below. As we can see, the provider class extracts the credentials from the Principal and then makes sure the password is correct for the username. It also populates the list of roles granted to the user in the form of Authority.
package in.springframework.blog.tutorials;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import javax.transaction.Transactional;
import java.util.Calendar;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
private final static Logger logger = Logger.getLogger(UsernamePasswordAuthenticationProvider.class);
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordPrincipal principal = (UsernamePasswordPrincipal) authentication.getPrincipal();
Optional<User> userOptional = userRepository.findUserByEmailOrUsername(principal.getName());
if (userOptional.isPresent()) {
User user = userOptional.get();
if (PasswordManager.INSTANCE.matches(principal.getPassword(), user.getPassword())) {
user = userRepository.findById(user.getId()).get();
user.setAuthToken(UUID.randomUUID().toString() + "-" + UUID.randomUUID());
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.HOUR, 24);
user.setExpiry(calendar.getTime());
RequestContext.currentUser.set(user);
return new UsernamePasswordAuthenticationToken(principal, null, UserAuthority.getAuthoritiesFromRoles(user.getMask()));
}
else {
throw new BadCredentialsException(String.format("User %s doesn't exist.", principal.getName()));
}
}
else {
throw new BadCredentialsException(String.format("User %s doesn't exist.", principal.getName()));
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

Similarly, we define a Token principal. This principal just contains the token value that is received from the user.
package in.springframework.blog.tutorials;
import java.security.Principal;
import java.util.Optional;
/**
* Created by vinay on 2/3/16.
*/
public class TokenPrincipal implements Principal {
public TokenPrincipal(Optional<String> token) {
this.name = (token.isPresent() ? token.get() : "None");
this.token = token;
}
public Optional<String> getToken() {
return token;
}
public void setToken(Optional<String> token) {
this.token = token;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
private String name;
private Optional<String> token;
}

We also define a token provider. The token provider looks up the user from the database by querying it from token and authenticates the user. In this example, we are making an implicit assumption that a token will be unique across all the users. If that were not to be the case, we will also have to pass the username or some other identifier for the user along with the token during authentication.
package in.springframework.blog.tutorials;
import com.google.gson.Gson;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import java.io.IOException;
import java.util.*;
public class TokenAuthenticationProvider implements AuthenticationProvider {
private final static Logger logger = Logger.getLogger(TokenAuthenticationProvider.class);
@Autowired
private UserRepository userRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
TokenPrincipal principal = (TokenPrincipal) authentication.getPrincipal();
Optional<User> userOptional = userRepository.findUserByAuthToken(principal.getName());
if (userOptional.isPresent()) {
User user = userOptional.get();
if (user.getExpiry() != null && user.getExpiry().after(new Date())) {
RequestContext.currentUser.set(user);
return new PreAuthenticatedAuthenticationToken(principal, null, UserAuthority.getAuthoritiesFromRoles(user.getMask()));
}
else {
throw new BadCredentialsException(String.format("User %s doesn't exist.", principal.getName()));
}
}
else {
throw new BadCredentialsException(String.format("User %s doesn't exist.", principal.getName()));
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
}

We also define a class RequestContext that will contain a set of thread local variables so that once authenticated, we will not need to look up the user details from the database. We will only need to hit the database in cases where we need to update the record in the User table.
package in.springframework.blog.tutorials;
public class RequestContext {
public static final ThreadLocal<User> currentUser = new ThreadLocal();
}

We can see in both provider classes, after successful authentication of the user, the User is set into the thread local so that we can use it within the thread boundaries of that request.
Now the providers and principals are in place, we need to start using them. Rather than using them on a case to case basis, the best way is to define a filter which applies on each request and authentication and authorization can be done on all requests.
package in.springframework.blog.tutorials;
import com.google.gson.Gson;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
/**
* Created by vinay on 2/3/16.
*/
public class AuthenticationFilter extends GenericFilterBean {
public static final String TOKEN_SESSION_KEY = "token";
public static final String USER_SESSION_KEY = "user";
private final static Logger logger = Logger.getLogger(AuthenticationFilter.class);
private AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* Entry point into the authentication filter. We check if the token and token cache is present, we do token based
* authentication. Otherwise we assume it to be username and password based authentication.
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = asHttp(response);
try {
HttpServletRequest httpRequest = asHttp(request);
if (httpRequest.getRequestURI().toString().equals("/authenticate") && httpRequest.getMethod().equals("POST")) {
Optional<String> username = getOptionalHeader(httpRequest,"username");
Optional<String> password = getOptionalHeader(httpRequest,"password");
UsernamePasswordPrincipal usernamePasswordPrincipal = new UsernamePasswordPrincipal(username, password);
processUsernameAuthentication(usernamePasswordPrincipal);
}
else {
Optional<String> token = getOptionalHeader(httpRequest,"token");
TokenPrincipal authTokenPrincipal = new TokenPrincipal(token);
processTokenAuthentication(authTokenPrincipal);
}
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException e) {
SecurityContextHolder.clearContext();
handleExceptions(e, asHttp(response));
logger.error("Internal authentication service exception", e);
} catch (AuthenticationException e) {
SecurityContextHolder.clearContext();
handleExceptions(e, asHttp(response));
} catch(Exception e) {
handleExceptions(e, asHttp(response));
}
finally {
}
}
private HttpServletRequest asHttp(ServletRequest request) {
return (HttpServletRequest) request;
}
private HttpServletResponse asHttp(ServletResponse response) {
return (HttpServletResponse) response;
}
private void processUsernameAuthentication(UsernamePasswordPrincipal usernamePasswordPrincipal) {
Authentication resultOfAuthentication = tryToAuthenticateWithUsername(usernamePasswordPrincipal);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithUsername(UsernamePasswordPrincipal usernamePasswordPrincipal) {
UsernamePasswordAuthenticationToken requestAuthentication
= new UsernamePasswordAuthenticationToken(usernamePasswordPrincipal, usernamePasswordPrincipal.getPassword());
return tryToAuthenticate(requestAuthentication);
}
private void processTokenAuthentication(TokenPrincipal tokenPrincipal) {
Authentication resultOfAuthentication = tryToAuthenticateWithToken(tokenPrincipal);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithToken(TokenPrincipal tokenPrincipal) {
PreAuthenticatedAuthenticationToken requestAuthentication
= new PreAuthenticatedAuthenticationToken(tokenPrincipal, null);
return tryToAuthenticate(requestAuthentication);
}
private Authentication tryToAuthenticate(Authentication requestAuthentication) {
Authentication responseAuthentication = authenticationManager.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException("Unable to authenticate Domain User for provided credentials");
}
logger.debug("User successfully authenticated");
return responseAuthentication;
}
private void handleExceptions(Exception ex, HttpServletResponse response) {
try {
if (ex instanceof EntityAlreadyExistsException) {
EntityAlreadyExistsException e = (EntityAlreadyExistsException) ex;
handleExceptionMsg(HttpStatus.UNPROCESSABLE_ENTITY.value(), e.getErrorCode(), response, e);
} else if (ex instanceof NotFoundException) {
NotFoundException e = (NotFoundException) ex;
handleExceptionMsg(HttpStatus.NOT_FOUND.value(), e.getErrorCode(), response, e);
} else {
handleExceptionMsg(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.value(), response, ex);
}
}
catch(Exception jsonex) {
logger.log(Level.ERROR, "Error during error generation", jsonex);
}
}
private void handleExceptionMsg(int status, int errorCode, HttpServletResponse response, Exception e)
throws IOException {
ExceptionResponse er
= new ExceptionResponseBuilder()
.setStatus(status)
.setCode(errorCode)
.setMessage(e.getMessage())
.setMoreInfo(String.format(SanjnanConstants.EXCEPTION_URL,errorCode))
.createExceptionResponse();
response.setStatus(status);
Gson gson = new Gson();
String tokenJsonResponse = gson.toJson(er);
response.addHeader("Content-Type", "application/json");
response.getWriter().print(tokenJsonResponse);
if (e instanceof BadRequestException ||
e instanceof BadCredentialsException ) {
// these are repetitive exceptions and having stacktrace in the logs does not help us..
// rather log becomes too big.
// Log only the message so we can keep track
logger.error(e.getMessage());
} else {
// log the entire details.. we need the stack trace here...
logger.error(tokenJsonResponse, e);
}
}
private Optional<String> getOptionalParameter(HttpServletRequest httpRequest, String parameterName) {
String[] values = httpRequest.getParameterValues(parameterName);
if (values.length == 0) {
return Optional.of((String)null);
}
return Optional.of(values[0]);
}
private Optional<String> getOptionalHeader(HttpServletRequest httpRequest, String headerName) {
return Optional.of(httpRequest.getHeader(headerName));
}
}

AuthenticationFilter provides the doFilter method which is called for each incoming request. As we can see within the doFilter method, if we receive a POST request to /authenticate URL we assume that to be a username and password authentication and we process it as a username and password authentication, in all other cases, it is assumed to be a token-based authentication. All the three parameters, username, password, and token are expected to be passed as headers.
Now we need to plug everything in so that it is called by the system. For that, we need to define a security configuration.
package in.springframework.blog.tutorials;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.http.HttpServletResponse;
/**
* Created by vinay on 1/27/16.
*/
@Configuration
@EnableWebSecurity
@EnableScheduling
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider());
auth.authenticationProvider(usernamePasswordAuthenticationProvider());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/manage/health");
web.ignoring().antMatchers("/swagger-ui.html");
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("/v2/api-docs/**");
web.ignoring().antMatchers("/error");
web.ignoring().antMatchers("/swagger-resources/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and().
authorizeRequests().
antMatchers(actuatorEndpoints()).hasAuthority(Role.ADMIN.name()).
anyRequest().authenticated().
and().
anonymous().disable().
exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint());;
http.addFilterBefore(new AuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
}
private String[] actuatorEndpoints() {
return new String[]{
SanjnanConstants.AUTOCONFIG_ENDPOINT,
SanjnanConstants.BEANS_ENDPOINT,
SanjnanConstants.CONFIGPROPS_ENDPOINT,
SanjnanConstants.ENV_ENDPOINT,
SanjnanConstants.MAPPINGS_ENDPOINT,
SanjnanConstants.METRICS_ENDPOINT,
SanjnanConstants.SHUTDOWN_ENDPOINT
};
}
@Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
@Bean
public AuthenticationProvider tokenAuthenticationProvider() {
return new TokenAuthenticationProvider();
}
@Bean
public AuthenticationProvider usernamePasswordAuthenticationProvider() {
return new UsernamePasswordAuthenticationProvider();
}
}
The SecurityConfiguration class is defined by extending WebSecurityConfigurerAdapter class. We override a few sets of methods. The first method defines a set of authentication providers that are available to the system.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(tokenAuthenticationProvider());
        auth.authenticationProvider(usernamePasswordAuthenticationProvider());
    }

The second method defines a set of URLs that would be ignored by the security system.
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/manage/health");
        web.ignoring().antMatchers("/swagger-ui.html");
        web.ignoring().antMatchers("/webjars/**");
        web.ignoring().antMatchers("/v2/api-docs/**");
        web.ignoring().antMatchers("/error");
        web.ignoring().antMatchers("/swagger-resources/**");
    }

The third method defines the security rules for all the requests.
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().
                sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
                and().
                authorizeRequests().
                antMatchers(actuatorEndpoints()).hasAuthority(Role.ADMIN.name()).
                anyRequest().authenticated().
                and().
                anonymous().disable().
                exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint());;

        http.addFilterBefore(new AuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
    }

The configuration first disables CSRF and then sets a session management policy. After that, it declares that all the actuator endpoints need to be authenticated by a user having the ADMIN roles. Then all other users need to be authenticated. With this, every request coming into the system will be automatically authenticated and authorized. Now we can annotate our endpoints with specific roles so that unauthorized users can not make the calls. For example, we add the following annotation to our endpoint that we had defined earlier.
@PreAuthorize("hasAuthority('USER')")

This annotation implies that a user with the role of USER can only call this endpoint. Any other user would not be able to call this endpoint.
package in.springframework.blog.tutorials;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional;
@RestController
@RequestMapping("/authenticate")
public class AuthenticateEndpoint {
@Autowired
private UserRepository userRepository;
@PreAuthorize(SanjnanConstants.ANNOTATION_ROLE_USER)
@RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public LoginResponse login(@RequestBody LoginRequest loginRequest) {
User user = RequestContext.currentUser.get();
return new LoginResponse(user.getAuthToken(), user.getExpiry());
}
@RequestMapping(method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
public LoginResponse logout(@RequestBody LoginRequest loginRequest) {
User user = RequestContext.currentUser.get();
user = userRepository.findById(user.getId()).get();
user.setAuthToken(null);
user.setExpiry(null);
RequestContext.currentUser.remove();
return new LoginResponse(user.getAuthToken(), user.getExpiry());
}
}

This is also our AuthenticationEndpoint. This will need to be called when a user wants to login using username and password. This now provides us with a functioning service with spring security enabled. In future posts, we will provide more details on it.  The complete code for this post is available in github repository with version 1.0. Click here to download it.