rounded out quickstart sample .war and implemented initial form-based AuthenticationWebInterceptor

git-svn-id: https://svn.apache.org/repos/asf/incubator/jsecurity/trunk@710810 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/samples/quickstart/WEB-INF/classes/log4j.properties b/samples/quickstart/WEB-INF/classes/log4j.properties
index 65abb3a..1f064c9 100644
--- a/samples/quickstart/WEB-INF/classes/log4j.properties
+++ b/samples/quickstart/WEB-INF/classes/log4j.properties
@@ -1,5 +1,5 @@
 # This file is used to format all logging output
-log4j.rootLogger=TRACE, stdout
+log4j.rootLogger=DEBUG, stdout
 
 log4j.appender.stdout=org.apache.log4j.ConsoleAppender
 log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
@@ -22,7 +22,8 @@
 # JSecurity
 # =============================================================================
 # JSecurity security framework
-log4j.logger.org.jsecurity=TRACE
+log4j.logger.org.jsecurity.realm.text.PropertiesRealm=INFO
 log4j.logger.org.jsecurity.cache.ehcache.EhCache=INFO
-log4j.logger.org.jsecurity.web.servlet=DEBUG
+log4j.logger.org.jsecurity.web.servlet.WebInterceptorFilter=INFO
+log4j.logger.org.jsecurity.web.WebSecurityManager=INFO
 log4j.logger.org.jsecurity.util.ThreadContext=INFO
\ No newline at end of file
diff --git a/samples/quickstart/WEB-INF/web.xml b/samples/quickstart/WEB-INF/web.xml
index de10557..4e48ee5 100644
--- a/samples/quickstart/WEB-INF/web.xml
+++ b/samples/quickstart/WEB-INF/web.xml
@@ -11,16 +11,16 @@
         <init-param><param-name>config</param-name><param-value>
 
             # The JSecurityFilter configuration is very powerful and flexible, while still remaining succinct.
-            # Please read the comprehensive example, with full comments and explaination, in the JavaDoc:
+            # Please read the comprehensive example, with full comments and explanations, in the JavaDoc:
             #
             # http://www.jsecurity.org/api/org/jsecurity/web/servlet/JSecurityFilter.html
-            
-            [main]
-            [interceptors]
 
+            [interceptors]
+            authc.successUrl = /index.jsp
+            
             [urls]
-            /account/** = authcBasic
-            /remoting/** = authcBasic, roles[b2bClient], perms[remote:invoke:"lan,wan"]
+            /account/** = authc
+            /remoting/** = authc, roles[b2bClient], perms[remote:invoke:"lan,wan"]
 
         </param-value></init-param>
     </filter>
diff --git a/samples/quickstart/account/index.jsp b/samples/quickstart/account/index.jsp
index 1c74127..3a8eedd 100644
--- a/samples/quickstart/account/index.jsp
+++ b/samples/quickstart/account/index.jsp
@@ -8,11 +8,11 @@
 
   <h2>Users only</h2>
 
-  <p>You have successfully logged in.</p>
+  <p>You are currently logged in.</p>
 
   <p><a href="<c:url value="/home.jsp"/>">Return to the home page.</a></p>
 
-  <p><a href="<c:url value="/logoutjsp"/>">Log out.</a></p>
+  <p><a href="<c:url value="/logout.jsp"/>">Log out.</a></p>
 
 </body>
 </html>
\ No newline at end of file
diff --git a/samples/quickstart/home.jsp b/samples/quickstart/home.jsp
index ddb3618..91cccab 100644
--- a/samples/quickstart/home.jsp
+++ b/samples/quickstart/home.jsp
@@ -10,12 +10,16 @@
 
   <p>Hi <jsec:guest>Guest</jsec:guest><jsec:user><jsec:principal/></jsec:user>!
       ( <jsec:user><a href="<c:url value="/logout.jsp"/>">Log out</a></jsec:user>
-        <jsec:guest><a href="<c:url value="/account/"/>">Log in</a></jsec:guest> )
+        <jsec:guest><a href="<c:url value="/login.jsp"/>">Log in</a> (sample accounts provided)</jsec:guest> )
   </p>
 
   <p>Welcome to the JSecurity Quickstart sample application.
       This page represents the home page of any web application.</p>
 
+  <jsec:user><p>Visit your <a href="<c:url value="/account"/>">account page</a>.</p></jsec:user>
+  <jsec:guest><p>If you want to access the user-only <a href="<c:url value="/account"/>">account page</a>,
+      you will need to log-in first.</p></jsec:guest>
+
   <h2>Roles</h2>
   
   <p>To show some taglibs, here are the roles you have and don't have.  Log out and log back in under different user
@@ -24,7 +28,6 @@
   <h3>Roles you have</h3>
 
   <p>
-      <jsec:hasRole name="guest">guest<br/></jsec:hasRole>
       <jsec:hasRole name="root">root<br/></jsec:hasRole>
       <jsec:hasRole name="president">president<br/></jsec:hasRole>
       <jsec:hasRole name="darklord">darklord<br/></jsec:hasRole>
@@ -35,7 +38,6 @@
   <h3>Roles you DON'T have</h3>
 
   <p>
-      <jsec:lacksRole name="guest">guest<br/></jsec:lacksRole>
       <jsec:lacksRole name="root">root<br/></jsec:lacksRole>
       <jsec:lacksRole name="president">president<br/></jsec:lacksRole>
       <jsec:lacksRole name="darklord">darklord<br/></jsec:lacksRole>
diff --git a/samples/quickstart/login.jsp b/samples/quickstart/login.jsp
new file mode 100644
index 0000000..0a0b824
--- /dev/null
+++ b/samples/quickstart/login.jsp
@@ -0,0 +1,90 @@
+<%@ include file="include.jsp" %>
+
+<html>
+<head>
+    <link type="text/css" rel="stylesheet" href="<c:url value="/style.css"/>"/>
+</head>
+<body>
+
+<h2>Please Log in</h2>
+
+<jsec:guest>
+    <p>Here are a few sample accounts to play with in the default text-based Realm (used for this
+        demo and test installs only). Do you remember the movie these names came from? ;)</p>
+
+
+    <style type="text/css">
+    table.sample {
+        border-width: 1px;
+        border-style: outset;
+        border-color: blue;
+        border-collapse: separate;
+        background-color: rgb(255, 255, 240);
+    }
+    table.sample th {
+        border-width: 1px;
+        padding: 1px;
+        border-style: none;
+        border-color: blue;
+        background-color: rgb(255, 255, 240);
+    }
+    table.sample td {
+        border-width: 1px;
+        padding: 1px;
+        border-style: none;
+        border-color: blue;
+        background-color: rgb(255, 255, 240);
+    }
+    </style>
+
+
+    <table class="sample">
+        <thead>
+            <tr>
+                <th>Username</th>
+                <th>Password</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>root</td>
+                <td>secret</td>
+            </tr>
+            <tr>
+                <td>presidentskroob</td>
+                <td>12345</td>
+            </tr>
+            <tr>
+                <td>darkhelmet</td>
+                <td>ludicrousspeed</td>
+            </tr>
+            <tr>
+                <td>lonestarr</td>
+                <td>vespa</td>
+            </tr>
+        </tbody>
+    </table>
+    <br/><br/>
+</jsec:guest>
+
+<form action="" method="post">
+    <table align="left" border="0" cellspacing="0" cellpadding="3">
+        <tr>
+            <td>Username:</td>
+            <td><input type="text" name="username" maxlength="30"></td>
+        </tr>
+        <tr>
+            <td>Password:</td>
+            <td><input type="password" name="password" maxlength="30"></td>
+        </tr>
+        <tr>
+            <td colspan="2" align="left"><input type="checkbox" name="rememberMe"><font size="2">Remember Me</font></td>
+        </tr>
+        <tr>
+            <td colspan="2" align="right"><input type="submit" name="submit" value="Login"></td>
+        </tr>
+    </table>
+</form>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/src/org/jsecurity/realm/AuthorizingRealm.java b/src/org/jsecurity/realm/AuthorizingRealm.java
index f13a3e4..0b7b738 100644
--- a/src/org/jsecurity/realm/AuthorizingRealm.java
+++ b/src/org/jsecurity/realm/AuthorizingRealm.java
@@ -213,8 +213,8 @@
 
         Account account = null;
 
-        if (log.isDebugEnabled()) {
-            log.debug("Retrieving Account for principal [" + principal + "]");
+        if (log.isTraceEnabled()) {
+            log.trace("Retrieving Account for principal [" + principal + "]");
         }
 
         Cache accountCache = getAccountCache();
diff --git a/src/org/jsecurity/web/interceptor/DefaultInterceptorBuilder.java b/src/org/jsecurity/web/interceptor/DefaultInterceptorBuilder.java
index 7958630..d9c7c90 100644
--- a/src/org/jsecurity/web/interceptor/DefaultInterceptorBuilder.java
+++ b/src/org/jsecurity/web/interceptor/DefaultInterceptorBuilder.java
@@ -20,6 +20,7 @@
 import org.apache.commons.logging.LogFactory;
 import org.jsecurity.util.ClassUtils;
 import org.jsecurity.web.interceptor.authc.BasicHttpAuthenticationWebInterceptor;
+import org.jsecurity.web.interceptor.authc.FormAuthenticationWebInterceptor;
 import org.jsecurity.web.interceptor.authz.PermissionsAuthorizationWebInterceptor;
 import org.jsecurity.web.interceptor.authz.RolesAuthorizationWebInterceptor;
 
@@ -37,6 +38,7 @@
 
     public Map<String, Object> buildDefaultInterceptors() {
         Map<String, Object> interceptors = new LinkedHashMap<String, Object>();
+        interceptors.put("authc", new FormAuthenticationWebInterceptor());
         interceptors.put("authcBasic", new BasicHttpAuthenticationWebInterceptor());
         interceptors.put("roles", new RolesAuthorizationWebInterceptor());
         interceptors.put("perms", new PermissionsAuthorizationWebInterceptor());
diff --git a/src/org/jsecurity/web/interceptor/authc/AuthenticationWebInterceptor.java b/src/org/jsecurity/web/interceptor/authc/AuthenticationWebInterceptor.java
index 177065c..9cbcc24 100644
--- a/src/org/jsecurity/web/interceptor/authc/AuthenticationWebInterceptor.java
+++ b/src/org/jsecurity/web/interceptor/authc/AuthenticationWebInterceptor.java
@@ -61,5 +61,5 @@
      * @return true if the request should continue to be processed; false if the subclass will handle/render 
      * the response directly.
      */
-    protected abstract boolean onUnauthenticatedRequest(ServletRequest request, ServletResponse response);
+    protected abstract boolean onUnauthenticatedRequest(ServletRequest request, ServletResponse response) throws Exception;
 }
diff --git a/src/org/jsecurity/web/interceptor/authc/FormAuthenticationWebInterceptor.java b/src/org/jsecurity/web/interceptor/authc/FormAuthenticationWebInterceptor.java
new file mode 100644
index 0000000..44d2fee
--- /dev/null
+++ b/src/org/jsecurity/web/interceptor/authc/FormAuthenticationWebInterceptor.java
@@ -0,0 +1,174 @@
+package org.jsecurity.web.interceptor.authc;
+
+import org.jsecurity.JSecurityException;
+import org.jsecurity.authc.AuthenticationException;
+import org.jsecurity.authc.UsernamePasswordToken;
+import org.jsecurity.util.StringUtils;
+import org.jsecurity.web.RedirectView;
+import org.jsecurity.web.WebUtils;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * @author Les Hazlewood
+ * @since 0.9
+ */
+public class FormAuthenticationWebInterceptor extends AuthenticationWebInterceptor {
+
+    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = FormAuthenticationWebInterceptor.class.getName() + "_AUTHC_FAILURE_KEY";
+
+    public static final String DEFAULT_LOGIN_URL = "/login.jsp";
+    public static final String DEFAULT_USERNAME_PARAM = "username";
+    public static final String DEFAULT_PASSWORD_PARAM = "password";
+    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";
+
+    private String usernameParam = DEFAULT_USERNAME_PARAM;
+    private String passwordParam = DEFAULT_PASSWORD_PARAM;
+    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;
+
+    private String successUrl = DEFAULT_LOGIN_URL;
+    private String failureKeyAtribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;
+
+    public FormAuthenticationWebInterceptor() {
+        setUrl(DEFAULT_LOGIN_URL);
+    }
+
+    public String getUsernameParam() {
+        return usernameParam;
+    }
+
+    public void setUsernameParam(String usernameParam) {
+        this.usernameParam = usernameParam;
+    }
+
+    public String getPasswordParam() {
+        return passwordParam;
+    }
+
+    public void setPasswordParam(String passwordParam) {
+        this.passwordParam = passwordParam;
+    }
+
+    public String getRememberMeParam() {
+        return rememberMeParam;
+    }
+
+    public void setRememberMeParam(String rememberMeParam) {
+        this.rememberMeParam = rememberMeParam;
+    }
+
+    public String getSuccessUrl() {
+        return successUrl;
+    }
+
+    public void setSuccessUrl(String successUrl) {
+        this.successUrl = successUrl;
+    }
+
+    public String getFailureKeyAtribute() {
+        return failureKeyAtribute;
+    }
+
+    public void setFailureKeyAtribute(String failureKeyAtribute) {
+        this.failureKeyAtribute = failureKeyAtribute;
+    }
+
+    public void init() throws JSecurityException {
+        if ( log.isTraceEnabled() ) {
+            log.trace("Adding default login url to applied paths." );
+        }
+        this.appliedPaths.put(getUrl(),null);
+    }
+
+    protected boolean onUnauthenticatedRequest(ServletRequest request, ServletResponse response) throws Exception {
+        if ( isLoginRequest(request,response) ) {
+            if ( isLoginSubmission(request,response)) {
+                if ( log.isTraceEnabled() ) {
+                    log.trace("Login submission detected.  Attempting to execute login." );
+                }
+                return executeLogin(request, response);   
+            } else {
+                if ( log.isTraceEnabled() ) {
+                    log.trace("Login page view.");
+                }
+                //allow them to see the login page ;)
+                return true;
+            }
+        } else {
+            if ( log.isTraceEnabled() ) {
+                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
+                        "Authentication url [" + getUrl() + "]" );
+            }
+            issueRedirect(request,response);
+            return false;
+        }
+    }
+
+    protected boolean isLoginSubmission(ServletRequest servletRequest, ServletResponse response ) {
+        return toHttp(servletRequest).getMethod().equalsIgnoreCase("POST");
+    }
+
+    protected boolean isLoginRequest(ServletRequest servletRequest, ServletResponse response ) {
+        HttpServletRequest request = toHttp(servletRequest);
+        String requestURI = WebUtils.getPathWithinApplication(request);
+        return pathMatcher.match( getUrl(), requestURI );
+    }
+
+    protected boolean executeLogin(ServletRequest request, ServletResponse response ) throws Exception {
+        String username = getUsername(request,response);
+        String password = getPassword(request,response);
+        boolean rememberMe = isRememberMe(request,response);
+        InetAddress inet = getInetAddress(request,response);
+        UsernamePasswordToken token = new UsernamePasswordToken(username, password.toCharArray(), rememberMe, inet );
+
+        try {
+            getSubject(request,response).login(token);
+            issueSuccessRedirect(request,response);
+            return false;
+        } catch (AuthenticationException e) {
+            String className = e.getClass().getName();
+            request.setAttribute(getFailureKeyAtribute(), className );
+            //login failed, let request continue back to the login page:
+            return true;
+        }
+    }
+
+    protected void issueSuccessRedirect( ServletRequest request, ServletResponse response ) throws Exception {
+        RedirectView view = new RedirectView( getSuccessUrl(), isContextRelative(), isHttp10Compatible() );
+        view.renderMergedOutputModel(getQueryParams(), toHttp(request), toHttp(response) );
+    }
+
+    protected String getUsername( ServletRequest request, ServletResponse response ) {
+        return StringUtils.clean(request.getParameter(getUsernameParam()));
+    }
+
+    protected String getPassword( ServletRequest request, ServletResponse response ) {
+        return StringUtils.clean(request.getParameter(getPasswordParam()));
+    }
+
+    protected boolean isRememberMe( ServletRequest request, ServletResponse response ) {
+        String rememberMe = StringUtils.clean(request.getParameter(getRememberMeParam()));
+        return rememberMe != null &&
+                (rememberMe.equalsIgnoreCase("true") ||
+                 rememberMe.equalsIgnoreCase("1") ||
+                 rememberMe.equalsIgnoreCase("y") || 
+                 rememberMe.equalsIgnoreCase("yes" ) );
+    }
+
+    protected InetAddress getInetAddress( ServletRequest request, ServletResponse response ) {
+        if ( request instanceof HttpServletRequest ) {
+            try {
+                return InetAddress.getByName( toHttp(request).getRemoteAddr() );
+            } catch (UnknownHostException e) {
+                if ( log.isTraceEnabled() ) {
+                    log.trace( "Unable to acquire host for HttpServlet request.", e );
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/org/jsecurity/web/servlet/JSecurityFilter.java b/src/org/jsecurity/web/servlet/JSecurityFilter.java
index 3361ffc..21df4a1 100644
--- a/src/org/jsecurity/web/servlet/JSecurityFilter.java
+++ b/src/org/jsecurity/web/servlet/JSecurityFilter.java
@@ -74,6 +74,19 @@
    # interceptor's JavaDoc to fully understand what each does and how it works as well as how it would
    # affect the user experience.
    #
+   # Form Authentication interceptor: requires the requestiing user to be authenticated for the request to continue
+   # and if they are not, forces the user to login via a login page that you specify.  If the login attempt fails
+   # the AuthenticationException fully qualified class name will be placed as a request attribute under the
+   # 'failureKeyAttribute' name below.  This FQCN can then be used as an i18n key or lookup mechanism that can then
+   # be used to show the user why their login attempt failed (e.g. no account, incorrect password, etc).
+   #authc = org.jsecurity.web.interceptor.authc.FormAuthenticationWebInterceptor
+   #authc.url = /login.jsp
+   #authc.usernameParam = username
+   #authc.passwordParam = password
+   #authc.rememberMeParam = rememberMe
+   #authc.successUrl = /login.jsp
+   #authc.failureKeyAttribute = org.jsecurity.web.interceptor.authc.FormAuthenticationWebInterceptor_AUTHC_FAILURE_KEY
+   #
    # Http BASIC Authentication interceptor: requires the requesting user to be authenticated for the request
    # to continue, and if they're not, forces the user to login via the HTTP Basic protocol-specific challenge.
    # Upon successful login, they're allowed to continue on to the requested resource/url.
diff --git a/src/org/jsecurity/web/servlet/WebInterceptorFilter.java b/src/org/jsecurity/web/servlet/WebInterceptorFilter.java
index 56503a5..73f8ce9 100644
--- a/src/org/jsecurity/web/servlet/WebInterceptorFilter.java
+++ b/src/org/jsecurity/web/servlet/WebInterceptorFilter.java
@@ -104,6 +104,7 @@
         if (interceptor == null) {
             throw new IllegalStateException("WebInterceptor property must be set.");
         }
+        LifecycleUtils.init(interceptor);
     }
 
     public void destroy() {