Dancer Two-Factor Authentication Demo
This demo shows how Two-Factor Authentication (2FA) can be implemented with Dancer2, Dancer2::Plugin::Auth::Extensible and a couple of other Perl modules.
It also shows how Dancer2::Plugin::Auth::Extensible enhances your Dancer application with user management using very little code.
Get the source code from GitHub
~# git clone https://github.com/racke/dancer2-tfa-demo.git
Start the app
~# cd dancer2-tfa-demo ~# plackup -o 127.0.0.1 bin/app.psgi HTTP::Server::PSGI: Accepting connections at http://127.0.0.1:5000/
Login and Setup 2FA
Go to the browser and enter the login URL http://127.0.0.1:5000/login.
The default credentials are dancer2 as username and 2fanow as password. You can change or add users in the configuration file.
Now use an 2FA app like Authy, Google Authenticator or FreeOTP+ to scan the QR code and confirm the token.
Finally log out and test your token.
Two-Factor Authentication
Creating the secret
The demo application creates the secret when the user is logged in and goes to the 2FA setup page for the first time. The secret is created with Data::SimplePassword, default length is 30 characters.
Generating image with QR code
First we create an object which is going to generate an image with the QR code.
my $qr = Imager::QRCode->new( size => 6, margin => 2, version => 1, level => 'M', casesensitive => 1, );
Now we construct the label that is going to be used by the authentication app. It consists of the fixed string and the user name in parenthesis.
my $instance = $self->qr_code_label; my $user_link = uri_escape("$instance (" . $username . ')'); my $data; my $img = $qr->plot("otpauth://totp/$user_link?secret=" . encode_base32($secret)); $img->write(data => \$data, type => 'png');
We send this back with:
$self->plugin->app->send_file (\$data, content_type => 'image/png');
Storing the secret
The secret is stored on the server and in the authentication app of the user. The demo keeps the secret in memory.
Authentication
The authentication code is split into two modules, a role with the logic for the specific routes and a demo provider which consumes that role and takes care of the secret's generation and the "storage".
Login
We intercept the standard authentication of Dancer2::Plugin::Auth::Extensible from the Demo provider using around:
around authenticate_user => sub { my ($orig, $self, $username, $password, @args) = @_; my $ret = $orig->($self, $username, $password, @args); return unless $ret; if ($self->check_tfa($username, $self->plugin->app->request->param('token'))) { return $ret; } else { return; } };
So we first call the original authenticate_user method and only if that is successful we are checking the token.
We determine the token that is valid at the current time and compare that with the token passed by the user:
my $expected = Authen::OATH->new->totp($secret); if ($token eq $expected) { ... } else { ... }
Configuration
We are using a fixed set of credentials in the configuration file config.yml
.
plugins: Auth::Extensible: realms: users: provider: Demo username_key_name: user users: - user: dancer2 pass: 2fanow
Routes
Dancer2::Plugin::Auth::Extensible supplements the demo with routes for login and logout, so we only need a few more routes specific to 2FA.
2FA
GET /tfa/setup/
Shows the form for 2FA setup with the QR code.
GET /tfa/qrcode.png
Produces QR code.
POST /tfa/setup/
Verifies token from 2FA setup form.
From plugin
The routes for 2FA are established by the plugin, e.g.
$app->add_route( method => 'get', regexp => '/tfa/setup', code => sub { $self->tfa_setup } );
Use cases
We are using Two-Factor Authentication for a couple of websites for ECA and an online shop in Germany.
Limitations
As the secrets are stored into memory, this demo should be run only as a single instance. Maybe Dancer2::Plugin::Cache::CHI could help here.
Author
This article has been written by Stefan Hornburg (Racke) for the Perl Dancer Advent Calendar 2020.