I'm trying to use HttpServletRequest authenticate within a JSF managed bean to implement fine-grained authentication, depending on the particular object requested.
When I call authenticate within a preRenderView event listener, if authenticate causes a redirect to the login page, an exception is thrown. I can't call responseComplete after the call to authenticate, because FacesContext.getCurrentInstance returns null. Is it possible to call authenticate() at all in JSF, or do I have to use a ServletFilter? HttpServletRequest login and logout work within JSF, so I think it's reasonable to assume authenticate should work. Is this a bug in Mojarra JSF?
Here is my code:
The page where event listener is registered:
<ui:composition template="/template.xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ice="http://www.icesoft.com/icefaces/component">
<ui:param name="pageTitle" value="Text Clustering Home Page"/>
<ui:define name="metadata">
<f:metadata>
<f:event type="preRenderView" listener="#{permissionBean.preRender}"/>
</f:metadata>
</ui:define>
<ui:define name="body">
Text Clustering Home Page
<h:form>
<h:panelGrid columns="1">
<ice:outputText rendered="#{loginService.loggedIn}" value="Logged in User: #{loginService.currentUser.username}"/>
<h:link rendered="#{!loginService.loggedIn}" value="Register" outcome="Register"/>
<h:commandLink value="Logout" rendered="#{loginService.loggedIn}" action="#{loginService.logout}"/>
<h:link value="Login" rendered="#{!loginService.loggedIn}" outcome="Login"/>
</h:panelGrid>
</h:form>
The bean that contains the listener:
#Named
#RequestScoped
public class PermissionBean implements java.io.Serializable {
public void preRender(CompenentSystemEvent event) {
HttpServletRequest request = (HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest();
HttpServletResponse response = (HttpServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse();
try {
if (!request.authenticate(response)) {
System.out.println("After authenticate, context = " +FacesContext.getCurrentInstance());
if (FacesContext.getCurrentInstance()!=null) {
FacesContext.getCurrentInstance().responseComplete();
}
}
} catch (Exception e) { // may throw ServletException or IOException
System.out.println("EXCEPTION calling authenticate");
e.printStackTrace();
}
}
}
The call to authenticate() doesn't throw an exception, but if it returns false, then FacesContext.getCurrentInstance() also returns null, and after the method exits
I get this error:
javax.enterprise.resource.webcontainer.jsf.context|_ThreadID=23;_Thread
Name=Thread-3;|Exception when handling error trying to reset the
response.
java.lang.NullPointerException
at
com.sun.faces.facelets.tag.jsf.core.DeclarativeSystemEventListener.processEvent(EventHandler.java:126 )
Thanks,
Ellen
From HttpServletRequest#authenticate() javadoc:
boolean authenticate(HttpServletResponse response)
throws java.io.IOException,
ServletException
Use the container login mechanism configured for the ServletContext to authenticate the user making this request.
This method may modify and commit the argument HttpServletResponse.
(emphasis mine)
The container's implementation will redirect the response to the configured login page when the user has not been authenticated. This has likely also caused the HttpServletRequest to be destroyed and thus the current FacesContext instance to be completely disappeared.
Your best bet is to let JSF perform the checking the presence of the logged-in user and doing the redirect instead of the container. True, this requires duplicating the login page location in the JSF side, but I don't see other ways which are workable in JSF without moving the task to a Filter.
public void preRender(CompenentSystemEvent event) {
ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext();
if (ec.getUserPrincipal() == null) {
ec.redirect(ec.getRequestContextPath() + "/login.xhtml");
}
}
The ExternalContext#redirect() will already implicitly call FacesContext#responseComplete(), so you don't need to do it yourself.
Related
I'm developing a web application with JSF 2.0. I implemented login through a managed bean (LoginHandler) and check if the user is logged in with a filter.
Now I got the request to be able to login to the application by sending a request with username and password as parameters. Which is the best way to do that?
I tried using f:metadata and a preRenderView event, but it seems a filter is the better solution? I also tried writing an HTTPFilter on url of the login page, but the problem is I need to store user data in my managed bean, and when I first access the application I don't have a session from which to get my manage bean.
Otpion 1: f:metadata and action in managedbean LoginHandler:
on login page:
<f:metadata>
<f:viewParam name="username" value="#{loginManager.username}"/>
<f:viewParam name="password" value="#{loginManager.password}"/>
<f:event type="preRenderView" listener="#{loginManager.singleSignOn}"/>
</f:metadata>
The method singleSignOn in LoginHandler checks if username and password are set. If that's the case, it does the same logic as submitting the login form (it calles the same method), and if the login is successful, it forwards to the welcome page. Otherwise it returns null (and the login page is displayed)
Otpion 2: filter:
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
String username = request.getParameter("username");
String password = request.getParameter("password");
LoginHandler loginHandler = null;
if (session != null) {
loginHandler = (LoginHandler) session.getAttribute("loginHandler");
if (loginHandler != null && username != null && password != null) {
boolean loginOk = false;
//do logic using methods and objects in loginHandler and set loginOk if login is successful
loginOk = loginHandler.login(username, password);
if (loginOk) {
// login OK
response.sendRedirect(request.getContextPath() + WELCOME_PAGE_URL);
}
}
}
chain.doFilter(request, response);
}
As said, option 2 has the problem that session isn't set the first time I access the application (i.e., i need to try to login twice - the second time everything works)
Thanks
EDIT1: updated comment and description to better reflect the role of the class LoginHandler
On when to use f:param & f:metadata , i will advice you read
this answer as a great one is provided as to such. never use f:viewParam to handle user inputs as data is sent over HTTP GET request.
It is also advised against using servlets with JSF application as
session scoped beans are attributes of HTTPSessions,you will find
an explanations as to why here.
finally there are four ways to register a system even listner in a
JSF application.
With a f:event tag as you used in your question
provided, by annotating a class with the
#ListnerFor(systemEventClass = typeOfEvent.class), by calling a
subscribeToEvent method and finally by registering a system event
listner in faces-config.xml file as so
<application>
<system-event-listener>
<system-event-listener-class>listenerClass</system-event-listener-class>
<system-event-class>eventClass</system-event-class>
</system-event-listener>
</application>
In the situation where you use the f:event tag(as you did in your question) you should make sure you enclose it in a f:view tag
<f:view>
<f:event type="preRenderView" listener="#{loginManager.singleSignOn}"/>
......
<f:viewParam name="username" value="#{loginManager.username}"/>
<f:viewParam name="password" value="#{loginManager.password}"/>
......
</f:view>
and your bean method accepting a ComponentSystemEvent as so
public void singleSignOn(ComponentSystemEvent event) {
if (!login) {
//do JSF stuff here when validating credentials fails or passes
//eg. perform navigation
}
}
NB: there is a an answer on this question that address how to implements filters with jsf,use it as head start
for additional material, I will advice you continue with Basic steps for starting session in jsf.
I went with the prerenderview event:
singleSignOn.xhtml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html">
<f:metadata>
<f:viewParam name="userName" value="#{loginCredentials.userName}" />
<f:viewParam name="password" value="#{loginCredentials.password}" />
<f:event type="preRenderView" listener="#{loginHandler.singleSignOn}" />
</f:metadata>
<h:head>
<title>singleSignOn</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</h:head>
<h:body />
</html>
LoginCredentials is a request scoped POJO with fields userName and password.
singleSignOn() in LoginHandler (part):
public void singleSignOn(ComponentSystemEvent event) {
if (loginCredentials.getUserName() != null
&& loginCredentials.getPassword() != null) {
login();
} else {
// error handling (i did output on syserr)
}
}
LoginCredentials and LoginHandler.login are the same bean/method that are bound to the h:form on the login page
I finally got messages passed between pages, but this is not redirecting me to the user login page (../../index.xhtml) instead it shows the forbidden page :
public String permission() throws IOException {
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> sessionMap = context.getExternalContext().getSessionMap();
String isLog = (String) sessionMap.get("isLogged");
if(isLog != "TRUE") {
System.out.println("*** The user has no permission to visit this page. *** ");
context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Info : ", "Loggez-vous"));
context.getExternalContext().getFlash().setKeepMessages(true);
//context.getExternalContext().redirect("../../index.xhtml");
return "../../index.xhtml?faces-redirect=true";
} else {
System.out.println("*** The session is still active. User is logged in. *** ");
}
return "../../index.xhtml?faces-redirect=true";
}
Of course, restriced page has this :
<f:event type="preRenderView" listener="#{userloginMB.permission()}"/>
Redirection using get external context will make the messages lost.
Ignoring the general design problem (look here for a starting point), it seems that you mixed up the new JSF 2.2 <f:viewAction> and the old JSF 2.0/2.1 <f:event type="preRenderView"> trick.
Returning a navigation outcome as String on a GET request is only supported in <f:viewAction>, not in <f:event type="preRenderView">. For the latter you need ExternalContext#redirect() which you happened to have outcommented.
So, you should do either
<f:event type="preRenderView" listener="#{bean.onload}"/>
public void onload() throws IOException {
// ...
ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext();
ec.redirect(ec.getRequestContextPath() + "/index.xhtml");
}
or
<f:metadata>
<f:viewAction action="#{bean.onload}"/>
</f:metadata>
public String onload() {
// ...
return "/index.xhtml"; // Note: no need for faces-redirect=true!
}
and not mix up them.
Note that I wrote the code in such way that you can always use an /path relative to the web root without the need to fiddle with ../ nonsense.
See also:
What can <f:metadata>, <f:viewParam> and <f:viewAction> be used for?
You need to specify absolute path from the root context when using faces-redirect=true.
so your outcome string should look like:
return "/dir1/dir2/index.xhtml?faces-redirect=true";
if index.xhtml resides in (context root) i.e. Web Content/index.xhtml then use this outcome string:
return "/index.xhtml?faces-redirect=true";
if index.xhtml resides in Web Content/pages/dir2/index.xhtml then use this outcome string:
return "/pages/dir2/index.xhtml?faces-redirect=true";
I have this login form:
<h:form>
<h:panelGrid columns="2" >
<h:outputLabel for="username" value="Login:"/>
<h:inputText id="username" value="#{userController.userName}" required="true"/>
<h:outputLabel for="password" value="#{msg.password}"/>
<h:inputSecret id="password" value="#{userController.password}" required="true"/>
<h:column/>
<h:commandButton value="#{msg.login}" action="#{userController.login}"/>
</h:panelGrid>
</h:form>
With this backing bean:
#ManagedBean(name = "userController")
#SessionScoped
public class UserController {
private String userName = "";
private String password = "";
//getter, setters
public String login(){
FacesContext context = FacesContext.getCurrentInstance();
HttpServletRequest request = (HttpServletRequest)context.getExternalContext().getRequest();
try {
request.login(userName, password);
} catch (ServletException e) {
}
return "next-page.xhtml"; //if login processes is proper, i redirect to next page
}
}
I read in Best practices in JSF: model, actions, getters, navigation, phaselisteners that
I always post back to the same view (return null or void and then render/include the result conditionally. For page-to-page navigation I don't use POST requests (for which navigation cases are mandatory) simply because that's plain bad for UX (User eXperience; browser back button doesn't behave as it should and URL's in browser address bar are always one step behind because it are by default forwards, not redirects) and SEO (Search Engine Optimization; searchbots doesn't index POST requests). I just use outputlinks or even plain HTML elements for page-to-page navigation.
So, what should I do when my login is proper and I want to immediately redirect to next-page.xhtml?
In the end of the try, perform the navigation with ?faces-redirect=true so that a redirect is performed. In the catch, return null so that it stays in the same page.
try {
request.login(userName, password);
return "next-page.xhtml?faces-redirect=true";
} catch (ServletException e) {
context.addMessage(null, new FacesMessage("Unknown login"));
return null;
}
For the sake of completeness, I added a faces message on login failure, otherwise the enduser would have no clue why the page seemingly reloads itself without any form of feedback. This message will be shown in a <h:messages globalOnly="true">.
See also:
Performing user authentication in Java EE / JSF using j_security_check (the 2nd half of the answer)
How to navigate in JSF? How to make URL reflect current page (and not previous one)
I have this f:viewParam to set value and do search in back bean in view:
<f:metadata>
<f:viewParam name="id"
value="#{editorBean.id}"
required="true" />
<f:event type="preRenderComponent"
listener="#{editorBean.search}" />
...
Back bean:
private String id; // getters setters
public void search(ComponentSystemEvent event) {
if (id != null) {
//search data in DB to construct TreeNode finBy(id)...
...
In browser I can't expand the second level of tree, because in backing Bean the id is null..
Debug:
How to f:viewParam be set in all calls?
It's caused because the <h:form> submits by default to an URL without the query string.
Either put the bean in the view scope,
#ManagedBean
#ViewScoped
public class EditorBean {
and skip the prerenderview during postback
public void search(ComponentSystemEvent event) {
if (FacesContext.getCurrentInstance().isPostback()) {
return;
}
// ...
}
A view scoped bean lives as long as you interact with the same view and thus the properties doesn't need to be initialized again and again.
Or make use of OmniFaces <o:form> which offers an includeViewParams attribute to include view parameters in form action URL:
<o:form includeViewParams="true">
See also:
Retain original GET request parameters across postbacks
How can I maintain param on ajax call?
What can <f:metadata>, <f:viewParam> and <f:viewAction> be used for?
I'm using JSF 2 and PrimeFaces 2.1 on GlassFish.
I have a page that is intended to allow people to perform an action after following a callback URL (e.g. as link embedded in email or as callback URL parameter of some external authentication or payment service). In my case I need to reset the password. The callback URL has a token GET parameter like so:
http://example.com/app/resetPasswordForm.jsf?token=abc123
On page load of resetPasswordForm.jsf, I need to check if the token is valid and redirect to the main app screen if it's not valid.
My thinking is to have a bean method like:
public String resetPasswordHandler.showResetForm(String token) {
if /* token is valid */ {
return "resetPasswordForm.jsf";
} else {
return "main.jsf";
}
}
But how would I cause that method to get hit on page load?
Not sure how to proceed -- suggestions are welcome.
Use <f:viewAction> to trigger a bean method before rendering of the view and simply return a navigation outcome (which will implicitly be treated as a redirect).
E.g.
<f:metadata>
<f:viewParam name="token" value="#{authenticator.token}" />
<f:viewAction action="#{authenticator.check}" />
</f:metadata>
with
#ManagedBean
#RequestScoped
public class Authenticator {
private String token;
public String check() {
return isValid(token) ? null : "main.jsf";
}
// Getter/setter.
}
If you're not on JSF 2.2 yet, then you can use the <f:event type="preRenderView"> workaround in combination with ExternalContext#redirect().
<f:metadata>
<f:viewParam name="token" value="#{authenticator.token}" />
<f:event type="preRenderView" listener="#{authenticator.check}" />
</f:metadata>
with
#ManagedBean
#RequestScoped
public class Authenticator {
private String token;
public void check() throws IOException {
if (!isValid(token)) {
FacesContext.getCurrentInstance().getExternalContext().redirect("main.jsf");
}
}
// Getter/setter.
}
See also:
Communication in JSF 2.0 - Processing GET request parameters
What can <f:metadata>, <f:viewParam> and <f:viewAction> be used for?
How do I process GET query string URL parameters in backing bean on page load?