Web Application
Node.js (Hono)

MojoAuth Hosted Login Page with Node.js (Hono)

Introduction

Hono is a small, simple, and ultrafast web framework for the edge and serverless environments. It supports various runtime environments including Cloudflare Workers, Fastly Compute, Deno, Bun, and Node.js. Hono (meaning "flame" in Japanese) focuses on providing high performance with a developer-friendly API.

This guide will walk you through connecting your Hono 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 (v16.0.0 or higher) installed on your development machine
  4. Basic knowledge of Hono framework

Install Required Packages

Create a new Hono project and install the required dependencies:

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

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 SESSION_SECRET=your-session-secret

Initialize Hono with OIDC

Create a new file called index.js and set up your Hono application with OIDC:

import { Hono } from 'hono'; import { serve } from '@hono/node-server'; import { logger } from 'hono/logger'; import { secureHeaders } from 'hono/secure-headers'; import { Issuer, generators } from 'openid-client'; import { session } from 'hono/session'; import dotenv from 'dotenv';   // Load environment variables dotenv.config();   // Initialize Hono app const app = new Hono();   // Middlewares app.use('*', logger()); app.use('*', secureHeaders()); app.use('*', session({  secret: process.env.SESSION_SECRET,  cookie: {  httpOnly: true,  secure: process.env.NODE_ENV === 'production',  sameSite: 'lax'  } }));   // OIDC client initialization let client; async function initializeOIDC() {  try {  const mojoAuthIssuer = await Issuer.discover(process.env.MOJOAUTH_ISSUER);  console.log(`Discovered issuer ${mojoAuthIssuer.issuer}`);    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);  } }   // Initialize OIDC client initializeOIDC();   // Authentication middleware async function authenticate(c, next) {  const user = c.get('session')?.user;  if (!user) {  return c.redirect('/login');  }  await next(); }   // Routes app.get('/', async (c) => {  const user = c.get('session')?.user;    const html = `  <!DOCTYPE html>  <html lang="en">  <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>MojoAuth OIDC Hono Example</title>  <style>  body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }  </style>  </head>  <body>  <h1>MojoAuth OIDC Hono 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>`  }  </body>  </html>  `;    return c.html(html); });   app.get('/login', async (c) => {  // Ensure client is initialized  if (!client) {  return c.text('OIDC client not initialized', 500);  }    // 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  c.set('session', {  ...c.get('session'),  code_verifier,  state  });    // Generate authorization URL  const authorizationUrl = client.authorizationUrl({  scope: 'openid profile email',  code_challenge,  code_challenge_method: 'S256',  state  });    return c.redirect(authorizationUrl); });   app.get('/callback', async (c) => {  try {  const session = c.get('session') || {};    // Get the callback parameters from the request  const params = client.callbackParams(c.req.raw);    // 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  c.set('session', {  ...session,  user: userinfo,  tokens: {  access_token: tokenSet.access_token,  id_token: tokenSet.id_token,  expires_at: tokenSet.expires_at  }  });    // Redirect to profile page  return c.redirect('/profile');  } catch (error) {  console.error('Error during callback processing:', error);  return c.text('Authentication failed', 500);  } });   app.get('/profile', authenticate, async (c) => {  const user = c.get('session').user;    const html = `  <!DOCTYPE html>  <html lang="en">  <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Profile - MojoAuth OIDC Hono Example</title>  <style>  body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }  pre { background: #f4f4f4; padding: 10px; border-radius: 5px; }  </style>  </head>  <body>  <h1>Profile</h1>  <p>Welcome ${user.name || user.email || 'User'}!</p>    <h2>User Information</h2>  <pre>${JSON.stringify(user, null, 2)}</pre>    <p><a href="/logout">Logout</a></p>  </body>  </html>  `;    return c.html(html); });   app.get('/logout', async (c) => {  // Clear the user session  c.set('session', {});  return c.redirect('/'); });   // Start server const port = process.env.PORT || 3000; console.log(`Server is running on port ${port}`);   serve({  fetch: app.fetch,  port });

Using ESM

For the above code to work, you need to configure your project to use ESM. Update your package.json file to include:

{  "name": "mojoauth-hono-example",  "version": "1.0.0",  "description": "MojoAuth Hosted Login Page integration with Hono",  "main": "index.js",  "type": "module",  "scripts": {  "start": "node index.js",  "dev": "nodemon index.js"  },  "keywords": [  "hono",  "mojoauth",  "oidc",  "authentication"  ],  "author": "",  "license": "MIT",  "dependencies": {  "@hono/node-server": "^1.0.0",  "dotenv": "^16.0.0",  "hono": "^3.0.0",  "openid-client": "^5.0.0"  },  "devDependencies": {  "nodemon": "^2.0.15"  } }

Using TypeScript (Optional)

Hono works exceptionally well with TypeScript. Here's how you would implement the same application with TypeScript:

  1. Install TypeScript and required types:
npm install typescript ts-node @types/node --save-dev
  1. Create a tsconfig.json file:
{  "compilerOptions": {  "target": "es2022",  "module": "NodeNext",  "moduleResolution": "NodeNext",  "esModuleInterop": true,  "strict": true,  "outDir": "dist"  },  "include": ["**/*.ts"] }
  1. Create index.ts with type annotations:
import { Hono } from 'hono'; import { serve } from '@hono/node-server'; import { logger } from 'hono/logger'; import { secureHeaders } from 'hono/secure-headers'; import { Issuer, generators, Client } from 'openid-client'; import { session } from 'hono/session'; import dotenv from 'dotenv';   // Define session type declare module 'hono' {  interface ContextVariableMap {  session: {  user?: any;  tokens?: {  access_token: string;  id_token: string;  expires_at: number;  };  code_verifier?: string;  state?: string;  };  } }   // Load environment variables dotenv.config();   // Initialize Hono app const app = new Hono();   // Middlewares app.use('*', logger()); app.use('*', secureHeaders()); app.use('*', session({  secret: process.env.SESSION_SECRET || 'default-secret',  cookie: {  httpOnly: true,  secure: process.env.NODE_ENV === 'production',  sameSite: 'lax'  } }));   // OIDC client initialization let client: Client | undefined; async function initializeOIDC() {  try {  const mojoAuthIssuer = await Issuer.discover(process.env.MOJOAUTH_ISSUER || '');  console.log(`Discovered issuer ${mojoAuthIssuer.issuer}`);    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);  } }   // Initialize OIDC client initializeOIDC();   // Routes with TypeScript types // ... same routes as in the JavaScript example

Testing the Flow

  1. Start your Hono application:

    npm start
  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

Edge Deployment Considerations

One of Hono's strengths is its ability to run in edge environments like Cloudflare Workers. When deploying to these environments:

  1. Environment Variables: Use the platform-specific way to set environment variables
  2. Session Storage: Consider using a distributed storage for sessions like KV store
  3. Node.js-specific Dependencies: Replace openid-client with OIDC libraries compatible with your edge runtime

Next Steps

  1. API Protection: Add protected API routes using the authenticate middleware
  2. Role-based Access: Implement authorization based on user claims
  3. Token Validation: Add JWT validation middleware
  4. Error Handling: Create better error handling with custom error pages
  5. UI Enhancement: Add a modern UI framework like Tailwind CSS
  6. Edge Deployment: Deploy your app to edge computing platforms like Cloudflare Workers or Vercel Edge Functions

Reference Links