A List Apart

Menu
Issue № 300

The Problem with Passwords

by Published in Browsers, JavaScript, Usability · 46 Comments

Usability researcher Jakob Nielsen’s recent column advocates a fundamental change to password field design on the web. He believes that the time has come “to show most passwords in clear text as users type them,” abandoning the traditional approach that displays a series of asterisks or bullets in place of the actual password.

Article Continues Below

Nielsen’s controversial proposal demonstrates the principle that most design decisions require trade-offs. User goals and business objectives do not always intersect. Security, usability, and aesthetic concerns often compete. We must set priorities and balance these interests to achieve the best results in each situation.

Security issues are particularly difficult to deal with because they’re an annoyance. We just want to let people get at the great tool we’ve created, but instead we have to build barriers between the user and the application. Users must prove their identities. We can’t trust any data they provide unless it’s been thoroughly sanitized.

Unfortunately, this is reality. A great deal of web traffic really is malicious, and sensitive data gets stolen. Typically, we ask users to supply a username (often an e-mail address) along with a password to sign in to an application. The username identifies the person, while the password proves that the person submitting the username is indeed the one who created the account. That’s the theory, based on two assumptions:

  1. A password will never be visible outside the mind of the person who created it.
  2. Both the username and password can be recalled from memory when needed.

This approach places a significant cognitive burden on people who use websites that require authentication. In general, we get by remarkably well, but it’s easy to see the weaknesses in the system. Passwords that are easy to remember are also easy to guess. When people are forced to choose strong passwords, they’re more likely to either write them down or forget them. The usual response is a password reset mechanism, which naturally undermines the strength of the entire system. It doesn’t matter that my password is encrypted with the strongest ciphers known to man when it can simply be reset by anyone who knows which high school I attended.

This is one of the reasons that Nielsen suggests abandoning password masking. People get frustrated and often reset passwords that they haven’t actually forgotten simply because they’ve mistyped. Providing clear feedback with unobscured letters will reduce errors, improve the user experience, and lessen the need for insecure alternatives.

However, making such a sweeping change to a fundamental user interaction could present serious problems. Consider some contexts in which a password might need to be entered in front of a large group of people, such as while using a conference room projector. And many years of web experience have set user expectations on how form elements should work. People understood that password masking was invented for their security. Failing to meet that expectation might undermine confidence, and we cannot afford to lose our users’ trust.

Is there a middle path—a way to provide feedback and reduce password errors that doesn’t sacrifice the user experience? At least two design patterns address this issue in offline applications, and with a little JavaScript, we can bring them to the web.

Now you see it, now you don’t

The simplest solution is to mask the password by default while giving users a way to switch the field to clear text. Nielsen even mentions this in passing. This approach allows the person to confirm that the password was entered correctly, but it also places control firmly in the user’s grasp. Such toggle controls are often seen on WiFi preference panels, but they’re rarely implemented elsewhere. (Note: at least one blog has advocated a similar technique while this article was in production.)

It should be simple to write a control that switches the type attribute of an HTML input element between password and text. Unfortunately, it’s not. Internet Explorer does not allow this particular attribute to be set by JavaScript, so we have to be slightly more creative. The following two functions should do the trick:

(Line wraps marked » —Ed.)

window.onload = function() {
if(document.getElementsByTagName) {
var inputs = document.getElementsByTagName('input');
for(var i in inputs) {
  var input = inputs[ i ];
  if(input.type == 'password') {
    toggle_control = document.createElement('label');
    toggle_control.innerHTML = "<input type=\"checkbox\ »
    " "+ "onclick=\"toggle_password('"+ input.id+"',this »
    .checked) \" />"+" Show password";
    input.parentNode.insertBefore(toggle_control, input »
    .nextSibling);
  }
}
}
}

function toggle_password(element_id, show_text) {
if(document.getElementById) {
var password_input = document.getElementById(element_id);
var new_input      = document.createElement('input');
with(new_input) {
  id        = password_input.id;
  name      = password_input.name;
  value     = password_input.value;
  size      = password_input.size;
  className = password_input.className;
  type      = show_text ? 'text' : 'password';
}
password_input.parentNode.replaceChild(new_input, »
password_input);
}
}

The first function scans the document for all input elements and collects those of the password type. Note that this code is only intended to demonstrate the concept, and the process might be improved by using a JavaScript framework such as Prototype.

After each input, the function inserts a labeled checkbox to toggle the field between masked and clear text. The second function controls the toggle behavior itself. When the user clicks the toggle control, the function creates a new input and swaps it for the existing one, juggling the value and other properties between them.

An alternative approach is to create the text input only once and toggle the display property to show or hide the appropriate field. One drawback to this method, though, is that an element’s id must be unique. Since the parallel text input would have its own id, it wouldn’t inherit any CSS rules that referenced the original element by ID.

Take a look at example one to see it in action. This solution is easy to implement, and it follows the principle of progressive enhancement: In the absence of JavaScript, password fields will retain their usual behavior. The toggle control empowers the user with a choice as to whether to show or hide a password in a particular circumstance. The main drawback is that it could still undermine a user’s concept of the password field as a “black box.” We’re so completely accustomed to thinking of our passwords as a secret that merely offering the option to display it in clear text could be unsettling.

A second alternative

Typing errors are especially common on touchscreen devices such as the iPhone, where fingers can’t locate the edges of keys by feel. Anticipating that password inputs without visual feedback would cause problems, Apple adopted an interesting approach. The last letter typed into the field remains visible for a couple of seconds before turning into a dot. This creates an opportunity to catch errors without showing the entire password at once.

We can reproduce this progressive masking behavior with HTML and JavaScript, although it will take a bit more code than the previous example. Consider the following:

window.onload = function() {
if(document.getElementsByTagName) {
var inputs = document.getElementsByTagName('input');
var password_inputs = Array();
for(var i in inputs) {
  if(inputs[ i ].type == 'password') {
    password_inputs.push(inputs[ i ]);
  }
}
for(var i in password_inputs) {
  var input = inputs[ i ];

  var masking_element = document.createElement('input');
  with(masking_element) {
    style.position = 'absolute';
    id             = input.name + '_mask';
    type           = 'text';
    size           = input.size;
    className      = input.className;
  }
  masking_element.onfocus = function(){this.nextSibling »
  .focus()};
  input.parentNode.insertBefore(masking_element, input);
  
  input.onchange = function() {
    
    if(this.timer){
      clearTimeout(this.timer);
    }
    
    var mask_character = "\u2022";
    var last_character = this.value.charAt(this »
    .value.length-1);
    
    var masked_text    = this.previousSibling.value;
    var password_text  = this.value;
    
    if(masked_text.length < password_text.length) {
      this.previousSibling.value = password_text.substr(0,
        password_text.length-1).replace(/./g,
        mask_character)+last_character;
    } else {
      this.previousSibling.value = password_text »
      .replace(/./g,mask_character);
    }
    this.timer = setTimeout("with(document.getElement »
    ById('"+masking_element.id+"')){value=value »
    .replace(/./g,'"+mask_character+"')}",2000);
  
  }
  input.onkeyup = input.onchange;
  input.onchange();

}
}
}

This time, we create a second text input to sit directly on top of each password input (the caveat about CSS inheritance from the previous example applies). By manipulating its value as the original field changes, we can control what the user sees. Let’s break down each step of the script:

window.onload = function() {
if(document.getElementsByTagName) {
var inputs = document.getElementsByTagName('input');
var password_inputs = Array();
for(var i in inputs) {
  if(inputs[ i ].type == 'password') {
    password_inputs.push(inputs[ i ]);
  }
}
for(var i in password_inputs) {
  var input = inputs[ i ];
  ...
}
}
}

Again, our first task scans the page for password inputs so that we can modify their behavior. However, there’s a critical difference between this function and the first example. In Internet Explorer, document.getElementsByTagName() doesn’t return a simple list of matching elements at the moment the script is run. Rather, it returns a reference to the collection of matching elements. If we create a new input element while looping over the results, we will increase the size of that collection on each pass, and the loop will continue indefinitely. This instantly crashes Internet Explorer (and not gracefully). So, instead, we need to copy the initial results of the function into an array and loop over that.

var masking_element = document.createElement('input');
with(masking_element) {
style.position = 'absolute';
id             = input.name + '_mask';
type           = 'text';
size           = input.size;
className      = input.className;
}
masking_element.onfocus = function(){this.nextSibling.focus()};
input.parentNode.insertBefore(masking_element, input);

With the new input inserted directly before the existing one, setting its position to absolute should place it directly on top. This should work in most layouts, but there may be exceptions where additional CSS is required to position it correctly. Of course, now that we’re covering the input with another element, we also need to make sure that clicking on the mask activates the input. Adding an onfocus handler takes care of this. We have to assign this handler outside the with statement for it to function correctly in Firefox 2.

input.onchange = function() {
...  
}
input.onkeyup = input.onchange;

With the new element in place, we’ll build a function to display the text of the progressively masked password. We’ll need this text to respond to changes in the contents of the password field. Usually, this will mean that a user is typing on the keyboard, but that may not always be the case. Someone might paste text into the field using a context menu, for example. Attaching our code to both the change and keyup events should cover all the bases.

var mask_character = "\u2022";
var last_character = this.value.charAt(this.value.length-1);

We can define any character we like to mask the passwords. Traditionally, most systems use asterisks or dots, so in this example we define the Unicode entity 2022, which is the bullet character. On the second line, we identify the last character of the current password value so that we can render it in clear text.

var masked_text    = this.previousSibling.value;
var password_text  = this.value;

if(masked_text.length < password_text.length) {
this.previousSibling.value = password_text.substr(0,
password_text.length-1).replace(/./g, »
mask_character)+last_character;
} else {
this.previousSibling.value = password_text.replace(/./g,
                             mask_character);
}

Now we can take the value of the password field, replace every character except the last one with a bullet, and put that text into the field that we’re using as a mask. However, we should only do this while the person is typing forward. In other words, if the user presses the backspace key, we don’t want to reveal the previous character again. After it’s been hidden, it should stay hidden. So before performing the replacement, we first check to see if the password value is longer than the masked text. The replacement itself can be done using a simple regular expression. The expression /./ matches any character in the password field. Adding the letter g to the end, (/./g) means that it scans the entire string of text instead of stopping at the first match.

this.timer = setTimeout("with(document.getElementById('"+
masking_element.id+"')){value=value.replace(/./g,'"+
mask_character+"')}",2000);

After a two-second delay, we want to mask the full password. However, our users probably won’t pause for two seconds after typing each letter. So we only want the behavior to take effect when the password field hasn’t changed at all for that length of time. Every time we call the setTimeout function in JavaScript, it returns an ID that we can use to reference that particular timer.

if(this.timer){
clearTimeout(this.timer);
}

By storing the timer ID in a variable and adding the above code at the beginning of our function, we can cancel the countdown as long as we observe that the field is still changing.

input.onchange();

Our last step is to run the function we just defined. This ensures that the mask will show the correct text if the password field was pre-filled before the page was loaded.

To see the full script in action, take a look at example two. It’s been tested in Internet Explorer 6-8, Firefox, Safari, and Chrome. Again, in the absence of JavaScript, this technique will degrade well—password fields will simply function normally.

Proceed with caution

When dealing with such a fundamental area of the web experience, we need to be careful because we’re dealing with deeply conditioned expectations. The username/password method of securing web applications isn’t perfect, but there are few good alternatives and it’s become the standard approach. We can best address the usability concerns of password fields by testing incremental changes like these to extend default behavior—without compromising the basic experience and losing the trust of our users.

46 Reader Comments

Load Comments