Saturday, August 24, 2019

Authentication in hapi

Overview

Most modern web apps use some form of authentication. Authentication within hapi is based on the concept of schemes and strategiesSchemes are a way of handling authentication within hapi. For example, the @hapi/basic and @hapi/cookie plugins would be considered schemes. A strategy is a pre-configured instance of a scheme. You use strategies to implement authentication schemes into your application.

Schemes

scheme is a method with the signature function (server, options). The server parameter is a reference to the server the scheme is being added to, while the options parameter is the configuration object provided when registering a strategy that uses this scheme.
This method must return an object with at least the key authenticate. Other optional methods that can be used are payload and response.
You can either write your own authentication scheme, or use one of the many hapi auth plugins, such as hapi-auth-basic or hapi-auth-cookie.

authenticate

The authenticate method has a signature of function (request, h), and is the only requiredmethod in a scheme.
In this context, request is the request object created by the server. It is the same object that becomes available in a route handler, and is documented in the API reference.
h is the standard hapi response toolkit.
When authentication is successful, you must call and return h.authenticated({ credentials, artifacts })credentials property is an object representing the authenticated user (or the credentials the user attempted to authenticate with). Additionally, you may also have an artifacts key, which can contain any authentication related data that is not part of the user's credentials.
The credentials and artifacts properties can be accessed later (in a route handler, for example) as part of the request.auth object.
If authentication is unsuccessful, you can either throw an error or call and return h.unauthenticated(error, [data]) where error is an authentication error and data is an optional object containing credentials and artifacts. There's no difference between calling return h.unauthenticated(error) or throwing an error if no data object is provided. The specifics of the error passed will affect the behavior. More information can be found in the API documentation for server.auth.scheme(name, scheme). It is recommend to use boom for errors.

payload

The payload method has the signature function (request, h).
Again, the standard hapi response toolkit is available here. To signal a failure throw an error, again it's recommended to use boom for errors.
To signal a successful authentication, return h.continue.

response

The response method also has the signature function (request, h) and utilizes the standard response toolkit.
This method is intended to decorate the response object (request.response) with additional headers, before the response is sent to the user.
Once any decoration is complete, you must return h.continue, and the response will be sent.
If an error occurs, you should instead throw an error where the error is recommended to be a boom.

Strategies

Once you've registered your scheme, you need a way to use it. This is where strategies come in.
As mentioned above, a strategy is essentially a pre-configured instance of a scheme.
To register a strategy, you must first have a scheme registered. Once that's complete, use server.auth.strategy(name, scheme, [options]) to register your strategy.
The name parameter must be a string, and will be used later to identify this specific strategy. scheme is also a string, and is the name of the scheme this strategy is to be an instance of. The options parameter is use to customize the options of the strategy.
server.auth.strategy('session', 'cookie', {
    name: 'sid-example',
    password: '!wsYhFA*C2U6nz=Bu^%A@^F#SF3&kSR6',
    isSecure: false
});
In the above example, you register the strategy with server.auth.strategy(). You name the strategy session, and say that you are using the cookie scheme. Lastly, you configure the strategy by giving it a namepassword, and setting isSecure: false.

Default Strategy

You may set a default strategy by using server.auth.default().
This method accepts one parameter, which may be either a string with the name of the strategy to be used as default, or an object in the same format as the route handler's auth options.
Note that any routes added before server.auth.default() is called will not have the default applied to them. If you need to make sure that all routes have the default strategy applied, you must either call server.auth.default() before adding any of your routes, or set the default mode when registering the strategy.

Route Configuration

Authentication can also be configured on a route, by the options.auth parameter. If set to false, authentication is disabled for the route.
It may also be set to a string with the name of the strategy to use, or an object with modestrategies, and payload parameters.
The mode parameter may be set to 'required''optional', or 'try' and works the same as when registering a strategy.
If set to 'required', in order to access the route, the user must be authenticated, and their authentication must be valid, otherwise they will receive an error.
If mode is set to 'optional' the strategy will still be applied to the route but in this case the user does not need to be authenticated. Authentication data is optional, but must be valid if provided.
The last mode setting is 'try'. The difference between 'try' and 'optional' is that with 'try'invalid authentication is accepted, and the user will still reach the route handler.
When specifying one strategy, you may set the strategy property to a string with the name of the strategy. When specifying more than one strategy, the parameter name must be strategies and should be an array of strings each naming a strategy to try. The strategies will then be attempted in order until one succeeds, or they have all failed.
Lastly, the payload parameter can be set to false denoting the payload is not to be authenticated, 'required' or true meaning that it must be authenticated, or 'optional' meaning that if the client includes payload authentication information, the authentication must be valid.
The payload parameter is only possible to use with a strategy that supports the payload method in its scheme.

basic

The first scheme we will look at is the @hapi/basic plugin. Just like the name says, the @hapi/basicplugin uses basic authentication to validate users. Here is an example of setting up @hapi/basic:
'use strict';

const Bcrypt = require('bcrypt');
const Hapi = require('@hapi/hapi');

const users = {
    john: {
        username: 'john',
        password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm',   // 'secret'
        name: 'John Doe',
        id: '2133d32a'
    }
};

const validate = async (request, username, password) => {

    const user = users[username];
    if (!user) {
        return { credentials: null, isValid: false };
    }

    const isValid = await Bcrypt.compare(password, user.password);
    const credentials = { id: user.id, name: user.name };

    return { isValid, credentials };
};

const start = async () => {

    const server = Hapi.server({ port: 4000 });

    await server.register(require('@hapi/basic'));

    server.auth.strategy('simple', 'basic', { validate });

    server.route({
        method: 'GET',
        path: '/',
        options: {
            auth: 'simple'
        },
        handler: function (request, h) {

            return 'welcome';
        }
    });

    await server.start();

    console.log('server running at: ' + server.info.uri);
};

start();
First, you define your users database, which is a simple object in this example. Then you define a validation function, which is a feature specific to @hapi/basic and allows you to verify that the user has provided valid credentials. For this validation function, you use Bcrypt to compare the user provided password with the hashed password in your database.
Next, you register the plugin, which creates a scheme with the name of basic. This is done within the plugin via server.auth.scheme().
Once the plugin has been registered, you use server.auth.strategy() to create a strategy with the name of simple that refers to your scheme named basic. You also pass an options object that gets passed to the scheme and allows you to configure its behavior.
The last thing you do is tell a route to use the strategy named simple for authentication.

cookie

@hapi/cookie is a plugin that will store a cookie in the users browser once they are authenticated. This has the option of keeping the user logged in, even after they leave the site. Here is an example of setting up @hapi/cookie:
In this example, the home route, "/", is restricted and can only be accessed once a user has authenticated themselves:
'use strict';

const Bcrypt = require('bcrypt');
const Hapi = require('@hapi/hapi');

const users = [
    {
        username: 'john',
        password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm',   // 'secret'
        name: 'John Doe',
        id: '2133d32a'
    }
];

const start = async () => {

    const server = Hapi.server({ port: 4000 });

    await server.register(require('@hapi/cookie'));

    server.auth.strategy('session', 'cookie', {
        cookie: {
            name: 'sid-example',
            password: '!wsYhFA*C2U6nz=Bu^%A@^F#SF3&kSR6',
            isSecure: false
        },
        redirectTo: '/login',
        validateFunc: async (request, session) => {

            const account = await users.find(
                (user) => (user.id === session.id)
            );

            if (!account) {

                return { valid: false };
            }

            return { valid: true, credentials: account };
        }
    });

    server.auth.default('session');

    server.route([
        {
            method: 'GET',
            path: '/',
            handler: function (request, h) {

                return 'Welcome to the restricted home page!';
            }
        },
        {
            method: 'GET',
            path: '/login',
            handler: function (request, h) {

                return ` <html>
                            <head>
                                <title>Login page</title>
                            </head>
                            <body>
                                <h3>Please Log In</h3>
                                <form method="post" action="/login">
                                    Username: <input type="text" name="username"><br>
                                    Password: <input type="password" name="password"><br/>
                                <input type="submit" value="Login"></form>
                            </body>
                        </html>`;
            },
            options: {
                auth: false
            }
        },
        {
            method: 'POST',
            path: '/login',
            handler: async (request, h) => {

                const { username, password } = request.payload;
                const account = users.find(
                    (user) => user.username === username
                );

                if (!account || !(await Bcrypt.compare(password, account.password))) {

                    return h.view('/login');
        }

                request.cookieAuth.set({ id: account.id });

                return h.redirect('/');
             }
        }
    ]);

    await server.start();

    console.log('server running at: ' + server.info.uri);
};

start();
First, you need to do is register the @hapi/cookie plugin with server.register. Once the plugin is registered, you configure your strategy by calling server.auth.strategyserver.auth.strategytakes three parameters: name of the strategy, what scheme you are using, and an options object. For your strategy, you name it session. For the scheme, you will be using the cookie scheme. If you were using @hapi/basic, this parameter would be basic. The last parameter is an options object. This is how you can customized your auth strategy to fit your needs.
The first property you configure is the cookie object. In your strategy, you will configure three properties of the cookie object. First, you set the name of the cookie, in this case sid-example. Next, you set the password that will be used to encrypt the cookie. This should be at least 32 characters long. Last, you set isSecure to false. This is ok for development while working over HTTP. In production, this should be switched back to true, which is the default setting.
The next property is redirectTo. This will tell the server where to redirect to if an unauthenticated user tries to access a resource that requires authentication.
The last property is the validateFunc function. The validateFunc validates that a current cookie is still valid. For example, if a user authenticates themselves successfully, receives a cookie, and then leaves the site. Once they return, the validateFunc will check if their current cookie is still valid.
You setup the default strategy by calling server.auth.default('session'). This will set the default auth strategy for all routes.
Once your strategy is set up, you need to set up route that will validate the provided username and password. In this case, your POST route to '/login' will do just that. First, it will pull the usernameand password from request.payload, which the user provided in the form from the '/login''GET' route. Next, you find the user from the database by searching for their username:
const account = users.find(
    (user) => user.username === username
);
If the user doesn't not exists, or if the provided password is wrong, you redirect the user back to the login page. You use Bcrypt to compare the user provided password with the hashed password from the database.

No comments:

Post a Comment

No String Argument Constructor/Factory Method to Deserialize From String Value

  In this short article, we will cover in-depth the   JsonMappingException: no String-argument constructor/factory method to deserialize fro...