Enterprise Java

Centralized Authorization with OAuth2 & Opaque Tokens using Spring Boot 2

If you are looking for JWT implementation please follow this link

This guide walks through the process to create a centralized authentication and authorization server with Spring Boot 2, a demo resource server will also be provided.

If you’re not familiar with OAuth2 I recommend this read.

Pre-req

Implementation Overview

For this project we’ll be using Spring Security 5 through Spring Boot. If you’re familiar with the earlier versions this Spring Boot Migration Guide might be useful.

OAuth2 Terminology

  • Resource Owner
    • The user who authorizes an application to access his account. The access is limited to the scope.
  • Resource Server:
    • A server that handles authenticated requests after the client has obtained an access token.
  • Client
    • An application that access protected resources on behalf of the resource owner.
  • Authorization Server
    • A server which issues access tokens after successfully authenticating a clientand resource owner, and authorizing the request.
  • Access Token
    • A unique token used to access protected resources
  • Scope
    • A Permission
  • Grant type

Authorization Server

To build our Authorization Server we’ll be using Spring Security 5.x through Spring Boot 2.0.x.

Dependencies

You can go to start.spring.io and generate a new project and then add the following dependencies:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>   
    </dependencies>

Database

For the sake of this guide we’ll be using H2 Database.
Here you can find a reference OAuth2 SQL schema required by Spring Security.

CREATE TABLE IF NOT EXISTS oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256) NOT NULL,
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4000),
  autoapprove VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256),
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

CREATE TABLE IF NOT EXISTS oauth_code (
  code VARCHAR(256), authentication BLOB
);

And then add the following entry

-- The encrypted client_secret it `secret`
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
  VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);

The client_secret above was generated using bcrypt.
The prefix {bcrypt} is required because we’ll using Spring Security 5.x’s new feature of DelegatingPasswordEncoder.

Bellow here you can find the User and Authority reference SQL schema used by Spring’s org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl.

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(256) NOT NULL,
  password VARCHAR(256) NOT NULL,
  enabled TINYINT(1),
  UNIQUE KEY unique_username(username)
);

CREATE TABLE IF NOT EXISTS authorities (
  username VARCHAR(256) NOT NULL,
  authority VARCHAR(256) NOT NULL,
  PRIMARY KEY(username, authority)
);

Same as before add the following entries for the user and its authority.

-- The encrypted password is `pass`
INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');

Spring Security Configuration

Add the following Spring configuration class.

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;

@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final DataSource dataSource;

    private PasswordEncoder passwordEncoder;
    private UserDetailsService userDetailsService;

    public WebSecurityConfiguration(final DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

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

    @Bean
    public PasswordEncoder passwordEncoder() {
        if (passwordEncoder == null) {
            passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
        return passwordEncoder;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        if (userDetailsService == null) {
            userDetailsService = new JdbcDaoImpl();
            ((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
        }
        return userDetailsService;
    }

}

Quoting from Spring Blog:

The @EnableWebSecurity annotation and WebSecurityConfigurerAdapter work together to provide web based security.

If you are using Spring Boot the DataSource object will be auto-configured and you can just inject it to the class instead of defining it yourself. it needs to be injected to the UserDetailsService in which will be using the provided JdbcDaoImpl provided by Spring Security, if necessary you can replace this with your own implementation.

As the Spring Security’s AuthenticationManager is required by some auto-configured Spring @Beans it’s necessary to override the authenticationManagerBean method and annotate is as a @Bean.

The PasswordEncoder will be handled by PasswordEncoderFactories.createDelegatingPasswordEncoder() in which handles a few of password encoders and delegates based on a prefix, in our example we are prefixing the passwords with {bcrypt}.

Authorization Server Configuration

The authorization server validates the client and user credentials and provides the tokens.

Add the following Spring configuration class.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;

    private TokenStore tokenStore;

    public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
                                            final AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }

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

    @Bean
    public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setAuthenticationManager(authenticationManager);
        return tokenServices;
    }

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

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

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

}

User Info Endpoint

Now we need to define an endpoint where the authorization token can be decoded into an Authorization object, to do so add the following class.

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

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

    @GetMapping("/me")
    public ResponseEntity get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

}

Resource Server Configuration

The resource server hosts the HTTP resources in which can be a document a photo or something else, in our case it will be a REST API protected by OAuth2.

Dependencies

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
           
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>                
   </dependencies>

Defining our protected API

The code bellow defines the endpoint /me and returns the Principal object and it requires the authenticated user to have the ROLE_USER to access.

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

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

    @GetMapping
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

}

The @PreAuthorize annotation validates whether the user has the given role prior to execute the code, to make it work it’s necessary to enable the prePost annotations, to do so add the following class:

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {

}

The important part here is the @EnableGlobalMethodSecurity(prePostEnabled = true)annotation, the prePostEnabled flag is set to false by default, turning it to true makes the @PreAuthorize annotation to work.

Resource Server Configuration

Now let’s add the Spring’s configuration for the resource server.

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

}

The @EnableResourceServer annotation, from the javadoc:

Convenient annotation for OAuth2 Resource Servers, enabling a Spring Security filter that authenticates requests via an incoming OAuth2 token. Users should add this annotation and provide a @Bean of type {@link ResourceServerConfigurer} (e.g. via {@link ResourceServerConfigurerAdapter}) that specifies the details of the resource (URL paths and resource id). In order to use this filter you must {@link EnableWebSecurity} somewhere in your application, either in the same place as you use this annotation, or somewhere else.

Now that we have all the necessary code in place we need to configure a RemoteTokenServices, lucky for us Spring provides a configuration property where we can set the url where the tokens can be translated to an Authentication object.

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/profile/me

Testing all together

To test all together we need to spin up the Authorization Server and the Resource Server as well, in my setup it will be running on port 9001 and 9101 accordingly.

Generating the token

$ curl -u clientId:secret -X POST localhost:9001/oauth/token\?grant_type=password\&username=user\&password=pass

{
  "access_token" : "e47876b0-9962-41f1-ace3-e3381250ccea",
  "token_type" : "bearer",
  "refresh_token" : "8e17a71c-cb39-4904-8205-4d9f8c71aeef",
  "expires_in" : 299,
  "scope" : "read write"
}

Accessing the resource

Now that you have generated the token copy the access_token and add it to the request on the Authorization HTTP Header, e.g:

$ curl -i localhost:9101/me -H "Authorization: Bearer c06a4137-fa07-4d9a-97f9-85d1ba820d3a"

{
  "authorities" : [ {
    "authority" : "ROLE_USER"
  } ],
  "details" : {
    "remoteAddress" : "127.0.0.1",
    "sessionId" : null,
    "tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
    "tokenType" : "Bearer",
    "decodedDetails" : null
  },
  "authenticated" : true,
  "userAuthentication" : {
    "authorities" : [ {
      "authority" : "ROLE_USER"
    } ],
    "details" : {
      "authorities" : [ {
        "authority" : "ROLE_USER"
      } ],
      "details" : {
        "remoteAddress" : "127.0.0.1",
        "sessionId" : null,
        "tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
        "tokenType" : "Bearer",
        "decodedDetails" : null
      },
      "authenticated" : true,
      "userAuthentication" : {
        "authorities" : [ {
          "authority" : "ROLE_USER"
        } ],
        "details" : {
          "grant_type" : "password",
          "username" : "user"
        },
        "authenticated" : true,
        "principal" : {
          "password" : null,
          "username" : "user",
          "authorities" : [ {
            "authority" : "ROLE_USER"
          } ],
          "accountNonExpired" : true,
          "accountNonLocked" : true,
          "credentialsNonExpired" : true,
          "enabled" : true
        },
        "credentials" : null,
        "name" : "user"
      },
      "clientOnly" : false,
      "oauth2Request" : {
        "clientId" : "clientId",
        "scope" : [ "read", "write" ],
        "requestParameters" : {
          "grant_type" : "password",
          "username" : "user"
        },
        "resourceIds" : [ ],
        "authorities" : [ {
          "authority" : "ROLE_CLIENT"
        } ],
        "approved" : true,
        "refresh" : false,
        "redirectUri" : null,
        "responseTypes" : [ ],
        "extensions" : { },
        "grantType" : "password",
        "refreshTokenRequest" : null
      },
      "credentials" : "",
      "principal" : {
        "password" : null,
        "username" : "user",
        "authorities" : [ {
          "authority" : "ROLE_USER"
        } ],
        "accountNonExpired" : true,
        "accountNonLocked" : true,
        "credentialsNonExpired" : true,
        "enabled" : true
      },
      "name" : "user"
    },
    "authenticated" : true,
    "principal" : "user",
    "credentials" : "N/A",
    "name" : "user"
  },
  "principal" : "user",
  "credentials" : "",
  "clientOnly" : false,
  "oauth2Request" : {
    "clientId" : null,
    "scope" : [ ],
    "requestParameters" : { },
    "resourceIds" : [ ],
    "authorities" : [ ],
    "approved" : true,
    "refresh" : false,
    "redirectUri" : null,
    "responseTypes" : [ ],
    "extensions" : { },
    "grantType" : null,
    "refreshTokenRequest" : null
  },
  "name" : "user"
}

Footnote

Published on Java Code Geeks with permission by Marcos Barbero, partner at our JCG program. See the original article here: Centralized Authorization with OAuth2 + Opaque Tokens using Spring Boot 2

Opinions expressed by Java Code Geeks contributors are their own.

Marcos Barbero

Marcos is a hands-on Software Architect who worked on e-commerce domain for several years and currently working in the banking domain. He is very experienced in the Spring Framework ecosystem.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Farrukh Mahmud
Farrukh Mahmud
5 years ago

typo in UserController public ResponseEntity get(final Principal principal) should be public ResponseEntity get(final Principal principal)

Back to top button