Wednesday 22 April 2015

Multiple Authentication Schemes in Spring Security

While developing server side applications using spring framework sometimes we encounter situations where need to support web based clients (typically developed in Backbone.js or Angular.js or JSP based multi form applications), Mobile clients (Android, IOS etc). The RESTfull services exposed may have third party clients as well.


If we have a consumer facing web interface typically accessed by web browsers where we need to maintain a user session we have to end up having a form based authentication mechanism. Third party clients for B2B service consumption we can have token based security in place and mobile users security could be supported by OAuth2. Lets see how we could implement all three types of security mechanisms in a web application using Spring Security.


Security Configuration files for REST and Form based security :-

@Configuration
@EnableWebSecurity

public class MultiHttpSecurityConfig {
   
   @Configuration
    @Order(1)                                                        
    public static class RestSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
    RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint();
    entryPoint.setRealmName("<your_realm_name>");
    return entryPoint;
    }

    @Bean
    public RestAuthenticationProvider restAuthenticationProvider() {
    RestAuthenticationProvider authProvider = new RestAuthenticationProvider();
    return authProvider;
    }

    @Bean
    public RestSecurityFilter restSecurityFilter() {

    RestSecurityFilter filter = null;
    try {
    filter = new RestSecurityFilter(authenticationManagerBean());
    } catch (Exception e) {
    e.printStackTrace();
    }

    return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http
    .antMatcher("/api/**")    
    .sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    //.addFilterBefore(restSecurityFilter(), BasicAuthenticationFilter.class)
    .exceptionHandling()
    .authenticationEntryPoint(restAuthenticationEntryPoint()).and()
    .authorizeRequests()
    .antMatchers("/api/**")
    .authenticated().and().addFilterBefore(restSecurityFilter(), BasicAuthenticationFilter.class);
    

    }
    
    @Override
        protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
    authManagerBuilder.authenticationProvider(restAuthenticationProvider());
        }
    }

    @Configuration
    public static class FormSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    @Autowired
    private <custom_user_detail_service> customUserDetailsService;

    @Autowired
    CustomSecuritySuccessHandler customSecuritySuccessHandler;

    @Autowired
    CustomSecurityFailureHandler customSecurityFailureHandler;

    @Autowired
    private <password_encoder> passwordEncoder;
    
    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

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

    protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeRequests()
    .antMatchers("/", "/login.html", "/app/**", "/assets/**", "/login","/failure","/register","/public/**", "/oauth/v1/**").permitAll().anyRequest().authenticated();
    http.formLogin().loginPage("/login").failureUrl("/")
    .successHandler(customSecuritySuccessHandler)
    .failureHandler(customSecurityFailureHandler).permitAll().and()

    .logout().logoutSuccessUrl("/login").permitAll().and()
                .rememberMe().and().exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
    
    
    return;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authManagerBuilder)
    throws Exception {
    authManagerBuilder.userDetailsService(customUserDetailsService)
    .passwordEncoder(passwordEncoder);
    }
    
    }

}


OAuth2 Security Configuartion:-

@Configuration
public class GlobalAuthenticationConfig extends GlobalAuthenticationConfigurerAdapter {
    
@Autowired
private <custom_user_detail_service> oAuthUserDetailService;
@Autowired
private <password_encoder> commonPasswordEncoder;
      
    @Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(oAuthUserDetailService).passwordEncoder(commonPasswordEncoder);

}
}


@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter  {

@Autowired
DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}

@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

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

}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
}

}

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter  {

private static final String HU_REST_RESOURCE_ID = "rest_api";

@Autowired
DataSource dataSource;

@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}


@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(HU_REST_RESOURCE_ID).stateless(false);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.
requestMatchers().antMatchers("/oauth/v1/**").and().
authorizeRequests().antMatchers("/oauth/v1/**").access("#oauth2.hasScope('read') or (!#oauth2.isOAuth() and hasRole('ROLE_USER'))");
}

}

With these configurations the incoming requests with URL pattern -
i. <context>/api/<version>/<some_request>  will be intercepted by RestSecurityConfig
ii. <context>/oauth/v1/<some_request> will be intercepted by OAuth2ResourceServerConfig
iii. All other requests will be intercepted by FormSecurityConfig

[Feel free to clone https://github.com/badalb/multi-security-config-web.git for detail code.]

Wednesday 8 April 2015

Token Based REST API Security

While writing REST APIs we sometimes wonder how to secure them. Hosting REST services over HTTPS will make the communication channel secured but not individual REST APIs. 

The possible options to secure REST API could be -



  • 1. HTTP Basic Authentication
  • 2. OAuth based Authentication
  • 3. Token Based Authentication



Lets see how we can use token based security for fully stateless REST APIs.  To understand toke based security we need to understand  -

ACCESS KEY: Access key is an unique key string passed in request header for every http request client sends to the server. We can consider access key as user identification for REST paradigm.

SECRET KEY: For every REST API user/client we generate a secret key. This secret key is used to encrypt some plain text to generate a cipher which will be used as request signature.

REQUEST SIGNATURE/MESSAGE HASH: Its the cipher text generated after encrypting a plain text using the secret key. This string is passed as the request signature.

HMAC(<some_plain_text> , <SECRET KEY>) = <SOME_REQUEST_SIGNATURE>

HMAC: A keyed-hash message authentication code (HMAC) is a specific construction for calculating a message authentication code (MAC) involving a cryptographic hash function in combination with a secret cryptographic key.

Using this concepts client will generate a request signature send it to server as request signature along with actual parameters and the access key. Lets move on to some code examples so that we can understand it better.

Code Example

Lets assume that we store user access and secret key in some database. So the database table will look like  - 
<rest_user_key>

id
app_id
access_key
hmac_key
aes_key
1
<some_app>
access_key1
sec_key1

2
<some_app>
access_key2
sec_key2

3
<some_app>
access_key3
sec_key3

4
<some_app>
access_key4
sec_key4



HMAC JAVA Code :

public class HMACKeyGeneratorServiceImpl {

private static final char[] symbols = new char[36];
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String SHA1PRNG_ALGORITHM="SHA1PRNG";
private static final String SHA1_ALGORITHM="SHA-1";
private final char[] buf;
private final Random random = new Random();
static {
for (int idx = 0; idx < 10; ++idx)
symbols[idx] = (char) ('0' + idx);
for (int idx = 10; idx < 36; ++idx)
symbols[idx] = (char) ('a' + idx - 10);
}

HMACKeyGeneratorServiceImpl() {
buf = new char[20];
}

public String generateAccessKey() {
for (int idx = 0; idx < buf.length; ++idx)
buf[idx] = symbols[random.nextInt(symbols.length)];
return new String(buf).toUpperCase();
}
public String generateHMACKey() throws GeneralSecurityException {
SecureRandom prng = SecureRandom.getInstance(SHA1PRNG_ALGORITHM);
String randomNum = new Integer(prng.nextInt()).toString();
MessageDigest sha = MessageDigest.getInstance(SHA1_ALGORITHM);
byte[] result = sha.digest(randomNum.getBytes());
return hexEncode(result);
}
private String hexEncode(byte[] aInput) {
StringBuilder result = new StringBuilder();
char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
for (int idx = 0; idx < aInput.length; ++idx) {
byte b = aInput[idx];
result.append(digits[(b & 0xf0) >> 4]);
result.append(digits[b & 0x0f]);
}
return result.toString();
}
public String generateHMAC(String data, String hexEncodedKey) throws GeneralSecurityException,IOException {
String result = "";
byte[] keyBytes = hexEncodedKey.getBytes();
SecretKeySpec signingKey = new SecretKeySpec(keyBytes,HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes());
byte[] hexBytes = new Hex().encode(rawHmac);
result = new String(hexBytes, "UTF-8");
return result;
}


}

Client Side Signature Generation:

Considering the fact that client want to post some data to server, client must generate a request signature shared with him. The server will generate a similar request signature and match them.
The client and server must agree on a common plaintext generating algorithm to get the same hash. 

Lets assume that we will follow <param_name1>=<param_value1>;<param_name2>=<param_value2>;
so we will consider param name, param value paired using = and multiple parameters are combined using; as delimiter.


Authentication Flow

Client Side: 

Client want to post to server n params, param1 till param n with values value1 till value n.

Client generates a signature taking subset of these parameters name value pair.

  • Access Key : <client_access_key>
  • Secret Key: <client_secret_key>
  • Request Signature: <signature> = HMAC(<param_name1>=<param_value1>;<param_name2>=<param_value2>;, <client_secret_key>)


So client send a request to server with ACCESS_KEY = <client_access_key> and MESSAGE_HASH=<signature> in header  along with the parameters.

the client should populate the bean below and send a list so that server can identify the parameters used to generate the signature 


public class RestParameter implements Comparable<RestParameter>, Serializable{

private static final long serialVersionUID = -8654122030780643503L;
private String paramName;
private String paramValue;
private String order;

........
}

Server Side:

  • Server side signature generation involves fetching the ACCESS KEY from request header. 
  • Using the access key which is unique it fetches the SECRET KEY from database. 
  • Using the List of RestParameter and with the help of order of the parameters it generates the signature plain text.
  • Using the Secret key from step 2 and HMAC algorithm written earlier it generates a message hash.

Considering the fact that same plaintext and secret key is used to generate the hash both message hash from header that client sent and the server generated hash must be equal to proceed further.



[A full working code base is available here: https://github.com/badalb/spring-rest-security-tokenbased]




Tuesday 7 April 2015

OAuth2.0 with Spring Security OAuth2 and Java Config

Introduction

OAuth2 is an open standard for authorization. OAuth provides client applications a 'secure delegated access' to server resources on behalf of a resource owner.Designed specifically to work with HTTP, OAuth2 essentially allows  access token to be issued to third-party clients by an authorization server, with the approval of the resource owner, or end-user. The client then uses the access token to access the protected resources hosted by the resource server. OAuth 2 provides authorization flows for web and desktop applications, and mobile devices.

[Please Refer: http://en.wikipedia.org/wiki/OAuth to know more about OAuth/OAuth2]

OAuth2 Flow

OAuth defines four roles:
  • Resource Owner
    The resource owner is the user who authorizes an application to access their account. The application's access to the user's account is limited to the "scope" of the authorization granted (e.g. read or write access).
  • Client - 
    The client is the application that wants to access the user's account
  • Resource Server - 
    The resource server hosts the protected user accounts
  • Authorization Server -
    The authorization server verifies the identity of the user then issues access tokens to the application.
The entire flow works like below -



Many articles are available for OAuth2, describing the flow and concepts we can refer them so better we jump into code. 

Code Example

Spring has added OAuth2 support with spring security. Spring Security OAuth2 support was available with xml based configuration. Java Config support for Spring security OAuth2 has been added recent past. Lets see the code we need to write to enable OAuth2 Java Config support for our spring projects.

Authorization Server Code:

package com.test.security.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter  {

@Autowired
DataSource dataSource;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.jdbc(dataSource);  // If you want to maintain client details is database

//Section below for in-memory clients

/*clients.inMemory().withClient("<client_id>")
.resourceIds(<resource_id>)
.authorizedGrantTypes("authorization_code", "implicit")
.authorities("<roles>")
.scopes("read", "write")
.secret("secret")
.and()
.withClient("<client_id>")
.resourceIds(<resource_id>)
.authorizedGrantTypes("authorization_code", "implicit")
.authorities("<roles>")
.scopes("read", "write")
.secret("secret")
.redirectUris(<redirect_url>)
.and()
    .withClient("<client_id>")
.resourceIds(<resource_id>)
          .authorizedGrantTypes("authorization_code", "client_credentials")
          .authorities("ROLE_CLIENT")
          .scopes("read", "trust")
          .redirectUris("http://anywhere?key=value")
    .and()
          .withClient("my-trusted-client")
            .authorizedGrantTypes("password","refresh_token")
            .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
            .scopes("read", "write", "trust")
            .accessTokenValiditySeconds(60)
            .refreshTokenValiditySeconds(600)
    .and()
          .withClient("my-trusted-client-with-secret")
            .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
            .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
            .scopes("read", "write", "trust")
            .secret("somesecret")
          .and()
            .withClient("my-less-trusted-client")
            .authorizedGrantTypes("authorization_code", "implicit")
            .authorities("ROLE_CLIENT")
            .scopes("read", "write", "trust")
          .and()
            .withClient("my-less-trusted-autoapprove-client")
                .authorizedGrantTypes("implicit")
                .authorities("ROLE_CLIENT")
                .scopes("read", "write", "trust")
                .autoApprove(true);*/
}


@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource); // access and refresh tokens will be maintain in database
}

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

}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
}

}


Resource Server Code:

package com.test.security.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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 org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter  {

private static final String HU_REST_RESOURCE_ID = "rest_api";

@Autowired
@Qualifier("dataSource")
DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}


@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(HU_REST_RESOURCE_ID).stateless(false);
}

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

//define URL patterns to enable OAuth2 security 

http.
requestMatchers().antMatchers("/api/v1/**").and().
authorizeRequests().antMatchers("/api/v1/**").access("#oauth2.hasScope('read') or (!#oauth2.isOAuth() and hasRole('ROLE_USER'))");
}

}

Authentication Config Code:

package com.test.security.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.authentication.configurers.GlobalAuthenticationConfigurerAdapter;

import com.test.aouth2.service.OAuthUserDetailService;
import com.test.config.security.util.CommonPasswordEncoder;

@Configuration
public class GlobalAuthenticationConfig extends GlobalAuthenticationConfigurerAdapter {
    
@Autowired
private OAuthUserDetailService oAuthUserDetailService;  // This Class extends UserDetails Service 
                                                                                               //and overrides loadUserByUsername() 


@Autowired
private CommonPasswordEncoder commonPasswordEncoder;  //password Encoder
      
    @Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(oAuthUserDetailService).passwordEncoder(commonPasswordEncoder);

}
}

Password Encoder Code:

package com.test.config.security.util;

import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class CommonPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
String rawpswd = (String) rawPassword;
return BCrypt.hashpw(rawpswd, BCrypt.gensalt());
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String rawpasswd = (String) rawPassword;
boolean status =  BCrypt.checkpw(rawpasswd, encodedPassword);
return status;
}
}

@Component 
public class OAuthUserDetailService implements UserDetailsService {

@Autowired
    private UserService userService; // Repository methods are here

    @Override
    public UserDetails loadUserByUsername(String userName)
            throws UsernameNotFoundException {
        User user = userService.findByUserName(userName);
        if(user == null){
            throw new UsernameNotFoundException("UserName "+userName+" not found");
        }
        return new OAuthUser(user);
    }

}

public class OAuthUser extends User implements UserDetails {

 //Implementation details

}

Testing

Lets assume that we have written a REST service like below - 
@RestController
@RequestMapping({ "/api/v1/", "" })
public class HomeController {


@RequestMapping(value = "/secured/hello", method = {RequestMethod.POST,RequestMethod.GET})

public String hello() {
return "Hello World !!";
}


}

Access Token

To get the access token sent a request like below from your terminal

 $ curl <client_id>:@host:port/oauth/token -d grant_type=password -d username=<db_user_name> -d password=<db_user_password>

It will give you a response like 

{"access_token”:”<ACCESS_TOKEN>”,”token_type":"bearer","expires_in":2592000,"refresh_token”:”<REFRESH_TOKEN>”,”scope":"read" ……}

Access Secured Resource


curl -H "Authorization:Bearer <ACCESS_TOKEN>“ http://localhost:8080/secured/hello



It will print Hello World !! in the terminal