Wednesday, October 19, 2016

Grails 3 quartz-plugin with Clustering Support

If you need to run Quartz in Grails 3 on a clustered Application Server environment, you must change the default config so it is Cluster aware. Otherwise, each Job on each node runs independently.

1. Create the DB Tables for Quartz

This was quite hard and I needed to dig into the Quartz Library Source Code to get a Schema for Mysql with InnoDB (which had a typo..). I then created a migration file for the Grails database-migration plugin. 
Just copy this migration file into your grails-app/migration directory and register it in changelog.groovy


2. Configure database-migration plugin

Next, you need to tweak the database-migration config, so it ignores the Quartz tables. Otherwise,  it would drop the tables with the next dbm_gorm_diff run. Example for application.groovy:

grails.plugin.databasemigration.excludeObjects = ['QRTZ_BLOB_TRIGGERS','QRTZ_CALENDARS', 'QRTZ_CRON_TRIGGERS', 'QRTZ_FIRED_TRIGGERS', 'QRTZ_JOB_DETAILS', 'QRTZ_LOCKS', 'QRTZ_PAUSED_TRIGGER_GRPS', 'QRTZ_SCHEDULER_STATE', 'QRTZ_SIMPLE_TRIGGERS', 'QRTZ_SIMPROP_TRIGGERS', 'QRTZ_TRIGGERS']


3. Configure quartz-plugin


Next, you need to configure the Grails Quartz Plugin to use the jdbc store, and enable clustering.

4. Test clustering

Startup your application. You must see such message:

  Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is clustered.


Friday, October 14, 2016

Grails 3.x Spring Basic Authentication with JSON handling

If you need to secure a JSON Api using Basic Authentication via HTTPS, you need to tweak the Spring Security configuration and use custom beans to support JSON / HTML error responses.

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

  1. Support Basic Auth only on the REST Api Urls, use default (web based) Authentication on all other Urls to be secured
  2. As the REST Api is stateless, no sessions should be created when accessing the Api
  3. 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")
  }

}

2. CustomBasicAuthenticationAccessDeniedHandlerImpl:


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")
  }
}

3. grails-app/conf/spring/resources.groovy:


  // 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')
  }

4. Configure the Spring Security Core plugin in grails-app/conf/application.groovy:


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





Thursday, October 6, 2016

Fortinet Route Based VPN with overlapping Networks

The other day I needed to establish an IPSEC VPN on a Fortinet 60D with Source NAT for an overlapping Subnet scenario. The remote subnet was the same as our local one.

I only found Policy Based examples in the Fortinet kb, so I tested it myself using a route based VPN.

The trick is to create an IP-Pool with the source NAT Subnet range, e.g. 192.168.99.0/24
This subnet is then presented to the remote IPSEC VPN (Proxy-ID) during IPSEC Phase 2 negotiation.

Whenever you access remote resources via the VPN, your local subnet IP (e.g. 192.168.1.2) is translated 1:1 into the IP-Pool subnet address (192.168.99.1) before entering the VPN.

1. create a IP Pool (Policy & Objects > IP Pools > Create New) with the following settings:
  • Type: Overload
  • Range: 192.168.99.0 - 192.168.99.255
  • ARP Reply: checked
2. Create your route based VPN (e.g. using the wizard). Type is "custom".
In Phase2:

  • Use your IP-Pool Subnet address (the source NAT translated one created in 1.)
  • Add all remote Subnets needed as Proxy-IDs. 
3. Add static routes for all remote subnets (Network > Static Routes):
  • Destination: Subnet
  • Subnet specification, e.g. 192.168.243.0/24
  • Device: <Tunnel Interface for the VPN>
  • Administative Distance: 10
4. Create Address Entries for local and remote subnets. If you use the VPN wizard, these entries are created automatically. If you configure the VPN manually or on the CLI, you must create address book entries on your own:
  • Create one entry for your local internal network, e.g: 192.168.1.0/24
  • Create entries for all remote subnets
5. Create a policy (Policy & Objects > IPv4 Policy > Create New:
  • Incoming Interface: internal
  • Outgoing Interface: <Tunnel Interface for the VPN>
  • Source: <Your local internal network Address entry created in 4.>
  • Destination Address: <remote network address definition(s) created in 4.>
  • Schedule: always
  • Service: ALL
  • Action: ACCEPT
  • NAT: enable
  • Fixed Port: disable
  • IP Pool Configuration: "Use Dynamic IP Pool". Select your Source-NAT IP Pool defined in 1.
  • Enable this policy: enabled
6. Test your communication to the remote subnet(s).