OAuth2 is a frequently used standard for authorization and with Spring Boot it is easy to set up authorization and resource server in no time.
In the following short tutorial I’d like to demonstrate how to set up an OAuth2 authorization server as well as a connected and secured resource server within a few minutes using Java, Maven and Spring Boot.
Creating the Authorization Server
We’re using the Spring Initializr to create our initial Maven project including some selected dependencies ..
This is what our pom.xml looks like (excerpt)..
<project>
[...]
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.1.RELEASE</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Brixton.M4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
The following Java class is all we need to create our authorization server as we’re using an in-memory store with only one oauth client and the server is configured to allow password, auth-code and refresh-token as grant types.
Our user method returns the Principal User and is used by the resource-server later.
package com.hascode.tutorial;
import java.security.Principal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@SpringBootApplication
@RestController
@EnableResourceServer
public class Oauth2AuthorizationServerApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Oauth2AuthorizationServerApplication.class, args);
}
@Configuration
@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("foo").secret("foosecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password").scopes("openid");
}
}
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
}
In our application.properties we’re adding some configuration for the server port, the credentials used for the basic-auth and our application’s context path:
server.port=9000
security.user.name=bar
security.user.password=barsecret
server.contextPath=/hascode
Minimalistic Spring Boot 1.3 Setup
The following, minimalistic setup is possible in Spring Boot 1.3 if we needed only one client (for demonstration purpose etc.), a detailed description has been documented here in the Spring Blog.
Our application controller would now look like this one:
package com.hascode.tutorial;
import java.security.Principal;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@SpringBootApplication
@RestController
@EnableResourceServer
@EnableAuthorizationServer
public class Oauth2AuthorizationServerApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Oauth2AuthorizationServerApplication.class, args);
}
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
}
And our modified application.properties would look similar to this one:
server.port=9000
security.user.name=bar
security.user.password=barsecret
server.contextPath=/hascode
security.oauth2.client.clientId=foo
security.oauth2.client.clientSecret=foosecret
security.oauth2.client.authorized-grant-types=authorization_code,refresh_token,password
security.oauth2.client.scope=openid
Creating the Resource Provider
Again we’re using the Spring Initializr to create our initial project here..
This is an excerpt from our pom.xml:
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.1.RELEASE</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
[...]
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Brixton.M4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
[..]
</repositories>
</project>
And this is our resource, calling the secured API simply return a success message an a generated UUID.
package com.hascode.tutorial;
import java.util.UUID;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableResourceServer
public class SampleResourceApplication {
public static void main(String[] args) {
SpringApplication.run(SampleResourceApplication.class, args);
}
@RequestMapping("/")
public String securedCall() {
return "success (id: " + UUID.randomUUID().toString().toUpperCase() + ")";
}
}
In our application.properties we’re telling our resource server
server.port=9001
server.contextPath=/resource
security.oauth2.resource.userInfoUri: http://localhost:9000/hascode/user
When using Spring Boot <1.3, the last property is named spring.oauth2.resource.userInfoUri!!
Running the Applications
We may run both applications by invoking the main class or using Maven in the command-line with mvn spring-boot:run:
$ mvn spring-boot:run
[..]
2017-05-01 14:45:10.643 INFO 13279 --- [ main] h.t.Oauth2AuthorizationServerApplication : Starting Oauth2AuthorizationServerApplication on styx with PID 13279 (/data/project/spring-oath2-sample/identity-server/target/classes started by soma in /data/project/spring-oath2-sample/identity-server)
[..]
2017-05-01 14:45:13.233 INFO 13279 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 9000 (http)
[..]
2017-05-01 14:45:14.217 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
2017-05-01 14:45:14.223 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-05-01 14:45:14.224 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-05-01 14:45:14.224 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
2017-05-01 14:45:14.224 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
2017-05-01 14:45:14.226 INFO 13279 --- [ost-startStop-1] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
[..]
2017-05-01 14:45:15.139 INFO 13279 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9000 (http)
2017-05-01 14:45:15.141 INFO 13279 --- [ main] h.t.Oauth2AuthorizationServerApplication : Started Oauth2AuthorizationServerApplication in 5.744 seconds (JVM running for 8.358)
OAuth2 Workflow in Action
Assuming we have both server instances up and running, we may now test if accessing our secured resource using OAuth2 is working.
Testing for Authentication Error
Accessing the resource in a direct GET request should not be possible and yield an error message like this one:
curl
$ curl http://localhost:9001/resource/
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
Postman
Obtaining the Access Token
Now we’re obtaining our access token from the authorization server:
curl
$ curl -XPOST -k foo:foosecret@localhost:9000/hascode/oauth/token \
-d grant_type=password -d client_id=foo -d client_secret=abc123 \
-d redirect_uri=https://www.hascode.com -d username=bar -d password=barsecret
{"access_token":"dec6c15a-137f-475a-aa02-530c23943f91","token_type":"bearer","refresh_token":"19b44e18-a25f-427c-9884-ebe6dcec1b96","expires_in":43192,"scope":"openid"}
The response contains our access token, its expiration date, the refresh token (for refresh after expiration), the token type and its scope.
Postman
Accessing the Resource
With the obtained access token, we may now access the secured resource like this:
curl
$ TOKEN=dec6c15a-137f-475a-aa02-530c23943f91
$ curl -H "Authorization: Bearer $TOKEN" http://localhost:9001/resource/
success (id: 27DCEF5E-AF11-4355-88C5-150F804563D0)
Postman
Refreshing the Access Token
May may refresh our access token using the refresh token received in our first authorization request:
curl
$ curl -v --data "grant_type=refresh_token&client_id=foo&refresh_token=19b44e18-a25f-427c-9884-ebe6dcec1b96" -k foo:foosecret@localhost:9000/hascode/oauth/token
{"access_token":"12f5c219-1e8b-45b6-be42-304a6833a3b1","token_type":"bearer","refresh_token":"19b44e18-a25f-427c-9884-ebe6dcec1b96","expires_in":43199,"scope":"openid"}%
Postman
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/spring-oauth2-example.git
Resources
Article Updates
-
2016-09-14: Added an example for refreshing the access token.
-
2017-05-01: Added section about running the application from the command-line.
-
2017-05-01: Postman examples and screen-shots added,