How to do Authentication in Mojolicious

Hello Mojo Learners, this article is in continuation of my previous article, How to add the User registration in Mojolicious. If you haven't finished the earlier blog, You can visit it here.

User authentication is one of the essential features for every web application and, Mojolicious has no exemptions for it. There are two methods for adding Authentication in Mojolicious:

  1. Use Mojolicious::Plugin:: Authentication, OOB plugin provided by Mojolicious.
  2. Create your custom Authentication.

We will use the former approach to add the authentication in our Application. Indeed, this plugin is not as powerful as provided by the Catalyst. We need to write a piece of code to make it more robust.

Let's pen down the tasks that need to be accomplished for auth implementation.

  1. The route (with get request) to get the login page.
  2. Login Controller to load the login template.
  3. Login template to show the login form to the user.
  4. Post request to submit the data to the server.
  5. Action in the controller to handle the login post request.
  6. Redirect URL with the login success message.
  7. Prevent unauthorized access to the redirect URL.

Add Route in App File

Open the MyApp.pm file and add the route under the startup subroutine.

    $r->get('/login')->to(
        controller => 'LoginController',
        action     => 'index'
    );

Create the controller LoginController under the Controller folder and add the index action subroutine.

package MyApp::Controller::LoginController;
use Mojo::Base 'Mojolicious::Controller';

sub index {
    my $c = shift;

    $c->render(
        template => 'login',
        error    => $c->flash('error')
    );
}

1;

We render the template login, and error message to show the users if there is any.

Create the login (login.html.ep) template under the templates folder.

% layout 'default';

<br /> <br />

<div class="container">
    <div class="card col-sm-6 mx-auto">
        <div class="card-header text-center">
            User Sign In 
        </div>
        <br /> <br />
        <form method="post" action='/login'>
            <input class="form-control" 
                   id="username" 
                   name="username" 
                   type="email" size="40"
                   placeholder="Enter Username" 
             />
            <br /> 
            <input class="form-control" 
                   id="password" 
                   name="password" 
                   type="password" 
                   size="40" 
                   placeholder="Enter Password" 
             />     
            <br /> 
            <input class="btn btn-primary" type="submit" value="Sign In">
            <br />  <br />
        </form>

        % if ($error) {
            <div class="error" style="color: red">
                <small> <%= $error %> </small>
            </div>
        %}
    </div>

</div>

Sign In looks like when you hit the localhost:3000/login

What Next?

We need to create the post request, to verify the credentials. Lets create it on the MyApp.pm

$r->post('/login')->to(
    controller => 'LoginController',
  action     => 'user_login'
);

Under the startup subroutine, we create post request for submitting the credentials. Request goes to the LoginController and action is user_login (Subroutine under the LoginController package.)

Next task is to create the user_login subroutine under the controller.

sub user_login {
    my $c = shift;

    my $username = $c->param('username');                               # From the form
    my $password = $c->param('password');                               # From the form

    my $db_object = $c->app->{_dbh};

    $c->app->plugin('authentication' => {
        autoload_user   => 1,
        wickedapp       => 'YouAreLogIn',
        load_user       => sub {
            my ($c, $user_key) = @_;
            my @user = $db_object->resultset('User')->search({
                id => $user_key
            });

            return \@user;
        },
        validate_user   => sub { 
            my ($c, $username, $password) = @_; 

            my $user_key = validate_user_login($db_object, $username, $password);

            if ( $user_key ) {
                $c->session(user => $user_key);
                return $user_key;
            }
            else {
                return undef;
            }
        },
    });

    my $auth_key = $c->authenticate($username, $password );

    if ( $auth_key )  {
        $c->flash( message => 'Login Success.');
        return $c->redirect_to('/books');
    }
    else {
        $c->flash( error => 'Invalid username or password.');
        $c->redirect_to('login');
    }
}

# Validate user from database
sub validate_user_login {
    my ($dbh, $username, $password) = @_;

    my $user = $dbh->resultset('User')->search({
        email => $username,
    });

    if (! $user->first ) {
        return 0;
    }
    else {        
        return ( validate_password( 
            $user->first->password, $password ) 
        ) ? $user->first->id : 0;
    }
}

# To validate the Password
sub validate_password {
    my ($user_pass, $password) = @_;

    my $pbkdf2 = Crypt::PBKDF2->new(
        hash_class => 'HMACSHA1', 
        iterations => 1000,       
        output_len => 20,        
        salt_len => 4,           
    );

    if ( $pbkdf2->validate($user_pass, $password) ) {
        return 1
    }
}

Couple things to notice,

 my $username = $c->param('username');
 my $password = $c->param('password');

The above method is to pass the parameters from a web page to the Mojolicious framework. Also these are the body parameters not the query parameters (Query parameters stored in differently. We will cover in another tutorial).

We need to use the authentication plugin, if not already installed, Please install Mojolicious::Plugin::Authentication using the cpan/cpanm. Load the plugin.

 $c->app->plugin('authentication' => {})

When the plugin is loaded, it validates the user first, using the

validate_user => sub {}

In this subroutine, We are calling another subroutine validate_user_login to verify the if the user passed correct credentials to our app.

If you remember in our previous tutorial, we saved the password in encrypted form in the database and we created validate_password() for password validation.

$pbkdf2->validate($user_pass, $password)

And if everything goes well, authenticate plugin return the user_id to the $auth_key variable.

 my $auth_key = $c->authenticate($username, $password );

If everything goes well, our app redirect to /books path url.

We also set session

$c->session(user => $user_key);

$user_key is the user id but we can use anything.

The good practice is to use encryption_key. There must be column created in the database user table and update this value with a random string generated at the time of the user creation. And this key should be unique. Why this approach is better than the using the user Id? Normally Id's are auto incremented and if the Id of one user is 1, it is not hard to guess the id of the second user. But lets we got the encryption_key of the user as "A6E4HTRUWE". We can't guess whats the key of another user.

Let's get back to this project.

There is one problem right now, if we open 'http://localhost:3000/books', we can access that. In order to protect it. Either we can add the below code the Books Controller.

if ( ! $c->session('user') ) {
        $c->redirect_to('login');
}
else {
        $c->render (template => 'books', books => \@books )
}

Or use this,

my $auth_required = $r->under('/')->to('LoginController#user_exists');

$authorized->get('/')->name('/books')->to(
    controller => 'LoginController', action => 'index'
); 

And in the login controller use the following:

sub user_exists {
        my $c = shift;
        if ( $c->session('user') ) {
                return 1;
        }
        else {
                $c->redirect_to('login');           
        }
}

You can use of the above approach to prototect the routes for unauthorised access.

Happy Coding in Mojolicious. See you in the next tutorial.