package org.jboss.seam.security;
import static org.jboss.seam.ScopeType.SESSION;
import static org.jboss.seam.annotations.Install.BUILT_IN;
import java.io.Serializable;
import java.rmi.server.UID;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.faces.context.FacesContext;
import org.jboss.seam.Component;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.faces.Selector;
import org.jboss.seam.security.management.IdentityManager;
import org.jboss.seam.util.Base64;
/**
* Remember-me functionality is provided by this class, in two different flavours. The first mode
* provides username-only persistence, and is considered to be secure as the user (or their browser)
* is still required to provide a password. The second mode provides an auto-login feature, however
* is NOT considered to be secure and is vulnerable to XSS attacks compromising the user's account.
*
* Use the auto-login mode with caution!
*
* @author Shane Bryzak
*/
@Name("org.jboss.seam.security.rememberMe")
@Scope(SESSION)
@Install(precedence = BUILT_IN, classDependencies = "javax.faces.context.FacesContext")
@BypassInterceptors
public class RememberMe implements Serializable
{
class UsernameSelector extends Selector
{
@Override
public String getCookieName()
{
return "org.jboss.seam.security.username";
}
@Override
public void setDirty()
{
super.setDirty();
}
@Override
public String getCookieValue()
{
return super.getCookieValue();
}
@Override
public void clearCookieValue()
{
super.clearCookieValue();
}
@Override
public void setCookieValueIfEnabled(String value)
{
super.setCookieValueIfEnabled(value);
}
}
class TokenSelector extends UsernameSelector
{
@Override
public String getCookieName()
{
return "org.jboss.seam.security.authtoken";
}
}
private class DecodedToken
{
private String username;
private String value;
public DecodedToken(String cookieValue)
{
if (cookieValue != null)
{
try
{
String decoded = new String(Base64.decode(cookieValue));
username = decoded.substring(0, decoded.indexOf(':'));
value = decoded.substring(decoded.indexOf(':') + 1);
}
catch (Exception ex)
{
// swallow
}
}
}
public String getUsername()
{
return username;
}
public String getValue()
{
return value;
}
}
private UsernameSelector usernameSelector;
private TokenSelector tokenSelector;
private TokenStore tokenStore;
private boolean enabled;
private int cookieMaxAge = Selector.DEFAULT_MAX_AGE;
private boolean autoLoggedIn;
private Random random = new Random(System.currentTimeMillis());
public enum Mode { disabled, usernameOnly, autoLogin}
private Mode mode = Mode.usernameOnly;
public Mode getMode()
{
return mode;
}
public void setMode(Mode mode)
{
this.mode = mode;
}
public boolean isEnabled()
{
return enabled;
}
public void setEnabled(boolean enabled)
{
if (this.enabled != enabled)
{
this.enabled = enabled;
// selector is null during component initialization (setup handled in @Create method)
if (usernameSelector != null && mode.equals(Mode.usernameOnly))
{
usernameSelector.setCookieEnabled(enabled);
usernameSelector.setDirty();
}
// selector is null during component initialization (setup handled in @Create method)
else if (tokenSelector != null && mode.equals(Mode.autoLogin))
{
tokenSelector.setCookieEnabled(enabled);
tokenSelector.setDirty();
}
}
}
public int getCookieMaxAge() {
return cookieMaxAge;
}
public void setCookieMaxAge(int cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
}
public TokenStore getTokenStore()
{
return tokenStore;
}
public void setTokenStore(TokenStore tokenStore)
{
this.tokenStore = tokenStore;
}
@Create
public void create()
{
if (mode.equals(Mode.usernameOnly))
{
usernameSelector = new UsernameSelector();
usernameSelector.setCookieEnabled(enabled);
}
else if (mode.equals(Mode.autoLogin))
{
tokenSelector = new TokenSelector();
tokenSelector.setCookieEnabled(enabled);
// Default to JpaTokenStore
if (tokenStore == null)
{
tokenStore = (TokenStore) Component.getInstance(JpaTokenStore.class, true);
}
}
}
protected String generateTokenValue()
{
StringBuilder sb = new StringBuilder();
sb.append(new UID().toString());
sb.append(":");
sb.append(random.nextLong());
return sb.toString();
}
protected String encodeToken(String username, String value)
{
StringBuilder sb = new StringBuilder();
sb.append(username);
sb.append(":");
sb.append(value);
return Base64.encodeBytes(sb.toString().getBytes(), Base64.DONT_BREAK_LINES);
}
public String getCookiePath()
{
FacesContext ctx = FacesContext.getCurrentInstance();
return ctx != null ? ctx.getExternalContext().getRequestContextPath() : null;
}
@Observer(Credentials.EVENT_INIT_CREDENTIALS)
public void initCredentials(Credentials credentials)
{
String cookiePath = getCookiePath();
if (mode.equals(Mode.usernameOnly))
{
if (cookiePath != null)
{
usernameSelector.setCookiePath(cookiePath);
}
String username = usernameSelector.getCookieValue();
if (username!=null)
{
setEnabled(true);
credentials.setUsername(username);
}
usernameSelector.setDirty();
}
else if (mode.equals(Mode.autoLogin))
{
if (cookiePath != null)
{
tokenSelector.setCookiePath(cookiePath);
}
String token = tokenSelector.getCookieValue();
if (token != null)
{
setEnabled(true);
DecodedToken decoded = new DecodedToken(token);
if (tokenStore.validateToken(decoded.getUsername(), decoded.getValue()))
{
credentials.setUsername(decoded.getUsername());
credentials.setPassword(decoded.getValue());
}
else
{
// Have we been compromised? Just in case, invalidate all authentication tokens
tokenStore.invalidateAll(decoded.getUsername());
}
}
}
}
/**
* I hate these hacks...
*/
private class BoolWrapper
{
boolean value;
}
@Observer(Identity.EVENT_QUIET_LOGIN)
public void quietLogin()
{
final Identity identity = Identity.instance();
if (mode.equals(Mode.autoLogin) && isEnabled())
{
final String username = identity.getCredentials().getUsername();
final BoolWrapper userEnabled = new BoolWrapper();
final List<String> roles = new ArrayList<String>();
// Double check our credentials again
if (tokenStore.validateToken(username, identity.getCredentials().getPassword()))
{
new RunAsOperation(true) {
@Override
public void execute()
{
if (IdentityManager.instance().isUserEnabled(username))
{
userEnabled.value = true;
for (String role : IdentityManager.instance().getImpliedRoles(username))
{
roles.add(role);
}
}
}
}.run();
if (userEnabled.value)
{
identity.unAuthenticate();
identity.preAuthenticate();
// populate the roles
for (String role : roles)
{
identity.addRole(role);
}
// Set the principal
identity.getSubject().getPrincipals().add(new SimplePrincipal(username));
identity.postAuthenticate();
autoLoggedIn = true;
}
}
}
}
@Observer(Identity.EVENT_LOGGED_OUT)
public void loggedOut()
{
if (mode.equals(Mode.autoLogin))
{
tokenSelector.clearCookieValue();
}
}
@Observer(Identity.EVENT_POST_AUTHENTICATE)
public void postAuthenticate(Identity identity)
{
if (mode.equals(Mode.usernameOnly))
{
// Password is set to null during authentication, so we set dirty
usernameSelector.setDirty();
if ( !enabled )
{
usernameSelector.clearCookieValue();
}
else
{
usernameSelector.setCookieMaxAge(cookieMaxAge);
usernameSelector.setCookieValueIfEnabled( Identity.instance().getCredentials().getUsername() );
}
}
else if (mode.equals(Mode.autoLogin))
{
tokenSelector.setDirty();
DecodedToken decoded = new DecodedToken(tokenSelector.getCookieValue());
// Invalidate the current token (if it exists) whether enabled or not
if (decoded.getUsername() != null)
{
tokenStore.invalidateToken(decoded.getUsername(), decoded.getValue());
}
if ( !enabled )
{
tokenSelector.clearCookieValue();
}
else
{
String value = generateTokenValue();
tokenStore.createToken(identity.getPrincipal().getName(), value);
tokenSelector.setCookieEnabled(enabled);
tokenSelector.setCookieMaxAge(cookieMaxAge);
tokenSelector.setCookieValueIfEnabled(encodeToken(identity.getPrincipal().getName(), value));
}
}
}
@Observer(Credentials.EVENT_CREDENTIALS_UPDATED)
public void credentialsUpdated()
{
if (mode.equals(Mode.usernameOnly))
{
usernameSelector.setDirty();
}
}
/**
* A flag that an application can use to protect sensitive operations if the user has been
* auto-authenticated.
*/
public boolean isAutoLoggedIn()
{
return autoLoggedIn;
}
}