Search This Blog

Loading...

Sunday, July 31, 2011

JSF form authentication on servlet 3 container

In the post about security realms JDBC security realm with glassfish and jsf I showed how to configure GlassFish and simple web application for a successful authentication and authorization via web form.

Servlet 3.0 containers, such as Glassfish 3, support easy programmatic login that allow more control over the login process.

A simple example could be when user authenticates and one should decide where to redirect user depending on some preconditions or if you need to fetch some data from database for authenticated user before redirecting to secured pages.

This example just builds on top of the example from the JDBC security realm with glassfish jsf.

We will change login form (login.xhtml) so that no longer has to contain mandatory fields j_username, j_password and action attribute j_security_check previously required by container. Login form is now a regular JSF form with user defined input components and login command button  with action method attached.

<h:body>  
      <p>Login to access secure pages:</p>  
      <h:messages />  
      <h:form id="loginForm">  
           <h:panelGrid columns="2">  
                <h:outputLabel for="username" value="Username:" />  
                <h:inputText id="username" value="#{authBackingBean.username}" />  
                                 
                <h:outputLabel for="password" value="Password:" />  
                <h:inputSecret id="password" value="#{authBackingBean.password}" />  
                                 
                <h:commandButton id="loginButton" value="Login" action="#{authBackingBean.login}" />  
           </h:panelGrid>  
      </h:form>  
 </h:body>  

To AuthBackingBean we just add login action method that will handle authentication and authorization. Username and password bean properties will hold user entered values after form is submited.

@ManagedBean  
 @RequestScoped  
 public class AuthBackingBean {  
        
      private static Logger log = Logger.getLogger(AuthBackingBean.class.getName());  
        
      private String username;  
      private String password;  
        
      public String login() {  
           FacesContext context = FacesContext.getCurrentInstance();  
           HttpServletRequest request = (HttpServletRequest) context  
                                              .getExternalContext().getRequest();  
             
           try {  
                request.login(username, password);  
           } catch (ServletException e) {  
                context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_WARN, "Login failed!", null));  
                return "login";  
           }  
             
           //you can fetch user from database for authenticated principal and do some action  
           Principal principal = request.getUserPrincipal();  
           log.info("Authenticated user: " + principal.getName());  
             
             
           if(request.isUserInRole("ADMIN")) {  
                return "/admins/admins?faces-redirect=true";  
           } else {  
                return "/users/users?faces-redirect=true";  
           }  
      }  
        
      public String logout() {  
           String result="/index?faces-redirect=true";  
             
           FacesContext context = FacesContext.getCurrentInstance();  
           HttpServletRequest request = (HttpServletRequest)context.getExternalContext().getRequest();  
             
           try {  
                request.logout();  
           } catch (ServletException e) {  
                log.log(Level.SEVERE, "Failed to logout user!", e);  
                result = "/loginError?faces-redirect=true";  
           }  
             
           return result;  
      }  
   
      public String getUsername() {  
           return username;  
      }  
   
      public void setUsername(String username) {  
           this.username = username;  
      }  
   
      public String getPassword() {  
           return password;  
      }  
   
      public void setPassword(String password) {  
           this.password = password;  
      }  
 }  

On Servlet 3.0 container, HttpServletRequest has login method that  will validate the provided username and password in the password validation realm used by the web  container login mechanism configured for the ServletContext.

28 comments:

  1. Thanks for the example! This helped me out in my own code.

    ReplyDelete
  2. I used this to replace my previous solution, that was a JSP form with action="j_security_check".

    It works fine but I have a question:
    In the old solution when clicking on a link in the restricted area the application continued with this page immediately after a successful login automatically.

    I tried to simulate this with a outcome of the login method built from request, but had no success, since I found only data from the login page, not the former URI.

    Any idea?

    ReplyDelete
  3. You can use javax.servlet.forward.request_uri request attribute to fetch original URI.
    Something like this:
    request.getAttribute("javax.servlet.forward.request_uri")
    I didn't try it but I think it should work.

    ReplyDelete
  4. Thanks Gordan, did not help, getAttribute returns null and when I look at all 9 attributes in debugger nothing suitable is there.

    ReplyDelete
  5. Hm, I think that when you click on restricted area link, you are forwarded to login page and at that point you should try to fetch javax.servlet.forward.request_uri attribute(when the login page is displayed) and put it in hidden field or session. And then after you submit login form you can fetch that URI attribute from session or request parameter if you used hidden field.

    ReplyDelete
  6. Gordan, Thanks on this excellent article. I've been trying the example w/o success. The only deviation from the example is I'm using JavaDB, not MySQL. Everything else is a copy from the article. I'm using GF3.1.1-build 12. Result.login always throws an exception and GF log shows only this:
    WARNING: PWC4011: Unable to set request character encoding to UTF-8 from context /LoginApp, because request parameters have already been read, or ServletRequest.getReader() has already been called
    SEVERE: jdbcrealm.invaliduserreason
    WARNING: WEB9102: Web Login Failed: com.sun.enterprise.security.auth.login.common.LoginException: Login failed: Security Exception
    WARNING: Exception

    Any suggestions on how to debug this. I know I made some mistake somewhere but, after several days, I can't find it.
    Thanks, Luiz

    ReplyDelete
  7. Hi Gordan,
    thanks to your hint I realized following solution to the forwarding problem:

    Moved the bean from @RequestScoped to @ViewScoped.
    In a @PostConstruct Method I set
    requestURI = (String) request.getAttribute("javax.servlet.forward.path_info");
    which I use directly as outcome from login method.
    This works and I could not recognize a problem till now.
    Note: The UTF8 warning from Luiz Comment above I also have in logs.
    Dieter

    ReplyDelete
  8. Here is solution to mentioned encoding warning:
    http://mpashworth.wordpress.com/2011/06/01/glassfish-3-1-pwc4011-warning-with-jsf-applications/
    Just replace former sun-web.xml with glassfish-web.xml and add parameter-encoding tag.
    The content of the xml file is the same, just the opening tag is glassfish-web-app instead of sun-web-app.
    That will fix the problem.

    Regarding that JavaDB problem - did you set correct database driver vendor for java db ?

    ReplyDelete
  9. I am having an issue finding my css when using the servlet 3 container solution. If I enter the correct username and password, subsequent pages in the same directories find the css file. I am using facelets/template. If I display the web page directly in a browser (no programming) it finds the css. If I use Firefox/Firebug it appears it can not find the css but shows my entire web page in the net/css tab. Do you know of anything that might be interferring with me finding the css other than newbie programming. Thanks for the great articles.

    ReplyDelete
  10. I am the anonymous from above. I tried removing the security info from my web.xml and using my login.xhtml (which I used as my 'form-login-page'as my welcome page and it found and used the specified css file. As a debugging measure I tried inserting request.contextPath in my template footer and with or without the security info it shows "/MyProject"

    ReplyDelete
  11. Hi!If you are using JSF2 put css file in resources folder in the top-level web application root. For example resources/css/mycssfile.css. In facelet template you include that css file with
    <h:outputStylesheet name="mycssfile.css" library="css"/>
    That should work.

    ReplyDelete
  12. Thanks for the quick response; however that is where my css file is. If I leave alone the login-config section in my web.xml but comment out the security-constraint section, it displays the login.xhtml correctly (with css style)

    ReplyDelete
  13. It's strange behavior you described. You can send me your web.xml to email and I'll take a look.

    ReplyDelete
  14. Hi!I checked your web.xml. I think the problem is that you have put your constraint on the root of application context:
    <url-pattern>/*</url-pattern>
    Browser can not fetch css files until you are logged in. If you check in firefox firebug you'll see that he is trying to fetch css from:
    http://localhost:8080/contextname/javax.faces.resource/fileName.css
    Try to put secure pages in subfolder and set url pattern in web.xml in constraint tag like:
    <url-pattern>/securepages/*</url-pattern>
    Let me know if it works:)

    ReplyDelete
  15. Gordon, it worked!!!! Thanks so much. I still not sure why "/securepages/*" works and "/*" does not. Is "/*" not a valid url pattern even tho it starts with a "/" and ends with a "*"? Why can it find something off the context root when it can't find the context root? I will investigate this further. THANKS AGAIN for all your help and getting me back on track.

    ReplyDelete
  16. If you have http://localhost:8080/contextname and pattern /* that means that everything after contextname in url is secured, and your css files are in http://localhost:8080/contextname/javax.faces.resource/ and can not be accessed. So, if you set pattern to /securepages/* that means that only urls that starts with http://localhost:8080/contextname/securepages/ are secured and http://localhost:8080/contextname/javax.faces.resource urls are accessible for everyone.

    ReplyDelete
  17. Duh. Sometimes the obvious escapes me. Maybe I need to be put in a "secure place". LOL. Thanks again.

    ReplyDelete
  18. Thanks for a wonderful tutorial. However the problem I'm having is no matter what..even if the credentials are correct it keep sending me back to loginerror page. I checked the connections, config files still no luck. Below is the error I'm having (the drivers are correctly selected)




    WARNING: WEB9102: Web Login Failed: com.sun.enterprise.security.auth.login.common.LoginException: Login failed: Security Exception
    WARNING: Exception
    com.sun.enterprise.security.auth.login.common.LoginException: Login failed: Security Exception
    at com.sun.enterprise.security.auth.login.LoginContextDriver.doPasswordLogin(LoginContextDriver.java:394)
    at com.sun.enterprise.security.auth.login.LoginContextDriver.login(LoginContextDriver.java:240)
    at com.sun.enterprise.security.auth.login.LoginContextDriver.login(LoginContextDriver.java:153)
    at com.sun.web.security.RealmAdapter.authenticate(RealmAdapter.java:512)
    at com.sun.web.security.RealmAdapter.authenticate(RealmAdapter.java:453)
    at org.apache.catalina.connector.Request.login(Request.java:1932)
    at org.apache.catalina.connector.Request.login(Request.java:1895)
    at org.apache.catalina.connector.RequestFacade.login(RequestFacade.java:1146)
    at gordan.showcase.AuthBackingBean.login(AuthBackingBean.java:34)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.sun.el.parser.AstValue.invoke(AstValue.java:234)
    at com.sun.el.MethodExpressionImpl.invoke(MethodExpressionImpl.java:297)
    at com.sun.faces.facelets.el.TagMethodExpression.invoke(TagMethodExpression.java:105)
    at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:88)
    at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:102)
    at javax.faces.component.UICommand.broadcast(UICommand.java:315)
    at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:794)
    at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:1259)
    at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:81)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
    at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:118)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:593)
    at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1539)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:281)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:655)
    at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:595)
    at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:98)
    at com.sun.enterprise.web.PESessionLockingStandardPipeline.invoke(PESessionLockingStandardPipeline.java:91)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:162)
    at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:330)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:231)
    at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:174)
    at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:828)
    at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:725)
    at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:1019)

    ReplyDelete
  19. Are you using glassfish 3.1? I had similar problem and solved it, here is explanation at the end of the article:
    http://jugojava.blogspot.com/2011/02/jdbc-security-realm-with-glassfish-and.html

    ReplyDelete
  20. Yes using glassfish 3.1.
    Thanks, Gordan. It helped.
    Keep up the great work!

    ReplyDelete
  21. Gordan,
    Do you have a similar tutorial for spring instead of JSF. If not can you guide to some good source (similar to the awesome work done here).

    ReplyDelete
  22. Thanks, You saved my day

    ReplyDelete
  23. I had code similar to yours working fine. I made lots of changes to other code in my project but I thought I kept this code the same; however now when I submit the form, log messages from the setUsername and setPassword indicate that the member variables are being set; yet when I go into my login method, and print log messages showing the values of username and password, the both say null. I would have thought this was a single call so that scope would not play a part. I have a beans.xml and my method is Serializable'. What am I missing here?

    ReplyDelete
    Replies
    1. Sorry. Imported wrong "@SessionScoped" from JSF rather than CDI. Please disregard above comment.

      Delete
  24. thank for tutorial however I have a question about request.login(username, password);

    what does this code mean? when we call request.login what is jvm doing ? can you explain it?

    ReplyDelete
  25. Hi! request.login(username, password) will validate provided username and password with authentication realm you have configured in your application container. In my example I have jdbc realm in glassfish app server configured so this will validate my data in database.

    ReplyDelete
  26. Hi Gordan, thanks for the tutorial. I have a problem with my application when trying to follow it, it does not recognize the login method of the HttpServletRequest. what could be missing?

    ReplyDelete
    Replies
    1. resolved dependencies problem

      Delete