Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement basic Spring security (session management) for Single Page AngularJS application

I am currently building a single page AngularJS application which communicates via REST to a backend. The structure is as follow:

One Spring MVC WebApp project which contains all AngularJS pages and resources and all REST controllers.

A true backend which has services and repositories for backend communication, an API if you will. The REST calls will talk to these service (the second project is included as a dependency of the first one).

I have been thinking about this a lot but I can't seem to find anything that can help me. Basically I just need some security on this application. I'd like some kind of session management which is extremely simple:

  • user logs in, session id is created and stored in JS/cookie on website
  • when user would reload page/ come back later a check needs to be done to see if the session id is still valid
  • no calls should reach the controllers if the session id is not valid

This is the general idea of basic session managament, what would be the easiest way to get this implemented in a Spring MVC webapp (no JSP's, just angular and REST controllers).

Thanks in advance!

like image 626
E. V. d. B. Avatar asked May 08 '15 14:05

E. V. d. B.


People also ask

How do I create a Spring Security session?

By default, Spring Security will create a session when it needs one — this is “ifRequired“. For a more stateless application, the “never” option will ensure that Spring Security itself won't create any session. But if the application creates one, Spring Security will make use of it.

Can we use Spring Security with angular?

Conclusion. We've learned how to implement a Spring Security login page with Angular. From version 4 onwards, we can make use of the Angular CLI project for easy development and testing. As always all the example we've discussed here can be found over GitHub project.


1 Answers

You have 2 options for the rest API: stateful or stateless.

1st option: HTTP session authentication - the "classical" Spring Security authentication mechanism. If you plan to scale your application on multiple servers, you need to have a load balancer with sticky sessions so that each user stays on the same server (or use Spring Session with Redis).

2nd option: you have the choice of OAuth or token-based authentication.

OAuth2 is a stateless security mechanism, so you might prefer it if you want to scale your application across several machines. Spring Security provides an OAuth2 implementation. The biggest issue with OAuth2 is that requires to have several database tables in order to store its security tokens.

Token-based authentication, like OAuth2, is a stateless security mechanism, so it's another good option if you want to scale on several different servers. This authentication mechanism doesn't exist by default with Spring Security. It is easier to use and implement than OAuth2, as it does not require a persistence mechanism, so it works on all SQL and NoSQL options. This solution uses a custom token, which is a MD5 hash of your user name, the expiration date of the token, your password, and a secret key. This ensures that if someone steals your token, he should not be able to extract your username and password.

I recommend you to look into JHipster. It will generate a web app skeleton for you with REST API using Spring Boot and the front end using AngularJS. When generating the application skeleton it will ask you to choose between the 3 authentication mechanisms that I described above. You can reuse the code that JHipster will generate in your Spring MVC application.

Here is an example of TokenProvider generated by JHipster:

public class TokenProvider {

    private final String secretKey;
    private final int tokenValidity;

    public TokenProvider(String secretKey, int tokenValidity) {
        this.secretKey = secretKey;
        this.tokenValidity = tokenValidity;
    }

    public Token createToken(UserDetails userDetails) {
        long expires = System.currentTimeMillis() + 1000L * tokenValidity;
        String token = userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires);
        return new Token(token, expires);
    }

    public String computeSignature(UserDetails userDetails, long expires) {
        StringBuilder signatureBuilder = new StringBuilder();
        signatureBuilder.append(userDetails.getUsername()).append(":");
        signatureBuilder.append(expires).append(":");
        signatureBuilder.append(userDetails.getPassword()).append(":");
        signatureBuilder.append(secretKey);

        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("No MD5 algorithm available!");
        }
        return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes())));
    }

    public String getUserNameFromToken(String authToken) {
        if (null == authToken) {
            return null;
        }
        String[] parts = authToken.split(":");
        return parts[0];
    }

    public boolean validateToken(String authToken, UserDetails userDetails) {
        String[] parts = authToken.split(":");
        long expires = Long.parseLong(parts[1]);
        String signature = parts[2];
        String signatureToMatch = computeSignature(userDetails, expires);
        return expires >= System.currentTimeMillis() && signature.equals(signatureToMatch);
    }
}

SecurityConfiguration:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Inject
    private Http401UnauthorizedEntryPoint authenticationEntryPoint;

    @Inject
    private UserDetailsService userDetailsService;

    @Inject
    private TokenProvider tokenProvider;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Inject
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers("/scripts/**/*.{js,html}");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
                .antMatchers("/api/register").permitAll()
                .antMatchers("/api/activate").permitAll()
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/protected/**").authenticated()
        .and()
            .apply(securityConfigurerAdapter());

    }

    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    }

    private XAuthTokenConfigurer securityConfigurerAdapter() {
      return new XAuthTokenConfigurer(userDetailsService, tokenProvider);
    }

    /**
     * This allows SpEL support in Spring Data JPA @Query definitions.
     *
     * See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
     */
    @Bean
    EvaluationContextExtension securityExtension() {
        return new EvaluationContextExtensionSupport() {
            @Override
            public String getExtensionId() {
                return "security";
            }

            @Override
            public SecurityExpressionRoot getRootObject() {
                return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};
            }
        };
    }

}

And the respective AngularJS configuration:

'use strict';

angular.module('jhipsterApp')
    .factory('AuthServerProvider', function loginService($http, localStorageService, Base64) {
        return {
            login: function(credentials) {
                var data = "username=" + credentials.username + "&password="
                    + credentials.password;
                return $http.post('api/authenticate', data, {
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded",
                        "Accept": "application/json"
                    }
                }).success(function (response) {
                    localStorageService.set('token', response);
                    return response;
                });
            },
            logout: function() {
                //Stateless API : No server logout
                localStorageService.clearAll();
            },
            getToken: function () {
                return localStorageService.get('token');
            },
            hasValidToken: function () {
                var token = this.getToken();
                return token && token.expires && token.expires > new Date().getTime();
            }
        };
    });

authInterceptor:

.factory('authInterceptor', function ($rootScope, $q, $location, localStorageService) {
    return {
        // Add authorization token to headers
        request: function (config) {
            config.headers = config.headers || {};
            var token = localStorageService.get('token');

            if (token && token.expires && token.expires > new Date().getTime()) {
              config.headers['x-auth-token'] = token.token;
            }

            return config;
        }
    };
})

Add authInterceptor to $httpProvider:

.config(function ($httpProvider) {

    $httpProvider.interceptors.push('authInterceptor');

})

Hope this is helpful!

This video from SpringDeveloper channel may be useful too: Great single page apps need great backends. It talks about some best practices (including session management) and demos working code examples.

like image 116
medvedev1088 Avatar answered Oct 31 '22 04:10

medvedev1088