Applications with Mojolicious – Part Three: Forms and Login

In this third post we introduce forms for user login to the blog app and handling of that login form's GET and POST request data.

Mojolicious Series

Recall the index template from the last post, where we referred to a named route called login_form.

1
%= link_to Login => 'login_form'

The syntax of link_to in the above template refers to login_form, which is a named route we still have to add to the router object. Named routes are especially useful in templates, either using link_to as above (creating an anchor tag) or url_for to create the link itself.

For the login page /login, we want to display a form upon GET and process it with a POST request. The following code creates two routes to the same request path, with separate actions for GET and POST.

1
2
$r->get('/login')->name('login_form')->to(template => 'login/login_form');
$r->post('/login')->name('do_login')->to('Login#on_user_login');

The first route links directly to a login form template, and handle the form POST in the Moblo::Login controller.

Forms

To create the form, Mojolicious provides useful (optional) helpers to create HTML tags using the aforementioned TagHelpers plugin. Open the template login/login_form.html.ep and create a login form.

1
2
3
4
5
6
7
8
9
%= form_for 'do_login' => (method => 'POST') => begin
    %= label_for username => 'Username'
    %= text_field 'username'

    %= label_for password => 'Password'
    %= password_field 'password'

    %= submit_button 'Log me in', class => 'btn'
% end

The form_for helper hooks up the form action with the route we defined above. The rest of the template is self-explanatory.

Note that the form points to the named route do_login, which points to the action on_user_login of the Login controller. With the template complete, we now create the controller and action to handle the form data.

Accessing Request Data

Within the form submit action, we use the $self->param() method of the controller to access parameters.

Create the Login controller under lib/Moblo/Login.pm.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package Moblo::Login;
use Mojo::Base 'Mojolicious::Controller';


# Mocked function to check the correctness
# of a username/password combination.
sub user_exists {
  my ($username, $password) = @_;

  return ($username eq 'foo' && $password eq 'bar');
}


# Called upon form submit
sub on_user_login {
  my $self = shift;

  # Grab the request parameters
  my $username = $self->param('username');
  my $password = $self->param('password');

  return $self->render(text => 'Logged in!')
  if (user_exists($username, $password));

  return $self->render(text => 'Wrong username/password', status => 403);
}

1;

The controller will return a HTTP 403 error, unless the login combination is correct as per the mocked login function user_exists. We will soon replace that function with a lookup in a database, but for the sole purpose of demonstrating the controller functionality, this is sufficient.

Sessions

While the above login procedure works, all it does is printing some text. Let's change that to something that actually logs the user in. We want to create a session upon sucessful user login and redirect the user to a restricted area.

With Mojolicious, sessions are by default stored as signed cookies on the client. This is sufficient for most use-cases, however comes with two pitfalls. First, you cannot store more than the maximum cookie size (which is 4KB, including name, expiry information, etc.). Second, the cookie is signed and Base64-encoded, but not encrypted. Thus, clients are able to see the session data, but not manipulate it. Bear that in mind when you use the sessions.

Upon successful login, we store two values in the session:

1
2
$self->session(logged_in => 1);
$self->session(username => $username);

To retrieve values from the session, we use the $self->session('key') getter. If you log in, a cookie named Mojolicious is created with its content of the following structure:

1
2
eyJsb2dnZWRfaW4iOjEsImV4cGlyZXMiOjEzOTcxMzQxMDUsInVzZXJuYW1lIjoiZm9vIn0---10a4d4a7779ae82c299898fc7e9ab3c666230f99
# Base64 encoded session storage --- signature

As already mentioned, the first part is simply the data base64-encoded and decodes to {"logged_in":1,"expires":1397134105,"username":"foo"}. The 'expired' key determines the unix timestamp when the session should expire. The default duration of a session is 60 minutes.

To change the duration of a session or the cookie name, use the $self->app->sessions object in the main application module. Important: Always set $self->secrets to a secure passphrase, as that is the key used for signing the session data. See this link for additional information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sub startup {

    # Allows to set the signing key as an array,
    # where the first key will be used for all new sessions
    # and the other keys are still used for validation, but not for new sessions.
    $self->secrets(['This secret is used for new sessionsLeYTmFPhw3q',
        'This secret is used _only_ for validation QrPTZhWJmqCjyGZmguK']);

    # The cookie name
    $self->app->sessions->cookie_name('moblo');

    # Expiration reduced to 10 Minutes
    $self->app->sessions->default_expiration('600');

    my $r = $self->routes;

    # ...
}

Additionally, instead of displaying some text, we refer the user to a named route restricted_area after sucessful login. Change the login action of the controller to reflect the changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Called upon form submit
sub on_user_login {
    my $self = shift;

    # Grab the request parameters
    my $username = $self->param('username');
    my $password = $self->param('password');

    if (user_exists($username, $password)) {

        $self->session(logged_in => 1);
        $self->session(username => $username);

        $self->redirect_to('restricted_area');
    } else {
        $self->render(text => 'Wrong username/password', status => 403);
    }
}

Restricted Routes

Until now, we've used the router object to distinguish between GET and POST requests, but now we want to create a route that is only accessible to logged in users (allowing to publish posts and the like). Route under (formerly bridges) is well suited for this task. When a route is tied to an under action, all requests first execute this action. Only if returns a true value, the actual route action is executed.

We can use this to force authentication for a whole set of routes, by adding a route action as follows:

1
my $authorized = $r->under('/admin')->to('Login#is_logged_in');

The $authorized object behaves just like the router object. For the restricted area, we can use it to define new, restricted routes.

1
$authorized->get('/')->name('restricted_area')->to(template => 'admin/overview');

Just add the under action is_logged_in to the Login controller, which should return a true value if the session key 'logged_in' is set to a true value.

1
2
3
sub is_logged_in {
    return shift->session('logged_in');
}

This does the trick, but fails silently if the user is not logged in. Let's add an error message:

1
2
3
4
5
6
7
8
9
10
sub is_logged_in {
    my $self = shift;

    return 1 if $self->session('logged_in');

    $self->render(
        inline => "<h2>Forbidden</h2><p>You're not logged in. <a href="#" Go to login page.="login_form"></a></p>",
        status => 403
    );
}

With that done, only logged in accounts will be able to access the internal overview page. To allow the user to logout, simply create a route which expires the session by setting $self->session(expires => 1).

1
2
3
4
5
6
7
8
9
$r->route('/logout')->name('do_logout')->to(cb => sub {
     my $self = shift;

     # Expire the session (deleted upon next request)
     $self->session(expires => 1);

     # Go back to home
     $self->redirect_to('home');
 });

In the next part, we create a database schema using DBIx::Class to add and modify posts and users.

Code at GitHub

You can browse the app at the current state as well as its history on Github.