http://qs1969.pair.com?node_id=11114043


in reply to 302 Found Location Message

Here's an implementation of a login system with Mojolicious::Lite and Mojo::SQLite. It may seem fairly long, but some of that code is because I added password encryption using PBKDF2::Tiny and Crypt::Random::Source (Update: 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. mojo_login_example.pl, install the aforementioned modules, and then run the command: morbo --listen=http://127.0.0.1:3000 --listen=https://127.0.0.1:4430 mojo_login_example.pl

#!/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>