Mojolicious (Part 3): Session Management

In the previous article we made our home page beautiful by adding a HTML template using the concept of Templates and Layouts. Let us now see how we can restrict access to our website to only registered users. We will achieve this by authenticating known users and using session cookies after successful login. We will break this task into the below steps and conquer it one by one:

  • Create Login and Logout Pages
  • Authentication and Session Management Logic
  • Verification

Before we proceed further, let us all remind ourselves how our Mojolicious website looks as of now:

and this is our project file structure:

Create Login and Logout Pages

Instead of the home page, we will redirect the users to the login page whenever they access our website. If they provide the correct username / password, we will create session cookies and re-direct them to the home page. Once they are done browsing our website, they can click the Logout link (which we will add to our homepage). This will remove the session cookies and safely logout.

We have already created a master layout in our previous session which is the skeleton of our website. This makes us not worry about the look and feel of the website as it is already taken care of. We just need to create template files for login and logout pages which will call our already created master layout.

Login Page

Let us create a new template called login.html.ep inside the template folder of our project: myWebSite/templates/myTemplates. We will add the below code to it:

% layout 'master';
% title 'Login Page';

<center>
<h1>Please Login to Access the Website</h1>

<h6>
<!-- Logic to display errors if any -->
<font color=red>
<% if($error_message){ %>
<%= $error_message %>
<% } %>
</font>
</h6>

<form action="/login" method="post">

    <b>UserName</b> <input type="text" name="username" required></br>
    <b>Password</b> <input type="password" name="pass" required></br>

    <input type="submit" value="Submit">
    <input type="reset" value="Reset" />

</form>
</center>

The variable $error_message is used to display any error encountered while validating the user credentials.

We are using the master layout that we created in the previous session. We changed the title helper to say it’s a Login Page. Then we created a very simple login form which on submit is routed to /login action.

Logout Page

We will now create a logout page template with the name logout.html.ep in our template folder: myWebSite/templates/myTemplates with the below content:

% layout 'master';
% title 'Logout Page';

<center>
<h6>You have successfully logged out of the website. Thank you for visiting. If you want to login again, please <a href="/">click here</a></h6>
</center>

Modify Home Page

Let’s also add a logout link to our homepage template that we created in the previous article.

% layout 'master';
% title 'Home Page';
      <a href="/logout">Log Out</a>
      <h1><%= $msg %></h1>
      <h5 class="w3-padding-32">This is my personal site built with Mojolicious. You can provide testimonials using the link <a href="#">here</a>. 
</h5>

Authentication and Session Management

Our login and logout pages are ready, now time to add logic to authenticate the registered users using session cookies. We will create the below 3 subroutines in our controller (CustomController.pm*):

  • displayLogin
  • validUserCheck
  • alreadyLoggedIn
  • logout

*myWebSite/lib/myWebSite/Controller/CustomController.pm

displayLogin

This subroutine will display the login page template that we created in the previous section of this article. In case the user is already logged in, they will be redirected to the home page.

sub displayLogin {

    my $self = shift;

    # If already logged in then direct to home page, if not display login page
    if(&alreadyLoggedIn($self)){
    # If you are using Mojolicious v9.25 and above use this if statement as re-rendering is forbidden
    # Thank you @Paul and @Peter for pointing this out.
    # if($self->session('is_auth')){

            &welcome($self);

    }else{

       $self->render(template => "myTemplates/login", error_message =>  "");

    }

}

validUserCheck

This subroutine will check the details entered by the user in the login page and authenticates the user to access the website. This also creates session cookies to keep the user’s session active. The users are prompted with proper error messages when incorrect credentials are passed. We set the variable error_message here which gets displayed in our login.html.ep template.

sub validUserCheck {

    my $self = shift;

    # List of registered users
    my %validUsers = ( "JANE" => "welcome123"
                      ,"JILL" => "welcome234"
                      ,"TOM"  => "welcome345"
                      ,"RAJ"  => "test123"
                      ,"RAM"  => "digitalocean123"
                     );

    # Get the user name and password from the page
    my $user = uc $self->param('username');
    my $password = $self->param('pass');

    # First check if the user exists
    if($validUsers{$user}){

        # Validating the password of the registered user
        if($validUsers{$user} eq $password){

            # Creating session cookies
            $self->session(is_auth => 1);             # set the logged_in flag
            $self->session(username => $user);        # keep a copy of the username
            $self->session(expiration => 600);        # expire this session in 10 minutes if no activity


            # Re-direct to home page
            &welcome($self);

        }else{

            # If password is incorrect, re-direct to login page and then display appropriate message
            $self->render(template => "myTemplates/login", error_message =>  "Invalid password, please try again");
        }

    }else{

        # If user does not exist, re-direct to login page and then display appropriate message
        $self->render(template => "myTemplates/login", error_message =>  "You are not a registered user, please get the hell out of here!");

    }

}

alreadyLoggedIn

This subroutine checks session cookies to see if the user has already logged in, if yes then the user will be automatically allowed to access the pages in the website

sub alreadyLoggedIn {

      my $self = shift;

      # checks if session flag (is_auth) is already set
      return 1 if $self->session('is_auth');


      # If session flag not set re-direct to login page again.
      $self->render(template => "myTemplates/login", error_message =>  "You are not logged in, please login to access this website");

      return;

}

logout

We need to destroy all the session cookies that were created when the user logged in, forcing any visitor to the website to enter the credentials again. logout subroutine does that.

sub logout {

    my $self = shift;

    # Remove session and direct to logout page
    $self->session(expires => 1);  #Kill the Session
    $self->render(template => "myTemplates/logout");

}

After all these changes your controller should look like this.

Now we need to tell Mojolicious to call these subroutine whenever there is any action on the page. What handles the action on the page? If you have gone through the previous articles, you know it’s the main library file: myWebSite/lib/myWebSite.pm. Right now we only have 1 routes in our main library file which looks like this:

# Normal route to controller
$r->get('/')->to('CustomController#welcome');

Lets add the routes to send requests to all the subroutines we created in the previous section

# Normal route to controller
$r->get('/')->to('CustomController#displayLogin');
$r->post('/login')->to('CustomController#validUserCheck');
$r->any('/logout')->to('CustomController#logout');

my $authorized = $r->under('/')->to('CustomController#alreadyLoggedIn');

Our main library file should finally look like this.

Let us just try to understand quickly what all we are trying to do:

$r->get('/')->to('CustomController#login'); Whenever anyone accesses our root path of the website redirect them to login subroutine(which renders login page), instead of the home page which was the case before.

$r->post('/login')->to('CustomController#validUserCheck'); When the credentials are entered and submitted in the login page we will route the request to validUserCheck subroutine which basically validates the user, creates session cookies and redirects the user to website home page.

$r->any('/logout')->to('CustomController#logout'); Whenever Log Out link is clicked, it directs the request to logout subroutine which destroys the session cookies.

my $authorized = $r->under('/')->to('CustomController#alreadyLoggedIn'); This is a special condition. The “under” call ensures nothing after this statement gets executed, if the subroutine in “under” call fails(in our case the subroutine is alreadyLoggedIn). This is especially useful when we have multiple webpages and you want only authorized users has access to it. We will see this is our next session when we build our testimonials page.

Verification

I guess we have done everything that is required to enable authentication to our website. Let us not waste any more time and start our web application with morbo:

When we access our website now, we are greeted with the login page instead of the home page:

If you give incorrect credentials, you will get not be allowed to login and will get the below messages:

incorrect username
incorrect password

Valid users will be logged into the website and they can access the home page. They will also see a logout link in their home page.

New homepage with logout
Logout Page

With that we have secured our website. This completes the session management and user authentication part of Mojolicious tutorial. Ofcourse in the real world, you would validate the user credentials from your database with encrypted passwords. For the purpose of this demo, I have kept it simple. In the next session, we will cover database management in Mojolicious. You can watch the demo of this article in my youtube channel below.

10 Thoughts to “Mojolicious (Part 3): Session Management”

  1. Jim

    In CustomController.pm, you can drop the ampersands on sub routine calls.
    example:
    &welcome($self);

    welcome($self);

    1. You are absolutely right, you can drop the ampersand if you like. I am a creature of habit, when i started learning perl couple of years ago, I made it a practice to add ampersands to subroutine calls so that I can easily distinguish between perl core functions and custom functions(subroutine) in my code. That habit stuck with me I guess.

  2. Paul

    Your article is awesome and useful for me ,Thank you !
    In this part (part 3 ), sub function alreadyLoggedIn() is unnecessary, It would bring extra trouble for novice like me . Below is enough:
    #############
    sub displayLogin{
    my $self = shift;
    if($self->session(‘is_auth’)){
    &welcome($self);
    }else{
    $self->render(template => “myTemplates/login”,
    error_message => “Login please”);
    }
    }
    ###########

    1. Hi Paul,

      Thank you for visiting the site. The reason for using the function alreadyLoggedIn is re-usability. The function is already used in the “under” to restrict access to certain parts of the website like testimonials. Keeping this logic in subroutine which can be used in multiple places helps when we want to change the logic in future. In such an event, you just have to change this subroutine and it starts working everywhere.

      Cheers!

      1. paul

        Thanks for your replying.
        The alreadyLoggedIn function is really useful in terms of reuse. And it works well in the “under” statement.
        But I tried many times, when calling alreadyLoggedIn in sub function displayLogin, the “get /” failed, warns:

        Mojo::Reactor::Poll: I/O watcher failed: A response has already been rendered at /home/paul/perl5/lib/perl5/Mojolicious/Controller.pm line 154.

        So I have to rewrite it like before to make it work. I guess it is caused by the next statement after “return 1”.

        1. Surprising. The same code that is developed in these articles can be found in GitHub – https://github.com/curioustechnoid/IntroductionToMojolicious
          Can you share your code with me, may be I can have a look ?

          1. Hi Paul,

            I looked at your code and found the cause for the issue you are facing. In the latest version of Mojolicious, it forbids you from rendering more than once which was not the case in past. You can check the change log here in the below link. Check version 9.25 comments.

            https://metacpan.org/dist/Mojolicious/changes

            I tried your code using my old mojolicious docker which has version 9.22 and your code worked like a charm. In case you want to use my docker you can check it here: https://hub.docker.com/r/curioustechnoid/mojolicious

            What you can do with the latest Mojolicious is that, use alreadyLoginIn subroutine to render or redirect based on login validation, and don’t render after this subroutine call. That should solve your problem.

            Hope this helps.

            Cheers!

  3. Peter

    Using perl-5.36.0 (perlbrew)
    # mojo version:
    Perl (v5.36.0, freebsd)
    Mojolicious (9.27, Waffle)

    Had error: A response has already been rendered at /home/mojo/perl5/perlbrew/perls/perl-5.36.0/lib/site_perl/5.36.0/Mojolicious/Controller.pm line 154.
    # trace] [xx5kwL28OTom] Rendering cached template “mojo/debug.html.ep”
    Mojo::Reactor::Poll: I/O watcher failed: A response has already been rendered at /home/mojo/perl5/perlbrew/perls/perl-5.36.0/lib/site_perl/5.36.0/Mojolicious/Controller.pm line 154.

    As the previous response changing this line make it works:
    ## if(&alreadyLoggedIn($self)){
    if($self->session(‘is_auth’)){

    Hope it save somebody frustration and enjoy your tutorial.
    So far is fantastic, easy to understand.

    1. Hi Peter,

      Thank you for taking the time to go through the article. I have changed the code so that others don’t face the same issue.

      Happy Coding.

      Cheers,
      RC

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.