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
Copy
npm install @hawcx/oauth-client
Step 2: Set Up Environment Variables
Create a.env file or configure your environment with:
Copy
# 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...
Copy
import dotenv from 'dotenv';
dotenv.config();
Step 3: Create an Authentication Endpoint
Create a new route to handle Hawcx OAuth callbacks:Copy
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:Copy
// 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:Copy
// 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
Copy
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:Copy
// 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
Copy
// 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:Copy
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:Copy
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
- Review the complete API reference for all available methods
- Explore MFA features for advanced user management
- Check out the demo app for a full working example
