Skip to main content

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)
OAUTH_TOKEN_ENDPOINT=https://oauth.hawcx.com/token
OAUTH_CLIENT_ID=your_client_id
OAUTH_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\n...

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

# For MFA features (if using delegation client)
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!,
      publicKey: process.env.OAUTH_PUBLIC_KEY!,
      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!,
  spId: process.env.OAUTH_CLIENT_ID!
});

// 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!,
      publicKey: process.env.OAUTH_PUBLIC_KEY!
    });

    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