DEV Community

Ashutosh
Ashutosh

Posted on

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'
    );
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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'
);
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Couple things to notice,

 my $username = $c->param('username');
 my $password = $c->param('password');
Enter fullscreen mode Exit fullscreen mode

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' => {})
Enter fullscreen mode Exit fullscreen mode

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

validate_user => sub {}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

 my $auth_key = $c->authenticate($username, $password );
Enter fullscreen mode Exit fullscreen mode

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

We also set session

$c->session(user => $user_key);
Enter fullscreen mode Exit fullscreen mode

$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 )
}
Enter fullscreen mode Exit fullscreen mode

Or use this,

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

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

Enter fullscreen mode Exit fullscreen mode

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');           
        }
}
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (4)

Collapse
 
bkerin profile image
bkerin

Seems chrome is watching and helping because when I enter foouser foopass into the fields then submit I get a popup saying "Please include an '@' in the email address. 'userfoo is missing an '@'". That doesn't seem to be coming from your code.

Collapse
 
bkerin profile image
bkerin

I guess this is due to type="email" being used in the form

Collapse
 
bkerin profile image
bkerin

Where's the code that's giving you the pretty user sign in popop? For me it comes up in style-free HTML page

Collapse
 
bkerin profile image
bkerin

ok its from dev.to/akuks/how-to-do-the-user-re... linked at the top