Click here to Skip to main content
15,031,055 members
Articles / Web Development / Spring
Article
Posted 22 Jun 2021

Stats

2.1K views
16 downloads

Tutorial on AngularJS Secured Session Timeout Management

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
22 Jun 2021MIT16 min read
In this tutorial, I will discuss how to handle secured session timeout in an AngularJS application.
The purpose of the sample application in this simple tutorial is to demonstrate the few ways in which AngularJS application handles secured session and session expiration.

Introduction

Imagine a scenario where a user logged into a web based application, via form based authentication. Then for two hours, the user did nothing. When the user decided to resume his/her activity, the long inactivity resulted in a session timeout. In this case, the login page should display no matter what. The challenge for my own projects (Spring Boot, Spring Security, Spring MVC and Spring REST) is that I mix the MVC code and RESTFul API code together, this would create a small problem with the proper handling of the session timeout, especially with the session timeout handling for AngularJS calls to RESTFul APIs.

Let me explain why this scenario can occur. When you examined the network traffic in the F12 developer tool on the browser side, you would imagine the session timeout would result in a response of 403 which the front end API invocation can know the timeout happened. It is not what would happen. Rather, you would see that the service returned a 302, which redirects to the login page, then the login page content was sent back to the front end. In the end, the front end received an HTML page and the status code is 200. The front end expects an meaningful object or an array of data, it gets an HTML page. In this case, I handle it as unknown error. And it is not ideal. The correct way to handle this is to identify the session timeout and force the browser to display the login page.

"This seems to be a configuration issue. Isn't it?" One may ask. And you are correct. It is a configuration issue with Spring Security. The hard part of this problem is that the MVC handling and REST API handling are combined in one application. If I separate the two, front end application using Spring MVC and the back end data processing are using only Spring REST, I wouldn't worry about the configuration. But it will create some other issues which are equally complex. So for my own projects, for now, I still combined both in the same application. This tutorial will discuss the additional configuration changes needed to distinguish the session timeout handling both for MVC and for RESTFul API in the same application.

There are two other concepts I will discuss, one is the configuration for setting the session timeout for the web application. Instead of the default 30 minutes, I can set it to 90 seconds, 90 minutes or any other reasonable number. Another concept I like to discuss is a simple solution to another minor problem. I noticed that when user logged in, then navigate back to the login page, the login page will shown again and allow user to login again. This is an annoying scenario which can be avoided, by checking whether the user has already logged in. If so, then the browser will redirect to the index page. I will begin with an overview of the sample application, following with the details of the application design.

Application Architecture

This sample application is a single page application. In order to access this page, user has to login. I re-used a lot of materials from one of my past tutorials, Using ThymeLeaf Page Template Engine with Spring Security. I needed the configuration for Spring Security so that I can use login page for secure access. Once user logged in, the index page will have a counter starting from 60. Every 3 seconds, the counter will decrease by 1, and ends at 0. There is also a button. When click on this button, the client side will invoke a simple RESTFul service. This service in only invokable after user logged in. Clicking the button will cause the browser to redirect to the login page. However, as long as the button is clicked before the counter reach 29, the counter will reset to 60. Also, if the user navigate to the login page when the counter is greater than 29, the browser will redirect to the login page.

I used Spring Boot for this application. All the pages are served by Spring Boot and Spring MVC. There is a simple service end point that provides a simple heart beat message (the client send a dummy request, and the server respond back with a simple message as acknowledgement). The page is a single page application. It can only be viewed as user logged in. I need to add some additional configuration on Spring Security, to distinguish the handling of session timeout as the following:

  • If the action is loading a page, it should cause the browser to show the login page when session expires.
  • If the action is perform a RESTFul API to the back end server, the session timeout should create a response of status 403 and a JSON object with some meaningful information.

The problem I had before is that, without this additional configuration, regardless what operation to server it is, the default session expire handling is detect the session expire as an security exception, then supply a default action of 302 redirect to the login page. To the front end AngularJS code, the RESTFul API call will get a web page instead of a meaningful and recognizable object. This is why I have to somehow distinguish the session expire handling for MVC page and RESTFul API. The next session, I will show you how this is done. And this is the first part of the most important aspect of this tutorial.

A Brief Review of Reused Components

This section will cover some of the important components which I have used in my previous tutorial. This is in case that you the reader does not want to read the other tutorial first, so I am offering a brief review. In this section, I will go over the Spring Security configuration, and the simple user authentication service.

First, this is the configuration of Spring Secuirty:

Java
package org.hanbo.boot.app.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
       WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.
       SavedRequestAwareAuthenticationSuccessHandler;
import org.hanbo.boot.app.security.UserAuthenticationService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
   @Autowired
   private UserAuthenticationService authenticationProvider;
   
   @Autowired
   private MyAuthenticationEntryPoint accessDeniedHandler;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      http.csrf().disable()
      .authorizeRequests()
          .antMatchers("/assets/**", "/public/**").permitAll()
          .anyRequest().authenticated()
      .and()
      .formLogin()
          .loginPage("/login")
          .permitAll()
          .usernameParameter("username")
          .passwordParameter("userpass")
          .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
          .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
          .and()
      .logout().logoutSuccessUrl("/public/logout")
          .permitAll()
          .and()
          .exceptionHandling()
          .authenticationEntryPoint(accessDeniedHandler);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
} 

I used the bold font to highlight the part that I have modified. This line is different from the source code of the previous tutorial. In this case, I am capturing the exceptions occurred related to Spring Security (via the invocation of .exceptionHandling()), then specify that I want to customize the handling for session expire (via the invocation of .authenticationEntryPoint(accessDeniedHandler)). This is the key point of this tutorial, the handling of the invocation of login can be customized. This is where I specify when to display login page when session does not exist or session is expired, or returning an JSON object showing access is denied. The invocation requires a parameter of type AuthenticationEntryPoint. I have provided my implementation, which is the next important point.

Here is my implementation of the interface AuthenticationEntryPoint:

Java
package org.hanbo.boot.app.config;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

@Component
public class MyAuthenticationEntryPoint
   implements AuthenticationEntryPoint
{
   @Override
   public void commence(HttpServletRequest request, 
                        HttpServletResponse response, AuthenticationException ex)
         throws IOException, ServletException
   {
      String url = request.getRequestURI();
      System.out.println(url);
      
      if (url != null && url.startsWith("/secure/api/"))
      {
         String jsonVal = createJsonResponse(false, "Access Denied");
         
         response.setContentType("application/json;charset=UTF-8");
         response.setStatus(HttpServletResponse.SC_FORBIDDEN);
         response.getWriter().write(jsonVal);
         response.getWriter().flush();
         response.getWriter().close();
      }
      else
      {
         response.sendRedirect("/login");
      }
   }
   
   private static String createJsonResponse(boolean isSuccess, String msgVal) 
           throws JsonProcessingException
   {
      ObjectMapper mapper = new ObjectMapper();
      ObjectNode rootNode = mapper.createObjectNode();
      
      Date dateNow = new Date();
      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      String dateNowVal = dateFormat.format(dateNow);
      
      rootNode.put("success", isSuccess);
      rootNode.put("timestamp", dateNowVal);
      rootNode.put("message", msgVal);
      String retVal = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
      
      return retVal;
   }
}

Implementing the AuthenticationEntryPoint is to solve a problem, for some URLs especially for page displays, when invoked after session expired, the login page must be displayed. But for any RESTFul API URLs, these invocations when session expired, should only receive an error response with status code 403 and an JSON object detail the error. I have tried everything to separate these two handling for different URLs with HttpSecurity object. It can't be done when RESTFul API methods and MVC handling methods are all mixed. So I thought of a trick to get around this. In the above implementation, the method commence() takes in HttpServletRequest object and HttpServletResponse object, this means that I can determine if what the request URI is, and making different decisions in the response.

Here is how I make a decision based on the request URI patterns:

Java
...
String url = request.getRequestURI();
System.out.println(url);

if (url != null && url.startsWith("/secure/api/"))
{
...
}
else
{
...
}
...AuthenticationEntryPoint

I use the HttpServletRequest object to get the URI (.getRequestURI()), which is a string. Then I checkdifferent whether the string is NULL or not. If it is not null, I check whether the string starts with prefix "/secure/api/". If this prefix is part of the string, then I would send back a response with error code 403 and a JSON response that describes the error, which is this:

Java
String jsonVal = createJsonResponse(false, "Access Denied");

response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(jsonVal);
response.getWriter().flush();
response.getWriter().close();

If the request URI does not have the prefix, then I would do a redirect to return the login page, and it is done like this:

Java
response.sendRedirect("/login");

I also provide a helper method to create a JSON object, which you can see in above. I will not list it again here. This is only part of the whole application. Here is the MVC controller for displaying public accessible page, such as login page, logout page, access error pages:

Java
package org.hanbo.boot.app.controllers;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class LoginController
{
   @RequestMapping(value="/login", method = RequestMethod.GET)
   public ModelAndView login()
   {      
      boolean isLoggedIn = SecurityContextHolder.getContext().getAuthentication() != null &&
            SecurityContextHolder.getContext().getAuthentication().isAuthenticated() &&
            !(SecurityContextHolder.getContext().getAuthentication() 
                  instanceof AnonymousAuthenticationToken);
      if (isLoggedIn) {
         ModelAndView retVal = new ModelAndView();
         retVal.setViewName("redirect:/secure/index");
         return retVal;
      }
      
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("loginPage");
      return retVal;
   }
   
   @RequestMapping(value="/public/logout", method = RequestMethod.GET)
   public ModelAndView logout()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("logoutPage");
      return retVal;
   }
   
   @RequestMapping(value="/public/authFailed", method = RequestMethod.GET)
   public ModelAndView authFailed()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("authFailedPage");
      return retVal;
   }
   
   @RequestMapping(value="/public/accessDenied", method = RequestMethod.GET)
   public ModelAndView accessDenied()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("accessDeniedPage");
      return retVal;
   }
}

The method called login() will display the login page. As I stated in the beginning of this tutorial, when user is already logged in, there is no point of presenting the user of the login page again. So when I implemented this method, I added some logic to it to detect such case, then redirect the user to the secured index page instead of showing the login page. Here it is:

Java
boolean isLoggedIn = SecurityContextHolder.getContext().getAuthentication() != null &&
      SecurityContextHolder.getContext().getAuthentication().isAuthenticated() &&
      !(SecurityContextHolder.getContext().getAuthentication() 
            instanceof AnonymousAuthenticationToken);
if (isLoggedIn) {
   ModelAndView retVal = new ModelAndView();
   retVal.setViewName("redirect:/secure/index");
   return retVal;
}

If the check from above fails, then user is not logged in and will see the login page shown on the browser. This is not the coolest part yet, but it should work, remove this part then retry the same scenario, and logged in user will still see the login page, when he/she type in the login page URL. This is the first part of this tutorial. Next, I will explain how to configure the session duration with any arbitrary number so that it is easier for us to test this sample application.

Configuration of Session Duration

Change the default session duration from 30 minutes to some different numbers is very easy to do. Especially with the default session management. All I have to do is add one line to the file application.properties, like this:

Java
server.servlet.session.timeout=90s

The value has a single character suffix "s", which means seconds. If I change it to "m", it would mean minutes. In the above code, I specify the session duration to be 90 seconds. This allows me to run the application after login, wait more than 90 seconds, then see how session expiration can be handled.

A word of warning, this will not work if I use MySQL and JDBC for session storage and retrieval. The session duration has to be hard coded in the annotation for the JDBC configuration for session storage.

Another gotcha here is that you cannot specify value smaller than 1 minute or 60 seconds. With this, I can test my session time out handling easily, I can login, and set the page idle for 140 seconds then try again in the page, and it will trigger the timeout. For this sample application, the session timeout duration is set to 90 seconds.

How Single Page Application Handles Secured Session Timeout

I discovered a few things about Spring Security, and session timeout and all that. The first idea I tried, is using a hidden heart beat message. This heart beat messaging is sent every minute or so, when it failed to get expected response, it will redirect to login page. This idea failed to work. The secured session caches every client server interaction, it logs the latest time when the the interaction occurred, and the time the session will expire, which is calculated from the latest time interaction happened. So every time the heart beat messaging occurs, the expiration time would be pushed further and further into the future, which means the secured session would never expire. So there it is, if I want to have my session never expire, I would use a repeated heart beat message against the server.

On the night I was trying this (heart beat to the server) out, I never saw the session expire, and I couldn't figure out because using the default session management, I don't know where my session data is stored (somewhere in the file system or even in memory). I finally was able to figure it out using one of my recent tutorial that I used MySQL to store the session data, which is query-able. Once I realized the session is getting extended automatically, I was able to check my current solution and find why it is not working. Anyways, I digressed, the point is, don't use constant heart beat to check the if session is going to expire soon, or your session would never expire.

Because I have properly configured the backend to handle session expiration in two different ways. So it is much easier for me to handle the session expiration on the front end. Front end mostly invokes API on the back end. When session expires, the API invocation will return HTTP status code 403 (access denied error), and the response will contain a JSON object. This allows front end to identify the access denied error. Then the AngularJS code can force browser to redirect to the login page.

Now that you know the concept, we can go through the front end design and see how it can identify session expire and make the appropriate decision. First, the AngularJS application declaration:

JavaScript
(function () {
   "use strict";
   var mod = angular.module("testSampleApp", [ "ngResource" ]);
   ...
})();

Next, I defined the heart beat API invocation using ngResource. This is used by the button on the page to test session expire. Here it is:

JavaScript
...
mod.factory("testService", ["$resource",
   function ($resource) {
      var retVal = {
         getHeartbeat: getHeartbeat
      };
      
      var apiRes = $resource(null, null, {
         heartbeat: {
            url: "/secure/api/heartbeat",
            method: "GET",
            isArray: false
         }
      });
      
      function getHeartbeat() {
         return apiRes.heartbeat().$promise;
      }
      
      return retVal;
   }
]);
...

Next, it is the controller used by the application. Here it is:

JavaScript
...
mod.controller("testSampleController", 
[ "$scope", "$interval", "httpErrCheckService", "testService",
   function ($scope, $interval, httpErrCheckService, testService) {
      var vm = this;
      vm.secsLeft = 60;
      vm.intvProm = null;
      
      vm.intvProm = $interval(function() {
         if (vm.secsLeft > 0) {
            vm.secsLeft -= 1;
         } else {
            if (vm.intvProm != null) {
               $interval.cancel(vm.intvProm);
            }
         }
      }, 3000);
      
      vm.clickBtn = function () {
         testService.getHeartbeat().then(
            function (result) {
               if (result && result.success) {
                  vm.secsLeft = 60;
                  console.log(result);
                  console.log("Heartbeat successful.");
               } else {
                  console.log(result);
                  console.log("Heartbeat failed.");
               }
            }, function (error) {
               if (error) {
                  console.log(error);
               }
               console.log("Error happened.");
               httpErrCheckService.checkAccessDenied(error);
            }
         );
      };
   }
]);

There are a few things to discuss about this controller. The first is the interval component used in this application. It will decrement the counter every 3 seconds, until the counter reaches 0. Once it reaches 0, the interval component will cancel itself.

JavaScript
...
vm.secsLeft = 60;
vm.intvProm = null;

vm.intvProm = $interval(function() {
   if (vm.secsLeft > 0) {
      vm.secsLeft -= 1;
   } else {
      if (vm.intvProm != null) {
         $interval.cancel(vm.intvProm);
      }
   }
}, 3000);
...

Another thing to discuss is the click event handler method. This method is where the session expiration can happen. If the session has not expired, the click event will send the heart beat to the server, and response will come back, which will reset the session and hence reset the counter. But if the session was expired, the response will be access denied error 403. In this case, the front end should force the browser to redirect to login page.

JavaScript
...
vm.clickBtn = function () {
   testService.getHeartbeat().then(
      function (result) {
         if (result && result.success) {
            vm.secsLeft = 60;
            console.log(result);
            console.log("Heartbeat successful.");
         } else {
            console.log(result);
            console.log("Heartbeat failed.");
         }
      }, function (error) {
         if (error) {
            console.log(error);
         }
         console.log("Error happened.");
         httpErrCheckService.checkAccessDenied(error);
      }
   );
};
...

Again, I highlighted the line that is important. This line is in the error handling part of the heart beat response processing. The error object that is passed in, is a raw http object that has a status property (status code), and a data object. In the case of 403 error, the data object will be a customized JSON object. The line I have highlighted can just check the status code equals 403, if it is 403, then it will force browser to redirect to login page. This is some work that can be moved to a service. Here is httpErrCheckService service that will perform the 403 check and browser redirect:

JavaScript
...
mod.factory("httpErrCheckService", ["$window",
   function ($window) {
      var retVal = {
         checkAccessDenied: checkAccessDenied
      };
      
      function checkAccessDenied(errResp) {
         if (resp && resp.status === 403) {
            $window.open("/login", "_self");
         }
      }
      
      return retVal;
   }
]);
...

These are all the front end setup for this application. Plus all the server side codes we have seeing before this section, we have all the essential pieces. Next two sections will be showing some miscellaneous parts of this application, and how to build and test this application.

Misc. Parts of Sample Application

Aside from the most essential parts which I have already covered above. There are some parts of the sample application worthy of a look. First, let me show you the HTML markup of the index page:

HTML
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

   <title>Login</title>
   <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}"/>
   <link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap-theme.min.css}"/>
   <link rel="stylesheet" th:href="@{/assets/css/index.css}"/>
</head>
<body>
   <div class="container" ng-app="testSampleApp" ng-controller="testSampleController as vm">
      <div class="row">
         <div class="col-xs-12">
            <div class="panel panel-default">
               <div class="panel-body">
                  <div class="row">
                     <div class="col-xs-12 text-center">
                        <p><span ng-class="{'text-danger': vm.secsLeft 
                        < vm.secsThreshold}">{{vm.secsLeft}}</span> seconds left</p>
                        <p>{{vm.secsLeft}} seconds left</p>
                        <p>After login wait for 3 minutes, then click the button. 
                        The login session is expired will redirect to login page.</p>
                        <button class="btn btn-default" ng-click="vm.clickBtn()">
                         Click Me</button>
                     </div>
                  </div>
               </div>
            </div>
         </div>
      </div>
   </div>
   
   <script type="text/javascript" th:src="@{/assets/jquery/js/jquery.min.js}"></script>
   <script type="text/javascript" 
    th:src="@{/assets/bootstrap/js/bootstrap.min.js}"></script>
   <script type="text/javascript" 
    th:src="@{/assets/angularjs/1.7.5/angular.min.js}"></script>
   <script type="text/javascript" 
    th:src="@{/assets/angularjs/1.7.5/angular-resource.min.js}"></script>
   <script type="text/javascript" 
    th:src="@{/assets/angularjs/1.7.5/angular-route.min.js}"></script>
   <script type="text/javascript" th:src="@{/assets/app/js/app.js}"></script>
</body>
</html>

I used Thymeleaf template mark up for this, and the page is super simple. There is a button, and a counter being displayed.

Here is the API controller for the heart beat communication, as well as for the index page display:

Java
package org.hanbo.boot.app.controllers;

import org.hanbo.boot.app.models.*;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SecuredPageController
{
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/index", method = RequestMethod.GET)
   public ModelAndView index1()
   {
        ModelAndView retVal = new ModelAndView();
        retVal.setViewName("indexPage");
        return retVal;  
   }
   
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/api/heartbeat", method = RequestMethod.GET)
   public ResponseEntity<GenericResponse> heartbeat()
   {
      GenericResponse resp = new GenericResponse();
      resp.setSuccess(true);
      resp.setStatusMessage("Heartbeat message received successfully.");
      
      return ResponseEntity.ok(resp);
   }
}

This sample application is for demonstrating how to handle session timeout with AngularJS application, the back end can be as simple as this.

I also needed a data model for the access denied error. It is the response JSON object send back along with the 403 error code. Here it is:

Java
package org.hanbo.boot.app.models;

public class GenericResponse
{
   private boolean success;
   
   private String statusMessage;

   public boolean isSuccess()
   {
      return success;
   }

   public void setSuccess(boolean success)
   {
      this.success = success;
   }

   public String getStatusMessage()
   {
      return statusMessage;
   }

   public void setStatusMessage(String statusMessage)
   {
      this.statusMessage = statusMessage;
   }
}

That is it. These are the ones that are not so important, but still vital part of the sample application. In the next section, I will discuss how this sample application can be built and tested.

How to Build and Test Sample Application

After getting the source code, please first rename the *.sj files to *.js in the sub folders of src/main/resources/static/. Afterward, use a command line prompt and cd into the base folder of the sample application. Then run the following command:

mvn clean install

To compile, please use JDK 15. And if you want to downgrade to JDK 8, please change the JDK version in the POM.xml. If everything goes well, the compile will complete successfully. Although downloading the dependency jars will take a while. But the compilation and package will take about 3 to 4 seconds.

To run the application, use the following command in the base directory:

java -jar target/angular-spring-session-sample1-1.0.0.jar

If you can compile and package the application, then the above application will run successfully. Next, you can run the application in the browser with the following URL:

http://localhost:8080/secure/index

This link will display the login page like this:

Image 1

You can login to the secure index page with crdential "user1" and password "user12345". Once logged in, you can see this:

Image 2

When the counter is below 29, the session would be expired. But since there has no interaction between the page and back end service, you can still see this secure index page.

Image 3

If you click the button "Click Me". You will be redirected to the login page, like this:

Image 4

if you log in again, then copy and paste the following URL into the browser:

http://localhost:8080/login

You will be redirected to the secure index page. I know this application really does not do any thing useful. The purpose is to demonstrating these functionalities:

  • When user logged in, going to the login page will redirect to the secure index page. No sense of re-login the page again.
  • When user session expires, the next invocation of server API should cause the server to return HTTP error 403 with some meaningful messages.

At this point, I think functionalities of the sample application should sufficiently demo these two.

Summary

This is the end of this tutorial, a simple tutorial. The sample application does not do anything useful. Its purpose is to demonstrating the few ways how AngularJS application handles secured session and session expiration. To make the demonstration work, I have to configure the session duration to 90 seconds instead of the default 30 minutes. It is done with just one line with application.properties file. Next, I have discussed how to configure the login action so that it can redirect to the secure pages when user is already in a secured session. There is no point of allow user to login again when user is already logged. It can be done with about 4 to 6 lines of code.

The most important part of this tutorial is the code logic for handling the session expiration exception. It can be done by creating a class implementing the interface AuthenticationEntryPoint. Then send it to the method .authenticationEntryPoint() (part of the Spring Security configuration). In my implementation class, I use hard coded logic to check the request URI to identify when to send the login page via a server redirect and when to send a 403 error, with additional info added to a JSON object.

With some care design, it is possible that the access denied error can be handled with configuration from file or from database instead of just hard coding the changes in the class itself. Aside from this, I have also explained why using constant heart beat check cannot detect session expiration. And why it is hard to handle the session expire with RESTFul API invocation (login page with 200 instead of HTTP error 403). This issue can only be resolved by implementing the interface AuthenticationEntryPoint. Once this is clear, everything becomes easy to resolve. I hope this would be useful. Good luck!

History

  • 5th June, 2021 - Initial draft
  • 18th June, 2021 - Second draft

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Han Bo Sun
Team Leader The Judge Group
United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --