On most websites, when the user is about to provide the username and password to log into the system, there's a checkbox like "Stay logged in". If you check the box, it will keep you logged in across all sessions from the same web browser. How can I implement the same in Java EE?
I'm using FORM based container managed authentication with a JSF login page.
<security-constraint>
<display-name>Student</display-name>
<web-resource-collection>
<web-resource-name>CentralFeed</web-resource-name>
<description/>
<url-pattern>/CentralFeed.jsf</url-pattern>
</web-resource-collection>
<auth-constraint>
<description/>
<role-name>STUDENT</role-name>
<role-name>ADMINISTRATOR</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>jdbc-realm-scholar</realm-name>
<form-login-config>
<form-login-page>/index.jsf</form-login-page>
<form-error-page>/LoginError.jsf</form-error-page>
</form-login-config>
</login-config>
<security-role>
<description>Admin who has ultimate power over everything</description>
<role-name>ADMINISTRATOR</role-name>
</security-role>
<security-role>
<description>Participants of the social networking Bridgeye.com</description>
<role-name>STUDENT</role-name>
</security-role>
Java EE 8 and up
If you're on Java EE 8 or newer, put #RememberMe on a custom HttpAuthenticationMechanism along with a RememberMeIdentityStore.
#ApplicationScoped
#AutoApplySession
#RememberMe
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
#Inject
private IdentityStore identityStore;
#Override
public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
Credential credential = context.getAuthParameters().getCredential();
if (credential != null) {
return context.notifyContainerAboutLogin(identityStore.validate(credential));
}
else {
return context.doNothing();
}
}
}
public class CustomIdentityStore implements RememberMeIdentityStore {
#Inject
private UserService userService; // This is your own EJB.
#Inject
private LoginTokenService loginTokenService; // This is your own EJB.
#Override
public CredentialValidationResult validate(RememberMeCredential credential) {
Optional<User> user = userService.findByLoginToken(credential.getToken());
if (user.isPresent()) {
return new CredentialValidationResult(new CallerPrincipal(user.getEmail()));
}
else {
return CredentialValidationResult.INVALID_RESULT;
}
}
#Override
public String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups) {
return loginTokenService.generateLoginToken(callerPrincipal.getName());
}
#Override
public void removeLoginToken(String token) {
loginTokenService.removeLoginToken(token);
}
}
You can find a real world example in the Java EE Kickoff Application.
Java EE 6/7
If you're on Java EE 6 or 7, homegrow a long-living cookie to track the unique client and use the Servlet 3.0 API provided programmatic login HttpServletRequest#login() when the user is not logged-in but the cookie is present.
This is the easiest to achieve if you create another DB table with a java.util.UUID value as PK and the ID of the user in question as FK.
Assume the following login form:
<form action="login" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="checkbox" name="remember" value="true" />
<input type="submit" />
</form>
And the following in doPost() method of a Servlet which is mapped on /login:
String username = request.getParameter("username");
String password = hash(request.getParameter("password"));
boolean remember = "true".equals(request.getParameter("remember"));
User user = userService.find(username, password);
if (user != null) {
request.login(user.getUsername(), user.getPassword()); // Password should already be the hashed variant.
request.getSession().setAttribute("user", user);
if (remember) {
String uuid = UUID.randomUUID().toString();
rememberMeService.save(uuid, user);
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE);
} else {
rememberMeService.delete(user);
removeCookie(response, COOKIE_NAME);
}
}
(the COOKIE_NAME should be the unique cookie name, e.g. "remember" and the COOKIE_AGE should be the age in seconds, e.g. 2592000 for 30 days)
Here's how the doFilter() method of a Filter which is mapped on restricted pages could look like:
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
User user = request.getSession().getAttribute("user");
if (user == null) {
String uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
user = rememberMeService.find(uuid);
if (user != null) {
request.login(user.getUsername(), user.getPassword());
request.getSession().setAttribute("user", user); // Login.
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
} else {
removeCookie(response, COOKIE_NAME);
}
}
}
if (user == null) {
response.sendRedirect("login");
} else {
chain.doFilter(req, res);
}
In combination with those cookie helper methods (too bad they are missing in Servlet API):
public static String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void removeCookie(HttpServletResponse response, String name) {
addCookie(response, name, null, 0);
}
Although the UUID is extremely hard to brute-force, you could provide the user an option to lock the "remember" option to user's IP address (request.getRemoteAddr()) and store/compare it in the database as well. This makes it a tad more robust. Also, having an "expiration date" stored in the database would be useful.
It's also a good practice to replace the UUID value whenever the user has changed its password.
Java EE 5 or below
Please, upgrade.
Normally this is done like this:
When you log in a user you also set a cookie on the client ( and store the cookie value in the database ) expiring after a certain time (1-2 weeks usually).
When a new request comes in you check that the certain cookie exists and if so look into the database to see if it matches a certain account. If it matches you will then "loosely" log in that account. When i say loosely i mean you only let that session read some info and not write information. You will need to request the password in order to allow the write options.
This is all that is. The trick is to make sure that a "loosely" login is not able to do a lot of harm to the client. This will somewhat protect the user from someone who grabs his remember me cookie and tries to log in as him.
You cannot login a user completely via HttpServletRequest.login(username, password) since you shouldn't keep both username and plain text password in the database. Also you cannot perform this login with a password hash which is saved in the database. However, you need to identify a user with a cookie/DB token but log him/her in without entering password using custom login module (Java class) based on Glassfish server API.
See the following links for more details:
http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/
Custom Security mechanism in Java EE 6/7 application
Although the answer by BalusC (the part for Java EE 6/7) gives useful hints, I doesn't work in modern containers, because you can't map a login filter to pages that are protected in a standard way (as confirmed in the comments).
If for some reason you can't use Spring Security (which re-implements the Servlet Security in an incompatible way), then it's better to stay with <auth-method>FORM and put all the logic into an active login page.
Here's the code (the full project is here: https://github.com/basinilya/rememberme )
web.xml:
<form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/login.jsp?error=1</form-error-page>
</form-login-config>
login.jsp:
if ("1".equals(request.getParameter("error"))) {
request.setAttribute("login_error", true);
} else {
// The initial render of the login page
String uuid;
String username;
// Form fields have priority over the persistent cookie
username = request.getParameter("j_username");
if (!isBlank(username)) {
String password = request.getParameter("j_password");
// set the cookie even though login may fail
// Will delete it later
if ("on".equals(request.getParameter("remember_me"))) {
uuid = UUID.randomUUID().toString();
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
Map.Entry<String,String> creds =
new AbstractMap.SimpleEntry<String,String>(username,password);
rememberMeServiceSave(request, uuid, creds);
}
if (jSecurityCheck(request, response, username, password)) {
return;
}
request.setAttribute("login_error", true);
}
uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
Map.Entry<String,String> creds = rememberMeServiceFind(request, uuid);
if (creds != null) {
username = creds.getKey();
String password = creds.getValue();
if (jSecurityCheck(request, response, username, password)) {
return; // going to redirect here again if login error
}
request.setAttribute("login_error", true);
}
}
}
// login failed
removeCookie(response, COOKIE_NAME);
// continue rendering the login page...
Here's some explanation:
Instead of calling request.login() we establish a new TCP connection to our HTTP listener and post the login form to the /j_security_check address. This allows the container to redirect us to the initially requested web page and restore the POST data (if any). Trying to obtain this info from a session attribute or RequestDispatcher.FORWARD_SERVLET_PATH would be container-specific.
We don't use a servlet filter for automatic login, because containers forward/redirect to the login page BEFORE the filter is reached.
The dynamic login page does all the job, including:
actually rendering the login form
accepting the filled form
calling /j_security_check under the hood
displaying login errors
automatic login
redirecting back to the initially requested page
To implement the "Stay Logged In" feature we save the credentials from the submitted login form in the servlet context attribute (for now). Unlike in the SO answer above, the password is not hashed, because only certain setups accept that (Glassfish with a jdbc realm). The persistent cookie is associated with the credentials.
The flow is the following:
Get forwarded/redirected to the login form
If we're served as the <form-error-page> then render the form and the error message
Otherwise, if some credentials are submitted, then store them and call /j_security_check and redirect to the outcome (which might be us again)
Otherwise, if the cookie is found, then retrieve the associated credentials and continue with /j_security_check
If none of the above, then render the login form without the error message
The code for /j_security_check sends a POST request using the current JSESSIONID cookie and the credentials either from the real form or associated with the persistent cookie.
I'm testing a simple log in with httpsession, so after i authenticate the user i add a user attribute to the http session :
#ManagedBean
#SessionScoped
public class loginView {
....
public String connect() {
FacesContext context = FacesContext.getCurrentInstance();
if (authenticated) {
context.getExternalContext().getSessionMap().put("user", login);
return "/home/NewFile?faces-redirect=true";
} else {
context.addMessage(null, new FacesMessage("Unknown login, try again"));
login = "";
pwd = "";
return null;
}
}
}
When i call this function from the login view it redirects to NewFile.xhtml as it's supposed to do. And inside the said xhtml i display the "user" attribute using #{user}. So far everything is working fine but when i refresh the page (NewFile.xhtml) or when i redirect to another page and try to display "user" attribute i get null, is this behavior expected ? does refreshing or redirecting creates another httpsession ? or is it just deleting the attribute i added ?
After some research i succeeded in solving my problem, turns out it's just a stupid mistake of my part. so i thought i should leave the answer here instead of deleting the question. So here goes :
After some looking around I have found out that this had to do with the cookies, so i did track the HTTP traffic using chrome's F12 and and it was the server sending new cookies each time I refresh/navigate. And after some more searching and testing i've found out what was causing the session to invalidate, so i was calling a logout function (that invalidates the session) this way : <h:button outcome="view.logout()"/> turns out outcome executes the function before loading the page so i had to change it to <p:commandButton action="view.logout()"/>
AutoLogin. The following exception occurs when trying to redirect to my custom portlet when user does not exist.
java.lang.NullPointerException
at com.liferay.portlet.PortletURLImpl.generateToString(PortletURLImpl.java:850)
at com.liferay.portlet.PortletURLImpl$ToStringPrivilegedAction.run(PortletURLImpl.java:1501)
at com.liferay.portlet.PortletURLImpl$ToStringPrivilegedAction.run(PortletURLImpl.java:1)
at com.liferay.portal.security.lang.DoPrivilegedUtil$NoPACL.wrap(DoPrivilegedUtil.java:64)
at com.liferay.portal.security.lang.DoPrivilegedUtil.wrap(DoPrivilegedUtil.java:26)
at com.liferay.portlet.PortletURLImpl.toString(PortletURLImpl.java:740)
when trying to call this
PortletURL portletURL = getRenderURL(request, somePortletName, somePlid);
response.sendRedirect(portletURL.toString());
public static PortletURL getRenderURL(HttpServletRequest request, String portletId, long plid)
{
return PortletURLFactoryUtil.create(
request,
portletId,
plid,
PortletRequest.RENDER_PHASE);
}
Is it forbidden to call sendRedirect in autologin or can You suggest me a workarround? story: after singleSignOn, if the user does not exists in liferay, it redirects him to my custom registration form and pre-fill retrieved known data.
Is there a way to avoid form based login in servlet application which uses acegi security module? I want to pass somehow login and password to URL. I tried to call something like this:
GrantedAuthority[] grantedAuthorities = new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ADMIN")};
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("admin", "passwordofadmin", grantedAuthorities);
WebAuthenticationDetails authdetails = (WebAuthenticationDetails) authentication.getDetails();
HttpSession session = event.getSession();
ProviderManager pm = (ProviderManager)WebApplicationContextUtils.getWebApplicationContext(session.getServletContext()).getBean("authenticationManager");
pm.doAuthentication(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
in handler of HttpSessionCreatedEvent event, and I received AuthenticationSuccessEvent event, but then Failure event also received and authentication form displayed.
First you must override UserDetailsService in xml. Something like this:
<bean id="userService" class="com.security.UserSecurityService" >
...
</bean>
Then write your own service.like:
public class UserSecurityService implements UserDetailsService {
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException{
User user =userDAO.findByUsername(username);
GrantedAuthority[] grantedRoles = {new GrantedAuthorityImpl("ROLE_USER")};
secUsr = new User(user.getUsername(), user.getPassword(), true, grantedRoles);
I hope this will be helpful for you
basic overview...
i have a site setup in iis...
- "mysite" (wwwroot\mysite) under that there are 2 virtual directory applications
- "uploads" (\uploadfiles)
- "app" (wwwroot\myapp)
I also have a subdomain that is set up as a different site in iis...
- "beta.mysite" (wwwroot\mysitebeta) under that there are 2 virtual directory
- "uploads" (\uploadfiles)
- "app" (wwwroot\myappbeta)
the sub domain is working fine.... i can type in https://beta.mysite.com/app ... and it brings up the beta site log in perfectly fine.... the problem is, when i click on any of the buttons that create a post back... it reverts to https://www.mysite.com/app...
all of the links display the correct relative path to their files.... and if i type in https://beta.mysite.com/app/dir/page.aspx... it will actually go to that page on the beta site, all the links are going to the right spots... its just the postbacks that are killing me...
Have you tried setting a different application pool for these two websites? Looks like it's trying to be "smart" and concludes that the two virtual directories are actually the same website.
If all else fails, you could rewrite the postback URL in the FORM-tag that ASP.NET generates manually. Using an App_Browsers file and a ControlAdapter are probably the cleanest way of doing that.
I have an example of such a ControlAdapter implementation, though it is intended to work with URL rewriting to prevent reverting to the actual behind-the-scenes URL on postback. However, I think it would work for your problem out-of-the-box
public class FormRewriterControlAdapter : System.Web.UI.Adapters.ControlAdapter
{
protected override void Render(HtmlTextWriter writer)
{
base.Render(new RewriteFormHtmlTextWriter(writer));
}
}
public class RewriteFormHtmlTextWriter : HtmlTextWriter
{
private const string contextItemKey = "FormActionWritten";
public RewriteFormHtmlTextWriter(HtmlTextWriter writer) : base(writer)
{
InnerWriter = writer.InnerWriter;
}
public RewriteFormHtmlTextWriter(System.IO.TextWriter writer) : base(writer)
{
base.InnerWriter = writer;
}
public override void WriteAttribute(string name, string value, bool fEncode)
{
// If the attribute we are writing is the "action" attribute, and we are not on a sub-control,
// then replace the value to write with the raw URL of the request - which ensures that we'll
// preserve the PathInfo value on postback scenarios
if (name == "action" && !HttpContext.Current.Items.Contains(contextItemKey))
{
// Use the Request.RawUrl property to retrieve the un-rewritten URL
value = HttpContext.Current.Request.RawUrl;
HttpContext.Current.Items[contextItemKey] = true;
}
base.WriteAttribute(name, value, fEncode);
}
}
Form.browser file:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="FormRewriterControlAdapter" />
</controlAdapters>
</browser>
</browsers>