Sep 172010
 

Quite some time ago, FreshPorts and The FreeBSD Diary both moved from clear text passwords to password hashes. As such, you are no longer able to recover your password. You had to email me. This is extra work for everyone. Let us automate this.

The first, step, is a table:

create table user_password_reset(
  user_id        int not null,
  date_requested timestamp with time zone default ('now'::text)::timestamp(6) with time zone,
  ip_address     inet not null,
  token          text not null
);

alter table user_password_reset
    add foreign key  (user_id)
       references users (id) on update cascade on delete cascade;

With this, I can record:

  • the user requesting the password change
  • the date/time it was requested
  • the IP address from which the request came
  • the token sent to the user which will permit them to change the password

Next, we need a function to populate the token field, I could do this with a default value on the column, but I think this solution better. There is more flexibility with a trigger.

CREATE OR REPLACE FUNCTION user_password_reset_token() RETURNS TRIGGER AS $$
  BEGIN
    NEW.token := ENCODE(DIGEST((random() * NEW.user_id )::text || now(), 'sha256'), 'hex');
    RETURN NEW;
  END
$$ LANGUAGE 'plpgsql';


  DROP TRIGGER user_password_reset_token ON user_password_reset;
CREATE TRIGGER user_password_reset_token
    BEFORE INSERT on user_password_reset
    FOR EACH ROW
    EXECUTE PROCEDURE user_password_reset_token();

The token is created from a random value, multiplied by the user id and appended with the current time. The resulting string is then hashed using sha256 and encoded into hex and stored in the column. At any time, I can change the token scheme by adjusting that function.

An example of what this produces:

$ psql freshports.org
psql (8.4.3)
Type "help" for help.

freshports.org=# select * from user_password_reset ;
 user_id | date_requested | ip_address | token
---------+----------------+------------+-------
(0 rows)

freshports.org=# insert into user_password_reset (user_id, ip_address) values (1, '127.0.0.1') returning token;
                              token
------------------------------------------------------------------
 a781b65b027e447e67b37f45ea6f150fade0b75f89652577953bbdc1784e8e50
(1 row)

INSERT 0 1
freshports.org=# select * from user_password_reset ;
 user_id |        date_requested        | ip_address |                              token
---------+------------------------------+------------+------------------------------------------------------------------
       1 | 2010-09-16 19:51:18.37022+01 | 127.0.0.1  | a781b65b027e447e67b37f45ea6f150fade0b75f89652577953bbdc1784e8e50
(1 row)

freshports.org=# insert into user_password_reset (user_id, ip_address) values (1, '127.0.0.1') returning token;
                              token
------------------------------------------------------------------
 1df39361bd1ac458da7c23a3de64fe0277cb247846dc2a654f5b0123d9069188
(1 row)

INSERT 0 1
freshports.org=# select * from user_password_reset ;
 user_id |        date_requested         | ip_address |                              token
---------+-------------------------------+------------+------------------------------------------------------------------
       1 | 2010-09-16 19:51:18.37022+01  | 127.0.0.1  | a781b65b027e447e67b37f45ea6f150fade0b75f89652577953bbdc1784e8e50
       1 | 2010-09-16 19:51:24.505328+01 | 127.0.0.1  | 1df39361bd1ac458da7c23a3de64fe0277cb247846dc2a654f5b0123d9069188
(2 rows)

freshports.org=#

When the user submits their changed password, the following procedure is used:

CREATE OR REPLACE FUNCTION reset_password_token(text, text) RETURNS int AS $$
DECLARE
 in_password ALIAS for $1;
 in_token    ALIAS for $2;
 RowCount    bigint;

BEGIN
  UPDATE users SET password_hash = crypt( in_password, gen_salt('md5'))
    WHERE id = (SELECT user_id from user_password_reset
                 WHERE token = in_token);
  GET DIAGNOSTICS RowCount = ROW_COUNT;
  IF RowCount = 1 THEN
    DELETE FROM user_password_reset WHERE token = in_token;
  END IF;
  RETURN RowCount;
END
$$ LANGUAGE 'plpgsql';

And for purging old expired tokens:

CREATE OR REPLACE FUNCTION user_password_reset_purge() RETURNS int AS $$
DECLARE
 RowCount int8;

BEGIN
 DELETE FROM user_password_reset WHERE date_requested < now() - interval '2 days';
 GET DIAGNOSTICS RowCount = ROW_COUNT;
 RETURN RowCount;
END
$$ LANGUAGE 'plpgsql';

This was originally published at http://news.freshports.org/2010/09/16/sending-out-a-url-for-password-reset/.

Website Pin Facebook Twitter Myspace Friendfeed Technorati del.icio.us Digg Google StumbleUpon Premium Responsive