package org.jboss.seam.ui.renderkit;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.http.HttpSession;
import org.jboss.seam.ui.RenderStampStore;
import org.jboss.seam.ui.UnauthorizedCommandException;
import org.jboss.seam.ui.component.UIToken;
import org.jboss.seam.ui.util.HTML;
import org.jboss.seam.ui.util.cdk.RendererBase;
import org.jboss.seam.util.Base64;
import org.jboss.seam.util.RandomStringUtils;
/**
* <p>
* The <strong>TokenRendererBase</strong> renders the form's signature as a
* hidden form field for the UIToken component. If the renderStampStore
* component is enabled, the actually signature will be stored in the session
* and the key to this token store in the hidden form field, providing the same
* guarantee for client-side state saving as with server-side state saving.
* </p>
*
* <p>
* The form signature is calculated as follows:
* </p>
*
* <pre>
* sha1(signature = contextPath + viewId + "," + formClientId + random alphanum, salt = clientUid)
* </pre>
*
* <p>
* The developer can also choose to incorporate the session id into this hash to
* generate a more secure token (at the cost of binding it to the session) by
* setting the requireSession attribute to true. Then the calculation becomes:
* </p>
*
* <pre>
* sha1(signature = contextPath + viewId + "," + formClientId + "," + random alphanum + sessionId, salt = clientUid)
* </pre>
*
* <p>
* The decode method performs the following steps:
* </p>
* <ol>
* <li>Check if this is a postback, otherwise skip the check</li>
* <li>Check that this form was the one that was submitted, otherwise skip the
* check</li>
* <li>Get the unique client identifier (from cookie), otherwise throw an
* exception that the browser must have unique identifier</li>
* <li>Get the javax.faces.FormSignature request parameter, otherwise throw an
* exception that the form signature is missing</li>
* <li>If the renderStampStore component is enabled, retrieve the render stamp
* from the store using the key stored in the render stamp attribute of the form.</li>
* <li>Generate the hash as before and verify that it equals the value of the
* javax.faces.FormSignature request parameter, otherwise throw an exception</li>
* </ol>
*
* <p>
* If all of that passes, we are okay to process the form (advance to validate
* phase as decode() is called in apply request values).
* </p>
*
* @author Dan Allen
* @author Stuart Douglas
* @see UnauthorizedCommandException
*/
public class TokenRendererBase extends RendererBase
{
public static final String FORM_SIGNATURE_PARAM = "javax.faces.FormSignature";
public static final String RENDER_STAMP_ATTR = "javax.faces.RenderStamp";
private static final String COOKIE_CHECK_SCRIPT_KEY = "org.jboss.seam.ui.COOKIE_CHECK_SCRIPT";
@Override
protected Class getComponentClass()
{
return UIToken.class;
}
@Override
protected void doDecode(FacesContext context, UIComponent component)
{
UIToken token = (UIToken) component;
UIForm form = token.getParentForm();
if (context.getRenderKit().getResponseStateManager().isPostback(context) && form.isSubmitted())
{
String clientToken = token.getClientUid();
String viewId = context.getViewRoot().getViewId();
if (clientToken == null)
{
throw new UnauthorizedCommandException(viewId, "No client identifier provided");
}
String requestedViewSig = context.getExternalContext().getRequestParameterMap().get(FORM_SIGNATURE_PARAM);
if (requestedViewSig == null)
{
throw new UnauthorizedCommandException(viewId, "No form signature provided");
}
if (!requestedViewSig.equals(generateViewSignature(context, form, !token.isAllowMultiplePosts(), token.isRequireSession(), clientToken)))
{
throw new UnauthorizedCommandException(viewId, "Form signature invalid");
}
RenderStampStore store = RenderStampStore.instance();
if (store != null)
{
// remove the key from the store if we are using it
store.removeStamp(String.valueOf(form.getAttributes().get(RENDER_STAMP_ATTR)));
}
form.getAttributes().remove(RENDER_STAMP_ATTR);
}
}
@Override
protected void doEncodeBegin(ResponseWriter writer, FacesContext context, UIComponent component) throws IOException
{
UIToken token = (UIToken) component;
UIForm form = token.getParentForm();
if (form == null)
{
throw new IllegalStateException("UIToken must be inside a UIForm.");
}
String renderStamp = RandomStringUtils.randomAlphanumeric(50);
RenderStampStore store = RenderStampStore.instance();
if (store != null)
{
// if the store is not null we store the key
// instead of the actual stamp; this puts the
// server in control of this value rather than
// the component tree, which is owned by the client
// when using client-side state saving
renderStamp = store.storeStamp(renderStamp);
}
writeCookieCheckScript(context, writer, token);
token.getClientUidSelector().seed();
form.getAttributes().put(RENDER_STAMP_ATTR, renderStamp);
writer.startElement(HTML.INPUT_ELEM, component);
writer.writeAttribute(HTML.TYPE_ATTR, HTML.INPUT_TYPE_HIDDEN, HTML.TYPE_ATTR);
writer.writeAttribute(HTML.NAME_ATTR, FORM_SIGNATURE_PARAM, HTML.NAME_ATTR);
writer.writeAttribute(HTML.VALUE_ATTR, generateViewSignature(context, form, !token.isAllowMultiplePosts(), token.isRequireSession(), token.getClientUidSelector().getClientUid()), HTML.VALUE_ATTR);
writer.endElement(HTML.INPUT_ELEM);
}
/**
* If the client has not already delivered us a cookie and the cookie notice is enabled, write out JavaScript that will show the user
* an alert if cookies are not enabled.
*/
private void writeCookieCheckScript(FacesContext context, ResponseWriter writer, UIToken token) throws IOException
{
if (!token.getClientUidSelector().isSet() && token.isEnableCookieNotice() && !context.getExternalContext().getRequestMap().containsKey(COOKIE_CHECK_SCRIPT_KEY)) {
writer.startElement(HTML.SCRIPT_ELEM, token);
writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", HTML.TYPE_ATTR);
writer.write("if (!document.cookie) {" +
" alert('This website uses a security measure that requires cookies to be enabled in your browser. Since you have cookies disabled, you will not be permitted to submit a form.');" +
" }");
writer.endElement(HTML.SCRIPT_ELEM);
context.getExternalContext().getRequestMap().put(COOKIE_CHECK_SCRIPT_KEY, true);
}
}
private String generateViewSignature(FacesContext context, UIForm form, boolean useRenderStamp, boolean useSessionId, String saltPhrase)
{
String rawViewSignature = context.getExternalContext().getRequestContextPath() + "," + context.getViewRoot().getViewId() + "," + form.getClientId(context);
if (useRenderStamp)
{
String renderStamp = form.getAttributes().get(RENDER_STAMP_ATTR).toString();
RenderStampStore store = RenderStampStore.instance();
if (store != null)
{
// if we are using the RenderStampStore the key to access the render
// stamp
// is stored in the view root instead of the actual render stamp
renderStamp = store.getStamp(renderStamp);
}
rawViewSignature += "," + renderStamp;
}
if (useSessionId)
{
rawViewSignature += "," + ((HttpSession) context.getExternalContext().getSession(true)).getId();
}
try
{
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(saltPhrase.getBytes());
byte[] salt = digest.digest();
digest.reset();
digest.update(rawViewSignature.getBytes());
digest.update(salt);
byte[] raw = digest.digest();
return Base64.encodeBytes(raw);
}
catch (NoSuchAlgorithmException ex)
{
ex.printStackTrace();
return null;
}
}
}