Cold Hard Code

Building the best "Forgot Password" Feature.

I see a tremendous amount of Forgot Password workflows, and while they all work some of them have some compromising or just plain bad requirements.

In a nutshell, my strategy for this mechanism is simple:
  • Generate single-use temporary passwords.
  • If a user logs in with a temporary password, force them to change their password.
  • Make all your login forms accept temporary passwords, so there isn't special handling.
  • If a user logs in with their "normal" password, delete the temporary password.
If you use a good framework, like Catalyst, this is actually remarkably easy.  I just had a "Progressive" authentication realm make its way into Catalyst::Plugin::Authentication, which is perfectly suited for this task.

This writeup assumes that you are using DBIx::Class, and the DBIC Store.

If you have an existing schema, the biggest (optional) refactor is creating either a separate table or another pairing off of your authentication table for storing temporary passwords.  I eschew the idea that the username and password belong in the "Person" table, so I already tend to have a structure that looks more like this:

Showing the idea of a Person and identities.

Here, each identity record has its own 'realm' column, and the identifier is unique only in realm (primary key is 'id' and 'realm'; and id is typically the username)

The reason for this schema structure is that it easily expands outwards as new offerings, such as OpenID or Facebook, become common-place and need to be supported.

Also, it works great for temporary passwords.

The downside of this is that you authenticate off of the 'identity' table and not off the 'Person' table, so you have to do (in Catalyst): $c->user->person;  Not a big deal, but something to be mindful of.

After that is setup, it's quite easy to get Progressive in place.  The first step is to make sure you have the latest Catalyst::Plugin::Authentication installed.   After that, your Catalyst configuration should look like:

__PACKAGE__->config(
    'Plugin::Authentication' => {
        default_realm => 'progressive',
        realms => {
            progressive => {
                class => 'Progressive',
                realms => [ 'temp', 'local' ],
                authinfo_munge => {
                    'local'     => { 'realm' => 'local' },
                    'temp'      => { 'realm' => 'temp' },
                }
            },
            'local' => {
                credential => {
                    class => 'Password',
                    password_field => 'secret',
                    password_type  => 'hashed',
                    password_hash_type => 'SHA-1',
                },
                store => {
                    class      => 'DBIx::Class',
                    user_class => 'Schema::Person::Identity',
                    id_field   => 'id',
                }
            },
            temp => {
                credential => {
                    class => 'Password',
                    password_field => 'secret',
                    password_type  => 'hashed',
                    password_hash_type => 'SHA-1',
                },
                store => {
                    class    => 'DBIx::Class',
                    user_class => 'Schema::Person::Identity',
                    id_field   => 'id',
                }
            },
        }
    },
);

Obviously, the user_class and id_fields will have to match your schema.  Also, I use DBIx::Class::DigestColumns on my identity table, with the column 'secret' always hashed.

Now, every login mechanism that doesn't explicitly state which realm to use will, by default, use progressive and attempt a login against the local password store and failing that, will go to the temporary store.

When you generate a temporary password for a user, store their login name as 'id' and the temporary password in 'secret'.  All they have to do is login, and away they go!

After the user is logged in, it is simple to check if $c->user_in_realm('temp') is true and force a password change or some other action.

I've found this to be the least intrusive way of securely handling forgotten passwords.  It's easy to generate, and however you determine to deliver them is up to you -- blindly emailing to an email address is fine for not so secure sites but if security is important you can alter delivery mechanisms to be more secure (public key encryption, etc).

Also, I've found that Crypt::PassGen is quite suitable for generating reasonable temporary password.

jshirley

Written by Jay Shirley

Jay Shirley combines technical fundamentals with modern, practical savvy. An open source veteran with plenty of notches in his personal and professional belt, the combination of his work and his field vision (soccer metaphor!) has few rivals.

Comments