Sunday, November 11, 2018

Micronaut and Redirect on Security Check Fail

Micronaut is a new framework for building micro-services.  Since it is focused on micro-services, it does not support a lot of the GUI features found in a framework like Grails.

One of the things missing is in the micronaut-security-session is the ability to forward onto the desired page after a successful login.  This post describes two replacement beans for the security module that works around this issue.

This example uses handlebars for the login view.  Someday, I will probably make a pull-request for the micronaut-security-session module.

The first is a redirect handler that sends the user to a login page, with the target page saved for the eventual success:

  package org.simplemes.eframe.security
  
  import groovy.util.logging.Slf4j
  import io.micronaut.context.annotation.Replaces
  import io.micronaut.core.async.publisher.Publishers
  import io.micronaut.http.HttpRequest
  import io.micronaut.http.HttpResponse
  import io.micronaut.http.HttpStatus
  import io.micronaut.http.MutableHttpResponse
  import io.micronaut.security.handlers.RejectionHandler
  import io.micronaut.security.session.SecuritySessionConfiguration
  import io.micronaut.security.session.SessionSecurityfilterRejectionHandler
  import org.reactivestreams.Publisher
  
  import javax.inject.Singleton
  
  /*
   * Copyright Michael Houston 2018. All rights reserved.
   * Original Author: mph
   *
  */
  /**
   * This handles any security rejection for any request.
   * This is different from the micronaut core implementation
   * that it replaces (SessionSecurityFilterRejectionHandler).
   * The main difference is that it saves the target path
   * for use after the login is successful.
   *
   * This class also use /login/auth and /login/denied as the
   * defaults for the unauthorized-target-url and
   * forbidden-target-url configuration properties.
   */
  @Slf4j
  @Singleton
  @Replaces(SessionSecurityfilterRejectionHandler)
  class RedirectToLoginRejectionHandler implements RejectionHandler {
  
    protected final SecuritySessionConfiguration securitySessionConfiguration
  
    /**
     * Constructor.
     * @param securitySessionConfiguration Security Session Configuration
     * @param sessionStore The session store
     * @param cookieHttpSessionStrategy The strategy for naming the cookies.
     */
    RedirectToLoginRejectionHandler(SecuritySessionConfiguration securitySessionConfiguration) {
      this.securitySessionConfiguration = securitySessionConfiguration
    }
  
    @Override
    Publisher<MutableHttpResponse<?>> reject(final HttpRequest<?> request,
                                             final boolean forbidden) {
      def accept = request.headers.get('Accept')
      if (accept?.contains('text/html') && !forbidden) {
        try {
          def location
          if (forbidden) {
            location = securitySessionConfiguration.getForbiddenTargetUrl() ?: '/login/denied'
          } else {
            location = securitySessionConfiguration.getUnauthorizedTargetUrl() ?: '/login/auth'
          }
          URI uri = new URI("${location}?target=${request.path}")
          log.debug('reject(): re-direct (303) to {} for request {}', uri, request)
          return Publishers.just(HttpResponse.seeOther(uri))
        } catch (URISyntaxException ignored) {
          return Publishers.just(HttpResponse.serverError())
        }
      }
  
      return Publishers.just(HttpResponse.status(forbidden ? HttpStatus.FORBIDDEN :
                                                   HttpStatus.UNAUTHORIZED))
     }
    
  }

The second replacement is for the successful login.  It will use the target to send the browser to the page the user originally asked for:

  package org.simplemes.eframe.security
  
  import groovy.util.logging.Slf4j
  import io.micronaut.context.annotation.Replaces
  import io.micronaut.core.convert.value.MutableConvertibleValues
  import io.micronaut.http.HttpRequest
  import io.micronaut.http.HttpResponse
  import io.micronaut.security.authentication.UserDetails
  import io.micronaut.security.filters.SecurityFilter
  import io.micronaut.security.session.SecuritySessionConfiguration
  import io.micronaut.security.session.SessionLoginHandler
  import io.micronaut.session.Session
  import io.micronaut.session.SessionStore
  import io.micronaut.session.http.HttpSessionFilter
  
  import javax.inject.Singleton
  
  /*
   * Copyright Michael Houston 2018. All rights reserved.
   * Original Author: mph
   *
  */
  
  /**
   * This class replaces the micronaut core SessionLoginHandler.  It does mostly 
   * what the core class does,
   * but it does support a target re-direct for the user.
   */
  @Slf4j
  @Singleton
  @Replaces(SessionLoginHandler)
  class RedirectSessionLoginHandler extends SessionLoginHandler {
  
    /**
     * Constructor.
     * @param securitySessionConfiguration Security Session Configuration
     * @param sessionStore The session store
     */
    RedirectSessionLoginHandler(SecuritySessionConfiguration securitySessionConfiguration,
                                SessionStore<Session> sessionStore) {
      super(securitySessionConfiguration, sessionStore)
    }
  
    @Override
    HttpResponse loginSuccess(UserDetails userDetails, HttpRequest<?> request) {
      def body = request.body?.get()
      def target = body?.target
      Session session = findSession(request)
      session.put(SecurityFilter.AUTHENTICATION, userDetails)
      try {
        if (target) {
          log.debug('loginSuccess() redirecting to {}', target)
        }
        URI location = new URI(target ?: securitySessionConfiguration.getLoginSuccessTargetUrl())
        return HttpResponse.seeOther(location)
      } catch (URISyntaxException ignored) {
        return HttpResponse.serverError()
      }
    }
  
    /**
     * Copied as-is from the core SessionLoginHandler.
     * @param request
     * @return
     */
    private Session findSession(HttpRequest<?> request) {
      MutableConvertibleValues<Object> attrs = request.getAttributes()
      Optional<Session> existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, 
                                             Session.class)
      if (existing.isPresent()) {
        return existing.get()
      } else {
        // create a new session store it in the attribute
        Session newSession = sessionStore.newSession()
        attrs.put(HttpSessionFilter.SESSION_ATTRIBUTE, newSession)
        return newSession
      }
    }
  }

Finally, I have a handlebars view that is used to log the user in.  It is pretty basic, but it passes the target to the server for the RedirectSessionLoginHandler above.

  <!DOCTYPE html>
  <html>
  <head>
    {{#if errors}}
      <title>Login Failed</title>
    {{else}}
      <title>Login</title>
    {{/if}}
  </head>
  <body>
  <form action="/login" method="POST">
    <ol>
      <li>
        <label for="username">Username</label>
        <input type="text" name="username" id="username"/>
      </li>
      <li>
        <label for="password">Password</label>
        <input type="password" name="password" id="password"/>
      </li>
      <li>
        <input type="submit" value="Login"/>
      </li>
      {{#if errors}}
        <li id="errors">
          <span style="color: red;">Login Failed</span>
        </li>
      {{/if}}
    </ol>
    <input id="target" name="target" type="hidden" value="{{target}}"/>
  </form>
  </body>
  </html>









Friday, February 2, 2018

Running JUnit/Spock Grails Tests From Intellij Triggers Application Restart

This problem has cropped up several times over the years.  We never tracked down the problem until we
spent some hours debugging the spring startup logic when used with an IDE test runner (Intellij).
This also affects tests run from Gradle.

The problem is that the Grails application would restart between integration tests under some conditions.  
This slowed things down a lot and caused errors.

Symptoms:
  • The first test class starts up fine.
  • This first class initializes the Grails context correctly.
  • All tests in the first test class runs correctly.
  • The second test class then starts up.
  • This attempts to start a new Grails context, which conflicts with the original.
  • This normally fails with a session not found exception, but sometimes it is a listener port already in use error.
The example output is shown below:

Configuring Spring Security Core ...
... finished configuring Spring Security Core

Grails application running at http://localhost:64901 in environment: test     



Configuring Spring Security Core ...
... finished configuring Spring Security Core

Grails application running at http://localhost:64901 in environment: test     


org.hibernate.HibernateException: No Session found for current thread
 at org.grails.orm.hibernate.GrailsSessionContext.currentSession(GrailsSessionContext.java:117)
 at org.hibernate.internal.SessionFactoryImpl.getCurrentSession(SessionFactoryImpl.java:688)
 at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:177)
 at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:170)

. . .

Root Cause:

The root cause is that the tests had different super-class hierarchies.  One test had an @Integration
annotation and its super-class also had an @Integration annotation.  In the internals of Spring
(AbstractTestContextBootstrapper.buildMergedContextConfiguration()), the MergedContextConfiguration
internally refers to two Application classes.  The other test class on referred to one Application class,
so Spring thinks they are two different application contexts.   Spring then tries to start the application again.

Fix:

Make sure your test classes have the @Integration annotation just once in their hierarchies (at the test
class itself).

Also, it turns out there is no need for the @Integration annotation in the super-class.