Session Management,REST+Oauth2+Spring

Well, REST applications are stateless, means they do not keep any client related data in Server.The server is stateless means that every server can serve any client at any time. The client’s state should never be stored anywhere in the Server. This is how the definition of REST goes.

Well, in my case, I had a requirement like this, “The user X must have single active session or in another words he can  log  in only at one place (browser or whatever).

So my idea is  as follows. As I am already using Oauth2 access token to access protected resources, I can use this access token as kind of session ID. When user X logs in to the application, first we check if he is holding an access token.If he is holding an access token, then we get this and delete it from the TokenStore and assign a new access token to him. As the access token he was holding previously has been deleted, he can no longer access to protected resources with that access token and thus invalidating his session. And thus, he can have only one active session.

Here is how it works,

d
Idea

 

So, the implementation is as follows.

Create the project

I am using Springboot to create the application. The project structure is as shown below.

Screenshot at 2016-07-31 10-33-43
Project structure.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>myoauth</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>oauthtest</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.3.6.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<angularjs.version>1.5.5</angularjs.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security.oauth</groupId>
			<artifactId>spring-security-oauth2</artifactId>
		</dependency>
		<!-- Web jars -->
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>angularjs</artifactId>
			<version>${angularjs.version}</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

Database access layer

For simplicity I am not using any database. Just a simple Map will hold our user data.

UserDAO.class

package com.anupam.dao;

import com.anupam.entity.UserVO;

public interface UserDAO {

	UserVO getUserByUsername(String username);

	void updateAccessToken(String username, String accessToken);

}

UserDAOImpl.class

package com.anupam.dao;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.stereotype.Repository;

import com.anupam.entity.UserVO;

@Repository
public class UserDAOImpl implements UserDAO {

	private static Map<String, UserVO> mapUsers;

	@PostConstruct
	public void init() {
		mapUsers = new HashMap<>();
		mapUsers.put("admin", new UserVO("admin", "1234", "ADMIN"));
		mapUsers.put("agogoi", new UserVO("agogoi", "1234", "ADMIN"));
	}

	@Override
	public UserVO getUserByUsername(String username) {
		UserVO user = mapUsers.get(username);
		return user;
	}

	@Override
	public void updateAccessToken(String username, String accessToken) {
		UserVO user = mapUsers.get(username);
		if (user != null) {
			user.setAccess_token(accessToken);
		}
	}
}

UserVO.class

package com.anupam.entity;

public class UserVO {

	String username, password, role, access_token;

	public UserVO(String username, String password, String role) {
		super();
		this.username = username;
		this.password = password;
		this.role = role;
	}

	public String getRole() {
		return role;
	}

	public void setRole(String role) {
		this.role = role;
	}

	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 getAccess_token() {
		return access_token;
	}

	public void setAccess_token(String access_token) {
		this.access_token = access_token;
	}

}

The Oauth2 configuration.

I have already written about the Oauth2 configuration in my previous post. Please do have a look into it.

AppSecurityConfig.class

package com.anupam.security;

import java.util.ArrayList;

import javax.servlet.ServletContext;

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.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

import com.anupam.dao.UserDAO;
import com.anupam.entity.UserVO;

@Configuration
@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	ServletContext ctx;

	@Autowired
	UserDAO userDAO;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// auth.inMemoryAuthentication().withUser("admin").password("1234").roles("USER");
		// auth.inMemoryAuthentication().withUser("agogoi").password("1234").roles("USER");
		auth.authenticationProvider(new AuthenticationProvider() {

			@Override
			public boolean supports(Class<?> authentication) {
				// TODO Auto-generated method stub
				return true;
			}

			@Override
			public Authentication authenticate(Authentication authentication) throws AuthenticationException {
				String username = authentication.getName();
				String password = authentication.getCredentials().toString();

				UserVO userVO = userDAO.getUserByUsername(username);
				if (userVO.getPassword().equals(password)) {

					// Set the current logged user.
					ctx.setAttribute("LOGGED_USER", userVO);

					UsernamePasswordAuthenticationToken u = new UsernamePasswordAuthenticationToken(userVO, password,
							new ArrayList<>());
					return u;
				} else {
					return null;
				}
			}
		});
	}

	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	/**
	 * Resource server.
	 * 
	 * @author anupam
	 *
	 */
	@Configuration
	@EnableResourceServer
	public static class ResourceServer extends ResourceServerConfigurerAdapter {

		@Override
		public void configure(ResourceServerSecurityConfigurer resources) {
			resources.resourceId("test");
		}

		@Override
		public void configure(HttpSecurity http) throws Exception {
			http.authorizeRequests().antMatchers("/login").permitAll();
			http.authorizeRequests().antMatchers("/data").authenticated();
		}

	}

}

AuthServer.class

package com.anupam.security;

import java.util.UUID;

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.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthServer extends AuthorizationServerConfigurerAdapter {

	@Bean
	public TokenStore tokenStore() {
		InMemoryTokenStore ts = new InMemoryTokenStore();

		// Force to generate unique token. Otherwise it generates reusable
		// access token.
		ts.setAuthenticationKeyGenerator(new AuthenticationKeyGenerator() {

			@Override
			public String extractKey(OAuth2Authentication authentication) {
				return UUID.randomUUID().toString();
			}
		});
		return ts;
	}

	@Autowired
	private AuthenticationManager authenticationManager;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.inMemory().withClient("web").secret("websecret").scopes("read", "write")
				.accessTokenValiditySeconds(3000)
				.authorizedGrantTypes("password", "refresh_token", "client_credentials");

	}

}

The simple controller.

MyController.class

package com.anupam.controller;

import java.util.Map;

import javax.servlet.ServletContext;

import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.RequestBody;
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.anupam.dao.UserDAO;
import com.anupam.entity.UserVO;

@RestController
public class MyController {

	@Autowired
	TokenStore tokenStore;

	@Autowired
	ServletContext ctx;

	@Autowired
	UserDAO userDAO;

	@RequestMapping(path = "/data", method = RequestMethod.POST)
	public @ResponseBody String getData(@RequestBody String params) {
		return "You have successfully accessed the service with the access token.";
	}

	@RequestMapping(path = "/login", method = RequestMethod.POST)
	public Map<?, ?> doLogin(@RequestBody Map<String, Object> params) throws Exception {

		String username = (String) params.get("username");
		String password = (String) params.get("password");

		// Get the access token for the currently successful logged in user.
		// This method call sets the logged user in a context attribute in the
		// {@link AppSecurityConfig} class.
		Map<?, ?> map = getAccessToken(username, password);

		// Get logged user from the context.
		UserVO loggedUser = (UserVO) ctx.getAttribute("LOGGED_USER");

		// Now revoke the access token he is holding.
		// This means that if the user is logged in in some browser, he will no
		// longer be able to access
		// the secure REST services as we are deleting his access token.
		if (loggedUser.getAccess_token() != null && !loggedUser.getAccess_token().equals("")) {
			revokeAccessToken(loggedUser.getAccess_token());
		}

		// Now update the user with the new access token.
		loggedUser.setAccess_token((String) map.get("access_token"));

		// We do not need any information of the logged in user.
		// So delete this information for the context attribute.
		ctx.removeAttribute("LOGGED_USER");

		return map;
	}

	private Map<?, ?> getAccessToken(String username, String password) throws Exception {
		String authUrl = "http://localhost:8080/app/oauth/token?";
		StringBuilder authParams = new StringBuilder("client_id=web&client_secret=websecret&grant_type=password");
		authParams.append("&username=").append(username).append("&password=").append(password);

		String url = authUrl.concat(authParams.toString());
		ResponseEntity<String> response = new TestRestTemplate("web", "websecret").postForEntity(url, null,
				String.class);
		String responseText = response.getBody();

		Map<?, ?> map = new ObjectMapper().readValue(responseText, Map.class);
		return map;
	}

	private void revokeAccessToken(String token) {
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
		if (accessToken != null) {
			tokenStore.removeAccessToken(accessToken);
		}
	}

}

The main class.

Application.class

package com.anupam;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

The tests

Lets test the application. I am using Postman to test.

In the figure 1, I have logged in using username=admin and password=1234. I got the access token and with the access token I have accessed a protected resource in the path /data as shown in figure 2.

22
Figure 1

After successful login, I have updated the user with this access token granted.If you look carefully there is a property called access_token in the UserVO.

Access the protected resource in the path /data.

33
Figure 2

 

Now lets login with the same username and password. (The right hand side postman window)

44
Logged in with same username and password. (Right window)

Now what exactly happened in the background ?

When I logged in with the same username and password, in the background, I searched for the user and checked if there was any existing access token associated with him. Yes, I have found an access token associated with him as the user had previously logged into the application. So, I get that access token and delete from the TokenStore. It means that in the previously logged in session, the user no longer can use that access token and thus his session is invalidated.

How to prove it ?

Lets access the resource at the path /data in the previously logged in session.

55
Token is not valid anymore.

So, I get a message saying that the access_token is no longer valid.

Lets access the protected resource in the newly logged in session.

66

We can successfully access the protected resource.

And thus we can let only active session per user.

Thank you for reading my blog.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s