Categories
Hapi

Server-Side Development with Hapi.js — Tokens, JWT, and Secrets

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Create Tokens

We can create tokens with the @hapi/iron module.

For example, we can write:

const Hapi = require('@hapi/hapi');
const iron = require('@hapi/iron')

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        return sealed
      } catch (err) {
        console.log(err);
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call the iron.seal method with the object we want to encrypt, the password to access the encrypted object, and the settings, which is iron.defaults .

Them the sealed string is a scrambled version of the object in string form.

Then to unseal it, we call the iron.unseal method.

For example, we can write:

const Hapi = require('@hapi/hapi');
const iron = require('@hapi/iron')

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        const unsealed = await iron.unseal(sealed, password, iron.defaults);
        return unsealed
      } catch (err) {
        console.log(err);
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to decrypt the encrypted string with the same password with the iron.unseal method.

JSON Web Token

We can create and verify JSON web tokens with the @hapi/jwt module.

For example, we can use it by writing:”

const Hapi = require('@hapi/hapi');
const Jwt = require('@hapi/jwt');
const jwt = require('jsonwebtoken');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register(Jwt);

  server.auth.strategy('my_jwt_stategy', 'jwt', {
    keys: 'some_shared_secret',
    verify: {
      aud: 'urn:audience:test',
      iss: 'urn:issuer:test',
      sub: false,
      nbf: true,
      exp: true,
      maxAgeSec: 14400,
      timeSkewSec: 15
    },
    validate: (artifacts, request, h) => {
      return {
        isValid: true,
        credentials: { user: artifacts.decoded.payload.user }
      };
    }
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        const token = jwt.sign({
          aud: 'urn:audience:test',
          iss: 'urn:issuer:test',
          sub: false,
          maxAgeSec: 14400,
          timeSkewSec: 15
        }, 'some_shared_secret');
        return token;
      },

}
  });

  server.route({
    method: 'GET',
    path: '/secret',
    config: {
      handler(request, h) {
        return 'secret';
      },
      auth: {
        strategy: 'my_jwt_stategy',
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the jsonwebtoken module to create the token.

Then we call server.auth.strategy to add the JWT auth strategy.

The keys have the keys we use to verify the token.

verify has the fields we want to verify.

validate has a function that returns isValid to indicate that the token is valid.

credentials have the data from the decoded token.

Then we call jwt.sign to sign the token in the / route handler.

And in the /secret route, we have the auth.strategy property to set the auth strategy.

Conclusion

We can create various kinds of tokens and verify them with Hapi addons.

Categories
Hapi

Server-Side Development with Hapi.js — File Names, Composing Server Parts, and Process Monitor

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

File Utility

We can generate a file path from path segments with the @hapi/file module.

For example, we can write:

const Hapi = require('@hapi/hapi');
const file = require('@hapi/file');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const fileName = file.uniqueFilename('/root', '.txt');
      return fileName
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call the file.uniqueFilename method to create a unique file name.

We should get something like:

/root/1604965754941-1144-2fbcec5c8927e564.txt

returned as the response.

Compose Different Server Components

We can use the @hapi/glue module to compose different server components.

To use it, we write:

index.js

const Hapi = require('@hapi/hapi');
const Glue = require('@hapi/glue');

const manifest = {
  server: {
    port: 3000,
    host: '0.0.0.0'
  },
  register: {
    plugins: [
      {
        plugin: require('./my-plugin'),
        options: {
          uglify: true
        }
      },
    ],
  }
};

const options = {
  relativeTo: __dirname
};

const init = async () => {
  const server = await Glue.compose(manifest, options);

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return 'hello'
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

my-plugin.js

const myPlugin = {
  name: 'myPlugin',
  version: '1.0.0',
  async register (server, options) {
    server.route({
      method: 'GET',
      path: '/test',
      handler (request, h) {
        return 'hello, world';
      }
    });
  }
};

module.exports = myPlugin

We created a plugin with one route within the my-plugin.js file.

Then in index.js , we have our manifest object with the server options.

register is a property with the plugin properties inside.

The plugins array has an array of plugins we want to register.

options has extra options we want to set for our server.

Then we compose everything together with the Glue.compose method.

It returns the Hapi server object which we can use to add more routes with the server.route method.

Process Monitor

We can add a simple process monitor to our Hapi app with the @hapi/good module.

To add it, we write:

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

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register({
    plugin: require('@hapi/good'),
    options: {
      ops: {
        interval: 1000
      },
      reporters: {
        myConsoleReporter: [
          {
            module: '@hapi/good-squeeze',
            name: 'Squeeze',
            args: [{ log: '*', response: '*', ops: '*' }]
          },
          {
            module: '@hapi/good-console'
          },
          'stdout'
        ]
      }
    }
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return 'hello'
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the reporters property to add a logger for monitoring resource usage.

We add the @hapi/good-squeeze module to add the items to log with the args array.

log has the timestamp, response has the HTTP response returned, and ops has the resource usage.

The @hapi/good-console lets us log the numbers to the console.

Conclusion

We can add various modules to our Hapi app to monitor resource usage, generate file names, and compose server components.

Categories
Hapi

Server-Side Development with Hapi.js — Content-Type and Crypto

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Parsing Content-Type Header

We can parse the Content-Type request header by using the @hapi/content module.

For instance, we can do this by writing:

const Hapi = require('@hapi/hapi');
const Content = require('@hapi/content');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const type = Content.type('application/json; some=property; and="another"');
      return type
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Content.type with the Content-Type request header string to parse it.

Then we get:

{"mime":"application/json"}

as the value of type .

Parse Content-Disposition Header

Also, we can use the Content.disposition method to generate an object from the Content-Disposition request header value.

To do this, we write:

const Hapi = require('@hapi/hapi');
const Content = require('@hapi/content');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const disp = Content.disposition('form-data; name="file"; filename=file.jpg');
      return disp
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{"name":"file","filename":"file.jpg"}

as the value of disp .

CSRF Crumb Generation and Validation

We can generate the CSRF crumb and validate it with the @hapi/crumb module.

For example, we can use it by writing:

const Hapi = require('@hapi/hapi');
const Crumb = require('@hapi/crumb');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register({
    plugin: Crumb,
    options: {}
  });

  server.route({
    path: '/login',
    method: 'GET',
    options: {
      plugins: {
        crumb: {}
      },
      handler(request, h) {
        return 'success'
      }
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We register the plugin with the server.register method.

Then we add a /login route that sets the crumbn property to accept the crumb.

Crypto

We can create random strings to use with our Hapi app with the @hapi/cryptiles module.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomString(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to return a random string response.

We generate the random string with the length 10 with cryptiles.randomString(10) .

Also, we can generate an alphanumeric string with cryptiles.randomAlphanumString:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomAlphanumString(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

And we can generate a random number with cryptiles.randomDigits:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomDigits(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Conclusion

We can parse the Content-Type and Content-Disposition header with the @hapi/content module.

And we can create random strings with the @hapi/cryptiles module.

Categories
Hapi

Server-Side Development with Hapi.js — 400 Series Errors

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Conflict

We can return a 409 response ith @hapi/boom with the Boom.conflict method by writing:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.conflict('conflict');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

Then when we go to the / route, we get:

{"statusCode":409,"error":"Conflict","message":"conflict"}

as the response.

Resource Gone Error

We can return the resource gone error by writing:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.resourceGone('gone');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

We call Boom.resourceGone to create the error object. The argument we pass in is the message.

Then we get:

{"statusCode":410,"error":"Gone","message":"gone"}

as the response when we go to / .

Length Required

We call the Boom.lengthRequired method to return a 411 response.

For example, we can write:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.lengthRequired('length required');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

We pass in the message as the argument.

Then we get:

{"statusCode":411,"error":"Length Required","message":"length required"}

as the response.

Precondition Failed Error

We can call Boom.preconditionFailed to returns a 412 response.

For instance, we can write:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.preconditionFailed('precondition failed');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

to pass in a message.

Entity Too Large

To return a 413 entity too large response, we call Boom.entityTooLarge .

For instance, we can write:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.entityTooLarge('too big');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

to throw the error.

And we get:

{"statusCode":413,"error":"Request Entity Too Large","message":"too big"}

as the response.

Unsupported Media Type

We can call Boom.unsupportedMediaType to throw a 415 unsupported media type error:

const Hapi = require('@hapi/hapi');  
const Boom = require('@hapi/boom');  
const init = async () => {  
  const server = new Hapi.Server({  
    port: 3000,  
    host: '0.0.0.0'  
  });  
  server.route({  
    method: 'GET',  
    path: '/',  
    handler(request, h) {        
      throw Boom.unsupportedMediaType('that media is not supported');  
    }  
  });  
  await server.start();  
  console.log('Server running at:', server.info.uri);  
};  
process.on('unhandledRejection', (err) => {  
  console.log(err);  
  process.exit(1);  
});  
init();

Then we get:

{"statusCode":415,"error":"Unsupported Media Type","message":"that media is not supported"}

as the response.

Conclusion

We can return kinds of error responses with Hapi Boom.

Categories
Hapi

Server-Side Development with Hapi.js — Throw 400-Series Errors

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Method Not Allowed Error

We can throw a method not allowed error with @hapi/boom ‘s methodNotAllowed method.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.methodNotAllowed('that method is not allowed');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{"statusCode":405,"error":"Method Not Allowed","message":"that method is not allowed"}

as the response.

We can set the Allow header with the 3rd argument.

To do this, we write:

const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.methodNotAllowed('that method is not allowed', 'data', ['foo', 'bar']);
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Now we should see that Allow response header has foo, bar as its value.

Not Acceptable Error

We can call the Boom.notAcceptable method to return a 406 error.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.notAcceptable('unacceptable');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{
    "statusCode": 406,
    "error": "Not Acceptable",
    "message": "unacceptable"
}

as the response.

We can also pass in extra data into the 2nd argument.

Proxy Required Error

We call the Boom.proxyAuthRequired method to return a 407 response.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.proxyAuthRequired('auth missing');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{
    "statusCode": 407,
    "error": "Proxy Authentication Required",
    "message": "auth missing"
}

as the response.

We can also pass in more data as the 2nd argument.

Request Timeout

We can throw a request timeout error with the Boom.clientTimeout method:

const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      throw Boom.clientTimeout('timed out');
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};
process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{"statusCode":408,"error":"Request Time-out","message":"timed out"}

as the response.

We can also pass in more data as the 2nd argument.

Conclusion

We can throw many types of errors easily with the @hapi/boom module.