Author: shane.bryzak(a)jboss.com
Date: 2008-06-06 11:57:22 -0400 (Fri, 06 Jun 2008)
New Revision: 8340
Added:
trunk/src/main/org/jboss/seam/security/JpaTokenStore.java
trunk/src/main/org/jboss/seam/security/RememberMe.java
trunk/src/main/org/jboss/seam/security/TokenStore.java
Modified:
trunk/src/main/org/jboss/seam/security/Credentials.java
trunk/src/main/org/jboss/seam/security/FacesSecurityEvents.java
trunk/src/main/org/jboss/seam/security/Identity.java
Log:
JBSEAM-2079
Modified: trunk/src/main/org/jboss/seam/security/Credentials.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/Credentials.java 2008-06-06 15:20:22 UTC (rev
8339)
+++ trunk/src/main/org/jboss/seam/security/Credentials.java 2008-06-06 15:57:22 UTC (rev
8340)
@@ -15,7 +15,6 @@
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
-import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.LogProvider;
@@ -25,9 +24,9 @@
@Scope(SESSION)
@Install(precedence = BUILT_IN)
@BypassInterceptors
-@Startup
public class Credentials implements Serializable
{
+ public static final String EVENT_INIT_CREDENTIALS =
"org.jboss.seam.security.initCredentials";
public static final String EVENT_CREDENTIALS_UPDATED =
"org.jboss.seam.security.credentialsUpdated";
private static final LogProvider log = Logging.getLogProvider(Credentials.class);
@@ -37,8 +36,16 @@
private boolean invalid = false;
+ private boolean initialized;
+
public String getUsername()
{
+ if (!initialized && Events.exists())
+ {
+ initialized = true;
+ Events.instance().raiseEvent(EVENT_INIT_CREDENTIALS, this);
+ }
+
return username;
}
Modified: trunk/src/main/org/jboss/seam/security/FacesSecurityEvents.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/FacesSecurityEvents.java 2008-06-06 15:20:22
UTC (rev 8339)
+++ trunk/src/main/org/jboss/seam/security/FacesSecurityEvents.java 2008-06-06 15:57:22
UTC (rev 8340)
@@ -3,7 +3,6 @@
import static org.jboss.seam.ScopeType.APPLICATION;
import static org.jboss.seam.annotations.Install.BUILT_IN;
-import javax.faces.context.FacesContext;
import javax.security.auth.login.LoginException;
import org.jboss.seam.annotations.Install;
@@ -12,13 +11,12 @@
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
-import org.jboss.seam.faces.Selector;
import org.jboss.seam.international.StatusMessages;
import org.jboss.seam.international.StatusMessage.Severity;
/**
- * Produces FacesMessages for certain security events, and decouples the
- * Identity component from JSF - and also handles cookie functionality.
+ * Produces FacesMessages in response of certain security events, and helps to decouple
the
+ * Identity component from JSF.
*
* @author Shane Bryzak
*/
@@ -27,58 +25,15 @@
@Install(precedence = BUILT_IN, classDependencies =
"javax.faces.context.FacesContext")
@BypassInterceptors
@Startup
-public class FacesSecurityEvents extends Selector
+public class FacesSecurityEvents
{
- @Override
- public String getCookieName()
- {
- return "org.jboss.seam.security.username";
- }
-
- @Observer("org.jboss.seam.postCreate.org.jboss.seam.security.identity")
- public void initCredentialsFromCookie(Identity identity)
- {
- FacesContext ctx = FacesContext.getCurrentInstance();
- if (ctx != null)
- {
- setCookiePath(ctx.getExternalContext().getRequestContextPath());
- }
-
- identity.setRememberMe(isCookieEnabled());
-
- String username = getCookieValue();
- if (username!=null)
- {
- setCookieEnabled(true);
- identity.setUsername(username);
- postRememberMe(identity);
- }
-
- setDirty();
- }
-
- @Observer(Credentials.EVENT_CREDENTIALS_UPDATED)
- public void credentialsUpdated()
- {
- setDirty();
- }
-
@Observer(Identity.EVENT_POST_AUTHENTICATE)
public void postAuthenticate(Identity identity)
- {
- // Password is set to null during authentication, so we set dirty
- setDirty();
-
- if ( !identity.isRememberMe() ) clearCookieValue();
- setCookieValueIfEnabled( identity.getUsername() );
+ {
+ //org.jboss.security.saml.SSOManager.processManualLoginNotification(
+ //ServletContexts.instance().getRequest(),
identity.getPrincipal().getName());
}
- @Observer(Identity.EVENT_REMEMBER_ME)
- public void postRememberMe(Identity identity)
- {
- setCookieEnabled(identity.isRememberMe());
- }
-
@Observer(Identity.EVENT_LOGIN_FAILED)
public void addLoginFailedMessage(LoginException ex)
{
@@ -111,7 +66,7 @@
getLoginSuccessfulMessageSeverity(),
getLoginSuccessfulMessageKey(),
getLoginSuccessfulMessage(),
- Identity.instance().getUsername());
+ Identity.instance().getCredentials().getUsername());
}
@Observer(Identity.EVENT_NOT_LOGGED_IN)
Modified: trunk/src/main/org/jboss/seam/security/Identity.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/Identity.java 2008-06-06 15:20:22 UTC (rev
8339)
+++ trunk/src/main/org/jboss/seam/security/Identity.java 2008-06-06 15:57:22 UTC (rev
8340)
@@ -56,8 +56,8 @@
public static final String EVENT_PRE_AUTHENTICATE =
"org.jboss.seam.security.preAuthenticate";
public static final String EVENT_POST_AUTHENTICATE =
"org.jboss.seam.security.postAuthenticate";
public static final String EVENT_LOGGED_OUT =
"org.jboss.seam.security.loggedOut";
- public static final String EVENT_REMEMBER_ME =
"org.jboss.seam.security.rememberMe";
- public static final String EVENT_ALREADY_LOGGED_IN =
"org.jboss.seam.security.alreadyLoggedIn";
+ public static final String EVENT_ALREADY_LOGGED_IN =
"org.jboss.seam.security.alreadyLoggedIn";
+ public static final String EVENT_QUIET_LOGIN =
"org.jboss.seam.security.quietLogin";
protected static boolean securityEnabled = true;
@@ -76,7 +76,7 @@
private Principal principal;
private Subject subject;
- private boolean rememberMe;
+ private RememberMe rememberMe;
private String jaasConfigName = null;
@@ -99,7 +99,8 @@
permissionMapper = (PermissionMapper)
Component.getInstance(PermissionMapper.class);
}
- credentials = (Credentials) Component.getInstance(Credentials.class);
+ rememberMe = (RememberMe) Component.getInstance(RememberMe.class, true);
+ credentials = (Credentials) Component.getInstance(Credentials.class);
}
public static boolean isSecurityEnabled()
@@ -258,12 +259,18 @@
{
try
{
- if (credentials.isSet())
+ if (Events.exists()) Events.instance().raiseEvent(EVENT_QUIET_LOGIN, this);
+
+ // Ensure that we haven't been authenticated as a result of the
EVENT_QUIET_LOGIN event
+ if (!isLoggedIn(false))
{
- authenticate();
- if (isLoggedIn(false) && Contexts.isEventContextActive())
+ if (credentials.isSet())
{
- Contexts.getEventContext().set(SILENT_LOGIN, true);
+ authenticate();
+ if (isLoggedIn(false) && Contexts.isEventContextActive())
+ {
+ Contexts.getEventContext().set(SILENT_LOGIN, true);
+ }
}
}
}
@@ -323,7 +330,7 @@
* authenticated user. This method may be overridden by a subclass if
* different post-authentication logic should occur.
*/
- protected void postAuthenticate()
+ void postAuthenticate()
{
// Populate the working memory with the user's principals
for ( Principal p : getSubject().getPrincipals() )
@@ -630,8 +637,26 @@
public void setPassword(String password)
{
credentials.setPassword(password);
+ }
+
+ /**
+ * @see org.jboss.seam.security.RememberMe#isEnabled()
+ */
+ @Deprecated
+ public boolean isRememberMe()
+ {
+ return rememberMe != null ? rememberMe.isEnabled() : false;
}
+ /**
+ * @see org.jboss.seam.security.RememberMe#setEnabled(boolean)
+ */
+ @Deprecated
+ public void setRememberMe(boolean remember)
+ {
+ if (rememberMe != null) rememberMe.setEnabled(remember);
+ }
+
public Credentials getCredentials()
{
return credentials;
@@ -647,20 +672,6 @@
this.authenticateMethod = authMethod;
}
- public boolean isRememberMe()
- {
- return rememberMe;
- }
-
- public void setRememberMe(boolean remember)
- {
- if (this.rememberMe != remember)
- {
- this.rememberMe = remember;
- if (Events.exists()) Events.instance().raiseEvent(EVENT_REMEMBER_ME, this);
- }
- }
-
public String getJaasConfigName()
{
return jaasConfigName;
Added: trunk/src/main/org/jboss/seam/security/JpaTokenStore.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/JpaTokenStore.java (rev
0)
+++ trunk/src/main/org/jboss/seam/security/JpaTokenStore.java 2008-06-06 15:57:22 UTC (rev
8340)
@@ -0,0 +1,162 @@
+package org.jboss.seam.security;
+
+import static org.jboss.seam.ScopeType.APPLICATION;
+import static org.jboss.seam.annotations.Install.BUILT_IN;
+
+import java.io.Serializable;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.Query;
+
+import org.jboss.seam.annotations.Create;
+import org.jboss.seam.annotations.Install;
+import org.jboss.seam.annotations.Name;
+import org.jboss.seam.annotations.Scope;
+import org.jboss.seam.annotations.intercept.BypassInterceptors;
+import org.jboss.seam.annotations.security.TokenUsername;
+import org.jboss.seam.annotations.security.TokenValue;
+import org.jboss.seam.core.Expressions;
+import org.jboss.seam.core.Expressions.ValueExpression;
+import org.jboss.seam.security.management.IdentityManagementException;
+import org.jboss.seam.util.AnnotatedBeanProperty;
+
+/**
+ * A TokenStore implementation, stores tokens inside a database table.
+ *
+ * @author Shane Bryzak
+ */
+(a)Name("org.jboss.seam.security.tokenStore")
+@Install(precedence = BUILT_IN, value=false)
+@Scope(APPLICATION)
+@BypassInterceptors
+public class JpaTokenStore implements TokenStore, Serializable
+{
+ private Class tokenClass;
+
+ private ValueExpression<EntityManager> entityManager;
+
+ private AnnotatedBeanProperty<TokenUsername> tokenUsernameProperty;
+ private AnnotatedBeanProperty<TokenValue> tokenValueProperty;
+
+ @Create
+ public void create()
+ {
+ if (entityManager == null)
+ {
+ entityManager =
Expressions.instance().createValueExpression("#{entityManager}",
EntityManager.class);
+ }
+
+ tokenUsernameProperty = AnnotatedBeanProperty.scanForProperty(tokenClass,
TokenUsername.class);
+ tokenValueProperty = AnnotatedBeanProperty.scanForProperty(tokenClass,
TokenValue.class);
+
+ if (tokenUsernameProperty == null)
+ {
+ throw new IllegalStateException("Invalid tokenClass " +
tokenClass.getName() +
+ " - required annotation @TokenUsername not found on any Field or
Method.");
+ }
+
+ if (tokenValueProperty == null)
+ {
+ throw new IllegalStateException("Invalid tokenClass " +
tokenClass.getName() +
+ " - required annotation @TokenValue not found on any Field or
Method.");
+ }
+ }
+
+ public void createToken(String username, String value)
+ {
+ if (tokenClass == null)
+ {
+ throw new IllegalStateException("Could not create token, tokenClass not
set");
+ }
+
+ try
+ {
+ Object token = tokenClass.newInstance();
+
+ tokenUsernameProperty.setValue(token, username);
+ tokenValueProperty.setValue(token, value);
+
+ lookupEntityManager().persist(token);
+ }
+ catch (Exception ex)
+ {
+ if (ex instanceof IdentityManagementException)
+ {
+ throw (IdentityManagementException) ex;
+ }
+ else
+ {
+ throw new IdentityManagementException("Could not create account",
ex);
+ }
+ }
+ }
+
+ public boolean validateToken(String username, String value)
+ {
+ return lookupToken(username, value) != null;
+ }
+
+ public void invalidateToken(String username, String value)
+ {
+ Object token = lookupToken(username, value);
+ lookupEntityManager().remove(token);
+ }
+
+ public void invalidateAll(String username)
+ {
+ Query query = lookupEntityManager().createQuery(
+ "select t from " + tokenClass.getName() + " t where " +
tokenUsernameProperty.getName() +
+ " = :username")
+ .setParameter("username", username);
+
+ for (Object token : query.getResultList())
+ {
+ lookupEntityManager().remove(token);
+ }
+ }
+
+ public Object lookupToken(String username, String value)
+ {
+ try
+ {
+ Object token = lookupEntityManager().createQuery(
+ "select t from " + tokenClass.getName() + " t where " +
tokenUsernameProperty.getName() +
+ " = :username and " + tokenValueProperty.getName() + " =
:value")
+ .setParameter("username", username)
+ .setParameter("value", value)
+ .getSingleResult();
+
+ return token;
+ }
+ catch (NoResultException ex)
+ {
+ return null;
+ }
+ }
+
+ public Class getTokenClass()
+ {
+ return tokenClass;
+ }
+
+ public void setTokenClass(Class tokenClass)
+ {
+ this.tokenClass = tokenClass;
+ }
+
+ private EntityManager lookupEntityManager()
+ {
+ return entityManager.getValue();
+ }
+
+ public ValueExpression getEntityManager()
+ {
+ return entityManager;
+ }
+
+ public void setEntityManager(ValueExpression expression)
+ {
+ this.entityManager = expression;
+ }
+}
Added: trunk/src/main/org/jboss/seam/security/RememberMe.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/RememberMe.java (rev
0)
+++ trunk/src/main/org/jboss/seam/security/RememberMe.java 2008-06-06 15:57:22 UTC (rev
8340)
@@ -0,0 +1,322 @@
+package org.jboss.seam.security;
+
+import static org.jboss.seam.ScopeType.SESSION;
+import static org.jboss.seam.annotations.Install.BUILT_IN;
+
+import java.rmi.server.UID;
+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
+ */
+(a)Name("org.jboss.seam.security.rememberMe")
+@Scope(SESSION)
+@Install(precedence = BUILT_IN, classDependencies =
"javax.faces.context.FacesContext")
+@BypassInterceptors
+public class RememberMe
+{
+ 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.token";
+ }
+ }
+
+ private class DecodedToken
+ {
+ private String username;
+ private String value;
+
+ public DecodedToken(String cookieValue)
+ {
+ String decoded = new String(Base64.decode(cookieValue));
+
+ username = decoded.substring(0, decoded.indexOf(':'));
+ value = decoded.substring(decoded.indexOf(':') + 1);
+ }
+
+ public String getUsername()
+ {
+ return username;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+ }
+
+ private UsernameSelector usernameSelector;
+
+ private TokenSelector tokenSelector;
+ private TokenStore tokenStore;
+
+ private boolean enabled;
+
+ 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;
+ usernameSelector.setCookieEnabled(enabled);
+ usernameSelector.setDirty();
+ }
+ }
+
+ 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();
+ }
+ else if (mode.equals(Mode.autoLogin))
+ {
+ tokenSelector = new TokenSelector();
+
+ // 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());
+ }
+
+ @Observer(Credentials.EVENT_INIT_CREDENTIALS)
+ public void initCredentials(Credentials credentials)
+ {
+ if (mode.equals(Mode.usernameOnly))
+ {
+ FacesContext ctx = FacesContext.getCurrentInstance();
+ if (ctx != null)
+ {
+
usernameSelector.setCookiePath(ctx.getExternalContext().getRequestContextPath());
+ }
+
+ String username = usernameSelector.getCookieValue();
+ if (username!=null)
+ {
+ setEnabled(true);
+ credentials.setUsername(username);
+ }
+
+ usernameSelector.setDirty();
+ }
+ else if (mode.equals(Mode.autoLogin))
+ {
+ FacesContext ctx = FacesContext.getCurrentInstance();
+ if (ctx != null)
+ {
+
tokenSelector.setCookiePath(ctx.getExternalContext().getRequestContextPath());
+ }
+
+ String token = usernameSelector.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());
+ }
+ }
+ }
+ }
+
+ @Observer(Identity.EVENT_QUIET_LOGIN)
+ public void quietLogin(Identity identity)
+ {
+ if (mode.equals(Mode.autoLogin) && isEnabled())
+ {
+ // Double check our credentials again
+ if (tokenStore.validateToken(identity.getCredentials().getUsername(),
+ identity.getCredentials().getPassword()))
+ {
+ // Success, authenticate the user
+ identity.getSubject().getPrincipals().add(new SimplePrincipal(
+ identity.getCredentials().getUsername()));
+ // And populate the roles
+ for (String role : IdentityManager.instance().getImpliedRoles(
+ identity.getCredentials().getUsername()))
+ {
+ identity.addRole(role);
+ }
+
+ identity.postAuthenticate();
+
+ autoLoggedIn = true;
+ }
+ }
+ }
+
+ @Observer(Identity.EVENT_LOGGED_OUT)
+ public void loggedOut()
+ {
+ if (mode.equals(Mode.autoLogin))
+ {
+ tokenSelector.getCookieValue();
+ }
+ }
+
+ @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();
+ usernameSelector.setCookieValueIfEnabled(
Identity.instance().getCredentials().getUsername() );
+ }
+ else if (mode.equals(Mode.autoLogin))
+ {
+ tokenSelector.setDirty();
+
+ DecodedToken decoded = new DecodedToken(tokenSelector.getCookieValue());
+
+ // Invalidate the current token whether enabled or not
+ tokenStore.invalidateToken(decoded.getUsername(), decoded.getValue());
+
+ if ( !enabled )
+ {
+ tokenSelector.clearCookieValue();
+ }
+ else
+ {
+ String value = generateTokenValue();
+ tokenStore.createToken(decoded.getUsername(), value);
+ tokenSelector.setCookieValueIfEnabled(encodeToken(decoded.getUsername(),
value));
+ }
+ }
+ }
+
+ @Observer(Credentials.EVENT_CREDENTIALS_UPDATED)
+ public void credentialsUpdated()
+ {
+ 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;
+ }
+}
\ No newline at end of file
Added: trunk/src/main/org/jboss/seam/security/TokenStore.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/TokenStore.java (rev
0)
+++ trunk/src/main/org/jboss/seam/security/TokenStore.java 2008-06-06 15:57:22 UTC (rev
8340)
@@ -0,0 +1,15 @@
+package org.jboss.seam.security;
+
+/**
+ * A store containing user authentication tokens. Used in conjunction with the
RememberMe
+ * component to auto-login users that present a valid cookie-based token.
+ *
+ * @author Shane Bryzak
+ */
+public interface TokenStore
+{
+ void createToken(String username, String value);
+ boolean validateToken(String username, String value);
+ void invalidateToken(String username, String value);
+ void invalidateAll(String username);
+}