Web Application
Node.js (Hapi)

MojoAuth Hosted Login Page with Node.js (Hapi)

Introduction

Hapi is a rich framework for building applications and services. It enables developers to focus on writing reusable application logic instead of spending time building infrastructure. Hapi was created by the mobile team at Walmart Labs to handle their Black Friday scale and is well suited for building APIs, websites, and backend services.

This guide will walk you through the process of connecting your Hapi application to MojoAuth's Hosted Login Page using OpenID Connect (OIDC).

Links:

Prerequisites

Before you begin, make sure you have:

  1. A MojoAuth account and an OIDC application set up in the MojoAuth dashboard
  2. Your Client ID and Client Secret from the MojoAuth dashboard
  3. Node.js (v14.0.0 or higher) installed on your development machine
  4. Basic knowledge of Hapi.js

Install Required Packages

Create a new Hapi project and install the required dependencies:

# Create a new directory for your project mkdir mojoauth-hapi-example cd mojoauth-hapi-example   # Initialize a new Node.js project npm init -y   # Install required dependencies npm install @hapi/hapi @hapi/cookie openid-client dotenv @hapi/vision @hapi/inert

Configure Environment Variables

Create a .env file in your project root directory to store your MojoAuth credentials:

MOJOAUTH_CLIENT_ID=your-client-id MOJOAUTH_CLIENT_SECRET=your-client-secret MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com MOJOAUTH_REDIRECT_URI=http://localhost:3000/callback COOKIE_PASSWORD=your-cookie-encryption-password-min-32-chars

Initialize the Hapi Server with OIDC

Create a new file called server.js and set up your Hapi application with OIDC:

'use strict';   const Hapi = require('@hapi/hapi'); const Cookie = require('@hapi/cookie'); const Vision = require('@hapi/vision'); const Inert = require('@hapi/inert'); const { Issuer, generators } = require('openid-client'); require('dotenv').config();   // OIDC client initialization let client;   const init = async () => {    // Create Hapi server  const server = Hapi.server({  port: process.env.PORT || 3000,  host: 'localhost'  });    // Register plugins  await server.register([  Inert,   Vision,   Cookie  ]);    // Configure cookie authentication strategy  server.auth.strategy('session', 'cookie', {  cookie: {  name: 'mojoauth_session',  password: process.env.COOKIE_PASSWORD,  isSecure: false, // Set to true in production with HTTPS  path: '/',  },  validateFunc: async (request, session) => {  // Check if session has user data  if (!session.user) {  return { valid: false };  }    // Session is valid  return { valid: true, credentials: session };  }  });    // Set default authentication strategy  server.auth.default('session');    // Initialize OIDC client  try {  const mojoAuthIssuer = await Issuer.discover(process.env.MOJOAUTH_ISSUER);  console.log('Discovered issuer %s %O', mojoAuthIssuer.issuer, mojoAuthIssuer.metadata);    client = new mojoAuthIssuer.Client({  client_id: process.env.MOJOAUTH_CLIENT_ID,  client_secret: process.env.MOJOAUTH_CLIENT_SECRET,  redirect_uris: [process.env.MOJOAUTH_REDIRECT_URI],  response_types: ['code'],  });    console.log('OIDC Client initialized successfully');  } catch (error) {  console.error('Failed to initialize OIDC client:', error);  }    // Define routes  await setupRoutes(server);    await server.start();  console.log('Server running on %s', server.info.uri); };   const setupRoutes = async (server) => {  // Home route  server.route({  method: 'GET',  path: '/',  options: {  auth: { mode: 'try' },  },  handler: (request, h) => {  const isAuthenticated = request.auth.isAuthenticated;  const user = isAuthenticated ? request.auth.credentials.user : null;    return `  <h1>MojoAuth OIDC Hapi Example</h1>  ${user ?   `<p>Logged in as ${user.name || user.email || 'User'}</p>  <a href="/profile">View Profile</a> |   <a href="/logout">Logout</a>` :   `<a href="/login">Login with MojoAuth</a>`  }  `;  }  });    // Login route  server.route({  method: 'GET',  path: '/login',  options: {  auth: { mode: 'try' },  },  handler: (request, h) => {  // Store a code_verifier for PKCE  const code_verifier = generators.codeVerifier();    // Generate a code_challenge from the code_verifier  const code_challenge = generators.codeChallenge(code_verifier);    // Generate state to prevent CSRF  const state = generators.state();    // Store values in session  request.cookieAuth.set({   code_verifier,  state  });    // Generate authorization URL  const authorizationUrl = client.authorizationUrl({  scope: 'openid profile email',  code_challenge,  code_challenge_method: 'S256',  state  });    return h.redirect(authorizationUrl);  }  });    // Callback route  server.route({  method: 'GET',  path: '/callback',  options: {  auth: { mode: 'try' },  },  handler: async (request, h) => {  try {  // Get session data  const session = request.auth.credentials || {};    // Get the callback parameters from the request  const params = client.callbackParams(request.raw.req);    // Verify the parameters against the stored state and code_verifier  const tokenSet = await client.callback(  process.env.MOJOAUTH_REDIRECT_URI,  params,  {  code_verifier: session.code_verifier,  state: session.state  }  );    // Get user info  const userinfo = await client.userinfo(tokenSet.access_token);    // Update session with user data and token  request.cookieAuth.set({   user: userinfo,  tokens: {  access_token: tokenSet.access_token,  id_token: tokenSet.id_token,  expires_at: tokenSet.expires_at  }  });    // Redirect to profile page  return h.redirect('/profile');  } catch (error) {  console.error('Error during callback processing:', error);  return h.response('Authentication failed').code(500);  }  }  });    // Profile route  server.route({  method: 'GET',  path: '/profile',  options: {  auth: 'session'  },  handler: (request, h) => {  const user = request.auth.credentials.user;    return `  <h1>Profile</h1>  <p>Welcome ${user.name || user.email || 'User'}!</p>  <pre>${JSON.stringify(user, null, 2)}</pre>  <a href="/logout">Logout</a>  `;  }  });    // Logout route  server.route({  method: 'GET',  path: '/logout',  options: {  auth: { mode: 'try' }  },  handler: (request, h) => {  request.cookieAuth.clear();  return h.redirect('/');  }  }); };   process.on('unhandledRejection', (err) => {  console.log(err);  process.exit(1); });   init();

Complete Example

To use the complete example:

  1. Save the code above as server.js
  2. Create a .env file with your MojoAuth credentials
  3. Install the required dependencies
  4. Start the server with node server.js

Here's the complete package.json file for reference:

{  "name": "mojoauth-hapi-example",  "version": "1.0.0",  "description": "MojoAuth Hosted Login Page integration with Hapi.js",  "main": "server.js",  "scripts": {  "start": "node server.js",  "dev": "nodemon server.js"  },  "keywords": [  "hapi",  "mojoauth",  "oidc",  "authentication"  ],  "author": "",  "license": "MIT",  "dependencies": {  "@hapi/cookie": "^11.0.0",  "@hapi/hapi": "^20.2.0",  "@hapi/inert": "^6.0.0",  "@hapi/vision": "^6.0.0",  "dotenv": "^10.0.0",  "openid-client": "^5.0.0"  },  "devDependencies": {  "nodemon": "^2.0.15"  } }

Testing the Flow

  1. Start your Hapi application:

    node server.js
  2. Open your browser and navigate to http://localhost:3000

  3. Click on "Login with MojoAuth"

  4. You will be redirected to MojoAuth's Hosted Login Page

  5. After successful authentication, you will be redirected back to your application's callback URL

  6. The application will process the authentication response and redirect you to the profile page

  7. Your user profile information will be displayed on the profile page

Next Steps

  1. Enhanced Security: Implement HTTPS in production and set isSecure: true in your cookie configuration
  2. Token Validation: Add middleware to validate tokens before processing requests
  3. Error Handling: Implement better error handling with user-friendly messages
  4. Logging: Add structured logging for better debugging
  5. Rate Limiting: Implement rate limiting to protect your authentication endpoints
  6. Role-based Access Control: Add authorization based on user claims

Reference Links