If possible, use a more sophisticated authentication scheme for REST Apis, e.g. the spring-security-rest Grails plugin, which supports token based authentication (OAUTH like).
If you still need to support Basic Auth for your Grails Rest API (e.g. server-to-server communication), read on.
Goals
- Support Basic Auth only on the REST Api Urls, use default (web based) Authentication on all other Urls to be secured
- As the REST Api is stateless, no sessions should be created when accessing the Api
- If Authentication or Authorization errors occur, the authenticator should return JSON error blocks back if accessed with a json Content-Type, and HTML errors if the Api was accessed by a Browser (e.g. for debugging or documentation purposes)
Implementation Details
1. CustomBasicAuthenticationEntryPoint:
import groovy.transform.CompileStatic import org.springframework.security.core.AuthenticationException import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse /** * AuthenticationEntryPoint for BasicAuthentication. * Triggered if user is not (successfully) authenticated on a secured Basic Auth URL resource. * Maps all errors to 401 status code and returns a HTML or JSON error string dependent on the request content type. * Also, sends a Basic Auth Challenge header (if accessing via Browser for test purposes, to show the login popup) * * Author: Robert Oschwald * License: Apache 2.0 * */ @CompileStatic public class CustomBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String errorMessage = authException.getMessage() int statusCode = HttpServletResponse.SC_UNAUTHORIZED response.addHeader("WWW-Authenticate", "Basic realm=\"${realmName}\"") if (request.contentType == "application/json") { log.warn("Basic Authentication failed (JSON): ${errorMessage}") response.setContentType("application/json") response.sendError(statusCode, "{error:${HttpServletResponse.SC_UNAUTHORIZED}, message:\"${errorMessage}\"") return } // non-json request response.sendError(statusCode, "$statusCode : $errorMessage") } }
import groovy.transform.CompileStatic import org.springframework.security.access.AccessDeniedException import org.springframework.security.web.access.AccessDeniedHandlerImpl import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse /** * Basic Auth Extended implementation of * {@link org.springframework.security.web.access.AccessDeniedHandlerImpl}. * Maps errors to a 403 status code and returns a HTML or JSON error string dependent on the request content type. * Author: Robert Oschwald * License: Apache 2.0 */ @CompileStatic class CustomBasicAuthenticationAccessDeniedHandlerImpl extends AccessDeniedHandlerImpl { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { String errorMessage = accessDeniedException.getMessage() int statusCode = HttpServletResponse.SC_FORBIDDEN if (request.contentType == "application/json"){ response.setContentType("application/json") response.sendError(statusCode, "{error:${statusCode}, message:\"${errorMessage}\"") return } // non-json request response.sendError(statusCode, "$statusCode : $errorMessage") } }
// No Sessions for Basic Auth statelessSecurityContextRepository(NullSecurityContextRepository) {} // No Sessions for Basic Auth customBasicRequestCache(NullRequestCache) statelessSecurityContextPersistenceFilter(SecurityContextPersistenceFilter, ref('statelessSecurityContextRepository')) {} statelessSecurityContextPersistenceFilterDeregistrationBean(FilterRegistrationBean){ filter = ref('securityContextPersistenceFilter') // To prevent Spring Boot automatic filter bean registration in the ApplicationContext enabled = false } /** * Sends HTTP 401 error status code + HTML/JSON error in body dependent on the request type * if user is not authenticated, or if authentication failed. */ customBasicAuthenticationEntryPoint(CustomBasicAuthenticationEntryPoint) { realmName = SpringSecurityUtils.securityConfig.basic.realmName } /** * Sends HTTP 403 error status code + HTML/JSON error in body dependent on the request type * if user is authenticated, but not authorized. */ basicAccessDeniedHandler(CustomBasicAuthenticationAccessDeniedHandlerImpl) customBasicAuthenticationFilter(BasicAuthenticationFilter, ref('authenticationManager'), ref('customBasicAuthenticationEntryPoint')) { authenticationDetailsSource = ref('authenticationDetailsSource') rememberMeServices = ref('rememberMeServices') credentialsCharset = SpringSecurityUtils.securityConfig.basic.credentialsCharset // 'UTF-8' } /** * basicExceptionTranslationFilter with customBasicRequestCache (no Sessions) * The bean name is used in Spring-Security by default. */ basicExceptionTranslationFilter(ExceptionTranslationFilter, ref('basicAuthenticationEntryPoint'), ref('customBasicRequestCache')) { accessDeniedHandler = ref('basicAccessDeniedHandler') authenticationTrustResolver = ref('authenticationTrustResolver') throwableAnalyzer = ref('throwableAnalyzer') }
// Spring Security Core plugin grails { plugin { springsecurity { securityConfigType = "InterceptUrlMap" // if using the chainmap in application.groovy. If you prefer Annotations, omit. auth.forceHttps = true useBasicAuth = true // Used for /api/ calls. See chainMap. basic.realmName = "App Authentication" // enforce SSL secureChannel.definition = [ [pattern:'/api', access:'REQUIRES_SECURE_CHANNEL'] // strongly recommended // your other secureChannel settings ] filterChain.chainMap = [ // For Basic Auth Chain: // - Use statelessSecurityContextPersistenceFilter instead of securityContextPersistenceFilter, // - no exceptionTranslationFilter // - no anonymousAuthenticationFilter // As springsec-core does not support (+) on JOINED_FILTERS yet, we must state the whole chain when adding our basic auth filters. See springsec-core #437. [pattern:'/api/**', filters: 'securityRequestHolderFilter,channelProcessingFilter,statelessSecurityContextPersistenceFilter,logoutFilter,authenticationProcessingFilter,customBasicAuthenticationFilter,securityContextHolderAwareRequestFilter,basicExceptionTranslationFilter,filterInvocationInterceptor'], // Use BasicAuth [pattern:'/**',filters:'JOINED_FILTERS,-statelessSecurityContextPersistenceFilter,-basicAuthenticationFilter,-basicExceptionTranslationFilter'] // normal auth ] interceptUrlMap = [ [pattern:'/api/**', access:['ROLE_API_EXAMPLE']], [pattern:'/**', access:['ROLE_USER']] } } } }
5. UrlMappings definition
For the example above, you need to map your Api Controllers to /api/ in UrlMappings.groovy.
No comments:
Post a Comment
Due to the high amount of Spam, you must solve a word verification.