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
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.
OAuth defines the four main roles
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.
In this example, we are using the following technology stack
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);
}
}
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);
}
}
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;
}
}
# 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.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>
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";
}
}
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);
}
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);
}
}
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
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.
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.
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.
Thank you for your time. Keep learning 🙂