OAUTH 2.0 with Spring Boot Securities


In this post we will be discussing about how we implemented OAUTH 2.0 security with spring boot. We will be implementing AuthorisationServer, ResourceServer and some REST API for different crud operations and test these APIs using Postman. We will be using in-memory (H2) database to read user credentials for authentication.

What is OAUTH

OAuth is an open-standard authorisation protocol or framework that provides applications the ability for “secure designated access.” For example, you can tell Facebook that it’s OK for quora.com to access your profile or post updates to your timeline without having to give Quora your Facebook password. This minimises risk in a major way: In the event quora suffers a breach, your Facebook password remains safe.OAuth doesn’t share password data but instead uses authorisation tokens to prove an identity between consumers and service providers. OAuth is an authentication protocol that allows you to approve one application interacting with another on your behalf without giving away your password.

What is OAUTH 2

OAuth 2 is an authorisation framework that enables applications to obtain limited access to user accounts on an HTTP service, such as Facebook, GitHub, and DigitalOcean. It works by delegating user authentication to the service that hosts the user account, and authorizing third-party applications to access the user account. OAuth 2 provides authorization flows for web and desktop applications, and mobile devices. This informational guide is geared towards application developers, and provides an overview of OAuth 2 roles, authorisation grant types, use cases, and flows.

OAuth Roles

  • The Third-Party Application: "Client" The client is the application that is attempting to get access to the user's account. It needs to get permission from the user before it can do so.
  • The API: "Resource Server"The resource server is the API server used to access the user's information.
  • The Authorization ServerThis is the server that presents the interface where the user approves or denies the request. In smaller implementations, this may be the same server as the API server, but larger-scale deployments will often build this as a separate component.
  • The User: "Resource Owner"The resource owner is the person who is giving access to some portion of their account.

Project Structure

Following is the structure of the Spring Boot OAUTH 2 Security. We can create simple Gradle project by spring.io














Dependencies

POM.XML

 <?xml version="1.0" encoding="UTF-8"?>  
 <project xmlns="http://maven.apache.org/POM/4.0.0"  
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  
      <modelVersion>4.0.0</modelVersion>  
      <groupId>com.sample</groupId>  
      <artifactId>SpringApp</artifactId>  
      <version>0.0.1-SNAPSHOT</version>  
      <parent>  
           <groupId>org.springframework.boot</groupId>  
           <artifactId>spring-boot-starter-parent</artifactId>  
           <version>2.1.3.RELEASE</version>  
      </parent>  
      <dependencies>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-web</artifactId>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-data-jpa</artifactId>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-security</artifactId>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.security.oauth</groupId>  
                <artifactId>spring-security-oauth2</artifactId>  
                <version>2.0.10.RELEASE</version>  
           </dependency>  
           <dependency>  
                <groupId>commons-dbcp</groupId>  
                <artifactId>commons-dbcp</artifactId>  
                <version>1.4</version>  
           </dependency>  
           <dependency>  
                <groupId>javax.xml.bind</groupId>  
                <artifactId>jaxb-api</artifactId>  
                <version>2.2.11</version>  
           </dependency>  
           <dependency>  
                <groupId>com.sun.xml.bind</groupId>  
                <artifactId>jaxb-core</artifactId>  
                <version>2.2.11</version>  
           </dependency>  
           <dependency>  
                <groupId>com.sun.xml.bind</groupId>  
                <artifactId>jaxb-impl</artifactId>  
                <version>2.2.11</version>  
           </dependency>  
           <dependency>  
                <groupId>javax.activation</groupId>  
                <artifactId>activation</artifactId>  
                <version>1.1.1</version>  
           </dependency>  
           <dependency>  
                <groupId>com.h2database</groupId>  
                <artifactId>h2</artifactId>  
                <scope>runtime</scope>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-test</artifactId>  
                <scope>test</scope>  
           </dependency>  
      </dependencies>  
      <properties>  
           <java.version>1.8</java.version>  
      </properties>  
      <build>  
           <plugins>  
                <plugin>  
                     <groupId>org.springframework.boot</groupId>  
                     <artifactId>spring-boot-maven-plugin</artifactId>  
                     <configuration>  
                          <addResources>true</addResources>  
                     </configuration>  
                </plugin>  
           </plugins>  
      </build>  
 </project>  

Gradle dependency as follows choose either one. To create a spring boot with gradle check below link.

build.gradle

 buildscript {  
   repositories {  
     mavenCentral()  
   }  
   dependencies {  
     classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE")  
   }  
 }  
 apply plugin: 'jacoco'  
 apply plugin: 'java'  
 apply plugin: 'org.springframework.boot'  
 apply plugin: 'io.spring.dependency-management'  
 bootJar {  
   baseName = 'oauth2-example-SB'  
   version = '0.0.1'  
 }  
 repositories {  
   mavenCentral()  
 }  
 sourceCompatibility = 1.8  
 targetCompatibility = 1.8  
 dependencies {  
   compile('org.springframework.boot:spring-boot-starter-web')  
   compile('org.springframework.boot:spring-boot-starter-data-jpa:2.0.0.RELEASE')  
   compile('org.springframework.boot:spring-boot-starter-security:2.0.0.RELEASE')  
   compile('org.springframework.security.oauth:spring-security-oauth2:2.0.10.RELEASE')  
   compile('mysql:mysql-connector-java:8.0.13')  
   compile('commons-dbcp:commons-dbcp:1.4')  
   compile('javax.xml.bind:jaxb-api:2.2.11')  
      compile('com.sun.xml.bind:jaxb-core:2.2.11')  
      compile('com.sun.xml.bind:jaxb-impl:2.2.11')  
      compile('javax.activation:activation:1.1.1')  
      compile('com.h2database:h2')  
   testCompile('junit:junit')  
 }  

Authorisation Server Configuration

This class extends AuthorizationServerConfigurerAdapter and is responsible for generating tokens specific to a client. Suppose, if a user wants to log in to quora.com, redbus, swiggy via facebook then facebook auth server will be generating tokens for quora this case, quora becomes the client which will be requesting for authorization code on behalf of the user from facebook - the authorization server. Following is a similar implementation that facebook will be using. Here, we are using in-memory credentials with client_id as arestech-client and CLIENT_SECRET as arestech-secret. But you are free to use JDBC implementation too.

@EnableAuthorizationServer: Enables an authorization server.AuthorizationServerEndpointsConfigurer defines the authorization and token endpoints and the token services.

CLIEN_ID = "arestech-client";
CLIENT_SECRET = "arestech-secret"; These are the authorisation user name and password.

AuthorisationServerConfig.java

 package com.sample.configuration;  
 import org.springframework.beans.factory.annotation.Autowired;  
 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.provider.token.TokenStore;  
 @Configuration  
 @EnableAuthorizationServer  
 public class AuthorisationServerConfig extends AuthorizationServerConfigurerAdapter {  
      static final String CLIEN_ID = "arestech-client";  
      static final String CLIENT_SECRET = "$2a$04$5BWfcf.5kdUKxbcwhFCdXepnr7n4gZUETlSktjcDKd7Ab1cEHHo5u";  
      static final String GRANT_TYPE_PASSWORD = "password";  
      static final String AUTHORIZATION_CODE = "authorization_code";  
   static final String REFRESH_TOKEN = "refresh_token";  
   static final String IMPLICIT = "implicit";  
      static final String SCOPE_READ = "read";  
      static final String SCOPE_WRITE = "write";  
   static final String TRUST = "trust";  
      static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60;  
   static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60;  
      @Autowired  
      private TokenStore tokenStore;  
      @Autowired  
      private AuthenticationManager authenticationManager;  
      @Override  
      public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {  
           configurer  
                     .inMemory()  
                     .withClient(CLIEN_ID)  
                     .secret(CLIENT_SECRET)  
                     .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT )  
                     .scopes(SCOPE_READ, SCOPE_WRITE, TRUST)  
                     .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS).  
                     refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);  
      }  
      @Override  
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {  
           endpoints.tokenStore(tokenStore)  
                     .authenticationManager(authenticationManager);  
      }  
 }  

Resource Server Config

Resource in our context is the REST API which we have exposed for the crud operation. To access these resources, the client must be authenticated. In real-time scenarios, whenever an user tries to access these resources, the user will be asked to provide his authenticity and once the user is authorized then he will be allowed to access these protected resources.

ResourceServerConfig.java

 package com.sample.configuration;  
 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 ResourceServerConfig extends ResourceServerConfigurerAdapter {  
      private static final String RESOURCE_ID = "resource_id";  
      @Override  
      public void configure(ResourceServerSecurityConfigurer resources) {  
           resources.resourceId(RESOURCE_ID).stateless(false);  
      }  
      @Override  
      public void configure(HttpSecurity http) throws Exception {  
     http.  
         anonymous().disable()  
         .authorizeRequests()  
         .antMatchers("/users/**").access("hasRole('ADMIN')")  
         .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());  
      }  
 }  

Security Config

This class extends WebSecurityConfigurerAdapter and provides the usual spring security configuration. Here, we are using bcrypt encoder to encode our passwords. You can try this online Bcrypt Tool to encode and match bcrypt passwords. Following configuration basically bootstraps the authorization server and resource server.
@EnableWebSecurity: Enables spring security web security support.
@EnableGlobalMethodSecurity: Support to have method level access control such as @PreAuthorize @PostAuthorize

SecurityConfig.java

 package com.sample.configuration;  
 import javax.annotation.Resource;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.boot.web.servlet.FilterRegistrationBean;  
 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.method.configuration.EnableGlobalMethodSecurity;  
 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.core.userdetails.UserDetailsService;  
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
 import org.springframework.security.oauth2.provider.token.TokenStore;  
 import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;  
 import org.springframework.web.cors.CorsConfiguration;  
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;  
 import org.springframework.web.filter.CorsFilter;  
 @Configuration  
 @EnableWebSecurity  
 @EnableGlobalMethodSecurity(prePostEnabled = true)  
 public class SecurityConfig extends WebSecurityConfigurerAdapter {  
   @Resource(name = "appService")  
   private UserDetailsService userDetailsService;  
   @Override  
   @Bean  
   public AuthenticationManager authenticationManagerBean() throws Exception {  
     return super.authenticationManagerBean();  
   }  
   @Autowired  
   public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {  
     auth.userDetailsService(userDetailsService)  
         .passwordEncoder(encoder());  
   }  
   @Override  
   protected void configure(HttpSecurity http) throws Exception {  
     http  
         .csrf().disable()  
         .anonymous().disable()  
         .authorizeRequests()  
         .antMatchers("/api-docs/**").permitAll();  
   }  
   @Bean  
   public TokenStore tokenStore() {  
     return new InMemoryTokenStore();  
   }  
   @Bean  
   public BCryptPasswordEncoder encoder(){  
     return new BCryptPasswordEncoder();  
   }  
   @Bean  
   public FilterRegistrationBean corsFilter() {  
     UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
     CorsConfiguration config = new CorsConfiguration();  
     config.setAllowCredentials(true);  
     config.addAllowedOrigin("*");  
     config.addAllowedHeader("*");  
     config.addAllowedMethod("*");  
     source.registerCorsConfiguration("/**", config);  
     FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));  
     bean.setOrder(0);  
     return bean;  
   }  
 }  

Rest APIs

Following are the very basic REST APIs that we have exposed for testing purpose.

AppController.java

 package com.sample.controller;  
 import java.util.List;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.web.bind.annotation.PathVariable;  
 import org.springframework.web.bind.annotation.RequestBody;  
 import org.springframework.web.bind.annotation.RequestMapping;  
 import org.springframework.web.bind.annotation.RequestMethod;  
 import org.springframework.web.bind.annotation.RestController;  
 import com.sample.model.User;  
 import com.sample.service.AppService;  
 @RestController  
 @RequestMapping("/users")  
 public class AppController {  
      @Autowired  
      private AppService userService;  
      @RequestMapping(value = "/user", method = RequestMethod.GET)  
      public List<User> listUser() {  
           return userService.findAll();  
      }  
      @RequestMapping(value = "/user", method = RequestMethod.POST)  
      public User create(@RequestBody User user) {  
           return userService.save(user);  
      }  
      @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)  
      public String delete(@PathVariable(value = "id") Long id) {  
           userService.delete(id);  
           return "success";  
      }  
 }  

Now let us define the AppService that is responsible for fetching user details from the database. Following is the implementation that spring will be used to validate the user.

AppServiceImpl.java

 package com.sample.service.impl;  
 import java.util.ArrayList;  
 import java.util.Arrays;  
 import java.util.List;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.security.core.authority.SimpleGrantedAuthority;  
 import org.springframework.security.core.userdetails.UserDetails;  
 import org.springframework.security.core.userdetails.UserDetailsService;  
 import org.springframework.security.core.userdetails.UsernameNotFoundException;  
 import org.springframework.stereotype.Service;  
 import com.sample.dao.UserDao;  
 import com.sample.model.User;  
 import com.sample.service.AppService;  
 @Service(value = "appService")  
 public class AppServiceImpl implements UserDetailsService, AppService {  
      @Autowired  
      private UserDao userDao;  
      public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {  
           User user = userDao.findByUsername(userId);  
           if(user == null){  
                throw new UsernameNotFoundException("Invalid username or password.");  
           }  
           return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());  
      }  
      private List<SimpleGrantedAuthority> getAuthority() {  
           return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));  
      }  
      public List<User> findAll() {  
           List<User> list = new ArrayList<>();  
           userDao.findAll().iterator().forEachRemaining(list::add);  
           return list;  
      }  
      @Override  
      public void delete(long id) {  
           userDao.deleteById(id);  
      }  
      @Override  
   public User save(User user) {  
     return userDao.save(user);  
   }  
 }  

DataBase Script

Include a data.sql file inside the resource folder so it will be executed each time the project is initialised. It will have a password encrypted with BCrypt.

 INSERT INTO user_detail (id, username, password, salary, age) VALUES (1, 'Ares', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 26);  
 INSERT INTO user_detail (id, username, password, salary, age) VALUES (2, 'Ashwin', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 35);  
 INSERT INTO user_detail (id, username, password, salary, age) VALUES (3, 'De', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 26);  

Testing the Application

Here, we will test the app with Postman. Run Application.java as a java application. We will be using postman to test the OAuth2 implementation.

Generate AuthToken: In the header, we have username and password as Ares and password respectively as Authorization header. As per Oauth2 specification, Access token request should use application/x-www-form-urlencoded.Following is the setup.

http://localhost:8080/oauth/token as follows,






















Common Errors

If you face the below error, cross-verify the username, password and grant_type




















Conclusion

In this tutorial we learned about securing REST API with OAUTH2 with implementation of resouce server and authorisation server. If you have anything that you want to add or share then please share it below in the comment section.You can download the source from github.

Comments

  1. Very informative. Keep going.

    ReplyDelete
  2. Great work. Keep going on.. information is wealth. U will attain great success.

    ReplyDelete
  3. Yes it's good to read more info.. keep rocking..

    ReplyDelete
  4. Thanks for this complete guide.

    ReplyDelete
  5. It's very useful.. keep updating

    ReplyDelete

Post a Comment

Popular posts from this blog

Spring boot with gradle