[JAVA] Let's experience the authorization code grant flow with Spring Security OAuth-Part 2: Creating an app for the time being

This time, in the second part of "Experience the authorization code grant flow with Spring Security OAuth", for the time being, create an application that authenticates and authorizes the REST API with the authorization code grant flow using Spring Security OAuth + Spring Boot. I will try it.

Functional requirements of the application

The functional requirements of the application created this time are ...

Service Provider

The Service Provider provides a REST API that manages (registers / updates / deletes / references) task information (title, details, deadline date, completion flag, registration date / time, update date / time) for each user, and accesses the REST API. Authenticates and authorizes using the access token issued by the OAuth 2.0 authorization code grant flow.

I will make it a requirement. Originally, I would like to prepare a Web UI for managing task information on the Service Provider side as well, but I will omit the explanation because the Web UI on the Service Provider side is out of the focus of this entry.

Resource Server

Create the following API as an API for managing the task information of the resource owner.

API name API overview Scope to allow access
GET /api/tasks API to get a list of task information read
POST /api/tasks API to register task information write
GET /api/tasks/{id} API to get task information read
PUT /api/tasks/{id} API to update task information write
DELETE /api/tasks/{id} API to delete task information write

Authorization Server

The following endpoints (hereinafter referred to as "authorization endpoints") are provided in order to allow the resource owner to access the task information. This endpoint is provided by Spring Security OAuth and does not need to be created by the developer.

Endpoint name Endpoint overview Conditions to allow access
GET /oauth/authorize Endpoint for displaying the screen for obtaining approval from the resource owner (hereinafter referred to as "authorization screen") Authenticated resource owner
POST /oauth/authorize?user_oauth_approval Authorization instruction from the resource owner(Allow / Deny)Received approval grant(Authorization code)Endpoint for issuing Authenticated resource owner

Create the following endpoint (hereinafter referred to as "token endpoint") in order for the client to issue an access token based on the authorization grant (authorization code) obtained from the resource owner. This endpoint is provided by Spring Security OAuth and does not need to be created by the developer.

Endpoint name Endpoint overview Conditions to allow access
POST /oauth/token Authorized grant obtained from resource owner(Authorization code etc.)Endpoint for issuing access tokens based on Authenticated Client

** Note: Authentication / authorization for various endpoints **

In this entry ... We will authenticate the resource owner and client to these endpoints using the basic authentication provided by Spring Security.

Client(Web UI)

The Client uses the REST API provided by the Service Provider to provide a Web UI (task list screen and task detail screen) for managing the task information of the resource owner.

Endpoint name Endpoint overview Conditions to allow access
GET /tasks Display the task information acquired from the resource server on the task list screen. Authenticated user
POST /tasks Create task information on the resource server Authenticated user
GET /tasks/{id} Display the task information acquired from the resource server on the task details screen Authenticated user
POST /tasks/{id}?update Update the task information managed by the resource server Authenticated user
POST /tasks/{id}?delete Delete the task information managed by the resource server Authenticated user

** Note: Task management UI authentication / authorization **

The task management screen prepared on the Client side requires user authentication of the application on the Client side, and the user authentication is performed using Basic authentication provided by Spring Security.

The screen image and screen transition are as follows.

spring-security-oauth-1st-app-client.png

Application configuration

This time, we will create two Spring Boot applications, "client" and "service provider (authorization server + resource server)", and build an application that authenticates and authorizes the API with the authorization code grant flow. The access token and the authentication information associated with the access token between the authorization server and the resource server are linked in-memory on the Web application.

spring-security-oauth-1st-app.png

Warning:

The application created in this entry runs using HTTP communication, but ... In the OAuth 2.0 protocol flow, ** Resource owners and endpoints that handle Client authentication and access tokens must use HTTPS communication. **.

Note:

Since the roles of the authorization server and the resource server are different, I think that there are many cases where the application is created as an independent Web application with the following feeling, but this time, prioritizing the simplicity of the application configuration, the authorization server and the resource We decided to implement the server with one Spring Boot application.

spring-security-oauth-std-app.png

When creating an authorization server and a resource server as separate Web applications, selecting the access token and the authentication information associated with the access token between the servers is one point. In addition, we plan to introduce the implementation method when the authorization server and the resource server are separated from the next time.

Operation verified version

The application created in this entry is verified using the following version of the library.

Creating a development project

Now, let's actually create an application and experience REST API authentication / authorization by authorization code grant flow. First, let's create a development project for the Spring Boot application. Here is an example of creating a project from the command line, but even if you generate it with SPRING INITIALIZR Web UI or the function of your IDE (of course) OK! !!

Since we will create two applications this time, we will create a parent directory to store those applications.

$ mkdir spring-security-oauth-demo
$ cd spring-security-oauth-demo

After navigating to the directory you created, create a Spring Boot application development project for the Service Provider and Client.

Creating a development project for a service provider

$ curl -s https://start.spring.io/starter.tgz\
       -d name=service-provider\
       -d artifactId=service-provider\
       -d dependencies=web,jdbc,h2\
       -d baseDir=service-provider\
       | tar -xzvf -

Create a development project for Client

$ curl -s https://start.spring.io/starter.tgz\
       -d name=client\
       -d artifactId=client\
       -d dependencies=thymeleaf\
       -d baseDir=client\
       | tar -xzvf -

Creating a project for build

Copy the required files, such as the Maven wrapper, from your Service Provider or Client development project.

$ cp -r client/.mvn .mvn
$ cp client/mvnw* .
$ cp client/.gitignore .
$ cp client/pom.xml .

Modify the copied pom.xml to the build settings. Here, service-provider and client are set to be managed as submodules. This will allow you to build Maven together with service-provider and client.

$ vi pom.xml

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>com.example</groupId>
        <artifactId>spring-security-oauth-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>pom</packaging>

        <name>spring-seucirty-oauth-demo</name>
        <description>Spring Security OAuth Demo project for Spring Boot</description>

        <modules>
                <module>service-provider</module>
                <module>client</module>
        </modules>

</project>

Added dependent libraries to Service Provider

Add "Spring Security OAuth", "Jackson's extension module for JSR 310" and "JTS Topology Suite" to the service provider. ("JTS Topology Suite" is directly related to this entry, but I added it because it is used by H2 Database and an error occurs at runtime.)

$ vi service-provider/pom.xml

service-provider/pom.xml


<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
	<groupId>com.vividsolutions</groupId>
	<artifactId>jts</artifactId>
	<version>1.13</version>
	<scope>runtime</scope>
</dependency>

Add dependent libraries to Client

Add "Spring Security OAuth", "JSR 310 extension module for Jackson", "Thymeleaf extension module for JSR 310", "Webjars Locator", and "WebJar for Bootstrap" to the Client.

$ vi client/pom.xml

client/pom.xml


<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.3.7-1</version>
</dependency>

Note:

For "Webjars Locator" and "WebJar for Bootstrap", see "Understanding Access to Static Resources on Spring MVC (+ Spring Boot)" (http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37) ) ”...

  • [Accessing static resources on Spring Boot-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9%E7% 94% A8-1)
  • [Access to static resources using Spring MVC's unique features-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9 % E7% 94% A8)

Is briefly explained, so please have a look if you are interested.

Confirm Maven build

Perform a Mavne build (Maven Package) in the directory that contains the pom.xml for the build, and check the validity of the pom.xml settings. If you see the following log, the Mavne build is successful.

$ ./mvnw package
...
[INFO] ------------------------------------------------------------------------
[INFO] Building spring-seucirty-oauth-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] service-provider ................................... SUCCESS [  3.899 s]
[INFO] client ............................................. SUCCESS [  2.697 s]
[INFO] spring-seucirty-oauth-demo ......................... SUCCESS [  0.000 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.088 s
[INFO] Finished at: 2017-02-25T09:05:35+09:00
[INFO] Final Memory: 29M/395M
[INFO] ------------------------------------------------------------------------

Creating a Service Provider

First, create a Service Provider.

Service Provider server settings

As introduced in the application configuration at the beginning, set the Service Provider port to 18081 and the context path to / provider. Also, use the H2 file-based database so that the task information handled by the REST API will not be lost when the application is restarted.

service-provider/src/main/resources/application.properties


server.port=18081
server.context-path=/provider
spring.datasource.url=jdbc:h2:~/.h2/service-provider

Note:

When the Service Provider and Client are run on the same host (localhost, etc.), if the context-path (default/) is the same, the cookie that manages the session ID conflicts and HTTP sessions cannot be handled correctly. Therefore, if you want to run it in the local environment, you also need to set context-path.

Table setup

Create a table to store task information.

service-provider/src/main/resources/schema.sql


CREATE TABLE IF NOT EXISTS tasks (
  id IDENTITY PRIMARY KEY
  , username VARCHAR(255) NOT NULL
  , title TEXT NOT NULL
  , detail TEXT
  , deadline DATE
  , finished BOOLEAN NOT NULL DEFAULT FALSE
  , created_at DATETIME DEFAULT SYSTIMESTAMP
  , updated_at DATETIME DEFAULT SYSTIMESTAMP
  , version BIGINT DEFAULT 1
);

CREATE INDEX IF NOT EXISTS idx_tasks_username ON tasks(username);

Creating a domain object

Create a domain object that holds the task information handled by the REST API.

service-provider/src/main/java/com/example/Task.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class Task {

	private long id;
	private String username;
	private String title;
	private String detail;
	private LocalDate deadline;
	private boolean finished;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;
	private long version;

	// getter/setter

}

Creating Repository

Create a Repository class for the domain object that holds the task information. In this entry, we will implement using the data access function (JdbcOperations) provided by Spring Framework without using O / R Mapper such as JPA. (Actually ... use NamedParameterJdbcOperations which can handle name-based parameters)

Note:

Beans of JdbcOperations and NamedParameterJdbcOperations are defined by the Spring Boot AutoConfigure mechanism, so there is no need for the developer to explicitly define the beans.

service-provider/src/main/java/com/example/TaskRepository.java


package com.example;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Repository
public class TaskRepository {
	private final NamedParameterJdbcOperations jdbcOperations;

	public TaskRepository(NamedParameterJdbcOperations jdbcOperations) {
		this.jdbcOperations = jdbcOperations;
	}

	public List<Task> findAll(String username) {
		return jdbcOperations.query(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE username = :username ORDER BY deadline DESC, id DESC",
				new MapSqlParameterSource("username", username), new BeanPropertyRowMapper<>(Task.class));
	}

	public Task findOne(long id) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
				new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
	}

	public void save(Task task) {
		if (task.getId() == null) {
			GeneratedKeyHolder holder = new GeneratedKeyHolder();
			jdbcOperations.update(
					"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
					new BeanPropertySqlParameterSource(task), holder);
			task.setId(holder.getKey().longValue());
		} else {
			jdbcOperations.update(
					"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
					new BeanPropertySqlParameterSource(task));
		}
	}

	public void remove(long id) {
		jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
	}

}

Creating RestController (REST API)

Create a Controller class that provides CRUD operations (REST API) for task information. Since ʻEmptyResultDataAccessException occurs when the target data is not found when using JdbcTemplate, in addition to the Handler method for REST API, implement an exception handling method for handling ʻEmptyResultDataAccessException and returning 404 Not Found. ..

service-provider/src/main/java/com/example/TaskRestController.java


package com.example;

import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.relativeTo;

import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Optional;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	private final TaskRepository repository;

	TaskRestController(TaskRepository repository) {
		this.repository = repository;
	}

	@GetMapping
	List<Task> getTasks(Principal principal) {
		return repository.findAll(extractUsername(principal));
	}

	@PostMapping
	ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
		task.setUsername(extractUsername(principal));
		repository.save(task);
		URI createdTaskUri = relativeTo(uriBuilder).withMethodCall(on(TaskRestController.class).getTask(task.getId()))
				.build().encode().toUri();
		return ResponseEntity.created(createdTaskUri).build();
	}

	@GetMapping("{id}")
	Task getTask(@PathVariable long id) {
		return repository.findOne(id);
	}

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task) {
		task.setId(id);
		repository.save(task);
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id) {
		repository.remove(id);
	}

	private String extractUsername(Principal principal) {
		return Optional.ofNullable(principal).map(Principal::getName).orElse("none");
	}

	@ExceptionHandler(EmptyResultDataAccessException.class)
	@ResponseStatus(HttpStatus.NOT_FOUND)
	void handleEmptyResultDataAccessException() {
		// NOP
	}

}

REST API operation check

Let's check if the REST API you created works properly.

Launch Service Provider

Launch the Spring Boot application using the Maven Plugin provided by Spring Boot.

$ ./mvnw -pl service-provider spring-boot:run

Calling "task information list acquisition API"

Then, let's innocently call the "task information list acquisition API"! !!

$ $ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
WWW-Authenticate: Basic realm="Spring"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 01:53:04 GMT

{"timestamp":1487987584305,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/provider/api/tasks"}

Apparently Basic authentication is required. This is because when AutoConfigure of Spring Boot finds a class of Spring Security ... By default, it requires Basic authentication for all request paths (/ **). Now that I want to check the operation of the REST API, let's disable Basic authentication once. Basic authentication required by Spring Boot can be disabled by adding the setting security.basic.enabled = false.

service-provider/src/main/resources/application.properties


security.basic.enabled=false

Warning:

** Please enable Basic authentication after checking the operation of REST API! !! ** **

If you access the REST API after disabling Basic authentication, you can now get an empty list of tasks.

$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:01:57 GMT

[]

Calling "task information creation API"

Immediately after starting, no task information is registered, so let's call the "task information creation API" to create task information.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201 
Location: http://localhost:18081/provider/api/tasks/1
Content-Length: 0
Date: Sat, 25 Feb 2017 02:17:37 GMT

If the task information is created successfully, the URL for accessing the created task information will be set in the Location header.

Calling "task information acquisition API"

Let's call the "task information acquisition API" (access the URL set in the Location header) to acquire the created task information.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:29:12 GMT

{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":[2017,2,28],"finished":false,"createdAt":[2017,2,25,11,17,37,671000000],"updatedAt":[2017,2,25,11,17,37,671000000],"version":1}

I was able to get some task information, but ... The format of the date and time is a bit disappointing, so let's try to output the formatted value. If you want to output the formatted date / date / time, set spring.jackson.serialization.write-dates-as-timestamps = false.

service-provider/src/main/resources/application.properties


spring.jackson.serialization.write-dates-as-timestamps=false

If you access it again after setting spring.jackson.serialization.write-dates-as-timestamps = false, you can see that it is formatted like that.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:31:13 GMT

{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:17:37.671","version":1}

Calling "task information update API"

Since it's a big deal ... I'll call the "task information update API" to update the task information.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -H "Content-Type: application/json" -X PUT -d '{"title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31"}'
HTTP/1.1 204 
Date: Sat, 25 Feb 2017 02:33:58 GMT

If you get the updated task information, you can confirm that it has been updated correctly.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:34:21 GMT

{"id":1,"username":"none","title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:33:58.51","version":2}

Calling "Delete task information API"

Call the last "Delete task information API" and try to delete the created task information.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -X DELETE
HTTP/1.1 204 
Date: Sat, 25 Feb 2017 02:35:20 GMT

If you try to get the deleted task information, you will get a client error (404: Not Found) notifying you that there is no target data.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 404 
Content-Length: 0
Date: Sat, 25 Feb 2017 02:35:37 GMT

Authorization server setup

Assign @EnableAuthorizationServer to the configuration class, define the beans required for OAuth authentication / authorization, and publish the" authorization endpoint "and" token endpoint "on the authorization server.

service-provider/src/main/java/com/example/AuthorizationServerConfiguration.java


package com.example;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration {
}

Also, by default in Spring Boot, the following values are randomly assigned at startup, so set a fixed value.

service-provider/src/main/resources/application.properties


security.user.password=password
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret

In addition, specify the scope and grant type that the default client can handle. "Passowrd: Resource owner password credential" is not required normally, but it is specified in order to check the operation of the authorization server and resource server using CUI (cURL command).

service-provider/src/main/resources/application.properties


security.oauth2.client.scope=read,write
security.oauth2.client.authorized-grant-types=authorization_code,password

If you start the service provider in this state, the following log will be output, and you can see that the endpoint for OAuth has been published to the authorization server.

...
2017-02-25 13:41:48.269  INFO 75893 --- [           main] .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-02-25 13:41:48.270  INFO 75893 --- [           main] .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-02-25 13:41:48.271  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2017-02-25 13:41:48.271  INFO 75893 --- [           main] .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-02-25 13:41:48.272  INFO 75893 --- [           main] .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-02-25 13:41:48.273  INFO 75893 --- [           main] .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-02-25 13:41:48.290  INFO 75893 --- [           main] .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)
...

Resource server setup

Assign @EnableAuthorizationServer to the configuration class, define the beans required for OAuth authentication / authorization, and overrideconfigure (HttpSecurity)of ResourceServerConfigurerAdapter to configure the authorization settings for the REST API.

service-provider/src/main/java/com/example/ResourceServerConfiguration.java


package com.example;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/api/**")
			.authorizeRequests()
			.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
			.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
			.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
			.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')");
	}
}

By making the above settings, OAuth authentication / authorization can be applied to requests under / api.

Authorization / resource server operation check

Now that you have set up the authorization server and resource server, let's access the resource server.

$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 09:39:55 GMT

{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

Somehow an error occurred. Looking at the details of the error ... An authentication error (401 Unauthorized) has occurred, and it has been notified that authentication with the OAuth Bearer token is required.

Obtaining an access token

The ultimate purpose of this entry is to explain how to use the "authorization code grant" to obtain an access token and access the REST API, but first of all ... "Resources" that allow you to easily obtain an access token. Let's get an access token to access the task information using "Owner Password Credentials".

$ curl -D - -s -u client:secret http://localhost:18081/provider/oauth/token -X POST -d grant_type=password -d username=user -d password=password
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:08:39 GMT

{"access_token":"d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097","token_type":"bearer","expires_in":43138,"scope":"read write"}

Access to resource server

Specify the obtained access token in the "Authorization" header to access the resource server again.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:11:17 GMT

[]

Since it's a big deal ... Let's create a new task and get the created task information.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097" -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:18081/provider/api/tasks/6
Content-Length: 0
Date: Sat, 25 Feb 2017 15:24:40 GMT

$ curl -D - -s http://localhost:18081/provider/api/tasks/6 -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 15:26:13 GMT

{"id":6,"username":"user","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-26T00:24:40.843","updatedAt":"2017-02-26T00:24:40.843","version":1}

Creating Client (Web UI)

Now that the Service Provider (authorization server and resource server) has been created, create a Web UI for operating the task information managed by the resource server.

Client server settings

As introduced in the application configuration at the beginning, set the Cleint port to 18080 and the context path to / client.

client/src/main/resources/application.properties


server.port=18080
server.context-path=/client

In this application, the user authentication on the Client side uses the Basic authentication set up by Spring Boot. If it is the default operation, the password of the default user changes every time it is started, so fix the password.

client/src/main/resources/application.properties


security.user.password=password

Client setup

By assigning @ EnableOAuth2Client to the configuration class, the component that manages the access token (ʻOAuth2ClientContext) and the component that guides the user agent (browser) to the authorization server in order to obtain authorization from the resource owner (ʻOAuth2ClientContext) ʻOAuth2ClientContextFilter) etc. is defined as a bean. In addition, make a Bean definition of RestTemplate (ʻOAuth2RestTemplate) extended for OAuth. ʻOAuth2RestTemplate` eliminates the need for the application to be aware of OAuth-related processes (such as the process of acquiring an access token from the authorization server), and calls the REST API in the same way as when authentication / authorization by OAuth is not performed. You will be able to.

client/src/main/java/com/example/ClientConfiguration.java


package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;

@EnableOAuth2Client
@Configuration
public class ClientConfiguration {

	@Bean
	OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context, OAuth2ProtectedResourceDetails details) {
		return new OAuth2RestTemplate(details, context);
	}

}

In addition, add OAuth related settings (API URL, authorization server URL, client information).

client/src/main/resources/application.properties


#API URL
api.url=http://localhost:18081/provider/api

#Authorization server endpoint URL
auth.url=http://localhost:18081/provider/oauth
security.oauth2.client.access-token-uri=${auth.url}/token
security.oauth2.client.user-authorization-uri=${auth.url}/authorize

#Client information settings
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.scope=read,write

Note:

By default in Spring Boot AutoConfigure, beans are defined to access the resource server using "authorization code grant flow".

Creating a domain object

Create a domain object that holds task information that you work with via the REST API. (Copy the Task class created when you created the Service Provider to the Client side)

client/src/main/java/com/example/Task.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class Task {

	private long id;
	private String username;
	private String title;
	private String detail;
	private LocalDate deadline;
	private boolean finished;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;
	private long version;

	// getter/setter

}

Creating Repository

Create a Repository class for the domain object that holds the task information. This class uses the method of the implementation class (ʻOAuth2RestTemplate) of the RestOperations` interface provided by Spring Security OAuth to access the task information managed on the resource server.

client/src/main/java/com/example/TaskRepository.java


package com.example;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import org.springframework.web.client.RestOperations;

@Repository
public class TaskRepository {

	private final RestOperations restOperations;
	private final String resourcesUrl;
	private final String resourceUrlTemplate;

	TaskRepository(RestOperations restOperations,
			@Value("${api.url}/tasks") String resourcesUrl) {
		this.restOperations = restOperations;
		this.resourcesUrl = resourcesUrl;
		this.resourceUrlTemplate = resourcesUrl + "/{id}";
	}

	public List<Task> findAll() {
		return Arrays.asList(restOperations.getForObject(resourcesUrl, Task[].class));
	}

	public Task findOne(long id) {
		return restOperations.getForObject(resourceUrlTemplate, Task.class, id);
	}

	public void save(Task task) {
		if (task.getId() == null) {
			restOperations.postForLocation(resourcesUrl, task);
		} else {
			restOperations.put(resourceUrlTemplate, task, task.getId());
		}
	}

	public void remove(long id) {
		restOperations.delete(resourceUrlTemplate, id);
	}

}

Creating a Controller

Create a Controller class that provides a Web UI for performing CRUD operations on task information.

client/src/main/java/com/example/TaskController.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.beans.BeanUtils;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@RequestMapping("/tasks")
@Controller
public class TaskController {

	private final TaskRepository repository;

	TaskController(TaskRepository repository) {
		this.repository = repository;
	}

	@ModelAttribute
	TaskForm setUpForm() {
		return new TaskForm();
	}

	@GetMapping
	String list(Model model) {
		List<Task> taskList = repository.findAll();
		model.addAttribute(taskList);
		return "task/list";
	}

	@PostMapping
	String create(@Validated TaskForm form, BindingResult bindingResult, Model model) {
		if (bindingResult.hasErrors()) {
			return list(model);
		}
		Task task = new Task();
		BeanUtils.copyProperties(form, task);
		repository.save(task);
		return "redirect:/tasks";
	}

	@GetMapping("{id}")
	String detail(@PathVariable long id, TaskForm form, Model model) {
		Task task = repository.findOne(id);
		BeanUtils.copyProperties(task, form);
		model.addAttribute(task);
		return "task/detail";
	}

	@PostMapping(path = "{id}", params = "update")
	String update(@PathVariable long id, @Validated TaskForm form, BindingResult bindingResult,
			Model model, RedirectAttributes redirectAttributes) {
		if (bindingResult.hasErrors()) {
			return "task/detail";
		}
		Task task = new Task();
		BeanUtils.copyProperties(form, task);
		repository.save(task);
		redirectAttributes.addAttribute("id", id);
		return "redirect:/tasks/{id}";
	}

	@PostMapping(path = "{id}", params = "delete")
	String delete(@PathVariable long id) {
		repository.remove(id);
		return "redirect:/tasks";
	}

	static class TaskForm {
		private static final String DATE_TIME_FORMAT = "uuuu-MM-dd HH:mm:ss";

		private Long id;
		@NotEmpty private String title;
		private String detail;
		@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate deadline;
		private boolean finished;
		@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime createdAt;
		@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime updatedAt;
		private long version;

		public Long getId() {
			return id;
		}

		public void setId(Long id) {
			this.id = id;
		}

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}

		public String getDetail() {
			return detail;
		}

		public void setDetail(String detail) {
			this.detail = detail;
		}

		public LocalDate getDeadline() {
			return deadline;
		}

		public void setDeadline(LocalDate deadline) {
			this.deadline = deadline;
		}

		public boolean isFinished() {
			return finished;
		}

		public void setFinished(boolean finished) {
			this.finished = finished;
		}

		public LocalDateTime getCreatedAt() {
			return createdAt;
		}

		public void setCreatedAt(LocalDateTime createdAt) {
			this.createdAt = createdAt;
		}

		public LocalDateTime getUpdatedAt() {
			return updatedAt;
		}

		public void setUpdatedAt(LocalDateTime updatedAt) {
			this.updatedAt = updatedAt;
		}

		public long getVersion() {
			return version;
		}

		public void setVersion(long version) {
			this.version = version;
		}
	}

}

Note:

The registration date (createdAt) and update date (ʻupdatedAt`) are not input items and should not be retained as form items, but it is also possible to get task information from the resource server every time an input check error occurs. It's subtle, so it's a bit rough, but this time I'll include it in the form item.

Creating a task list screen

It displays the task list obtained from the resource server and provides a Web UI for creating new task information.

client/src/main/resources/templates/task/list.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Task List</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
          th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
    <style type="text/css">
        .strike {
            text-decoration: line-through;
        }
    </style>
</head>
<body>

<div class="container">

    <h1>Task List</h1>

    <div id="taskForm">
        <form action="list.html" method="post" class="form-horizontal"
              th:action="@{/tasks}" th:object="${taskForm}">
            <div class="form-group">
                <label for="title" class="col-sm-1 control-label">Title</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="title" th:field="*{title}"/>
                    <span class="text-error" th:errors="*{title}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-1 control-label">Detail</label>
                <div class="col-sm-10">
                    <textarea class="form-control" id="detail" th:field="*{detail}">
                    </textarea>
                    <span class="text-error" th:errors="*{detail}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-1 control-label">Deadline</label>
                <div class="col-sm-4">
                    <input type="date" class="form-control" id="detail" th:field="*{deadline}"/>
                    <span class="text-error" th:errors="*{deadline}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-1 col-sm-10">
                    <button type="submit" class="btn btn-default">Create</button>
                </div>
            </div>
        </form>
    </div>

    <table id="todoList" class="table table-hover" th:if="${not #lists.isEmpty(taskList)}">

        <tr>
            <th>#</th>
            <th>Title</th>
            <th>Deadline</th>
            <th>Created Datetime</th>
        </tr>

        <tr th:each="task : ${taskList}">
            <td th:text="${taskStat.count}">1</td>
            <td>
                <span th:class="${task.finished} ? 'strike'">
                    <a href="detail.html"
                       th:text="${task.title}" th:href="@{/tasks/{id}(id=${task.id})}">
                        Create Sample Application
                    </a>
                </span>
            </td>
            <td>
                <span th:text="${#temporals.format(task.deadline,'uuuu-MM-dd')}" th:if="${task.deadline != null}">2017-02-28</span>
            </td>
            <td>
                <span th:text="${#temporals.format(task.createdAt,'uuuu-MM-dd HH:mm.ss')}">2017-02-27 15:17:02</span>
            </td>
        </tr>

    </table>

</div>
</body>
</html>

Creating a task details screen

Provides a Web UI for displaying, updating and deleting task information acquired from the resource server.

client/src/main/resources/templates/task/detail.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Task Detail</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
          th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
</head>
<body>

<div class="container">

    <h1>Task Detail</h1>

    <div id="taskForm">
        <form action="detail.html" method="post" class="form-horizontal"
              th:action="@{/tasks/{id}(id=*{id})}" th:object="${taskForm}">
            <div class="form-group">
                <label for="title" class="col-sm-2 control-label">Title</label>
                <div class="col-sm-10">
                    <input class="form-control" id="title" value="Create Sample Application" th:field="*{title}"/>
                    <span class="text-error" th:errors="*{title}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-2 control-label">Detail</label>
                <div class="col-sm-10">
                    <textarea class="form-control" id="detail" th:field="*{detail}">
                    </textarea>
                    <span class="text-error" th:errors="*{detail}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-2 control-label">Deadline</label>
                <div class="col-sm-4">
                    <input type="date" class="form-control" id="detail" value="2017-03-10" th:field="*{deadline}"/>
                    <span class="text-error" th:errors="*{deadline}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="finished" class="col-sm-2 control-label">Finished ?</label>
                <div class="col-sm-2">
                    <input type="checkbox" id="finished" th:field="*{finished}"/>
                </div>
            </div>
            <div class="form-group">
                <label for="createdAt" class="col-sm-2 control-label">Created Datetime</label>
                <div class="col-sm-4">
                    <input id="createdAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{createdAt}" readonly="readonly"/>
                </div>
            </div>
            <div class="form-group">
                <label for="updatedAt" class="col-sm-2 control-label">Updated Datetime</label>
                <div class="col-sm-4">
                    <input id="updatedAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{updatedAt}" readonly="readonly"/>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-10">
                    <input type="hidden" th:field="*{version}"/>
                    <button type="submit" class="btn btn-default" name="update">Update</button>
                    <button type="submit" class="btn btn-default" name="delete">Delete</button>
                </div>
            </div>
        </form>
    </div>

    <hr/>

    <a href="list.html" class="btn btn-default" th:href="@{/tasks}">Task List</a>

</div>
</body>
</html>

Experience the authorization code grant flow

Now that you have created the Service Provider and Client applications, let's actually use the applications and experience the authorization code grant flow! !!

Launch application

First, launch the Service Provider and Client applications.

$ ./mvnw -pl service-provider spring-boot:run
...
2017-02-27 00:29:12.820  INFO 78931 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-02-27 00:29:12.867  INFO 78931 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18081 (http)
2017-02-27 00:29:12.872  INFO 78931 --- [           main] com.example.ServiceProviderApplication   : Started ServiceProviderApplication in 3.247 seconds (JVM running for 5.825)
$ ./mvnw -pl client spring-boot:run
...
2017-02-27 00:29:49.282  INFO 78940 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-02-27 00:29:49.337  INFO 78940 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18080 (http)
2017-02-27 00:29:49.344  INFO 78940 --- [           main] com.example.ClientApplication            : Started ClientApplication in 3.033 seconds (JVM running for 6.17)

Displaying the task list screen

Enter http: // localhost: 18080 / client / tasks in the address bar of your browser to display the task list screen. When accessing for the first time, user authentication (Basic authentication) on the Client side is required first, so enter the user name (ʻuser) and password (password`) in the dialog and press the" Login "button.

spring-security-oauth-1st-app-client-auth.png

If the user authentication on the client side is successful, you will be redirected to the authorization endpoint (/ oauth / authorize) provided by the service provider to obtain the authorization grant (authorization code) from the resource owner. At the first access, resource owner authentication (Basic authentication) is required on the Service Provider side, so enter the user name (ʻuser) and password (password`) in the dialog and press the" Login "button. please.

spring-security-oauth-1st-app-provider-auth.png

If the authentication of the resource owner on the service provider side is successful, the screen (authorization screen) for obtaining the authorization of the resource owner for the scope requested by the client will be displayed, so select allow / deny for each scope and select " Click the "Authorize" button. (Please allow all scopes here)

spring-security-oauth-1st-app-provider-authorize.png

Note:

In this entry, the authorization screen provided by Spring Security OAuth is used, but I think that it is common to customize in actual application development, so I would like to introduce the customization method from the next time onwards.

After authorization from the resource owner, the task list screen will be displayed: laughing:

spring-security-oauth-1st-app-client-list.png

Task registration

However ... I don't know if the task information can be properly obtained from the resource server because the task is not registered ...: sweat_smile: So ... Next, let's register the task using the Web UI. Enter the task information in the input form on the task list screen and click the "Create" button.

spring-security-oauth-1st-app-client-create.png spring-security-oauth-1st-app-client-list-after-created.png

Display task details screen

Since the task list does not display anything other than the title, deadline, and creation date, let's display the task details screen and check the detailed information of the task. Since the title of the task list is a link, click the title (link) of the task you want to display.

spring-security-oauth-1st-app-client-detail.png

Update / delete tasks

To update or delete a task, click the "Update" or "Delete" button on the task details screen. Here, the deadline is updated to "2017/03/15". (In addition, deletion is omitted)

spring-security-oauth-1st-app-client-update.png spring-security-oauth-1st-app-client-detail-after-updated.png

Try rejecting write scopes (some scopes)

To destroy the access token and authorization information, restart the Service Provider ("Ctrl + C" + "./mvnw spring-boot: run") and display the task list screen again. Then ... The Service Provider authorization screen will be displayed, and access to the write scope will be denied.

spring-security-oauth-1st-app-provider-reject-write.png

When I try to create a new task on the task list screen ... The error message "Insufficient scope for this resource" is displayed and the call to "Task Creation API" is rejected.

spring-security-oauth-1st-app-client-reject-write.png

Try rejecting all scopes

To destroy the access token and authorization information, restart the Service Provider ("Ctrl + C" + "./mvnw spring-boot: run") and display the task list screen again. Then ... The Service Provider authorization screen will be displayed, and access to all scopes will be denied. Then ... Instead of the "authorization code", a parameter that notifies that the resource owner has denied access is added and redirected to the page on the Client side.

spring-security-oauth-1st-app-client-reject-all.png

** [Super Important] Eliminate security issues with REST APIs! !! ** **

Actually, the application (REST API) created so far has a serious security problem. Do you know what the problem is? It is ... how ... ** You can refer to and update the task information of others **: scream:: scream:: scream:

There are two possible ways to solve this problem.

Which method you choose depends on your security requirements. The former behaves the same as when operating on a non-existent task (404 Not Found), while the latter behaves as access denied (403 Forbidden), so it is necessary to record that there was an unauthorized access. In some cases, I think it's better to use the latter method.

Add username to condition in SQL

If you want to add the user name to the condition (SQL condition) when accessing the task information, add the user name to the argument of the Repository method and add the user name to the SQL condition. Here, for the update and delete process, calling the findOne method to check whether it is the process for its own task information before performing the process.

@Transactional
@Repository
public class TaskRepository {
	// ...

	public Task findOne(long id, String username) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id AND username = :username",
				new MapSqlParameterSource("id", id).addValue("username", username), new BeanPropertyRowMapper<>(Task.class)); //★★★ Correction
	}

	public void save(Task task, String username) {
		if (task.getId() == null) {
			GeneratedKeyHolder holder = new GeneratedKeyHolder();
			jdbcOperations.update(
					"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
					new BeanPropertySqlParameterSource(task), holder);
			task.setId(holder.getKey().longValue());
		} else {
			findOne(task.getId(), username); //★★★ Added
			jdbcOperations.update(
					"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
					new BeanPropertySqlParameterSource(task));
		}
	}

	public void remove(long id, String username) {
		findOne(id, username); //★★★ Added
		jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
	}

}

The Controller class is also modified according to the argument change of the Repository method.

@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	// ...

	@PostMapping
	ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
		task.setUsername(extractUsername(principal));
		repository.save(task, task.getUsername()); //★★★ Correction
		URI createdTaskUri = relativeTo(uriBuilder)
				.withMethodCall(on(TaskRestController.class).getTask(task.getId(), principal)).build().encode().toUri(); //★★★ Correction
		return ResponseEntity.created(createdTaskUri).build();
	}

	@GetMapping("{id}")
	Task getTask(@PathVariable long id, Principal principal) {
		return repository.findOne(id, extractUsername(principal)); //★★★ Correction
	}

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task, Principal principal) {
		task.setId(id);
		repository.save(task, extractUsername(principal)); //★★★ Correction
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id, Principal principal) {
		repository.remove(id, extractUsername(principal)); //★★★ Correction
	}

	// ...

}

If you access the task information of another person, you will get an error (404 Not Found) that occurs when the resource does not exist as shown below.

spring-security-oauth-1st-app-client-notfound-other-owner.png

Perform owner check

If you want to check if the owner of the task information and the user of the authentication information match, it is quick and easy to use the method security mechanism provided by Spring Security.

First, grant @EnableGlobalMethodSecurity to the configuration class to enable the method security mechanism.

service-provider/src/main/java/com/example/ResourceServerConfiguration.java


@EnableGlobalMethodSecurity(prePostEnabled = true) //★★★ Added
@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	// ...
}

Next, set the authorization so that only the resource owner can access the method that acquires task information.

service-provider/src/main/java/com/example/TaskRepository.java


@Transactional
@Repository
public class TaskRepository {
	// ...
	@PostAuthorize("returnObject.username == authentication.name") //★★★ Added
	public Task findOne(long id) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
				new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
	}
	// ...
}

Finally, modify it to call the findOne method when updating or deleting.

service-provider/src/main/java/com/example/TaskRestController.java


@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	// ...

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task) {
		repository.findOne(id); //★★★ Added
		task.setId(id);
		repository.save(task);
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id) {
		repository.findOne(id); //★★★ Added
		repository.remove(id);
	}

	// ...

}

If you access the task information of another person, you will get an authorization error (403 Forbidden) as shown below.

spring-security-oauth-1st-app-client-reject-other-owner.png

Completed version of application

The completed version of the application created in this entry is published in the following GitHub repository.

Summary

The explanation is a little long, but ... I made an application that authenticates and authorizes the REST API using the authorization code grant flow. This time, the purpose was to let you experience (touch) the authorization code grant flow with Spring Security OAuth + Spring Boot, so make the most of Spring Boot's AutoConfigure mechanism (default operation) and apply it. I tried to make. However ... In actual application development, user (resource owner) and client information is generally managed in a database, etc., and access tokens and authorization information are also managed in a database instead of being managed in the memory of the application. In many cases, it is required to make it persistent. Furthermore, since it is assumed that the service provider handles not one type of resource but multiple resources, or the client also accesses the resources of multiple service providers, the application that can withstand actual operation with only the contents introduced this time. The reality is that it is difficult to develop. So ... From the next time onward, I would like to explain the architecture of Spring Security OAuth and introduce how to develop applications using the extension points of Spring Boot, Spring Security, and Spring Security OAuth.

See you next time! !!

Recommended Posts

Let's experience the authorization code grant flow with Spring Security OAuth-Part 2: Creating an app for the time being
Let's experience the authorization code grant flow with Spring Security OAuth-Part 1: Review of OAuth 2.0
Creating an app and deploying it for the first time on heroku
The training for newcomers was "Make an app!", So I made an app for the time being.
Try running Spring Cloud Config for the time being
Hello World with Ruby extension library for the time being
Create an app with Spring Boot 2
Create an app with Spring Boot
Authentication / authorization with Spring Security & Thymeleaf
Spring AOP for the first time
For the time being, run the war file delivered in Java with Docker
[First Java] Make something that works with Intellij for the time being
Source code for finding an orthonormal basis with the Gram-Schmidt orthogonalization method