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>









No comments:

Post a Comment