Secure Spring REST With Spring Security and OAuth2

In this post, we are going to demonstrate Spring Security + OAuth2 for securing REST API endpoints on an example Spring Boot project. Clients and user credentials will be stored in a relational database (example configurations prepared for H2 and PostgreSQL database engines). To do it we will have to:

  • Configure Spring Security + database.
  • Create an Authorization Server.
  • Create a Resource Server.
  • Get an access token and a refresh token.
  • Get a secured Resource using an access token.

To simplify the demonstration, we are going to combine the Authorization Server and Resource Server in the same project. As a grant type, we will use a password (we will use BCrypt to hash our passwords).

Before you start you should familiarize yourself with OAuth2 fundamentals.

Introduction

The OAuth 2.0 specification defines a delegation protocol that is useful for conveying authorization decisions across a network of web-enabled applications and APIs. OAuth is used in a wide variety of applications, including providing mechanisms for user authentication.

OAuth Roles

OAuth specifies four roles:

  • Resource owner (the User) – an entity capable of granting access to a protected resource (for example end-user).
  • Resource server (the API server) – the server hosting the protected resources, capable of accepting responding to protected resource requests using access tokens.
  • Client – an application making protected resource requests on behalf of the resource owner and with its authorization.
  • Authorization server – the server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.

Grant Types

OAuth 2 provides several "grant types" for different use cases. The grant types defined are:

  • Authorization Code
  • Password
  • Client credentials
  • Implicit

The overall flow of a Password Grant:

Image title

Application

Let’s consider the database layer and application layer for our example application.

Business Data

Our main business object is Company:

Image title

Based on CRUD operations for Company and Department objects ,we want to define following access rules:

  • COMPANY_CREATE

  • COMPANY_READ

  • COMPANY_UPDATE

  • COMPANY_DELETE

  • DEPARTMENT_CREATE

  • DEPARTMENT_READ

  • DEPARTMENT_UPDATE

  • DEPARTMENT_DELETE

In addition, we want to create ROLE_COMPANY_READER role.

OAuth2 Client Setup

We need to create the following tables in the database (for internal purposes of OAuth2 implementation):

Let’s assume that we want to call a resource server like ‘resource-server-rest-api.’ For this server, we define two clients called:

  • spring-security-oauth2-read-client (authorized grant types: read)
  • spring-security-oauth2-read-write-client (authorized grant types: read, write)
INSERT INTO OAUTH_CLIENT_DETAILS(CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY)
 VALUES ('spring-security-oauth2-read-client', 'resource-server-rest-api',
 /*spring-security-oauth2-read-client-password1234*/'$2a$04$WGq2P9egiOYoOFemBRfsiO9qTcyJtNRnPKNBl5tokP7IP.eZn93km',
 'read', 'password,authorization_code,refresh_token,implicit', 'USER', 10800, 2592000);

INSERT INTO OAUTH_CLIENT_DETAILS(CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY)
 VALUES ('spring-security-oauth2-read-write-client', 'resource-server-rest-api',
 /*spring-security-oauth2-read-write-client-password1234*/'$2a$04$soeOR.QFmClXeFIrhJVLWOQxfHjsJLSpWrU1iGxcMGdu.a5hvfY4W',
 'read,write', 'password,authorization_code,refresh_token,implicit', 'USER', 10800, 2592000);

Note that password is hashed with BCrypt (4 rounds).

Authorities and Users Setup

Spring Security comes with two useful interfaces:

To store authorization data we will define following data model:

Image title

Because we want to come with some pre-loaded data, below is the script that will load all authorities:

INSERT INTO AUTHORITY(ID, NAME) VALUES (1, 'COMPANY_CREATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (2, 'COMPANY_READ');
INSERT INTO AUTHORITY(ID, NAME) VALUES (3, 'COMPANY_UPDATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (4, 'COMPANY_DELETE');

INSERT INTO AUTHORITY(ID, NAME) VALUES (5, 'DEPARTMENT_CREATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (6, 'DEPARTMENT_READ');
INSERT INTO AUTHORITY(ID, NAME) VALUES (7, 'DEPARTMENT_UPDATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (8, 'DEPARTMENT_DELETE');

Here is the script to load all users and assigned authorities:

INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
  VALUES (1, 'admin', /*admin1234*/'$2a$08$qvrzQZ7jJ7oy2p/msL4M0.l83Cd0jNsX6AJUitbgRXGzge4j035ha', FALSE, FALSE, FALSE, TRUE);

INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
  VALUES (2, 'reader', /*reader1234*/'$2a$08$dwYz8O.qtUXboGosJFsS4u19LHKW7aCQ0LXXuNlRfjjGKwj5NfKSe', FALSE, FALSE, FALSE, TRUE);

INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
  VALUES (3, 'modifier', /*modifier1234*/'$2a$08$kPjzxewXRGNRiIuL4FtQH.mhMn7ZAFBYKB3ROz.J24IX8vDAcThsG', FALSE, FALSE, FALSE, TRUE);

INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
  VALUES (4, 'reader2', /*reader1234*/'$2a$08$vVXqh6S8TqfHMs1SlNTu/.J25iUCrpGBpyGExA.9yI.IlDRadR6Ea', FALSE, FALSE, FALSE, TRUE);

INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 1);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 2);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 3);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 4);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 5);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 6);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 7);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 8);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 9);

INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (2, 2);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (2, 6);

INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (3, 3);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (3, 7);

INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (4, 9);

Note that the password is hashed with BCrypt (8 rounds).

Application Layer

The test application is developed in Spring boot + Hibernate + Flyway with an exposed REST API. To demonstrate data company operations, the following endpoints were created:

@RestController
@RequestMapping("/secured/company")
public class CompanyController {

    @Autowired
    private CompanyService companyService;

    @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody
    List getAll() {
        return companyService.getAll();
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody
    Company get(@PathVariable Long id) {
        return companyService.get(id);
    }

    @RequestMapping(value = "/filter", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody
    Company get(@RequestParam String name) {
        return companyService.get(name);
    }

    @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public ResponseEntity> create(@RequestBody Company company) {
        companyService.create(company);
        HttpHeaders headers = new HttpHeaders();
        ControllerLinkBuilder linkBuilder = linkTo(methodOn(CompanyController.class).get(company.getId()));
        headers.setLocation(linkBuilder.toUri());
        return new ResponseEntity(headers, HttpStatus.CREATED);
    }

    @RequestMapping(method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public void update(@RequestBody Company company) {
        companyService.update(company);
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public void delete(@PathVariable Long id) {
        companyService.delete(id);
    }
}

PasswordEncoders

Since we are going to use different encryptions for OAuth2 client and user, we will define separate password encoders for encryption:

  • OAuth2 client password – BCrypt (4 rounds)
  • User password – BCrypt (8 rounds)
@Configuration
public class Encoders {

    @Bean
    public PasswordEncoder oauthClientPasswordEncoder() {
        return new BCryptPasswordEncoder(4);
    }

    @Bean
    public PasswordEncoder userPasswordEncoder() {
        return new BCryptPasswordEncoder(8);

Spring Security Configuration

Provide UserDetailsService

Because we want to get users and authorities from the database, we need to tell Spring Security how to get this data. To do it we have to provide an implementation of the UserDetailsService  interface:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);

        if (user != null) {
            return user;
        }

        throw new UsernameNotFoundException(username);
    }
}

To separate the service and repository layers we will create UserRepository with JPA Repository:

@Repository
public interface UserRepository extends JpaRepository {

    @Query("SELECT DISTINCT user FROM User user " +
            "INNER JOIN FETCH user.authorities AS authorities " +
            "WHERE user.username = :username")
    User findByUsername(@Param("username") String username);
}

Setup Spring Security

The @EnableWebSecurity annotation and WebSecurityConfigurerAdapter work together to provide security to the application. The @Order annotation is used to specify which WebSecurityConfigurerAdapter should be considered first.

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@Import(Encoders.class)
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder userPasswordEncoder;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(userPasswordEncoder);
    }
}

OAuth2 Configuration

First of all, we have to implement the following components:

  • Authorization Server
  • Resource Server

Authorization Server

The authorization server is responsible for the verification of user identity and providing the tokens.

Image title

Spring Security handles the Authentication and Spring Security OAuth2 handles the Authorization. To configure and enable the OAuth 2.0 Authorization Server we have to use @EnableAuthorizationServer annotation.

@Configuration
@EnableAuthorizationServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import(ServerSecurityConfig.class)
public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder oauthClientPasswordEncoder;

    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    @Bean
    public OAuth2AccessDeniedHandler oauthAccessDeniedHandler() {
        return new OAuth2AccessDeniedHandler();
    }

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager).userDetailsService(userDetailsService);
    }
}

Some important points. We:

  • Defined the TokenStore bean to let Spring know to use the database for token operations.
  • Overrode the configure methods to use the custom UserDetailsService implementation, AuthenticationManager bean, and OAuth2 client’s password encoder.
  • Defined handler bean for authentication issues.
  • Enabled two endpoints for checking tokens (/oauth/check_token and /oauth/token_key) by overriding the configure (AuthorizationServerSecurityConfigureroauthServer) method.

Resource Server

A Resource Server serves resources that are protected by the OAuth2 token.

Image title

Spring OAuth2 provides an authentication filter that handles protection. The @EnableResourceServer annotation enables a Spring Security filter that authenticates requests via an incoming OAuth2 token.

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource-server-rest-api";
    private static final String SECURED_READ_SCOPE = "#oauth2.hasScope('read')";
    private static final String SECURED_WRITE_SCOPE = "#oauth2.hasScope('write')";
    private static final String SECURED_PATTERN = "/secured/**";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers(SECURED_PATTERN).and().authorizeRequests()
                .antMatchers(HttpMethod.POST, SECURED_PATTERN).access(SECURED_WRITE_SCOPE)
                .anyRequest().access(SECURED_READ_SCOPE);
    }
}

The configure(HttpSecurity http) method configures the access rules and request matchers (path) for protected resources using the HttpSecurity class. We secure the URL paths /secured/*. It’s worth noting that to invoke any POST method request, the ‘write’ scope is needed.

Let’s check if our authentication endpoint is working – invoke:

curl -X POST 
  http://localhost:8080/oauth/token 
  -H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLXdyaXRlLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtd3JpdGUtY2xpZW50LXBhc3N3b3JkMTIzNA==' 
  -F grant_type=password 
  -F username=admin 
  -F password=admin1234 
  -F client_id=spring-security-oauth2-read-write-client

Below are screenshots from Postman:

Image title

and

Image title

You should get a response similar to the following:

{
    "access_token": "e6631caa-bcf9-433c-8e54-3511fa55816d",
    "token_type": "bearer",
    "refresh_token": "015fb7cf-d09e-46ef-a686-54330229ba53",
    "expires_in": 9472,
    "scope": "read write"
}

Access Rules Configuration

We decided to secure access to the Company and Department objects on the service layer. We have to use the @PreAuthorize annotation.

@Service
public class CompanyServiceImpl implements CompanyService {

    @Autowired
    private CompanyRepository companyRepository;

    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasAuthority('COMPANY_READ') and hasAuthority('DEPARTMENT_READ')")
    public Company get(Long id) {
        return companyRepository.find(id);
    }

    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasAuthority('COMPANY_READ') and hasAuthority('DEPARTMENT_READ')")
    public Company get(String name) {
        return companyRepository.find(name);
    }

    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasRole('COMPANY_READER')")
    public List getAll() {
        return companyRepository.findAll();
    }

    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_CREATE')")
    public void create(Company company) {
        companyRepository.create(company);
    }

    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_UPDATE')")
    public Company update(Company company) {
        return companyRepository.update(company);
    }

    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_DELETE')")
    public void delete(Long id) {
        companyRepository.delete(id);
    }

    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_DELETE')")
    public void delete(Company company) {
        companyRepository.delete(company);
    }
}

Let’s test if our endpoint is working fine:

curl -X GET 
  http://localhost:8080/secured/company/ 
  -H 'authorization: Bearer e6631caa-bcf9-433c-8e54-3511fa55816d'

Let’s see what will happen if we authorize with it ‘spring-security-oauth2-read-client’ – this client has only the read scope defined.

curl -X POST 
  http://localhost:8080/oauth/token 
  -H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtY2xpZW50LXBhc3N3b3JkMTIzNA==' 
  -F grant_type=password 
  -F username=admin 
  -F password=admin1234 
  -F client_id=spring-security-oauth2-read-client

Then for the below request:

  http://localhost:8080/secured/company 
  -H 'authorization: Bearer f789c758-81a0-4754-8a4d-cbf6eea69222' 
  -H 'content-type: application/json' 
  -d '{
    "name": "TestCompany",
    "departments": null,
    "cars": null
}'

We are getting the following error:

{
    "error": "insufficient_scope",
    "error_description": "Insufficient scope for this resource",
    "scope": "write"
}

Summary

In this blog post, we showed OAuth2 authentication with Spring. Access rights were defined straightforward – by establishing a direct connection between User and Authorities. To enhance this example we can add an additional entity – Role  – to improve the structure of the access rights.

The source code for the above listings can be found in this GitHub project.

文章来源于互联网:Secure Spring REST With Spring Security and OAuth2

发布者:小站,转转请注明出处:http://blog.gzcity.top/4271.html

(1)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年5月3日 18:08
下一篇 2022年5月3日 18:09

相关推荐

  • Angular + React: Vulnerability Cheatsheet

    Securing applications is not the easiest thing to do. An application has many components: server-side logic, client-side logic, data storage, data transportation, API, and more. Wi…

    安全 2022年5月3日
    661100
  • Apache Log4J2 远程代码执行漏洞处置手册

    1、漏洞概述 Apache Log4j2是一个基于Java的日志记录工具。该日志框架被大量用于业务系统开发,用来记录日志信息。此次爆发的0day漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。 漏洞细节 漏洞PoC 漏洞EXP 在野利用已公开 已公开 已公开 存在 参考链接:https://issues.apache.org/jira…

    2022年8月3日
    645720
  • 关于Apache log4j存在远程代码执行漏洞的预警

    12月9日晚,互联网上公开了Apache log4j2的一个严重级别的远程代码执行漏洞。由于Apache log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求触发远程代码执行漏洞,而无需特殊配置。Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受此漏洞影响。鉴于该框架应用范围极广,且漏洞利…

    2022年8月3日
    295110
  • Keys to a Great API Security Solution

    So, do you think your APIs are secure? You might want to take another look at your security.  Figure 1 What is API security? APIs are everywhere, and API Security is of the utmost …

    2022年5月3日
    59930
  • Hacking and Securing Python Applications

    Securing applications is not the easiest thing to do. An application has many components: server-side logic, client-side logic, data storage, data transportation, API, and more. Wi…

    安全 2022年5月3日
    35150

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注