#!/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 <%= title %>
<%= content %>
@@ index.html.ep % layout 'main', title => 'Hello, World!';
Hello, World!
@@ login.html.ep % layout 'main', title => 'Login'; % if ( flash 'login_error' ) {
<%= flash 'login_error' %>
% }
%= 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
@@ secure.html.ep % layout 'main', title => 'Top Secret';
You've accessed the top secret area!