API Documentation
/
Backend
/
Node.js
/
Integrate in Existing Node.js Project

Integrate in Existing Node.js Project

Add Hawcx authentication to an existing Node.js application

Integrating Hawcx Backend SDK into Existing Node.js Projects

This guide walks through adding Hawcx OAuth authentication to an existing Node.js backend.

Step 1: Install the SDK

npm install @hawcx/oauth-client

Step 2: Set Up Environment Variables

Create a .env file or configure your environment with:

# OAuth Configuration (required)
HAWCX_API_KEY=your_api_key
OAUTH_TOKEN_ENDPOINT=https://example.hawcx.com/oauth2/token
OAUTH_CLIENT_ID=your_client_id
JWKS_URL="https://example.hawcx.com/.well-known/jwks.json"

# Optional but recommended
OAUTH_ISSUER=https://example.hawcx.com
OAUTH_AUDIENCE=your_client_id

# For MFA features (if using delegation client)
HAWCX_BASE_URL=https://example.hawcx.com
SP_ED25519_PRIVATE_KEY_PEM=-----BEGIN PRIVATE KEY-----\n...
SP_X25519_PRIVATE_KEY_PEM=-----BEGIN PRIVATE KEY-----\n...
IDP_ED25519_PUBLIC_KEY_PEM=-----BEGIN PUBLIC KEY-----\n...
IDP_X25519_PUBLIC_KEY_PEM=-----BEGIN PUBLIC KEY-----\n...

Load environment variables in your application:

import dotenv from 'dotenv';
dotenv.config();

Step 3: Create an Authentication Endpoint

Create a new route to handle Hawcx OAuth callbacks:

import express from 'express';
import { exchangeCodeForClaims } from '@hawcx/oauth-client';

const router = express.Router();

router.post('/auth/hawcx/callback', async (req, res) => {
  try {
    const { code } = req.body;

    if (!code) {
      return res.status(400).json({ error: 'Missing authorization code' });
    }

    // Exchange the authorization code for verified claims
    const claims = await exchangeCodeForClaims({
      code,
      oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
      clientId: process.env.OAUTH_CLIENT_ID!,
      apiKey: process.env.HAWCX_API_KEY!,
      jwksUrl: process.env.JWKS_URL!,  // Dynamic key fetching (recommended)
      audience: process.env.OAUTH_AUDIENCE || process.env.OAUTH_CLIENT_ID,
      issuer: process.env.OAUTH_ISSUER,
      leeway: 10
    });

    // Here: Find or create user in your database
    const user = await findOrCreateUser({
      id: claims.sub,
      email: claims.email,
      emailVerified: claims.email_verified
    });

    // Create your application's session/JWT
    const sessionToken = generateSessionToken(user);

    res.json({
      success: true,
      sessionToken,
      user: {
        id: user.id,
        email: user.email
      }
    });
  } catch (error) {
    console.error('Hawcx callback error:', error);
    res.status(401).json({ error: 'Authentication failed' });
  }
});

export default router;

Step 4: Integrate with Your User Management

Update your user service to handle Hawcx identities:

// services/userService.ts
import db from '../db';

interface HawcxUser {
  id: string;              // Hawcx user ID (sub claim)
  email: string;
  emailVerified: boolean;
}

export async function findOrCreateUser(hawcxUser: HawcxUser) {
  // Check if user exists
  let user = await db.users.findOne({
    hawcx_id: hawcxUser.id
  });

  if (user) {
    // Update email_verified if needed
    if (hawcxUser.emailVerified && !user.email_verified) {
      await db.users.update(
        { id: user.id },
        { email_verified: true }
      );
    }
    return user;
  }

  // Create new user
  const newUser = {
    hawcx_id: hawcxUser.id,
    email: hawcxUser.email,
    email_verified: hawcxUser.emailVerified,
    created_at: new Date(),
    last_login: new Date()
  };

  const { id } = await db.users.insert(newUser);

  return { id, ...newUser };
}

export async function generateSessionToken(user: any) {
  // Use JWT or your preferred session method
  return jwt.sign(
    {
      userId: user.id,
      email: user.email,
      hawcxId: user.hawcx_id
    },
    process.env.SESSION_SECRET!,
    { expiresIn: '7d' }
  );
}

Step 5: Add Middleware for Protected Routes

Create middleware to verify session tokens:

// middleware/authMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export interface AuthRequest extends Request {
  user?: {
    userId: string;
    email: string;
    hawcxId: string;
  };
}

export function requireAuth(req: AuthRequest, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Missing authorization token' });
  }

  try {
    const payload = jwt.verify(token, process.env.SESSION_SECRET!);
    req.user = payload as AuthRequest['user'];
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

export function optionalAuth(req: AuthRequest, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (token) {
    try {
      const payload = jwt.verify(token, process.env.SESSION_SECRET!);
      req.user = payload as AuthRequest['user'];
    } catch (error) {
      // Token is invalid, but that's okay for optional auth
    }
  }

  next();
}

Step 6: Use Protected Routes

import { requireAuth, optionalAuth, AuthRequest } from '../middleware/authMiddleware';

// Protected route - requires authentication
router.get('/me', requireAuth, (req: AuthRequest, res) => {
  res.json({
    userId: req.user?.userId,
    email: req.user?.email
  });
});

// Optional authentication - works with or without auth
router.get('/articles', optionalAuth, (req: AuthRequest, res) => {
  const userId = req.user?.userId;
  // Return different content based on whether user is authenticated
  res.json({
    articles: getArticles(userId)
  });
});

// Logout endpoint
router.post('/logout', requireAuth, (req: AuthRequest, res) => {
  // In a session-based approach, you might track invalidated tokens
  // In a stateless JWT approach, just delete client-side
  res.json({ success: true, message: 'Logged out successfully' });
});

Step 7: Set Up MFA (Optional)

If you want to manage user MFA settings programmatically:

// routes/mfaRoutes.ts
import express from 'express';
import { HawcxDelegationClient, MfaMethod } from '@hawcx/oauth-client';
import { requireAuth, AuthRequest } from '../middleware/authMiddleware';

const router = express.Router();
const delegationClient = HawcxDelegationClient.fromKeys({
  spSigningKey: process.env.SP_ED25519_PRIVATE_KEY_PEM!,
  spEncryptionKey: process.env.SP_X25519_PRIVATE_KEY_PEM!,
  idpVerifyKey: process.env.IDP_ED25519_PUBLIC_KEY_PEM!,
  idpEncryptionKey: process.env.IDP_X25519_PUBLIC_KEY_PEM!,
  baseUrl: process.env.HAWCX_BASE_URL!,
  spId: process.env.OAUTH_CLIENT_ID!,
  apiKey: process.env.HAWCX_API_KEY
});

// Initiate MFA setup for the authenticated user
router.post('/mfa/initiate', requireAuth, async (req: AuthRequest, res) => {
  try {
    const { mfaMethod, phoneNumber } = req.body;
    const userEmail = req.user?.email;

    const result = await delegationClient.initiateMfaChange({
      userid: userEmail!,
      mfaMethod,
      phoneNumber
    });

    // Store session ID temporarily (e.g., in cache with TTL)
    await cache.set(`mfa_session:${userEmail}`, result.session_id, 600); // 10 min TTL

    res.json({
      success: true,
      message: 'MFA setup initiated. Check your email/SMS for verification code.'
    });
  } catch (error) {
    console.error('MFA initiation failed:', error);
    res.status(400).json({ error: 'Failed to initiate MFA setup' });
  }
});

// Verify MFA code
router.post('/mfa/verify', requireAuth, async (req: AuthRequest, res) => {
  try {
    const { otp } = req.body;
    const userEmail = req.user?.email;

    // Retrieve stored session ID
    const sessionId = await cache.get(`mfa_session:${userEmail}`);
    if (!sessionId) {
      return res.status(400).json({ error: 'MFA session expired. Please try again.' });
    }

    const result = await delegationClient.verifyMfaChange({
      userid: userEmail!,
      sessionId,
      otp
    });

    if (result.status !== 'success') {
      return res.status(400).json({ error: 'Invalid verification code' });
    }

    // Clean up session ID
    await cache.delete(`mfa_session:${userEmail}`);

    res.json({
      success: true,
      message: 'MFA setup completed successfully'
    });
  } catch (error) {
    console.error('MFA verification failed:', error);
    res.status(400).json({ error: 'MFA verification failed' });
  }
});

// Get user's current MFA settings
router.get('/mfa/status', requireAuth, async (req: AuthRequest, res) => {
  try {
    const userEmail = req.user?.email;

    const credentials = await delegationClient.getUserCredentials(userEmail!);

    res.json({
      mfaEnabled: credentials.mfa_method !== 'none',
      mfaMethod: credentials.mfa_method
    });
  } catch (error) {
    console.error('Failed to fetch MFA status:', error);
    res.status(500).json({ error: 'Failed to fetch MFA status' });
  }
});

export default router;

Step 8: Register Routes in Main App

// app.ts
import express from 'express';
import authRoutes from './routes/authRoutes';
import mfaRoutes from './routes/mfaRoutes';

const app = express();
app.use(express.json());

// Authentication routes
app.use('/api/auth', authRoutes);

// MFA routes (protected)
app.use('/api/user', mfaRoutes);

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

Step 9: Error Handling

Add proper error handling for Hawcx-specific errors:

import {
  OAuthExchangeError,
  JWTVerificationError,
  InvalidPublicKeyError
} from '@hawcx/oauth-client';

export function handleHawcxError(error: unknown, res: express.Response) {
  if (error instanceof OAuthExchangeError) {
    return res.status(401).json({
      error: 'OAuth exchange failed',
      message: 'The authorization code is invalid or expired'
    });
  }

  if (error instanceof JWTVerificationError) {
    return res.status(401).json({
      error: 'JWT verification failed',
      message: 'The authentication token could not be verified'
    });
  }

  if (error instanceof InvalidPublicKeyError) {
    console.error('Invalid public key configuration:', error);
    return res.status(500).json({
      error: 'Server configuration error'
    });
  }

  throw error;
}

Complete Example

Here's a minimal complete example:

import express, { Express } from 'express';
import { exchangeCodeForClaims, OAuthExchangeError } from '@hawcx/oauth-client';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();

const app: Express = express();
app.use(express.json());

// Callback endpoint
app.post('/api/auth/callback', async (req, res) => {
  try {
    const { code } = req.body;

    const claims = await exchangeCodeForClaims({
      code,
      oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
      clientId: process.env.OAUTH_CLIENT_ID!,
      apiKey: process.env.HAWCX_API_KEY!,
      jwksUrl: process.env.JWKS_URL!
    });

    const sessionToken = jwt.sign(
      { userId: claims.sub, email: claims.email },
      process.env.SESSION_SECRET!,
      { expiresIn: '7d' }
    );

    res.json({ sessionToken });
  } catch (error) {
    console.error('Auth error:', error);
    res.status(401).json({ error: 'Authentication failed' });
  }
});

// Protected endpoint
app.get('/api/me', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Unauthorized' });

  try {
    const payload = jwt.verify(token, process.env.SESSION_SECRET!);
    res.json(payload);
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Next Steps