Search code examples
javarestspring-securityspring-java-config

Java APIs return well formatted JSON for success and failure including authentication errors


Not an expert in developing APIs so need some help/advice from the community. I have an application that used Java Spring MVC for APIs. I am using ResponseEntity to return responses back to anyone who would use the APIs (third-party or UI). Example of my API

@Controller
@RequestMapping("/Test/")
public ResponseEntity<TestGroup> getTestsById(@PathVariable("id") Integer id) {
    TestGroup testGroup = testService.getTestById(id);  //calls a service that returns test from the db
    if(testGroup != null) {
        return new ResponseEntity(testGroup, HttpStatus.OK);
    } else {
        return new ResponseEntity(HttpStatus.NOT_FOUND);
    }
}

I have other APIs similar to this. My question is whether there are any frameworks or ways so that I can make my APIs return JSON response for errors or success in a well formatted JSON. Example

{
  "code": 400,
  "message": "Bad Request",
  "description": "There were no credentials found."
}

{
  "code": 200,
  "data": "{<JSON blob of the object to be returned>}"
}

Also, Have implemented a Filter to check for session information (think of them as oauth tokes) for authentication before any of the routes are mapped. I need this process of error handling to work at that stage too, so that if there is an expired token or invalid token I get back a well formatted JSON. Example

{
  "code": 401,
  "message": "Unauthorized",
  "description": "The token used in the request is incorrect or has expired."
}

Right now I get the default message Spring's authentication Filter throws in the form of an HTML

<html><head><title>Apache Tomcat/7.0.50 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 500 - Access is denied</h1><HR size="1" noshade="noshade"><p><b>type</b> Exception report</p><p><b>message</b> <u>Access is denied</u></p><p><b>description</b> <u>The server encountered an internal error that prevented it from fulfilling this request.</u></p><p><b>exception</b> <pre>org.springframework.security.access.AccessDeniedException: Access is denied
org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83)
org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206)
org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
com.test.security.CookieAuthenticationFilter.doFilter(CookieAuthenticationFilter.java:95)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:343)
org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:260)
</pre></p><p><b>note</b> <u>The full stack trace of the root cause is available in the Apache Tomcat/7.0.50 logs.</u></p><HR size="1" noshade="noshade"><h3>Apache Tomcat/7.0.50</h3></body></html>

Apologize for not communicating this, I am not using spring security for the APIs and my web.xml for the filter looks like this

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">

<display-name>Webapp</display-name>

<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.test.spring.config</param-value>
</context-param>

<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.test.spring.config=</param-value>
    </init-param>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Here is how the security configuration is handled.

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

    @Autowired
    private AbstractConfiguration configurationManager;

    public SecurityConfiguration() {
        super(true);
    }

    @Override
    @Bean
    protected AuthenticationManager authenticationManager() {
        return new CustomAuthenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/health/**").permitAll().and().authorizeRequests()
                .antMatchers("/complete/**").permitAll().and()
                .addFilterBefore(cookieAuthenticationFilter(), ChannelProcessingFilter.class).authorizeRequests()
                .antMatchers("/**").hasAuthority("tests");
    }

    @Bean
    public GenericFilterBean cookieAuthenticationFilter() {
        if (System.getProperty("noauth") != null) {
            return new SecurityFilterMock();
        } else {
            return new CookieAuthenticationFilter(redisTemplate());
        }
    }
}

As per suggestions if you read below, I have made the following EntryPoint class to handle the token authentication and display a JSON

public class TokenAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
private static final Logger log = LoggerFactory.getLogger(TokenAuthenticationEntryPoint.class);

@Override
public void commence(HttpServletRequest request,HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    log.info(String.format("Unauthorized access with session id '%s'",
            request.getSession().getId()));

    ErrorHandler errorResponse = new ErrorHandler();
    // populate your response object with all the info you need
    errorResponse.setCode(401);
    errorResponse.setDescription("The token used in the request is incorrect or invalid.");
    errorResponse.setMessage("Unauthorized");

    ObjectMapper jsonMapper = new ObjectMapper();

    response.setContentType("application/json;charset=UTF-8");
    response.setStatus(HttpStatus.UNAUTHORIZED.value()); 
    PrintWriter out = response.getWriter();
    out.print(jsonMapper.writeValueAsString(errorResponse));
}
}

Any suggestions or directions would be really appreciated


Solution

  • To return JSON formatted errors to authentication with Spring you can have a look at my answer here: How to return JSON response for unauthorized AJAX calls instead of login page as AJAX response?

    Basically, you'll have to tweak authentication mechanism and redefine the entry point to properly hendle the response.