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:
- MojoAuth Hosted Login Page Documentation (opens in a new tab)
- openid-client Documentation (opens in a new tab)
- Hapi Documentation (opens in a new tab)
Prerequisites
Before you begin, make sure you have:
- A MojoAuth account and an OIDC application set up in the MojoAuth dashboard
- Your Client ID and Client Secret from the MojoAuth dashboard
- Node.js (v14.0.0 or higher) installed on your development machine
- 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:
- Save the code above as
server.js
- Create a
.env
file with your MojoAuth credentials - Install the required dependencies
- 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
-
Start your Hapi application:
node server.js
-
Open your browser and navigate to
http://localhost:3000
-
Click on "Login with MojoAuth"
-
You will be redirected to MojoAuth's Hosted Login Page
-
After successful authentication, you will be redirected back to your application's callback URL
-
The application will process the authentication response and redirect you to the profile page
-
Your user profile information will be displayed on the profile page
Next Steps
- Enhanced Security: Implement HTTPS in production and set
isSecure: true
in your cookie configuration - Token Validation: Add middleware to validate tokens before processing requests
- Error Handling: Implement better error handling with user-friendly messages
- Logging: Add structured logging for better debugging
- Rate Limiting: Implement rate limiting to protect your authentication endpoints
- Role-based Access Control: Add authorization based on user claims