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:

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

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:

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

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

    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
    
            @Override
            public Map<String, Object> getErrorAttributes(
                    RequestAttributes requestAttributes,
                    boolean includeStackTrace) {
                Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
                Object errorMessage = requestAttributes.getAttribute(RequestDispatcher.ERROR_MESSAGE, RequestAttributes.SCOPE_REQUEST);
                if (errorMessage != null) {
                    errorAttributes.put("message",  errorMessage);
                }
                return errorAttributes;
            }
    
        };
    }
    

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):

components:

  responses:
    ...
    401Unauthorized:
      description: Authorization required
      schema:
        $ref: '#/definitions/OAuth2ErrorResponse'
    403Forbidden:
      description: Access is denied
      schema:
        $ref: '#/definitions/OAuth2ErrorResponse'

  schema:
    OAuth2ErrorResponse:
      description: |-
        Spring-Security-OAuth2 implements resource server specification according to
        [RFC6749 section 7.2](https://tools.ietf.org/html/rfc6749#section-7.2)
      properties:
        error:
          description: |-
            A single ASCII (USASCII) error code from the list
          type: string
          enum:
           - invalid_request
           - invalid_client
           - invalid_grant
           - unauthorized_client
           - unsupported_grant_type
           - invalid_scope
           - insufficient_scope
           - invalid_token
           - redirect_uri_mismatch
           - unsupported_response_type
           - access_denied
        error_description:
          description: |-
            Human-readable ASCII (USASCII) text providing
            additional information, used to assist the client developer in
            understanding the error that occurred.
          type: string
          pattern: "[\x20-\x7E|\x23-\x5B|\x5D-\x7E]+"
        error_uri:
          description: |-
            A URI identifying a human-readable web page with
            information about the error, used to provide the client
            developer with additional information about the error.
          type: string
          pattern: "[\x20-\x7E|\x23-\x5B|\x5D-\x7E]+"
      externalDocs:
        description: OAuth 2.0 (RFC6749) Section 7.2
        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):

<build>
    <plugins>
        ...
        <plugin>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-codegen-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>generate-java-api</id>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                    <configuration>
                        ...
                        <language>spring</language>
                        <configOptions>
                            ...
                            <useBeanValidation>true</useBeanValidation>
                            <java8>true</java8>
                            <interfaceOnly>true</interfaceOnly>
                        </configOptions>            
                        <importMappings>
                            <importMapping>OAuth2ErrorResponse=org.springframework.security.oauth2.common.exceptions.OAuth2Exception
                            </importMapping>
                        </importMappings>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
</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):

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint

@Configuration
@EnableResourceServer
class OAuth2ResourceServerConfiguration(
    private val exceptionTranslator: CustomWebResponseExceptionTranslator
) : ResourceServerConfigurerAdapter() {

    override fun configure(resources: ResourceServerSecurityConfigurer) {
        val authenticationEntryPoint = OAuth2AuthenticationEntryPoint()
        authenticationEntryPoint.setExceptionTranslator(exceptionTranslator)
        resources.authenticationEntryPoint(authenticationEntryPoint)

        val accessDeniedHandler = OAuth2AccessDeniedHandler()
        accessDeniedHandler.setExceptionTranslator(exceptionTranslator)
        resources.accessDeniedHandler(accessDeniedHandler)
    }
}

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

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    CustomWebResponseExceptionTranslator exceptionTranslator;
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        ...
        endpoints.exceptionTranslator(exceptionTranslator);
    }
}

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

import org.springframework.http.ResponseEntity
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator
import org.springframework.stereotype.Component
import java.lang.Exception
import java.time.Clock
import java.time.ZoneId
import java.time.format.DateTimeFormatter

@Component
class CustomWebResponseExceptionTranslator(private val clock: Clock) : DefaultWebResponseExceptionTranslator() {

    private val dateTimeFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC"))

    override fun translate(e: Exception): ResponseEntity<OAuth2Exception> {
        return with(super.translate(e)) {
            body?.let {
                it.addAdditionalInformation("timestamp", dateTimeFormat.format(clock.instant()))
                it.addAdditionalInformation("status", it.httpErrorCode.toString())
                it.addAdditionalInformation("message", it.message)
                it.addAdditionalInformation("code", it.oAuth2ErrorCode.toUpperCase())
            }
            this
        }
    }
}

Error response will now have an extra fields:

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource",
    "code": "UNAUTHORIZED",
    "message": "Full authentication is required to access this resource",
    "status": "401",
    "timestamp": "2018-06-28T23:55:28.86Z"
}

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".