#!/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(44
+30) );
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 att
+acks!
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 => 'logo
+ut' %> ]
% } 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=>'requir
+ed' )
%= label_for password => 'Password'
%= password_field password => ( placeholder=>"Password", required=>'re
+quired' )
%= submit_button 'Login'
%= end
</div>
@@ secure.html.ep
% layout 'main', title => 'Top Secret';
<div>You've accessed the top secret area!</div>