Spring Boot + OAuth2 + Securing REST API + MySQL

In this blog, we will learn about OAuth2 and how we can secure our REST API using OAuth2 with Spring JPA in the Spring Boot application. Spring boot oauth2 rest API example. #spring boot + oauth2 + mysql

What is OAuth2? Why it is needed?

OAuth2 provides authorization flows for web and desktop applications, and mobile devices.
OAuth2 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service, such as Facebook, GitHub, and DigitalOcean.

What are OAuth2 Roles

OAuth defines the four main roles

  • Resource Owner
  • Client
  • Resource Server
  • Authorization Server

Resource Owner: User – The resource owner is the user who authorizes an application to access their account

Resource / Authorization Server – The resource server hosts the protected user accounts, and the authorization server verifies the identity of the user then issues access tokens to the application.

Client: Application – The client is the application that wants to access the user’s account. The user must allow it before it may do so, and the API must validate the authorization.

OAuth2 High-Level Workflow

OAuth2 High Level Workflow, spring boot oauth2 mysql - spring boot oauth2 rest api example

OAuth2 Implicit Workflow

OAuth2 Implicit Workflow - spring boot oauth2 rest api example

Spring Boot: Application Structure

In this example, we are using the following technology stack

  • Spring Boot 2.4.4
  • Java 8
  • MySql
Application Structure - spring boot + oauth2 + mysql
package com.the.basic.tech.info;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Spring Boot: Implement Oauth2 Authorization Server

OAuth2AuthorizationServerConfig class extends AuthorizationServerConfigurerAdapter and is responsible for generating tokens specific to a client.
Suppose, if a user wants to log in to www.xyz.com via Facebook then Facebook Auth Server will be generating tokens for Devglan. In this case, Devglan 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.
@EnableAuthorizationServer:
Enables an authorization server. AuthorizationServerEndpointsConfigurer defines the authorization and token endpoints and the token services.

package com.the.basic.tech.info.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.AuthorizationServerSecurityConfigurer;

@SuppressWarnings("deprecation")
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Value("${user.oauth.clientId}")
    private String clientID;

    @Value("${user.oauth.clientSecret}")
    private String clientSecret;

    @Value("${user.oauth.redirectUris}")
    private String redirectURLs;

    @Value("${user.oauth.accessTokenValidity}")
    private int accessTokenValidity;

    @Value("${user.oauth.refreshTokenValidity}")
    private int refreshTokenValidity;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()");
    }

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
            .inMemory()
            .withClient(clientID)
            .secret(passwordEncoder.encode(clientSecret))
            .authorizedGrantTypes("password", "authorization_code", "refresh_token")
            .scopes("user_info")
            .authorities("READ_ONLY_CLIENT")
            .redirectUris(redirectURLs)
            .accessTokenValiditySeconds(accessTokenValidity)
            .refreshTokenValiditySeconds(refreshTokenValidity);
    }
}

Spring Security Configuration

Below Spring configuration class enables and configures an OAuth authorization server.

package com.the.basic.tech.info.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@SuppressWarnings("deprecation")
@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/api/**").authenticated()
            .antMatchers("/").permitAll();
    }
}
package com.the.basic.tech.info.config;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
@Order(1)
public class OauthSecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Value("${user.oauth.user.username}")
    private String username;

    @Value("${user.oauth.user.password}")
    private String password;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .antMatcher("/**")
            .authorizeRequests()
            .antMatchers("/oauth/authorize**", "/login**", "/error**")
            .permitAll()
            .and()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser(username).password(passwordEncoder().encode(password)).roles("ADMIN");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @SuppressWarnings({ "rawtypes", "unchecked" })
	@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;
    }
}

Application Properties (application.yaml)

# DB Configuration
spring:
 datasource:
  url: jdbc:mysql://localhost:3306/bootdb
  username: root
  password: admin
 jpa: 
  hibernate.ddl-auto: update
  show-sql: true
 user.datasource.driver-class-name: com.mysql.jdbc.Driver
user.oauth.clientId: devglan-client
user.oauth.clientSecret: devglan-secret
user.oauth.redirectUris: http://localhost:8081/login
user.oauth.user.username: Alex123
user.oauth.user.password: password

user.oauth.accessTokenValidity: 3600
user.oauth.refreshTokenValidity: 21600
security.oauth2.resource.filter-order: 3

Maven File (pom.xm)

Maven file (pom.xml) would be like as below

<?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.example</groupId>
	<artifactId>spring-boot-security-oauth2</artifactId>
	<version>1.0-SNAPSHOT</version>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.4</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.4.0.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.4</version>
		</dependency>
		<dependency>
			<groupId>javax.persistence</groupId>
			<artifactId>javax.persistence-api</artifactId>
		</dependency>
	</dependencies>

	<properties>
		<java.version>1.8</java.version>
	</properties>


	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Securing Resources (Rest API) – UserController

package com.the.basic.tech.info.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.the.basic.tech.info.model.User;
import com.the.basic.tech.info.service.UserService;

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService 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";
    }

}

Application Model and Dao Layer (Spring JPA)

package com.the.basic.tech.info.model;

import javax.persistence.*;

import javax.persistence.Id;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private long id;
    @Column
    private String username;
    @Column
    @JsonIgnore
    private String password;
    @Column
    private long salary;
    @Column
    private int age;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public long getSalary() {
        return salary;
    }

    public void setSalary(long salary) {
        this.salary = salary;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
package com.the.basic.tech.info.dao;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.the.basic.tech.info.model.User;

@Repository
public interface UserDao extends CrudRepository<User, Long> {
    User findByUsername(String username);
}

Application Service Layer

package com.the.basic.tech.info.service;

import java.util.List;

import com.the.basic.tech.info.model.User;

public interface UserService {

    User save(User user);
    List<User> findAll();
    void delete(long id);
}
package com.the.basic.tech.info.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.the.basic.tech.info.dao.UserDao;
import com.the.basic.tech.info.model.User;
import com.the.basic.tech.info.service.UserService;


@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
	
	@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);
    }
}

Spring Boot: Application Run and Testing

To run the application mvn spring-boot:run command needs to be executed.

2021-06-05 14:56:27.772  INFO 18240 --- [           main] com.the.basic.tech.info.Application      : No active profile set, falling back to default profiles: default
2021-06-05 14:56:28.467  INFO 18240 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2021-06-05 14:56:28.536  INFO 18240 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 53 ms. Found 1 JPA repository interfaces.
2021-06-05 14:56:29.809  INFO 18240 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-06-05 14:56:29.809  INFO 18240 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-06-05 14:56:29.809  INFO 18240 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.44]
2021-06-05 14:56:29.971  INFO 18240 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-06-05 14:56:29.971  INFO 18240 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2121 ms
2021-06-05 14:56:30.157  INFO 18240 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2021-06-05 14:56:30.210  INFO 18240 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.29.Final
2021-06-05 14:56:30.326  INFO 18240 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2021-06-05 14:56:30.426  INFO 18240 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-06-05 14:56:30.758  INFO 18240 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-06-05 14:56:30.781  INFO 18240 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
2021-06-05 14:56:31.429  INFO 18240 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2021-06-05 14:56:31.445  INFO 18240 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2021-06-05 14:56:31.715  WARN 18240 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2021-06-05 14:56:32.147  INFO 18240 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-06-05 14:56:32.666  INFO 18240 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Or [Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']] with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@c11332b, org.springframework.security.web.context.SecurityContextPersistenceFilter@26a45089, org.springframework.security.web.header.HeaderWriterFilter@3a88f6fb, org.springframework.security.web.authentication.logout.LogoutFilter@c318864, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@45bd4753, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5ab5924c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@191f4d65, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7e5e6573, org.springframework.security.web.session.SessionManagementFilter@844e66d, org.springframework.security.web.access.ExceptionTranslationFilter@74d22ddd, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@520a95ff]
2021-06-05 14:56:32.686  INFO 18240 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/**'] with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4ecd8ab1, org.springframework.security.web.context.SecurityContextPersistenceFilter@606d6d2c, org.springframework.security.web.header.HeaderWriterFilter@e3ed455, org.springframework.security.web.csrf.CsrfFilter@12c76d6e, org.springframework.security.web.authentication.logout.LogoutFilter@697a92af, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@69ce14e6, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2ab8589a, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@57bdceaa, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2e3cd732, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@56d822dc, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@52909a97, org.springframework.security.web.session.SessionManagementFilter@27bb74e1, org.springframework.security.web.access.ExceptionTranslationFilter@7956f93a, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4c65d8e3]
2021-06-05 14:56:32.749  INFO 18240 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-06-05 14:56:32.764  INFO 18240 --- [           main] com.the.basic.tech.info.Application      : Started Application in 5.779 seconds (JVM running for 6.467)
2021-06-05 14:56:41.146  INFO 18240 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-06-05 14:56:41.146  INFO 18240 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-06-05 14:56:41.152  INFO 18240 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms

How to Get Authorization Grant Code

http://localhost:8080/oauth/authorize?client_id=devglan-client&response_type=code&scope=user_info

This URL will redirect to the login page. Once the customer provides the login information, the system redirects to the grant access page. The page gives an option to the customer to approve or reject the request or to provide certain access to the third-party applications.

Enter the credentials as Alex123/password

OAuth Approval page will be opened

Once we approve the request, it will redirect to an URL (check the user.oauth.redirectUris property). This redirect URL will also contain a code as part of the query string (http://localhost:8081/login?code=5BsHRS). This code is the authorization code for the third-party application.

Generate Auth Token + Get Access Token

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

Once you make the request you will get the following result. It has an access token as well as a refresh token.

postman screen

Accessing Resource Without Token

Accessing Resource Without Token

Accessing Resource With Token

Accessing Resource With Token

Using refresh_token to refresh the token

Usually, the token expiry time is significantly less in the case of oAuth2 and you can use the following API to refresh the token once it is expired.

Using refresh_token to refresh the token

Download Source Code (Attached)

Thank you for your time. Keep learning 🙂