Skip to main content

Hawcx OAuth Client SDK for Node.js

Add passwordless authentication to your Node.js backend. Exchange OAuth codes for verified user claims and manage MFA.

Installation

npm install @hawcx/oauth-client

Quick Start

OAuth Code Exchange

The most common flow: exchange a Hawcx authorization code for verified user claims.
import { exchangeCodeForClaims } from '@hawcx/oauth-client';

app.post('/api/auth/callback', async (req, res) => {
  try {
    // Exchange the authorization code for verified claims
    const claims = await exchangeCodeForClaims({
      code: req.body.code,
      oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
      clientId: process.env.OAUTH_CLIENT_ID!,
      publicKey: process.env.OAUTH_PUBLIC_KEY!,
      // Optional (recommended for production):
      codeVerifier: req.session?.pkceVerifier,  // PKCE support
      audience: 'my-app',  // Validate 'aud' claim
      issuer: 'https://oauth.example.com',  // Validate 'iss' claim
      leeway: 10  // Clock skew tolerance
    });

    // Use the verified claims
    const userId = claims.sub;
    const email = claims.email;
    
    // Mint your own access token or session
    const sessionToken = jwt.sign(
      { sub: claims.sub, email: claims.email },
      process.env.APP_SESSION_SECRET!,
      { expiresIn: '1h' }
    );

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

Hawcx Delegation Client (MFA Setup)

For advanced use cases like setting up MFA for users programmatically:
import { HawcxDelegationClient, MfaMethod } from '@hawcx/oauth-client';

// Initialize the delegation client with your signing/encryption keys
const client = 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: 'https://ceasar-api.hawcx.com',
  spId: process.env.OAUTH_CLIENT_ID!
})

// Initiate MFA setup (Email, SMS, or TOTP)
const result = await client.initiateMfaChange({
  userid: "[email protected]",
  mfaMethod: MfaMethod.SMS, 
  phoneNumber: "+15551234567"
});

// Verify OTP and complete MFA setup
await client.verifyMfaChange({
  userid: "[email protected]",
  sessionId: result.session_id,
  otp: "123456"
});

// Get user credentials
const creds = await client.getUserCredentials("[email protected]");
console.log(`MFA method: ${creds.mfa_method}`);

Configuration

Environment Variables

For OAuth Code Exchange:
OAUTH_TOKEN_ENDPOINT="https://oauth.hawcx.com/token"
OAUTH_CLIENT_ID="your-client-id"
OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
OAUTH_ISSUER="https://oauth.hawcx.com"  # Optional but recommended
OAUTH_AUDIENCE="your-client-id"  # Optional but recommended
For Hawcx Delegation:
SP_ED25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
SP_X25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
IDP_ED25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
IDP_X25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
OAUTH_CLIENT_ID="your-client-id"

Public Key Formats

The SDK accepts public keys in multiple formats: From environment variable (recommended):
const claims = await exchangeCodeForClaims({
  // ...
  publicKey: process.env.OAUTH_PUBLIC_KEY!  // PEM string
});
From file path:
const claims = await exchangeCodeForClaims({
  // ...
  publicKey: '/path/to/public.pem'  // Absolute path
});
Expected PEM format:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

Error Handling

The SDK provides specific exceptions for different failure scenarios:
import {
  exchangeCodeForClaims,
  OAuthExchangeError,
  JWTVerificationError,
  InvalidPublicKeyError
} from '@hawcx/oauth-client';

try {
  const claims = await exchangeCodeForClaims({
    code: authCode,
    oauthTokenUrl: tokenEndpoint,
    clientId: clientId,
    publicKey: publicKey
  });
} catch (error) {
  if (error instanceof OAuthExchangeError) {
    console.error('OAuth exchange failed:', error.message);
    // The authorization code may be invalid or expired
  } else if (error instanceof JWTVerificationError) {
    console.error('JWT verification failed:', error.message);
    // The JWT signature or claims validation failed
  } else if (error instanceof InvalidPublicKeyError) {
    console.error('Invalid public key:', error.message);
    // The provided public key is malformed
  } else {
    throw error;
  }
}

Integration Examples

Express.js

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

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

// Callback endpoint after user completes Hawcx authentication
app.post('/auth/callback', async (req: Request, res: Response) => {
  const { code } = req.body;

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

  try {
    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_CLIENT_ID
    });

    // Create user session
    req.session.userId = claims.sub;
    req.session.email = claims.email;

    res.json({ success: true, userId: claims.sub });
  } catch (error) {
    console.error('Authentication failed:', error);
    res.status(401).json({ error: 'Authentication failed' });
  }
});

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

Fastify

import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { exchangeCodeForClaims } from '@hawcx/oauth-client';

const app = Fastify();

app.post<{ Body: { code: string } }>('/auth/callback', async (request: FastifyRequest, reply: FastifyReply) => {
  const { code } = request.body;

  try {
    const claims = await exchangeCodeForClaims({
      code,
      oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
      clientId: process.env.OAUTH_CLIENT_ID!,
      publicKey: process.env.OAUTH_PUBLIC_KEY!
    });

    reply.send({ success: true, userId: claims.sub });
  } catch (error) {
    reply.status(401).send({ error: 'Authentication failed' });
  }
});

app.listen({ port: 3000 }, (err) => {
  if (err) throw err;
  console.log('Server running on port 3000');
});

PKCE Support

For enhanced security, especially in native or SPA applications, use PKCE:
// On client: Generate code verifier
const codeVerifier = generateRandomString(128);
const codeChallenge = base64url(sha256(codeVerifier));

// On backend: Exchange with verifier
const claims = await exchangeCodeForClaims({
  code: authCode,
  codeVerifier,  // Include the verifier from client session
  oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
  clientId: process.env.OAUTH_CLIENT_ID!,
  publicKey: process.env.OAUTH_PUBLIC_KEY!
});

Next Steps