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:
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.
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.
In CustomController.pm, you can drop the ampersands on sub routine calls.
example:
&welcome($self);
welcome($self);
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.
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”);
}
}
###########
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!
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”.
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 ?
Of course, thank you ! I put it here:
https://github.com/Paul-sinbud2004/Mojolicious_notes/blob/main/xpsweb/lib/xpsweb/Controller/Xpscontroller.pm
The main issue is in line 23 of this file.
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!
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.
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