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.
Wednesday, 22 April 2015
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.
HMAC JAVA Code :
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
Subscribe to:
Posts (Atom)