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.
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.
Very informative. Keep going.
ReplyDeleteGreat work. Keep going on.. information is wealth. U will attain great success.
ReplyDeleteYes it's good to read more info.. keep rocking..
ReplyDeleteThanks for this complete guide.
ReplyDeleteIt's very useful.. keep updating
ReplyDelete