note
haukex
<p>Here's an implementation of a login system with [mod://Mojolicious::Lite] and [mod://Mojo::SQLite]. It may seem fairly long, but some of that code is because I added password encryption using [mod://PBKDF2::Tiny] and [mod://Crypt::Random::Source] (<small><i>Update:</i></small> and of course because it's entirely self-contained, it includes all the templates etc.). The security could even be expanded, such as adding brute force attack prevention (often done via a delay on unsuccessful attempts), or even hashing the password on the client side. Download the following code as e.g. <c>mojo_login_example.pl</c>, install the aforementioned modules, and then run the command: <c>morbo --listen=http://127.0.0.1:3000 --listen=https://127.0.0.1:4430 mojo_login_example.pl</c></p>
<readmore title="code">
<c>
#!/usr/bin/env perl
use 5.028;
use Mojolicious::Lite -signatures;
use Mojo::SQLite;
use PBKDF2::Tiny qw/derive_hex verify_hex/;
use Crypt::Random::Source qw/get_strong/;
#app->secrets(['A Login Example - TODO: set this string!']);
app->sessions->secure(1);
# disable template cache in development mode (e.g. under morbo):
app->renderer->cache->max_keys(0) if app->mode eq 'development';
helper sql => sub { state $db = Mojo::SQLite->new('sqlite:/tmp/test.db') };
# Database setup:
app->sql->migrations->from_string(<<'END_MIGRATIONS')->migrate;
-- 1 up
CREATE TABLE Users ( Username TEXT, Salt TEXT, Password TEXT );
-- 1 down
DROP TABLE IF EXISTS Users;
END_MIGRATIONS
# for testing, insert a sample user if the DB is empty:
if ( not app->sql->db->query('SELECT COUNT(*) FROM Users')->arrays->[0][0] ) {
my $salt = unpack 'H*', get_strong(64);
app->sql->db->insert('Users', { Username => 'Foo', Salt => $salt,
Password => derive_hex('SHA-512', 'Bar', $salt, 5000) } );
}
helper logged_in => sub ($c) {
length( $c->session('username') ) ? $c->session : undef };
any '/' => sub ($c) { $c->render('index') } => 'index';
group { # everything in this group requires HTTPS b/c of this "under":
under sub ($c) {
return 1 if $c->req->is_secure;
$c->redirect_to( $c->url_for->to_abs->scheme('https')->port(4430) );
return undef;
};
get '/login' => sub ($c) { $c->render('login') } => 'login';
post '/login' => sub ($c) { # form handler
return $c->render(text => 'Bad CSRF token!', status => 403)
if $c->validation->csrf_protect->has_error('csrf_token');
# WARNING: This does not (yet) protect against brute-force attacks!
eval {
my $u = $c->sql->db->select( 'Users', ['Salt','Password'],
{ Username => $c->param('username') } )->hashes;
die "Username not found" unless @$u==1;
utf8::encode( my $salt = $u->[0]{Salt} );
utf8::encode( my $pass = $c->param('password') );
die "Incorrect password" unless verify_hex(
$u->[0]{Password}, 'SHA-512', $pass, $salt, 5000);
$c->session( username => $c->param('username') );
$c->redirect_to('secure');
;1} or do {
$c->flash(login_error => 'Wrong username or password');
$c->redirect_to('login');
};
} => 'login';
any '/logout' => sub ($c) {
delete $c->session->{username};
$c->redirect_to('index');
} => 'logout';
group { # everything in this group requires login
under sub ($c) {
return 1 if $c->logged_in;
$c->redirect_to('login');
return undef;
};
any '/secure' => sub ($c) { $c->render('secure') } => 'secure';
};
};
app->start;
__DATA__
@@ layouts/main.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
<nav style="margin-bottom:1em;"><small>
[ <%= link_to Main => 'index' %> | <%= link_to Secure => 'secure' %> ]
% if ( my $s = logged_in ) {
[ Logged in as <%= $s->{username} %> | <%= link_to Logout => 'logout' %> ]
% } else {
[ <%= link_to Login => 'login' %> ]
% }
</small></nav>
<main>
<%= content %>
</main>
</body>
</html>
@@ index.html.ep
% layout 'main', title => 'Hello, World!';
<div>Hello, World!</div>
@@ login.html.ep
% layout 'main', title => 'Login';
% if ( flash 'login_error' ) {
<div style="margin-bottom:1em;"><strong><%= flash 'login_error' %></strong></div>
% }
<div>
%= form_for login => ( method => 'post' ) => begin
%= csrf_field
%= label_for username => 'Username'
%= text_field username => ( placeholder=>"Username", required=>'required' )
%= label_for password => 'Password'
%= password_field password => ( placeholder=>"Password", required=>'required' )
%= submit_button 'Login'
%= end
</div>
@@ secure.html.ep
% layout 'main', title => 'Top Secret';
<div>You've accessed the top secret area!</div>
</c>
11114021
11114021