Customizing REST API Error Response in Spring Boot / Spring-Security-OAuth2

Defining error format is important part of REST API design.

Spring-Boot and Spring Security provide pretty nice error handling for RESTful APIs out of the box. Although it has to be documented, especially when contract-first approach to API design is used.

It is good idea to follow some common format for error responses. But OAuth2 specification and Spring Boot format may not satisfy those requirements.

By default, Spring Boot returns errors messages like this:

json
1{
2    "timestamp": "2018-06-27T16:36:47.390+0000",
3    "status": 404,
4    "error": "Not Found",
5    "message": "Not Found",
6    "path": "/service/v1/user/1d28c5cb-54fe-4f75-9af0-37fd611d0ece"
7}

The format of this response is defined by the DefaultErrorAttributes class.

It make sense to adopt your “custom” error message format to this one just to save make your life easier.

There are few possible ways to define your custom error response:

  • You may use @ControllerAdvice to create a single global error handling component:

    java
     1@ExceptionHandler(ServiceException.class)
     2@ResponseBody
     3public ErrorResponse handleServiceException(HttpServletRequest req, HttpServletResponse response, ServiceException e) 
     4{
     5    ErrorResponse error = new ErrorResponse();
     6    error.setResponseMessage(e.getMessage());
     7    //Set custom non standard http status code
     8    response.setStatus(499);
     9    return error;
    10}
  • You may hide exception from DefaultErrorAttributes by clearing a request attribute:

    java
    1@ExceptionHandler(IllegalArgumentException.class)
    2void handleIllegalArgumentException(HttpServletRequest request, HttpServletResponse response) throws IOException {
    3    request.setAttribute(DefaultErrorAttributes.class.getName() + ".ERROR", null);
    4    response.sendError(HttpStatus.BAD_REQUEST.value(), "custom message");
    5}
  • You may also provide your own ErrorAttributes implementation to get full control: on error payload:

    java
     1@Bean
     2public ErrorAttributes errorAttributes() {
     3    return new DefaultErrorAttributes() {
     4
     5        @Override
     6        public Map<String, Object> getErrorAttributes(
     7                RequestAttributes requestAttributes,
     8                boolean includeStackTrace) {
     9            Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
    10            Object errorMessage = requestAttributes.getAttribute(RequestDispatcher.ERROR_MESSAGE, RequestAttributes.SCOPE_REQUEST);
    11            if (errorMessage != null) {
    12                errorAttributes.put("message",  errorMessage);
    13            }
    14            return errorAttributes;
    15        }
    16
    17    };
    18}

This may work for you…Unless you’re using Spring-Security-OAuth2.

Spring-Security-OAuth2

Spring-Security-OAuth2 implements resource server specification according to OAuth 2.0 (RFC6749) section 7.2

If a resource access request fails, the resource server SHOULD inform the client of the error. While the specifics of such error responses are beyond the scope of this specification, this document establishes a common registry in Section 11.4 for error values to be shared among OAuth token authentication schemes.

New authentication schemes designed primarily for OAuth token authentication SHOULD define a mechanism for providing an error status code to the client, in which the error values allowed are registered in the error registry established by this specification.

Such schemes MAY limit the set of valid error codes to a subset of the registered values. If the error code is returned using a named parameter, the parameter name SHOULD be “error”.

Other schemes capable of being used for OAuth token authentication, but not primarily designed for that purpose, MAY bind their error values to the registry in the same manner.

New authentication schemes MAY choose to also specify the use of the “error_description” and “error_uri” parameters to return error information in a manner parallel to their usage in this specification.

So error response will look like:

HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
    "error":"invalid_request",
    "error_description":"..."
}

Fragment of OpenAPI 3 definition will look like this (openapi.yaml):

yaml
 1components:
 2
 3  responses:
 4    ...
 5    401Unauthorized:
 6      description: Authorization required
 7      schema:
 8        $ref: '#/definitions/OAuth2ErrorResponse'
 9    403Forbidden:
10      description: Access is denied
11      schema:
12        $ref: '#/definitions/OAuth2ErrorResponse'
13
14  schema:
15    OAuth2ErrorResponse:
16      description: |-
17        Spring-Security-OAuth2 implements resource server specification according to
18        [RFC6749 section 7.2](https://tools.ietf.org/html/rfc6749#section-7.2)
19      properties:
20        error:
21          description: |-
22            A single ASCII (USASCII) error code from the list
23          type: string
24          enum:
25           - invalid_request
26           - invalid_client
27           - invalid_grant
28           - unauthorized_client
29           - unsupported_grant_type
30           - invalid_scope
31           - insufficient_scope
32           - invalid_token
33           - redirect_uri_mismatch
34           - unsupported_response_type
35           - access_denied
36        error_description:
37          description: |-
38            Human-readable ASCII (USASCII) text providing
39            additional information, used to assist the client developer in
40            understanding the error that occurred.
41          type: string
42          pattern: "[\x20-\x7E|\x23-\x5B|\x5D-\x7E]+"
43        error_uri:
44          description: |-
45            A URI identifying a human-readable web page with
46            information about the error, used to provide the client
47            developer with additional information about the error.
48          type: string
49          pattern: "[\x20-\x7E|\x23-\x5B|\x5D-\x7E]+"
50      externalDocs:
51        description: OAuth 2.0 (RFC6749) Section 7.2
52        url: https://tools.ietf.org/html/rfc6749#section-7.2

If you’re using swagger-codegen-plugin, it make sense to define import mapping for this type (pom.xml):

xml
 1<build>
 2    <plugins>
 3        ...
 4        <plugin>
 5            <groupId>io.swagger</groupId>
 6            <artifactId>swagger-codegen-maven-plugin</artifactId>
 7            <executions>
 8                <execution>
 9                    <id>generate-java-api</id>
10                    <phase>generate-sources</phase>
11                    <goals>
12                        <goal>generate</goal>
13                    </goals>
14                    <configuration>
15                        ...
16                        <language>spring</language>
17                        <configOptions>
18                            ...
19                            <useBeanValidation>true</useBeanValidation>
20                            <java8>true</java8>
21                            <interfaceOnly>true</interfaceOnly>
22                        </configOptions>            
23                        <importMappings>
24                            <importMapping>OAuth2ErrorResponse=org.springframework.security.oauth2.common.exceptions.OAuth2Exception
25                            </importMapping>
26                        </importMappings>
27                    </configuration>
28                </execution>
29            </executions>
30        </plugin>
31        ...
32    </plugins>
33</build>

If you want to describe common error response in swagger, in some cases, you can not differentiate error thrown by spring-security-oauth2 from errors thrown by controller.

E.g. for access_denied case: the reason could be declarative security (@PreAuthorize), custom business logic in your service (thus security exception is thrown by your code) or oauth2-related exception which is thrown at the higher level.

This is because spring-security-oauth2 has a different class for error response: OAuth2Exception.

But you may still tune error response by adding some additional fields. You may define some common set of fields that are present in all error responses, thus define a consistent contract for API consumers.

Customizing Error Response for OAuth2

The class DefaultWebResponseExceptionTranslator translates thrown exceptions (e.g. InsufficientAuthenticationException) to OAuth2Exception.

You may add some additional information to error response by customizing OAuth2Exception.additionalFields. You have to use your own WebResponseExceptionTranslator instead of default one.

In case of resource server you may inject your ExceptionTranslator (OAuth2ResourceServerConfiguration.kt):

kotlin
 1import org.springframework.context.annotation.Configuration
 2import org.springframework.security.config.annotation.web.builders.HttpSecurity
 3import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
 4import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
 5import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer
 6import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler
 7import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint
 8
 9@Configuration
10@EnableResourceServer
11class OAuth2ResourceServerConfiguration(
12    private val exceptionTranslator: CustomWebResponseExceptionTranslator
13) : ResourceServerConfigurerAdapter() {
14
15    override fun configure(resources: ResourceServerSecurityConfigurer) {
16        val authenticationEntryPoint = OAuth2AuthenticationEntryPoint()
17        authenticationEntryPoint.setExceptionTranslator(exceptionTranslator)
18        resources.authenticationEntryPoint(authenticationEntryPoint)
19
20        val accessDeniedHandler = OAuth2AccessDeniedHandler()
21        accessDeniedHandler.setExceptionTranslator(exceptionTranslator)
22        resources.accessDeniedHandler(accessDeniedHandler)
23    }
24}

If you’re hacking Authorization server - then solution is:

java
 1@Configuration
 2@EnableAuthorizationServer
 3protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 4    
 5    @Autowired
 6    CustomWebResponseExceptionTranslator exceptionTranslator;
 7    
 8    @Override
 9    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
10        ...
11        endpoints.exceptionTranslator(exceptionTranslator);
12    }
13}

…and then hack a OAuth2Exception before marshalling (CustomWebResponseExceptionTranslator.kt):

kotlin
 1import org.springframework.http.ResponseEntity
 2import org.springframework.security.oauth2.common.exceptions.OAuth2Exception
 3import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator
 4import org.springframework.stereotype.Component
 5import java.lang.Exception
 6import java.time.Clock
 7import java.time.ZoneId
 8import java.time.format.DateTimeFormatter
 9
10@Component
11class CustomWebResponseExceptionTranslator(private val clock: Clock) : DefaultWebResponseExceptionTranslator() {
12
13    private val dateTimeFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC"))
14
15    override fun translate(e: Exception): ResponseEntity<OAuth2Exception> {
16        return with(super.translate(e)) {
17            body?.let {
18                it.addAdditionalInformation("timestamp", dateTimeFormat.format(clock.instant()))
19                it.addAdditionalInformation("status", it.httpErrorCode.toString())
20                it.addAdditionalInformation("message", it.message)
21                it.addAdditionalInformation("code", it.oAuth2ErrorCode.toUpperCase())
22            }
23            this
24        }
25    }
26}

Error response will now have an extra fields:

json
1{
2    "error": "unauthorized",
3    "error_description": "Full authentication is required to access this resource",
4    "code": "UNAUTHORIZED",
5    "message": "Full authentication is required to access this resource",
6    "status": "401",
7    "timestamp": "2018-06-28T23:55:28.86Z"
8}

Now you may describe a generic error message in your swagger file with required fields having both "error" field (which is required by OAuth 2.0 specification) and some other fields, e.g. "timestamp", "code", and "message".

Konstantin Pavlov

Konstantin Pavlov

Software Engineer working with Kotlin, Java, Swift, and AI. Making Kotlin ecosystem a better place for building AI-infused apps. Passionate about testing and Open-Source projects.