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