Persistent cookies / "Remember me"

Hi!

I’m looking into how to implement a “remember me” type of option to login. It will of course require persistent cookies. I haven’t found a way to set them from a Toolkit Application. Any ideas?

I noticed that this sort of thing was mentioned
here
as an idea for the Toolkit Sampler.

I think, you can set your own persistent cookie after user was authenticated for the first time. So next time, you can check that cookie and force transparent authentication. However, Im not sure (just did not test) if it is possible to access HttpServletResonse from an application to call setCookie method, so guys from ITMill should tell you more details on this.

I think our
LoginForm
component could support this at some level at least. Would that be enough?

If it is not possible to set cookies from the application (by the application, I mean), probably it would be better to add a functionality for just such persistent cookie support set/update. This will open more room from application developers, as not everybody using login screen and may have their own implementations.

However, if the cookies manipulation is possible from the application, I think this should be enough.

I agree with dll, some applications might find it useful to remember for example theme/language settings for anonymous users via persistent cookies.

I did make a generic cookie widget, that enabled you to set/get cookies from server-side. I used it to implement this “remember me” feature.

Small drawback with this approach is that you cannot read any cookies before the component was added to application window. Basically, this caused an small extra reload when application was started. But quite acceptable.

I don’t have code right here, but I can post it if you’d like to try.

Sure, I think others might find it interesting as well.

I’ll quickly post the code here. There are two files: the server-side component and the client side widget. The widget is not visible, but just reads the cookies and posts them back to server.

The server-side component has a listener that is invoked when the cookies become available at server (i.e. they have been read from the client).

ClientCookies.java (the server component):


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.itmill.toolkit.terminal.PaintException;
import com.itmill.toolkit.terminal.PaintTarget;
import com.itmill.toolkit.ui.AbstractComponent;

public class ClientCookies extends AbstractComponent {

	private static final String VALUE_SEPARATOR = "\t";
	private Map<String, String> cookies = new HashMap<String, String>();
	private boolean reload;
	private Map<String, String> setList = new HashMap<String, String>();
	private Collection<UpdateListener> listeners = new ArrayList<UpdateListener>();
	private boolean cookiesReadFromClient;

	public ClientCookies() {
		this(false);
	}

	public ClientCookies(boolean readAllCookies) {
		this.reload = readAllCookies;
	}

	@Override
	public String getTag() {
		return "cookiemgr";
	}

	@Override
	public void paintContent(PaintTarget target) throws PaintException {
		super.paintContent(target);

		if (!setList.isEmpty()) {
			String[] newCookies = new String[setList.size()]
;
			int i = 0;
			for (String key : setList.keySet()) {
				newCookies[i++]
 = key + VALUE_SEPARATOR + setList.get(key);
			}
			target.addVariable(this, "setcookies", newCookies);
		}

		if (reload) {
			target.addVariable(this, "getcookies", true);
			target.addVariable(this, "cookies", new String[] {});
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public void changeVariables(Object source, Map variables) {
		super.changeVariables(source, variables);

		if (variables.containsKey("cookies")) {
			reload = false;
			this.getCookies().clear();
			Object variable = variables.get("cookies");
			if (variable instanceof String[]) {
				String[] newCookies = (String[]
) variable;
				for (int i = 0; i < newCookies.length; i++) {
					String[] cookie = newCookies[i]
.split(VALUE_SEPARATOR);
					this.getCookies().put(cookie[0]
, cookie[1]
);
				}
			}
			cookiesReadFromClient = true;
			fireCookiesUpdatedEvent();
		}

	}

	public void addListener(UpdateListener listener) {
		if (listener == null)
			return;
		this.listeners.add(listener);
	}

	public void removeListener(UpdateListener listener) {
		if (listener == null)
			return;
		this.listeners.remove(listener);
	}

	public void removeAllListeners() {
		this.listeners.clear();
	}

	protected void fireCookiesUpdatedEvent() {
		List<UpdateListener> copy = new ArrayList<UpdateListener>(
				this.listeners);
		for (UpdateListener l : copy) {
			l.cookiesUpdated(this);
		}

	}

	public interface UpdateListener {
		public void cookiesUpdated(ClientCookies clientCookies);
	}

	public String getCookie(String name) {
		if (cookies.size() <= 0) {
			loadFromClient();
		}
		String cookie = cookies.get(name);
		return "undefined".equals(cookie)? "" : cookie;
	}

	public Map<String, String> getCookies() {
		return cookies;
	}

	public void setCookie(String name, String value) {
		this.setList.put(name, value);
		loadFromClient();
	}

	private void loadFromClient() {
		reload = true;
		requestRepaint();
	}

	public boolean isUpdated() {
		return this.cookiesReadFromClient;
	}
}

IClientCookies.java (The client-side widget):


import java.util.ArrayList;
import java.util.Collection;

import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.ui.HTML;
import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
import com.itmill.toolkit.terminal.gwt.client.Paintable;
import com.itmill.toolkit.terminal.gwt.client.UIDL;

public class IClientCookies extends HTML implements Paintable {

	private static final String VALUE_SEPARATOR = "\t";

	public IClientCookies() {
		super();
		setHTML("<div style=\"display:none;\"><!-- Cookies --></div>");
	}

	public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
		if (client.updateComponent(this, uidl, true)) {
			return;
		}
		
		if (uidl.hasVariable("setcookies")) {
			String[] newCookies = uidl.getStringArrayVariable("setcookies");
			for (int i = 0; i < newCookies.length; i++) {
				String[] cookie = newCookies[i]
.split(VALUE_SEPARATOR);
				Cookies.setCookie(cookie[0]
, cookie[1]
);
			}
		}

		if (uidl.hasVariable("getcookies")) {
			// Read all cookies
			Collection<String> names = Cookies.getCookieNames();
			Collection<String> values = new ArrayList<String>();
			for (String name : names) {
				values.add(name + VALUE_SEPARATOR + Cookies.getCookie(name));
			}
			client.updateVariable(uidl.getId(), "cookies", values.toArray(),
					true);
		}
	}
}

in addition to these you need to modify your widgetset code to handle the component to widget mapping. Something like:

public class MyWidgetSet extends DefaultWidgetSet {
	@SuppressWarnings("unchecked")
	@Override
	public Paintable createWidget(UIDL uidl) {
		if (IClientCookies.class == classType) {
			return new IClientCookies();
		}
		// Let the DefaultWidgetSet handle creation of default widgets
		return super.createWidget(uidl);
	}

	@SuppressWarnings("unchecked")
	@Override
	/* Resolves UIDL tag name to class name. */
	protected Class resolveWidgetType(UIDL uidl) {
		final String tag = uidl.getTag();
		if ("cookiemgr".equals(tag)) {
			return IClientCookies.class;
		}

		// Let the DefaultWidgetSet handle resolution of default widgets
		return super.resolveWidgetType(uidl);
	}
}

… and compile the widgetset.

Thank you very much, Sami. That was very helpful.

I made a couple of quick changes so that I can set the expiration and other parameters.

ClientCookies.java:


public void setCookie(String name, String value) {
	this.setList.put(name, value);
	loadFromClient();
}

public void setCookie(String name, String value, Date expires) {
	String val = value + VALUE_SEPARATOR + expires.getTime();
	this.setList.put(name, val);
	loadFromClient();
}

public void setCookie(String name, String value, Date expires, String domain, String path,
		Boolean secure) {
	String val = value + VALUE_SEPARATOR + expires.getTime() + VALUE_SEPARATOR + domain
			+ VALUE_SEPARATOR + path + VALUE_SEPARATOR + secure.toString();
	this.setList.put(name, val);
	loadFromClient();
}

And IClientCookies.java:


if (uidl.hasVariable("setcookies")) {
	String[] newCookies = uidl.getStringArrayVariable("setcookies");
	for (int i = 0; i < newCookies.length; i++) {
		String[] cookie = newCookies[i]
.split(VALUE_SEPARATOR);
		switch (cookie.length) {
		case 6:
			String name6 = cookie[0]
;
			String value6 = cookie[1]
;
			Date expires6 = new Date(Long.parseLong(cookie[2]
));
			String domain6 = cookie[3]
;
			String path6 = cookie[4]
;
			Boolean secure6 = Boolean.parseBoolean(cookie[5]
);
			Cookies.setCookie(name6, value6, expires6, domain6, path6, secure6);
			break;
		case 3:
			String name3 = cookie[0]
;
			String value3 = cookie[1]
;
			Date expires3 = new Date(Long.parseLong(cookie[2]
));
			Cookies.setCookie(name3, value3, expires3);
			break;
		case 2:
		default:
			String name2 = cookie[0]
;
			String value2 = cookie[1]
;
			Cookies.setCookie(name2, value2);
			break;
		}
	}
}

Very handy extensions! My implementation certainly missed the expiration control, but I wasn’t sure how it works so I left it out.

Now this really starts to look like a useful component. :slight_smile: Maybe this should be published to
contrib or incubator
?

Yes. Please.

I know that the general purpose cookie needs are there, but the “remember me” checkbox for LoginForm would be generally useful, especially if it can be tweaked to remember just the username or both (for those who don’t need real security). In our case, we just want to remember the login name.

Is my best bet to just override LoginForm.getLoginHtml() to do this? I’d guess I’d add a checkbox with some javascript to write the cookie when checked and clear the cookie when unchecked. Is that the right approach? I’m using 6.3 nightly builds right now.

Here it is. To let everyone enjoy this, I attached the Vaadin add-on jar file that implements this feature with a callback API.

Just drop the jar into WEB-INF/lib and recompile the client-side widgetset (Vaadin plugin for Eclipse install a button on toolbar for this).

Here is a sample code:

// Label for cookie data
        final Label cookieTxt = new Label("<cookies not read>", Label.CONTENT_RAW);
        mainWindow.addComponent(cookieTxt);

        // BrowserCookies widget (by default reads the cookie values)
        final BrowserCookies cookies = new BrowserCookies();
        mainWindow.addComponent(cookies);

        // Listen when cookies are available and read them
        cookies.addListener(new BrowserCookies.UpdateListener() {

            public void cookiesUpdated(BrowserCookies bc) {
                String txt = "";
                for (String name : bc.getCookieNames()) {
                    txt += name + " = '" + bc.getCookie(name) + "'<br />";
                }
                cookieTxt.setValue(txt);
            }
        });

11210.jar (14.3 KB)

Hello,

I am using Vaadin 6.3.2 and BrowserCookies 1.0.2.

I set up completly new project and still example from the directory gives me empty site.

As I see (System.out.println) application is starting, on scripts on site starting but nothing happens, site is empty.

What is the problem??

Thanks,
Arthur.

After 2 wasted hours I discovered that disabling


private static final long serialVersionUID = -8873245242163572864L;

helps.

Also “cookies not read”


cookieTxt = new Label(“”, Label.CONTENT_RAW);

is threated as a html tag, and it is invisible.

But still main page is blank :(.


EDIT: OK, now it works. Instead of adding components to Panels, and Panels to mainWindow I added components to VerticalLayout and Horizontal layouts and then layouts to mainWindow.

You probably overcome all the problems already, but just a tip to everyone else testing with the simple cookie lister code: if it appears empty the reason might simply be that there are no cookies set in the browser. Try setting a cookie.

You mean helps on disabling the session serialization and reloads work? For this using the ‘restartApplication’ url parameter is a easier (and “the right”) way.

Ok, that makes sense although if everything works that should not even show. Changed that to the sample code.

Also, I now tested with Vaadin 6.3.3 and uploaded the new add-on version. So visit the Directory for samples/demos/download:


http://vaadin.com/directory#addon/6

Hi,

I think the biggest problem was that - I don’t know why (maybe I don’t know Vaadin so well) but - adding components to Panels generates blank page. I deleted panels, put VerticalPanel and HorizontalPanel indead and then I saw DateField for the first time. It should be there always, even when there are no cookies.

As I understand sample code (BrowserCookiesApplication) works for you so I wonder why panels are blank at my Firefox 3.6.3 (latest). There sould be no differences but…

(Now I see that you put ‘online demo’, the date picker (this small calendar) works now, it never shown in my modifed version.)

Hi, Sami! I use this widget, and… How I can save cookies manually? Can you implement this method? I would appreciate:)
Without this method I’m forced use cheats…

I’m sorry, now I don’t quite understand what you are looking for. Do you have already implemented that in some way? A piece of code might help. Thanks!

Hello!
I think that save cookies only on repaint - is no very good idea. In my task, I should save/remove a data (key and value) into cookies without repaint, and I need public methods save and restore cookies. Yes, it is not a bug, but it would convenient? how do you think?

and…
I have exception at cookies.put(cookie[0]
, cookie[1]
); without if(cookie.length >= 2) (with empty cookie value).
this is minor quick fix

@SuppressWarnings("unchecked")
    @Override
    public void changeVariables(Object source, Map variables) {
        super.changeVariables(source, variables);

        if (variables.containsKey("cookies")) {
            reload = false;
            cookies.clear();
            Object variable = variables.get("cookies");
            if (variable instanceof String[]) {
                String[] newCookies = (String[]
) variable;
                for (int i = 0; i < newCookies.length; i++) {
                    String[] cookie = newCookies[i]
.split(VALUE_SEPARATOR);
					if(cookie.length >= 2)						
						cookies.put(cookie[0]
, cookie[1]
);
					else
						cookies.remove(cookie[0]
);
                }
            }
            cookiesReadFromClient = true;
            fireCookiesUpdatedEvent();
        }

    }

perhaps it will be useful