PerlDuck
PerlDuck

Reputation: 5730

How to handle locked/disabled user accounts with Dancer2::Plugin::Auth::Extensible?

I'm currently migrating a CGI application to Dancer2. I previously used a "hand-crafted" authentication mechanism using MySQL and a user table with the attributes email, password, and a state. The state indicates whether the account is active or locked. locked means the account is disabled (logically deleted).

I also have tables roles and user_roles to implement my two roles: admin and user.

Everything works like a charm, with one exception:

With my old "hand-crafted" mechanism I was able to lock users, i.e. logically delete them without removing them from the database. A login was only successful if email and hash_of(password) matched and the account was not locked.

How do I implement that with Dancer2::Plugin::Auth::Extensible and Dancer2::Plugin::Auth::Extensible::Provider::Database?

I hoped that the hook after_authenticate_user could return true or false to overwrite the result of authenticate_user, but that is not the case. At least, it is not documented.

One thing I thought of was to have an additional role active and then – for every route – require_role active instead of just require_login.

So my question is: How can I make Dancer2::Plugin::Auth::Extensible consider only active users?

Upvotes: 0

Views: 230

Answers (2)

simbabque
simbabque

Reputation: 54333

Borodin suggested to create a view and use that as the user table. I've done some testing and can say that that is indeed the easiest way to achieve this.

Warning: because of the nature of views, this makes it impossible for the application to modify or add users!

Consider the following Dancer2 application. I started with the dancer2 create script.

$ dancer2 gen -a Foo
$ cd Foo

I created the following simple sqlite database.

$ echo "
CREATE TABLE users (
    id       INTEGER     PRIMARY KEY AUTOINCREMENT,
    username VARCHAR(32) NOT NULL UNIQUE,
    password VARCHAR(40) NOT NULL,
    disabled TIMESTAMP   NULL
);

CREATE VIEW active_users (id, username, password) AS
    SELECT id, username, password FROM users WHERE disabled IS NULL;

INSERT INTO users ( username, password, disabled )
VALUES  ( 'foo', 'test', null),
        ( 'bar', 'test', '2017-10-01 10:10:10');
" | sqlite3 foo.sqlite

There is only a users table with the default columns as suggested by the plugin, plus a column disabled, which can be NULL or a timestamp. I thought it would be easier to illustrate with disabled than with active.

Then I made the following changes to lib/Foo.pm. All of this is basically from the documentation of Dancer2::Plugin::Auth::Extensible and Dancer2::Plugin::Auth::Extensible::Provider::Database.

package Foo;
use Dancer2;
use Dancer2::Plugin::Database;
use Dancer2::Plugin::Auth::Extensible;

our $VERSION = '0.1';

get '/' => sub {
    template 'index' => { 'title' => 'Foo' };
};

get '/users' => require_login sub {
    my $user = logged_in_user;
    return "Hi there, $user->{username}";
};

true;

Next, the plugins needed to go into the config. Edit config.yml and replace it with this.

appname: "Foo"
layout: "main"
charset: "UTF-8"
template: "simple"
engines:
  session:
    Simple:
      cookie_name: testapp.session

# this part is interesting
plugins:
    Auth::Extensible:
        realms:
            users:
                provider: 'Database'

############### here we set the view
                users_table: 'active_users'
    Database:
        driver: 'SQLite'
        database: 'foo.sqlite'
        on_connect_do: ['PRAGMA foreign_keys = ON']
        dbi_params:
            PrintError: 0
            RaiseError: 1

Now we're all set to try.

$ plackup bin/app.psgi
HTTP::Server::PSGI: Accepting connections at http://0:5000/

Visit http://localhost:5000/users in your browser. You'll see the default login form.

login page

Enter foo and test. This should work, and you should see the /users route. (Or not, as in my case, where the redirect seems to be broken...).

foo is logged in

Now go to http://localhost:5000/logout to get rid of foo's cookie and open http://localhost:5000/users again. This time, enter bar and test.

You will see that the login does not work.

bar cannot log in

To make a counter-test, replace the users_table in config.yml and restart the app.

# config.yml
                users_table: 'users'

Now the user foo will be able to log in.

This method is not only easy to implement, it should also by far be the way with the highest performance, as the database handles all the logic (and has most likely already cached it).

Your application, and especially the authentication plugin, do not need to know about the existence of the active or disabled fields at all. They don't need to care. Stuff will just work.

Upvotes: 2

simbabque
simbabque

Reputation: 54333

You can subclass Dancer2::Plugin::Auth::Extensible::Provider::Database and wrap the get_user_details method to check if the user is active or not.

Consider the same application I used in my other answer. Add the following class.

package Provider::Database::ActiveOnly;

use Moo;
extends 'Dancer2::Plugin::Auth::Extensible::Provider::Database';

around 'get_user_details' => sub {
    my $orig = shift;
    my $self = shift;

    # do nothing if we there was no user
    my $user = $self->$orig(@_) or return;

    # do nothing if the user is disabled
    return if $user->{disabled};

    return $user;
};

1;

The code is straightforward. After a user gets looked up, we have have the user data, so we can check the disabled column. If there is a anything in it, the user was disabled, and we abort.

You also need to make the following changes to the config.yml.

# config.yml
plugins:
    Auth::Extensible:
        realms:
            users:
                provider: 'Provider::Database::ActiveOnly'
                users_table: 'users'

Now the application should behave exactly as in the other answer.


To understand why this works we need to look at the source. The authenticating happens in authenticate_user. Initially I thought that's what ought to be replaced, but this solution is smarter, because we only need to fetch the user data once.

The authenticate_user method fetches the user data with the get_user_details method, so we can hook in there. Our around wrapper will inject the check for the activeness of the user transparently, and the rest of the code doesn't even know there is a difference.

Inactive users will not show up in any interaction related to Plugin::Auth::Extensible.

Upvotes: 1

Related Questions