Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

JSF Application Level Access Control

DZone's Guide to

JSF Application Level Access Control

· Java Zone
Free Resource

Download Microservices for Java Developers: A hands-on introduction to frameworks and containers. Brought to you in partnership with Red Hat.

After trying to work out how to do generically, or at least easily configurable, I wrote a handy little tool for JSF which allows you to perform security control similar to how you can in the web.xml using security-constraint tags but allowing for application level authentication.

The trick is to use a PhaseListener, listening to the RESTORE_VIEW phase and checking the viewId in the afterPhase(). All requests go through this lifecycle, including the result of an action, allowing filtering to be performed on the viewId (URL).  For example a very simple check would be:

public class AccessControlPhaseListener implements PhaseListener
{
public void afterPhase(PhaseEvent event)
{
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionBean sessionBean = (SessionBean) session.getAttribute("sessionBean");
if (!sessionBean.isLoggedIn() && !"/login.xhtml".equals(context.getViewRoot().getViewId()))
context.getApplication().getNavigationHandler().handleNavigation(context, null, "login");
}
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}
}

To see my full code posting check out my blog post Access Control in JSF using a PhaseListener. I have written it to use configurable URL filters to set different required security levels.

package devgrok.jsf;

import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.ADMIN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.LOGGED_IN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.NONE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.faces.util.MessageFactory;

import devgrok.jsf.SessionForm;
import devgrok.jsf.UrlFilter;

/**
* Phase Listener that checks the viewId (URL) against a set of filters to determine the required access level. If the
* correct level is not there then redirect.
*
* See {@link UrlFilter} for details on the url matching.
*
* @author Chris Watts 2009
*
*/
public class AccessControlPhaseListener implements PhaseListener
{
/** Logger for this class */
private static final Logger log = LoggerFactory.getLogger(AccessControlPhaseListener.class);

/** */
private static final long serialVersionUID = 1L;
private final static String SESSION_BEAN = "sessionBean";
private final HashMap<AccessLevel, List<UrlFilter>> levelFilters = new HashMap<AccessLevel, List<UrlFilter>>();

public enum AccessLevel
{
NONE, LOGGED_IN, USER_ACTIVE, ADMIN;
}

/**
*
*/
public AccessControlPhaseListener()
{
initLevels();

requires(LOGGED_IN)
.include("*")
.exclude("/index.xhtml")
.exclude("/login.xhtml")
.exclude("/user/newUser.xhtml");

requires(USER_ACTIVE)
.include("/user/*")
.exclude("/user/newUser.xhtml");

requires(ADMIN)
.include("/admin/*");
}

private void initLevels()
{
AccessLevel[] levels = AccessLevel.values();
for (int i = 1; i < levels.length; i++)
{
levelFilters.put(levels[i], new ArrayList<UrlFilter>());
}
}

private UrlFilter requires(AccessLevel level)
{
//ALL is default
if (level == NONE)
return null;

UrlFilter filter = new UrlFilter();
List<UrlFilter> list = levelFilters.get(level);
list.add(filter);
return filter;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
*/
public void afterPhase(PhaseEvent event)
{
try
{
//check have correct access
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionForm sessionBean = (SessionForm) session.getAttribute(SESSION_BEAN);
if (sessionBean == null)
{
log.error("Could not obtain instance of sessionBean");
return;
}

//can't use this here. only valid at render response phase?
String viewId = context.getViewRoot().getViewId();
AccessLevel required = requiredLevel(viewId);
log.debug("Required level={} for viewId={}", required, viewId);

//check if page require access:
switch (required) {
case NONE:
break;
case LOGGED_IN:
if (!sessionBean.isLoggedIn())
redirectLogin(event.getFacesContext(), sessionBean);
break;
case USER_ACTIVE:
if (!sessionBean.isActive())
redirectActive(event.getFacesContext());
break;
case ADMIN:
if (!sessionBean.isAdmin())
redirectAdmin(event.getFacesContext());
break;
default:
//error
log.error("huh?");
throw new IllegalArgumentException("Not a valid access level");
}
}
catch (Exception e)
{
// TODO Auto-generated catch block
log.error("beforePhase caught exception", e);
}

}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
*/
public void beforePhase(PhaseEvent event)
{

}

private void redirectLogin(FacesContext context, SessionForm sessionForm)
{
//trigger login popup to be shown on render.
sessionForm.logIn();
addError(context, "access.loginrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "index");
}

private void redirectActive(FacesContext context)
{
addError(context, "access.activerequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "userActivate");
}

private void redirectAdmin(FacesContext context)
{
addError(context, "access.adminrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "home");
}

/**
* Add keyed error/message.
*
* @param level
* @param key
* message key
*/
private void addError(FacesContext context, String key)
{
FacesMessage fMessage = MessageFactory.getMessage(key);
if (fMessage != null)
{
FacesContext facesContext = FacesContext.getCurrentInstance();
fMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
facesContext.addMessage(null, fMessage);
}
}

/**
* Checks defined filters for view id, checks starting at the highest level down to NONE.
*
* @return the matching level or {@link AccessLevel#NONE} if none matching.
*/
private AccessLevel requiredLevel(String viewId)
{
AccessLevel[] levels = AccessLevel.values();
for (int i = levels.length - 1; i > 0; i--)
{
if (checkLevel(levels[i], viewId))
return levels[i];
}

return AccessLevel.NONE;
}

private boolean checkLevel(AccessLevel level, String viewId)
{
return matchUri(levelFilters.get(level), viewId);
}

private boolean matchUri(List<UrlFilter> list, String uri)
{
for (UrlFilter filter : list)
{
if (filter.matches(uri))
return true;
}
return false;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#getPhaseId()
*/
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}

}

 

package devgrok.jsf;

import java.util.ArrayList;
import java.util.regex.Pattern;

/**
* An inclusion/exclusion filterset, similar to ant's fileset but does not support directories in the same style(**,
* etc).
*
* For example:
* <ul>
* <li>/servlet/* matches all urls starting with "/servlet/" e.g. /servlet/this.html
* <li>*.do matches all urls that end in ".do" - e.g. mypage.do
* <li>/servlet/*.do matches all urls starting with "/servlet/" and end in ".do" - e.g. /servlet/mypage.do
* </ul>
*
* @author Chris Watts 2009
*
*/
public class UrlFilter
{
private ArrayList<Pattern> include = new ArrayList<Pattern>();
private ArrayList<Pattern> exclude = new ArrayList<Pattern>();

public UrlFilter()
{

}

/**
* Include the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter include(String pattern)
{
include.add(generateExpression(pattern));
return this;
}

/**
* Exclude the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter exclude(String pattern)
{
exclude.add(generateExpression(pattern));
return this;
}

/**
* Checks to see if uri matches at least ONE inclusion filter and doesn't match ANY exclusion filters.
*
* @param uri
* @return
*/
public boolean matches(String uri)
{
boolean match = false;

//check inclusions
for (Pattern pattern : include)
{
match = match || pattern.matcher(uri).matches();
}

if (!match)
return false;

//check exclusions
for (Pattern pattern : exclude)
{
match = match && !pattern.matcher(uri).matches();
}
return match;
}

/** regular expression special character */
private static char[] specialChars = { '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')' };

/**
*
* @param input
* @return
*/
private static Pattern generateExpression(String input)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++)
{
char letter = input.charAt(i);
if (letter == '*')
{
sb.append(".*");
}
else if (contains(specialChars, letter))
{
sb.append("\\" + letter);
}
else
{
sb.append(letter);
}
}
return Pattern.compile(sb.toString());
}

private static boolean contains(char[] array, char value)
{
if (array == null || array.length == 0)
{
return false;
}

for (int i = 0; i < array.length; i++)
{
char o = array[i];
if (o == value)
{
return true;
}
}

return false;
}
}

Download Modern Java EE Design Patterns: Building Scalable Architecture for Sustainable Enterprise Development.  Brought to you in partnership with Red Hat

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}