Secure your Spring REST API using OAuth2

- Java 1.8
- Maven
- Any IDE of your choice
- Postman HTTP client
1. Brief Intro to OAuth2 concept
Before starting our development works, let’s go through the OAuth2 concept in general. This will help us to understand the overall picture in a better way. The OAuth 2.0 authorization framework enables a third party application to obtain limited access to HTTP service (resource). It does this by introducing an authorization layer and separating the role of the client from that of the resource owner. In OAuth, the client requests access to resources controlled by the resource owner and hosted by the resource server, and is issued a different set of credentials than those of the resource owner.1.1. Roles
OAuth defines four roles: resource owner An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user. resource server The server hosting the protected resources, capable of accepting and 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. The term “client” does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices). authorization server The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.1.2 Protocol flow

Authorization Grant - credentials representing the resource owner's authorization.
].
(B) The authorization server authenticates the client and validates the authorization grant, and if valid, issues an access token and a refresh token.
(C) The client makes a protected resource request to the resource server by presenting the access token.
(D) The resource server validates the access token, and if valid, serves the request.
(E) Steps (C) and (D) repeat until the access token expires. If the client knows the access token expired, it skips to step (G); otherwise, it makes another protected resource request.
(F) Since the access token is invalid, the resource server returns an invalid token error.
(G) The client requests a new access token by authenticating with the authorization server and presenting the refresh token. The client authentication requirements are based on the client type and on the authorization server policies.
(H) The authorization server authenticates the client and validates the refresh token, and if valid, issues a new access token (and, optionally, a new refresh token).
1.3 OAuth2 Grant types
Grant types can be either of four types.- Authorization code
- Implicit
- Resource owner password credentials
- Client credentials
Resource owner password credentials
grant type for this example.

2. Developing the Authorization Server
Now that we have seen the OAuth2 concept, we will start developing our Authorization Server which issues access and refresh tokens to client upon successful authenticating the resource owner. Create a simple Maven project and add the below dependencies in thepom.xml
. Alternatively, you can use spring initializr to bootstrap the application at ease.
2.1. Maven dependencies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <dependencies> <!-- JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Security and OAuth2 --> <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.3.3.RELEASE</version> </dependency> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- H2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>compile</scope> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.rayfocus.oauth2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; @SpringBootApplication @EnableAuthorizationServer @EnableResourceServer public class Oauth2Application { public static void main(String[] args) { SpringApplication.run(Oauth2Application.class, args); } } |
@EnableAuthorizationServer
tells the Spring to expose REST endpoints which will be used in the Oauth2 process. Then with the annotation @EnableResourceServer
, Spring will enable a security filter that authenticates the requests via an incoming OAuth2 token.
2.3. REST Controller for exposing oauth/user
endpoint
Next, we will expose the endpoint oauth/user
in the controller class. This endpoint will be used by the protected resources to validate the OAuth2 access token and to retrieve the roles associated with the user accessing the protected resource.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.rayfocus.oauth2.controller; import java.util.HashMap; import java.util.Map; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class OAuth2Controller { @RequestMapping(value = { "oauth/user" }, produces = "application/json") public Map<String, Object> user(OAuth2Authentication user) { Map<String, Object> userInfo = new HashMap<>(); userInfo.put("user", user.getUserAuthentication().getPrincipal()); userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities())); return userInfo; } } |
OAuth2Config.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.rayfocus.oauth2.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; 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.configurers.AuthorizationServerEndpointsConfigurer; 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; @Configuration public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private DataSource dataSource; @Autowired private PasswordEncoder passwordEncoder; @Autowired private TokenStore tokenStore; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(this.tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource) .passwordEncoder(passwordEncoder); } @Bean public DefaultTokenServices tokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setSupportRefreshToken(true); tokenServices.setTokenStore(this.tokenStore); tokenServices.setAccessTokenValiditySeconds(3600); return tokenServices; } @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } } |
AuthorizationServerConfigurerAdapter
class. The first one takes the parameter endpoints of type AuthorizationServerEndpointsConfigurer
. In this method, we have declared the components tokenStore
, authenticationManager
and userDetailsService
to be used by the Spring. Also, we have registered the tokenStore
bean using the dataSource reference.
The second configure method takes the parameter clients of type ClientDetailsServiceConfigurer
. In this method, we are instructing the Spring to refer the client details from the H2 database by providing the necessary JDBC details. Spring will in turn query the entries in the table OAUTH_CLIENT_DETAILS
to check whether the client application invoking the request is allowed to access the services protected by OAuth2 service.
You may also note that we have provided the reference of passwordEncoder
. This means we have encrypted and stored some client secrets in database. Spring will then use the passwordEncoder reference to resolve the encrypted values. We have also registered the tokenServices
bean using the tokenStore reference and customized the access token validity to 3600 seconds.
2.5 Configuring user details and roles
In last step, we have configured the client application details and the secrets with OAuth2 service. Next, we need to set up the application user credentials and their roles. These details can be stored in-memory, JDBC data store or in LDAP server. For our example, we are going to use JDBC data store.
Below listing shows the code for WebSecurityConfig.java
which contains the configuration details.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package com.rayfocus.oauth2.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; 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.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private PasswordEncoder passwordEncoder; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override @Bean public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery("select username, password, enabled from USERS where username=?") .authoritiesByUsernameQuery("select username, role from USER_ROLES where username=?") .passwordEncoder(passwordEncoder); } } |
AuthenticationManager
and UserDetailsService
. Also to note, that we have used the default implementation provided by WebSecurityConfigurerAdapter
class.
Then, we have overridden configure() method which takes auth as parameter of type AuthenticationManagerBuilder
. In this method, we have instructed the Spring to use JDBC dataSource for getting the details needed for authenticating the users. The user credentials will be referred from USERS
table and their roles will be referred from USER_ROLES
table.
2.6 Schema details of OAuth2 tables for H2 database
We have used the JDBC data store in our example. Hence we need the database schema in place to have the flow working properly. Below listing shows the schema.sql for H2 database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
--------------- H2 --------------- drop table if exists oauth_client_details; create table oauth_client_details ( client_id VARCHAR(255) PRIMARY KEY, resource_ids VARCHAR(255), client_secret VARCHAR(255), scope VARCHAR(255), authorized_grant_types VARCHAR(255), web_server_redirect_uri VARCHAR(255), authorities VARCHAR(255), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(255) ); drop table if exists oauth_client_token; create table if not exists oauth_client_token ( token_id VARCHAR(255), token LONGVARBINARY, authentication_id VARCHAR(255) PRIMARY KEY, user_name VARCHAR(255), client_id VARCHAR(255) ); drop table if exists oauth_access_token; create table if not exists oauth_access_token ( token_id VARCHAR(255), token LONGVARBINARY, authentication_id VARCHAR(255) PRIMARY KEY, user_name VARCHAR(255), client_id VARCHAR(255), authentication LONGVARBINARY, refresh_token VARCHAR(255) ); drop table if exists oauth_refresh_token; create table if not exists oauth_refresh_token ( token_id VARCHAR(255), token LONGVARBINARY, authentication LONGVARBINARY ); drop table if exists oauth_code; create table if not exists oauth_code ( code VARCHAR(255), authentication LONGVARBINARY ); drop table if exists oauth_approvals; create table if not exists oauth_approvals ( userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); drop table if exists ClientDetails; create table if not exists ClientDetails ( appId VARCHAR(255) PRIMARY KEY, resourceIds VARCHAR(255), appSecret VARCHAR(255), scope VARCHAR(255), grantTypes VARCHAR(255), redirectUrl VARCHAR(255), authorities VARCHAR(255), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(255) ); -- User details related tables drop table if exists users; create table if not exists users ( username VARCHAR(45) PRIMARY KEY, password VARCHAR(100), enabled BOOLEAN ); drop table if exists user_roles; create table if not exists user_roles ( user_role_id bigint auto_increment PRIMARY KEY, username varchar(45), role varchar(45), FOREIGN KEY (username) REFERENCES users (username) ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
-- OAuth Client Details INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('rayfocus-bookstore', '$2a$10$T1XME2CdpI1eGH0cmOwfjuCcDeu00Oec7ORfDWSPJcsYwW587ZH8a', 'webclient,mobileclient', 'password,authorization_code,refresh_token', null, null, 3600, 3600, null, true); --Users INSERT INTO users (username, password, enabled) VALUES ('bookstore.user', '$2a$10$bffAVFOFkyQYCacKYrrW9OW2lLTTWjHjxc51kSnMEVfVGO2OgO6Rq', true); INSERT INTO users (username, password, enabled) VALUES ('bookstore.admin', '$2a$10$R6rgx0AcWDEIk2EJ2.NvueK6Gp3gBk66n58JEtfTctNjIJF7D6Wd2', true); -- User Roles INSERT INTO user_roles (username, role) VALUES ('bookstore.user', 'ROLE_USER'); INSERT INTO user_roles (username, role) VALUES ('bookstore.admin', 'ROLE_ADMIN'); INSERT INTO user_roles (username, role) VALUES ('bookstore.admin', 'ROLE_USER'); |
schema.sql
and data.sql
is in the classpath resource, then Spring will execute them when bootstrapping the application. You can notice that the credential values are encrypted and stored. For this example, to make the things simple I have used the BCryptPasswordEncoder
to encrypt the values. Below listing shows the code for utility class BCryptEncoderUtil.java
for your reference.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.rayfocus.oauth2.util; import java.util.Arrays; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BCryptEncoderUtil { private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); public static void main(String[] args) { String[] toEncodePasswordStrings = {"userPassword","adminPassword","secretbook"}; Arrays.asList(toEncodePasswordStrings).forEach(p -> { String encodedValue = passwordEncoder.encode(p); System.out.println("Encoded value for {"+p +"} : "+encodedValue); }); } } /**Result * Encoded value for {userPassword} : $2a$10$bffAVFOFkyQYCacKYrrW9OW2lLTTWjHjxc51kSnMEVfVGO2OgO6Rq Encoded value for {adminPassword} : $2a$10$R6rgx0AcWDEIk2EJ2.NvueK6Gp3gBk66n58JEtfTctNjIJF7D6Wd2 Encoded value for {secretbook} : $2a$10$T1XME2CdpI1eGH0cmOwfjuCcDeu00Oec7ORfDWSPJcsYwW587ZH8a * */ |
application.properties
1 2 3 |
server.port=8088 spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:mem:bookstore |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.rayfocus.oauth2.config; import java.sql.SQLException; import org.h2.server.web.WebServlet; import org.h2.tools.Server; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class H2Config { @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public ServletRegistrationBean h2servletRegistration() { ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet()); registration.addUrlMappings("/h2-console/*"); return registration; } @Bean(initMethod = "start", destroyMethod = "stop") public Server h2Server() throws SQLException { return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092"); } } |
spring.datasource.url=jdbc:h2:tcp://localhost:9092/mem:bookstore
2.7 Bypass the access check for H2 console
The endpoints exposed by our OAuth2 service is protected and only the authenticated users can access it. However for this example, we can relax the rule for H2 console so that we can access it without any authentication.
To do so, we need to use the ResourceServerConfigurerAdapter
to configure the access rule. The below listing shows the code for ResourceServerConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.rayfocus.oauth2.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; @Configuration public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/h2-console/**") .permitAll() .anyRequest() .authenticated(); http.headers() .frameOptions() .disable(); } } |
3. Develop the Resource Server
Okay! We have completed developing our Authorization server which issues access tokens for the authorized client applications. Now, we are ready to develop the resource server and to protect them using OAuth2. We will develop a book store API where users can get the details of book using ISBN, add new book to the store. The users with the admin role will additionally have access to delete a book using ISBN.3.1 Maven dependencies
To start with, create a simple maven project and add the below dependencies in thepom.xml
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <dependencies> <!-- Actuator --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Security and OAuth2 --> <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.0.3.RELEASE</version> </dependency> <!-- H2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>compile</scope> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> |
3.2 Bootstrap class for Resource server
Below listing shows the code for resource server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.rayfocus.api.bookstore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; @SpringBootApplication @EnableResourceServer public class BookstoreApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApplication.class, args); } } |
@EnableResourceServer
annotation tells the Spring that it is protected resource and will enable a filter that intercepts all the requests to check whether a valid OAuth2 access token is present in the HTTP header. The validity of the access token is determined by making a call to the callback URL defined in the property security.oauth2.resource.user-info-uri
. We will configure this shortly in the next sections.
3.3 Protecting the resource via user specific role
We can configure the access control rules using theResourceServerConfigurerAdapter
class in Spring. We need to extend this class and override the configure()
method. The configure method will take parameter of type HttpSecurity
class which will expose necessary methods to define our access rules. Below listing shows the code for ResourceServerConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package com.rayfocus.api.bookstore.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; 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.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; @Configuration public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private TokenStore tokenStore; @Override public void configure(HttpSecurity http) throws Exception { // allows all requests for h2-console // allows requests with DELETE operation only to those having ADMIN role // allows all other requests only for authenticated users http.authorizeRequests() .antMatchers("/h2-console/**") .permitAll() .antMatchers(HttpMethod.DELETE, "/book-store/admin/**") .hasRole("ADMIN") .anyRequest() .authenticated(); http.headers() .frameOptions() .disable(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(this.tokenStore); } @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } } |
ADMIN
role. Also, if the URL contains /book-store/admin/**
then it will be allowed only for ADMIN users. All other endpoints are allowed for authenticated users. For the purpose of this example, we have allowed H2 console URL to all users with out any authentication.
Also, we have used the same tokenStore
we have used for OAuth2 server. This is important since the resource server has to validate the access token available in the incoming request.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.rayfocus.api.bookstore.config; import org.h2.server.web.WebServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class H2Config { @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public ServletRegistrationBean h2servletRegistration() { ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet()); registration.addUrlMappings("/h2-console/*"); return registration; } } |
3.4 Application.properties for Resource Server
Theapplication.properties
for Resource server is listed as below:
1 2 3 4 5 6 |
server.port=7077 spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:tcp://localhost:9092/mem:bookstore logging.level.org.springframework.web=ERROR logging.level.org.hibernate=ERROR security.oauth2.resource.user-info-uri=http://localhost:8088/oauth/user |
security.oauth2.resource.user-info-uri
to get the details about the User accessing the protected resource from OAuth2 server.
3.5 Developing the other aspects of Resource Server
We have configured the security aspects of the resource server so far. We will now quickly complete the development of endpoints for accessing the bookstore database. 3.5.1 Controller class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package com.rayfocus.api.bookstore.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.rayfocus.api.bookstore.http.HttpResponse; import com.rayfocus.api.bookstore.model.Book; import com.rayfocus.api.bookstore.service.BookStoreService; @RestController @RequestMapping("/book-store") public class BookStoreController { Logger logger = LoggerFactory.getLogger(BookStoreController.class); @Autowired private BookStoreService bookStoreService; @GetMapping("/book") public ResponseEntity<HttpResponse> getBookByISBN( @RequestParam(value="isbn", required=true) String isbn ) { return bookStoreService.getBookByISBN(isbn); } @PostMapping("/book") public ResponseEntity<HttpResponse> addNewBook(@RequestBody Book book){ return bookStoreService.addNewBook(book); } @DeleteMapping("/admin/book") public ResponseEntity<HttpResponse> deleteBook( @RequestParam(value="isbn", required=true) String isbn ){ return bookStoreService.deleteBookByISBN(isbn); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
package com.rayfocus.api.bookstore.model; import com.fasterxml.jackson.annotation.JsonProperty; public class Book { private Integer bookId; private String isbn; private String title; private String author; private String publisher; private Integer editionNumber; private Integer totalPages; public Book() { // default constructor } public Book(@JsonProperty("isbn") String isbn, @JsonProperty("title") String title, @JsonProperty("author") String author, @JsonProperty("publisher") String publisher, @JsonProperty("editionNumber") Integer editionNumber, @JsonProperty("totalPages") Integer totalPages) { this.isbn = isbn; this.title = title; this.author = author; this.publisher = publisher; this.editionNumber = editionNumber; this.totalPages = totalPages; } public Book(Builder builder) { this.bookId = builder.bookId; this.isbn = builder.isbn; this.title = builder.title; this.author = builder.author; this.publisher = builder.publisher; this.editionNumber = builder.editionNumber; this.totalPages = builder.totalPages; } // Book builder class public static class Builder{ private Integer bookId; private String isbn; private String title; private String author; private String publisher; private Integer editionNumber; private Integer totalPages; public Builder bookId(Integer val) { bookId = val; return this; } public Builder isbn(String val) { isbn = val; return this; } public Builder title(String val) { title = val; return this; } public Builder author(String val) { author = val; return this; } public Builder publisher(String val) { publisher = val; return this; } public Builder editionNumber(Integer val) { editionNumber = val; return this; } public Builder totalPages(Integer val) { totalPages = val; return this; } public Book build() { return new Book(this); } } /** Getters */ public Integer getBookId() { return bookId; } public String getIsbn() { return isbn; } public String getTitle() { return title; } public String getAuthor() { return author; } public String getPublisher() { return publisher; } public Integer getEditionNumber() { return editionNumber; } public Integer getTotalPages() { return totalPages; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
package com.rayfocus.api.bookstore.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import com.rayfocus.api.bookstore.http.HttpResponse; import com.rayfocus.api.bookstore.model.Book; import com.rayfocus.api.bookstore.repository.BookStoreRepository; import com.rayfocus.api.bookstore.util.BookStoreUtil; @Component public class BookStoreService { Logger logger = LoggerFactory.getLogger(BookStoreService.class); @Autowired private BookStoreRepository bookStoreRepository; /** Get the book from Book Store based on the input ISBN * @param isbn ISBN value in String * @return ResponseEntity object * */ public ResponseEntity<HttpResponse> getBookByISBN(String isbn) { ResponseEntity<HttpResponse> serviceResponse; try { Book book = bookStoreRepository.findBookByIsbn(isbn); serviceResponse = buildServiceResponse(book, BookStoreUtil.GET_BOOK_SUCCESS); } catch(Exception e) { serviceResponse = buildExceptionResponse(e); } return serviceResponse; } /** Add new book to the Book Store * @param book Book object * @return ResponseEntity object * */ public ResponseEntity<HttpResponse> addNewBook(Book book) { ResponseEntity<HttpResponse> serviceResponse = null; try { bookStoreRepository.addBook(book); serviceResponse = buildServiceResponse(book, BookStoreUtil.ADD_BOOK_SUCCESS); } catch(Exception e) { serviceResponse = buildExceptionResponse(e); } return serviceResponse; } /** Delete the book from Book Store based on the input ISBN * This operation has to be allowed for users with Admin access * @param isbn ISBN value in String * @return ResponseEntity object * */ public ResponseEntity<HttpResponse> deleteBookByISBN(String isbn){ ResponseEntity<HttpResponse> serviceResponse = null; try { bookStoreRepository.deleteBookByIsbn(isbn); serviceResponse = buildServiceResponse(null, BookStoreUtil.DELETE_BOOK_SUCCESS); } catch(Exception e) { serviceResponse = buildExceptionResponse(e); } return serviceResponse; } /** Build service response for success scenarios * @param book Book object * @return ResponseEntity object * */ private ResponseEntity<HttpResponse> buildServiceResponse(Book book, String respDesc){ HttpStatus statusCode = HttpStatus.OK; String statusMessage = "SUCCESS"; if(book == null) { statusCode = HttpStatus.NO_CONTENT; } return new HttpResponse.Builder().statusCode(statusCode) .statusMessage(statusMessage) .responseData(book) .responseDesc(respDesc) .build(); } /** Build service response for exception scenarios * @param excp Exception object * @return ResponseEntity object * */ private ResponseEntity<HttpResponse> buildExceptionResponse(Exception excp){ logger.error("Exception while accessing BookStore", excp.getMessage()); if( excp instanceof EmptyResultDataAccessException) { return new HttpResponse.Builder().statusCode(HttpStatus.OK) .statusMessage("WARNING") .responseData(null) .responseDesc(BookStoreUtil.NO_BOOK_FOUND_FOR_ISBN) .build(); } return new HttpResponse.Builder().statusCode(HttpStatus.INTERNAL_SERVER_ERROR) .statusMessage("ERROR") .responseData(null) .responseDesc(BookStoreUtil.DB_ERROR) .build(); } } |
1 2 3 4 5 6 7 8 9 |
package com.rayfocus.api.bookstore.util; public class BookStoreUtil { public static final String GET_BOOK_SUCCESS = "GET Book Successful"; public static final String DB_ERROR = "Database access error"; public static final String NO_BOOK_FOUND_FOR_ISBN = "No book found for the requested ISBN"; public static final String ADD_BOOK_SUCCESS = "ADD New Book Successful"; public static final String DELETE_BOOK_SUCCESS = "DELETE Book Successful"; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
package com.rayfocus.api.bookstore.repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import com.rayfocus.api.bookstore.model.Book; @Repository public class BookStoreRepository { Logger logger = LoggerFactory.getLogger(BookStoreRepository.class); @Autowired private JdbcTemplate jdbcTemplate; public Book findBookByIsbn(String isbn) { Book book = null; try { book = jdbcTemplate.queryForObject("SELECT * FROM BOOKS WHERE isbn=?", new Object[] { isbn }, new BookStoreRowMapper()); } catch(DataAccessException dae) { logger.info("Data Access Exception while retrieving the book from bookstore.",dae); throw dae; } catch(Exception e) { logger.info("Unknown Exception while retrieving the book from bookstore.",e); throw e; } return book; } public int addBook(Book book) { String addBookSql = "INSERT INTO BOOKS" + "(" + "book_id," + "isbn," + "title," + "author," + "publisher," + "edition_number," + "total_pages" + ")" + " VALUES (BOOK_ID_SEQ.nextVal, ?, ?, ?, ?, ?, ?)"; Object[] params = new Object[]{ book.getIsbn(), book.getTitle(), book.getAuthor(), book.getPublisher(), book.getEditionNumber(), book.getTotalPages() }; int rowCount = 0; try { rowCount = jdbcTemplate.update(addBookSql, params); } catch(DataAccessException dae) { logger.info("Data Access Exception while adding the book to bookstore.",dae); throw dae; } catch(Exception e) { logger.info("Unknown Exception while adding the book to bookstore.",e); throw e; } return rowCount; } public int deleteBookByIsbn(String isbn) { int rowCount = 0; try { rowCount = jdbcTemplate.update("DELETE FROM BOOKS WHERE isbn=?", new Object[] {isbn}); } catch(DataAccessException dae) { logger.info("Data Access Exception while deleting the book from bookstore.",dae); throw dae; } catch(Exception e) { logger.info("Unknown Exception while deleting the book from bookstore.",e); throw e; } return rowCount; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.rayfocus.api.bookstore.repository; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; import com.rayfocus.api.bookstore.model.Book; public class BookStoreRowMapper implements RowMapper<Book>{ @Override public Book mapRow(ResultSet rSet, int rowNum) throws SQLException { Book book = new Book.Builder().bookId(rSet.getInt("BOOK_ID")) .isbn(rSet.getString("ISBN")) .title(rSet.getString("TITLE")) .author(rSet.getString("AUTHOR")) .publisher(rSet.getString("AUTHOR")) .editionNumber(rSet.getInt("EDITION_NUMBER")) .totalPages(rSet.getInt("TOTAL_PAGES")) .build(); return book; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
package com.rayfocus.api.bookstore.http; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; public class HttpResponse { private final HttpStatus statusCode; private final String statusMessage; private final String responseDesc; private final Object responseData; private HttpResponse(Builder builder) { this.statusCode = builder.statusCode; this.statusMessage = builder.statusMessage; this.responseDesc = builder.responseDesc; this.responseData = builder.responseData; } // Response builder class public static class Builder{ private HttpStatus statusCode = HttpStatus.OK; private String statusMessage = "SUCCESS"; private String responseDesc = "Request processed successfully"; private Object responseData = new Object(); public Builder statusCode(HttpStatus val) { statusCode = val; return this; } public Builder statusMessage(String val) { statusMessage = val; return this; } public Builder responseDesc(String val) { responseDesc = val; return this; } public Builder responseData(Object val) { responseData = val; return this; } @SuppressWarnings({ "unchecked", "rawtypes" }) public ResponseEntity<HttpResponse> build() { return new ResponseEntity(new HttpResponse(this), statusCode); } } /** Getters */ public HttpStatus getStatusCode() { return statusCode; } public String getStatusMessage() { return statusMessage; } public String getResponseDesc() { return responseDesc; } public Object getResponseData() { return responseData; } } |
4. Test the OAuth2 and Resource Server
Okay! We have completed developing our OAuth2 and Resource Server. We will test them to see how it works. Let’s make use of Postman client for this purpose. 4.1 Get the OAuth2 access token For this we have to make a request tohttp://localhost:8088/oauth/token
. The port number is 8088 since we have configured it in the OAuth2 server’s application.properties file.
First, we are providing the user credentials in the Authorization tab as shown in the below listing.

- grant_type – Grant type is password since we are using resource owner password credentials grant for this example.
- scope – Scope is either webclient or mobileclient. Since we have configured these two in the
data.sql
available in the OAuth2 server’s resources folder. - username – Name of the user logging in.
- password – password of the user logging in.

- access_token – The Oauth2 token that has to be provided with the each service call to the protected resource.
- token_type – The type of the OAuth2 token being provided.
- refresh_token – The refresh token which can be presented to OAuth2 server to reissue a access token after it has been expired.
- expires_in – The number of seconds for which the access token is valid. We have configured it as 3600 for this example.
- scope – The scope that this OAuth2 token is valid for.

Authorization
. We have configured the port for resource server as 7077. So we can now access the book from book-store API with the URL http://localhost:7077/book-store/book?isbn=978-3-598-21506-3
- Authorization – The header contains the value of the format
Bearer <access_token_from_oauth2_server>
.


bookstore.user
. This user doesn’t have Admin role.

bookstore.admin
. For this you need another access token from OAuth2 server by providing the username and password of Admin user. Refer the section 4.1 where we have done the same for bookstore.user
.
After getting the access token for Admin credentials, provide them in the request header for DELETE operation. The response should be successful now.

204 No Content
since we have configured our API to return the No Content for DELETE operation.
Conclusion
Great! We have completed our development and testing of OAuth2 and Resource server.- We have developed our OAuth2 server and configured it to provide access tokens.
- Then we have protected our Resource server using OAuth2.
- The resource server is configured to check the validity of the access token by calling the endpoint exposed by OAuth2 server(/oauth/user).
- Also, we have configured the role specific access for the specific operations like Http DELETE.
0 Comments