Jumat, 10 Maret 2017

Secure RESTFul API using OAuth2 : Spring

This blog is the extension of my previous blog in which we have explained how to develop RESTFul APIs using Spring and Hibernate.

This time is to implement OAuth 2 in REST APIs. Security is the primary concern for all organization.

OAuth 2 is one of the most popular protocol for the security implementation. This is totally based on the token mechanism. However there lot's of security mechanism that anybody can use to secure APIs or any web application.

In this blog, we are gonna to implement OAuth only. Let's have a quick look on what OAuth 2 says.

The OAuth2 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.

This is as per the documentation of OAuth2

Most of the big enterprises like Google, Facebook, Twitter are using OAuth2 security.

Let's have quick look on the four key concept of OAuth:


1) OAuth 2 Roles: There are four roles defined in OAuth:

  • resource owner: It could be an entity capable of granting access to the protected resource. When the entity owner is a person, it is referred to as an end-user.
  • resource server: The resource server is capable to host the secure resources, accepting request and responding to the protected request resource using the access token.
  • client: This is an application making request of the secured resources on behalf of resources owner and with its authorization. It could be a mobile client asking to access your Facebook feeds,  a web site providing an alternative login using gmail account.
  • authorization server: The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.

2. OAuth2 Authorization Grant types

An authorization grant is a credential representing the resource owner’s authorization (to access its protected resources) used by the client to obtain an access token. The specification defines four grant types:

  • authorization code
  • implicit
  • resource owner password credentials
  • client credentials

We will be using resource owner password credentials grant type.

3. OAuth2 Tokens

Tokens are random strings, generated by the authorization server and are issued when the client requests them.

  • Access Token: Sent with each request, usually valid for a very limited amount of  time
  • Refresh Token: Mainly used to get a new access token, not sent with each request, usually lives longer than access token.


4. OAuth2 Access Token Scope

Client can ask for the resource with specific access rights using scope [want to access feeds & photos of this users facebook account], and authorization server in turn return scope showing what access rights were actually granted to the client [Resource owner only allowed feeds access, no photos e.g.].

Let's get started with the actual implementation:

1) Resource Server: Resource servers are capable to host the resources(for ex REST APIs) the client is interested in. Resources are located on /employees/. @EnableResourceServer annotation, applied on OAuth2 Resource Servers, enables a Spring Security filter that authenticates requests using an incoming OAuth2 token.

package com.test.security;

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.error.OAuth2AccessDeniedHandler;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

private static final String RESOURCE_ID = "rest_api";

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

@Override
public void configure(HttpSecurity http) throws Exception {
http.
anonymous().disable()
.requestMatchers().antMatchers("/employees/**")
.and().authorizeRequests()
.antMatchers("/employees/**").access("hasRole('ADMIN')")
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}

}




2. Authorization Server

Authorization server is the one responsible for verifying credentials and if credentials are valid, it generated the tokens[refresh-token as well as access-token]. It also contains information about registered clients and possible access scopes and grant types. The token store is used to store the token. We will be using an in-memory token store.

package com.test.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

private static String REALM="REALM";

@Autowired
private TokenStore tokenStore;

@Autowired
private UserApprovalHandler userApprovalHandler;

@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;

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

clients.inMemory()
.withClient("secure-client")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")
.secret("password@123")
.accessTokenValiditySeconds(60).//Access token is only valid for 1 minutes.
refreshTokenValiditySeconds(300);//Refresh token is only valid for 5 minutes.
}

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

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.realm(REALM+"/client");
}

}



3. Security Configuration

Endpoint /oauth/token is used to request a token [access or refresh]. Resource owners(ankur and devesh) are configured.

package com.test.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private ClientDetailsService clientDetailsService;

@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ankur").password("password@123").roles("ADMIN").and()
.withUser("devesh").password("password@123").roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}


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

@Bean
@Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}

@Bean
@Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}

}

Additionally, enable Global method security which will activate @PreFilter, @PostFilter, @PreAuthorize @PostAuthorize annotations if we want to use them.

package com.test.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private OAuth2SecurityConfiguration securityConfig;

@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}

Let's see the below sequence, how to make protected resource call with access token

1) Make the HTTP POST request for access and refresh tokens with the below URL. Additionally,  send the client credentials, in the Authorization header.

http://localhost:8081/RestApi/oauth/token?grant_type=password&username=ankur&password=password@123

2) Make the HTTP GET/POST/PUT/DELETE request with the access token. For ex, if you want to make HTTP GET request, below URL would be used:

http://localhost:8081/RestApi/employees/2?access_token=cbbe9860-df48-44aa-b1fa-b8de01ca8916

3) If your access token expires, make the HTTP POST request to get the new access token. Additionally,  send the client credentials, in the Authorization header.

http://localhost:8081/RestApi/oauth/token?grant_type=refresh_token&refresh_token=235b9a77-baea-4ea6-bc15-8b975f8fbf89

Let's hit some request using POSTMAN client:

1) Let's hit the request without access token and see the response:



2) Let's generate the access token:


3) Let's hit the HTTP GET request with the access token in query parameter:


4) Wait for a minute and hit the HTTP GET request again, you will see token expiration message.


5) Generate a new access token with the help of refresh token whose validity is 5 minutes.


6) Hit the HTTP GET request again with the new access token



7) Let's hit refresh token request after 5 minutes and see refresh token is also get expired:





Download source code from here

Tidak ada komentar:

Posting Komentar