Securing REST API using custom tokens (stateless, no UI, no cookies, no basic authentication, no OAuth, no login page)

32,084

Solution 1

My sample app does exactly this - securing REST endpoints using Spring Security in a stateless scenario. Individual REST calls are authenticated using an HTTP header. Authentication information is stored on the server side in an in-memory cache and provides the same semantics as those offered by the HTTP session in a typical web application. The app uses the full Spring Security infrastructure with very minimum custom code. No bare filters, no code outside of the Spring Security infrastructure.

The basic idea is to implement the following four Spring Security components:

  1. org.springframework.security.web.AuthenticationEntryPoint to trap REST calls requiring authentication but missing the required authentication token and thereby deny the requests.
  2. org.springframework.security.core.Authentication to hold the authentication information required for the REST API.
  3. org.springframework.security.authentication.AuthenticationProvider to perform the actual authentication (against a database, an LDAP server, a web service, etc.).
  4. org.springframework.security.web.context.SecurityContextRepository to hold the authentication token in between HTTP requests. In the sample, the implementation saves the token in an EHCACHE instance.

The sample uses XML configuration but you can easily come up with the equivalent Java config.

Solution 2

You're right, it isn't easy and there aren't many good examples out there. Examples i saw made it so you couldn't use other spring security stuff side by side. I did something similar recently, here's what i did.

You need a custom token to hold your header value

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

You need a spring security filter to extract the header and ask the manager to authenticate it, something like thisemphasized text

public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

Register your custom filter in spring security chain

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

A custom auth provider to validate that token extracted with the filter.

public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomToken.class.isAssignableFrom(authentication);
    }


}

Finally, register your provider as a bean so the authentication manager finds it in some @Configuration class. You probably could just @Component it too, i prefer this method

@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}

Solution 3

The code secure all endpoints - but I'm sure that you can play with that :). The token is stored in Redis using Spring Boot Starter Security and you have to define our own UserDetailsService which you pass into AuthenticationManagerBuilder.

Long story short - copy paste EmbeddedRedisConfiguration and SecurityConfig and replace AuthenticationManagerBuilder to your logic.

HTTP:

Requesting token - sending basic HTTP auth content in a request header. A token is given back in a response header.

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

Same request but using token:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

If you pass wrong username/password or token you get 401.

JAVA

I added those dependencies into build.gradle

compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

Then Redis configration

@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

Security config:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

Usually in tutorials you find AuthenticationManagerBuilder using inMemoryAuthentication but there is a lot more choices (LDAP, ...) Just take a look into class definition. I'm using userDetailsService which requires UserDetailsService object.

And finally my user service using CrudRepository.

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}
Share:
32,084
Karthik Karuppannan
Author by

Karthik Karuppannan

Updated on March 10, 2020

Comments

  • Karthik Karuppannan
    Karthik Karuppannan about 4 years

    There are lots of guidelines, sample codes that show how to secure REST API with Spring Security, but most of them assume a web client and talk about login page, redirection, using cookie, etc. May be even a simple filter that checks for the custom token in HTTP header might be enough. How do I implement security for below requirements? Is there any gist/github project doing the same? My knowledge in spring security is limited, so if there is a simpler way to implement this with spring security, please let me know.

    • REST API served by stateless backend over HTTPS
    • client could be web app, mobile app, any SPA style app, third-party APIs
    • no Basic Auth, no cookies, no UI (no JSP/HTML/static-resources), no redirections, no OAuth provider.
    • custom token set on HTTPS headers
    • The token validation done against external store (like MemCached/Redis/ or even any RDBMS)
    • All APIs need to be authenticated except for selected paths (like /login, /signup, /public, etc..)

    I use Springboot, spring security, etc.. prefer a solution with Java config (no XML)