#Securing RESTful APIs using Basic Authentication
The restful-api plugin does not itself address security requirements. This document is provided to show how the Spring Security Core Plugin may be used to protect RESTful API endpoints using Basic Authentication.
###Install Spring Security Core Plugin
First, install the Spring Security Core Plugin by editing grails-app/conf/BuildConfig as described at http://grails.org/plugin/spring-security-core
Next, we'll perform steps that are similar to those in chapter 23 of the Spring Security Core Plugin reference documentation (which is a tutorial).
We'll, however, assume we already have an existing application for which the restful-api plugin is being used to expose RESTful endpoints.
So, we will start out by creating User and Role classes by using the s2-quickstart script (as described in step 3 of the above-mentioned tutorial).
grails s2-quickstart {your-package} User RoleNext we'll modify BootStrap.groovy to instantiate a user and a role during initializaiton (similarly to step 7 of the tutorial). We'll create a single 'ROLE_API_USER' role instead of the two roles created in the tutorial.
Please add the following to the 'init' closure within BootStrap.groovy (except use the correct package names for your own User and Role classes).
def userRole = new Role(authority: 'ROLE_API_USER').save(flush: true)
def testUser = new User(username: 'api', enabled: true, password: 'password')
testUser.save(flush: true)
UserRole.create testUser, userRole, true
assert User.count() == 1
assert Role.count() == 1
assert UserRole.count() == 1The Spring Security Core Plugin tutorial proceeds to create a Controller secured using a '@Secured' annotation. We'll assume you'd rather not modify the RestfulAPiController controller (as it's in the restful-api plugin), and instead we'll configure an interceptUrlMap.
Please add the following to your Config.groovy file:
import grails.plugins.springsecurity.SecurityConfigType
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.InterceptUrlMap
grails.plugins.springsecurity.rejectIfNoRule = true
grails.plugins.springsecurity.interceptUrlMap = [
'/api/**': ['ROLE_API_USER'],
]####Enable Basic Authentication
Add the following to Config.groovy
grails.plugins.springsecurity.useBasicAuth = true
grails.plugins.springsecurity.basic.realmName = "HTTP Basic Auth Demo"We have now protected the API endpoint, so if we hit an API without providing correct credentials we will get a '401 Unauthorized' response.
$ curl -i --noproxy localhost -H "Accept: application/json" http://localhost:8080/restfulapi-tutorial/api/foos?max=10
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=EE7CBBD50B45147546C048B901C61CF2; Path=/restfulapi-tutorial/; HttpOnly
WWW-Authenticate: Basic realm="Example RESTful API Realm"
Content-Type: text/html;charset=utf-8
Content-Length: 1061
Date: Thu, 26 Sep 2013 22:37:34 GMT
(content not shown)Unfortunately, the above Content-Type is 'text/html' and the login page markup was returned. This isn't what we want, so we'll fix that below.
####Create a Basic Authentication Entry Point
We really want a basic authentication entry point that can support content negotiation, so that if JSON is requested we respond with a Content-Type of application/json, and if XML is requested we respond with a Content-Type of application/xml.
To do this, create a src/groovy/net/hedtech/api/security/RestApiAuthenticationEntryPoint.groovy file with the following content:
package net.hedtech.api.security
import java.io.IOException
import java.io.PrintWriter
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import net.hedtech.restfulapi.MediaType
import net.hedtech.restfulapi.MediaTypeParser
import org.springframework.http.HttpHeaders
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint
public class RestApiAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
MediaTypeParser mediaTypeParser = new MediaTypeParser()
@Override
public void commence( HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
String contentType
String content
MediaType[] acceptedTypes = mediaTypeParser.parse(request.getHeader(HttpHeaders.ACCEPT))
def type = acceptedTypes.size() > 0 ? acceptedTypes[0].name : ""
switch(type) {
case ~/.*xml.*/:
contentType = 'application/xml'
content = "<Errors><Error><Code>${HttpServletResponse.SC_UNAUTHORIZED}</Code></Error></Errors>"
break
case ~/.*json.*/:
contentType = 'application/json'
content = "{ \"errors\" : [ { \"code\":\"${HttpServletResponse.SC_UNAUTHORIZED}\" } ] }"
break
default:
contentType = 'plain/text'
content = HttpServletResponse.SC_UNAUTHORIZED + " - " + authException.getMessage()
break
}
response.addHeader("Content-Type", contentType )
response.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealmName() + "\"")
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
PrintWriter writer = response.getWriter()
writer.println(content)
}
}Next we need to tell Spring Security to use our custom entry point for Basic Authentication. We'll register the new RestApiAuthenticationEntryPoint bean within the application context, by editing the resources.groovy file:
import net.hedtech.api.security.RestApiAuthenticationEntryPoint
import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.authentication.www.BasicAuthenticationFilterAdd include the following beans:
restApiAuthenticationEntryPoint(RestApiAuthenticationEntryPoint) {
realmName = 'Example RESTful API Realm'
}
basicAuthenticationFilter(BasicAuthenticationFilter) {
authenticationManager = ref('authenticationManager')
authenticationEntryPoint = ref('restApiAuthenticationEntryPoint')
}
basicExceptionTranslationFilter(ExceptionTranslationFilter) {
authenticationEntryPoint = ref('restApiAuthenticationEntryPoint')
accessDeniedHandler = ref('accessDeniedHandler')
}
Invoking our endpoint now, without credentials or with bad credentials, will now result in the following:
$ curl -i --noproxy localhost -H "Accept: application/json" http://localhost:8080/restfulapi-tutorial/api/foos?max=10
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=83E8F414B0CE2968180E8A4438663F15; Path=/restfulapi-tutorial/; HttpOnly
WWW-Authenticate: Basic realm="Example RESTful API Realm"
Content-Type: application/json;charset=ISO-8859-1
Content-Length: 36
Date: Thu, 26 Sep 2013 22:58:33 GMT
{ "errors" : [ { "code":"401" } ] }
Much better :-)