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

20 comments:

  1. Hi Badal, Thanks for the nice post.! Could you please send me the source code of this example ? My mail id is pkc.mvit@gmail.com.
    Thanks in advance..!

    ReplyDelete
    Replies
    1. Hi,
      Thanks a lot you can find code here: -

      https://github.com/badalb/spring-security-oauth2-javaconfig

      Regards,
      Badal

      Delete
  2. Thanks a lot for this great post. Could you please send me the source code in my email : edonzogaj1@gmail.com , Thanks a lot

    ReplyDelete
    Replies
    1. Hi,
      Thanks a lot you can find code here: -

      https://github.com/badalb/spring-security-oauth2-javaconfig

      Regards,
      Badal

      Delete
  3. Thanks a lot for this great post. Could you please send me the source code in my email : edonzogaj1@gmail.com , Thanks a lot

    ReplyDelete
    Replies
    1. Hi,
      Thanks a lot you can find code here: -

      https://github.com/badalb/spring-security-oauth2-javaconfig

      Regards,
      Badal

      Delete
  4. Great article , could you please send me the source code to " alaayameen2012@gmail.com

    , thanks

    ReplyDelete
    Replies
    1. Hi,
      Thanks a lot you can find code here: -

      https://github.com/badalb/spring-security-oauth2-javaconfig

      Regards,
      Badal

      Delete
  5. Thanks a lot for this great post. Could you please send me the source code in my email : arunn472@gmail.com , Thanks a lot

    ReplyDelete
    Replies
    1. Hi,
      Thanks a lot you can find code here: -

      https://github.com/badalb/spring-security-oauth2-javaconfig

      Regards,
      Badal

      Delete
  6. Hi
    Thanks for this post.
    I'm trying to use this source code, but an error occurred while executing
    a command to get the token:

    {"timestamp": 1445293690462, "status": 401, "error": "Unauthorized", "message": "In AuthenticationProvider found fororg.springframework.security.authentication.UsernamePasswordAuthenticationToken","path":"/oauth/token"}* Connection # 0 to host localhost left intact

    Could you help me please ?

    ReplyDelete
    Replies
    1. Hi,

      Please refer my github code here:

      https://github.com/badalb/spring-security-oauth2-javaconfig

      If you face any difficulties please do let me know.

      Regards,
      Badal

      Delete
  7. HELLO,

    I have created three of project in eclipse. I added all dependency to POM.XML. I fixed all errors. There are no any compile time error, but when i run project i am getting below errors. If you can help then it will be very helpful to me.
    Error -
    Exception in thread "background-preinit" java.lang.NoClassDefFoundError: org/springframework/boot/autoconfigure/BackgroundPreinitializer$MBeanFactoryInitializer
    at org.springframework.boot.autoconfigure.BackgroundPreinitializer$1.run(BackgroundPreinitializer.java:61)
    at java.lang.Thread.run(Unknown Source)
    Caused by: java.lang.ClassNotFoundException: Illegal access: this web application instance has been stopped already. Could not load [org.springframework.boot.autoconfigure.BackgroundPreinitializer$MBeanFactoryInitializer]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
    at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1343)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1206)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1167)
    ... 2 more
    Caused by: java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load [org.springframework.boot.autoconfigure.BackgroundPreinitializer$MBeanFactoryInitializer]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
    at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1353)
    at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1341)
    ... 4 more

    ReplyDelete
  8. Hi sir,

    Thanks for you post.

    I'm try create OAuth2 server(Tokens & data stored on database).

    I'm already complete OAuth2 data services, can you help me to complete OAuth2 server Spring Java Configuration?

    Thank you in advance.


    Best regards,

    Ghislain MAMAT

    ReplyDelete
  9. Hi Badal,

    We are facing a strange situation here. We are able to get the token by using oauth/token url in windows machine. But the same thing is throwing an error in ubuntu (linux) machine saying POST request not supported. Could you please help me in this.

    ReplyDelete
    Replies
    1. Hi,
      Thanks for posting the issue you are facing. Could you please paste the log you are getting?

      Regards,
      Badal

      Delete
  10. Hi Badal,

    I have an application that has RestFul web services (resources) which are protected using spring OAuth 2.0. Now I need to configure OAuth so as to allow only certain clients (client ID) to access certain web services (resources). Could some one please guide me on how this can be done? Thank you in advance.

    ReplyDelete
  11. We are able to get the token by using oauth/token url . But the token generated and the token which is inserted in DB both are different.

    ReplyDelete
    Replies
    1. Please refer https://github.com/badalb/spring-security-oauth2-javaconfig . Clone the repo and try to run locally. It will work

      Delete
  12. Great post !! I am glad to find your impressive way of writing the post.Thanks for sharing
    best website development services | best seo services

    ReplyDelete