@hawcx/oauth-client
v4.0.1
Published
Simple OAuth client for Hawcx authentication with delegation support
Readme
@hawcx/oauth-client
Simple, production-ready OAuth client SDK for Hawcx authentication.
Features
- Simple JWT Verification - Uses
joselibrary for automatic JWKS caching and signature handling - PKCE Support - Native PKCE (RFC 7636) support for enhanced OAuth security
- Delegation API - Server-to-server user management (MFA, devices)
- Step-Up Auth - Step-up authentication flows for sensitive operations
- TypeScript First - Full type definitions included
- Zero Config Crypto - All cryptographic complexity hidden behind a single secret key
Installation
npm install @hawcx/oauth-clientQuick Start
import { HawcxOAuth } from '@hawcx/oauth-client';
const oauth = new HawcxOAuth({ configId: 'your-config-id' });
// Exchange authorization code for tokens
const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
console.log(claims.sub); // user ID
console.log(claims.email); // user emailModules
| Module | Purpose | |--------|---------| | OAuth | Token exchange and JWT verification | | Delegation | Server-to-server user management (MFA, devices) | | Step-Up | Step-up authentication flows |
OAuth Module
Core module for OAuth 2.1 + PKCE authentication.
HawcxOAuth
import { HawcxOAuth } from '@hawcx/oauth-client';
const oauth = new HawcxOAuth({
configId: 'your-config-id', // Required: Your Hawcx config ID
baseUrl: 'https://api.hawcx.com', // Optional: API base URL
timeout: 10000, // Optional: Request timeout in ms
});Methods
exchangeCode(code, codeVerifier): Promise<ExchangeResult>
Exchange an authorization code for tokens.
const { idToken, claims } = await oauth.exchangeCode(
'authorization-code',
'pkce-code-verifier'
);Returns:
interface ExchangeResult {
idToken: string; // Raw JWT token
claims: JwtClaims; // Verified claims
}verifyToken(token): Promise<JwtClaims>
Verify an existing JWT token.
const claims = await oauth.verifyToken(idToken);Returns:
interface JwtClaims {
sub?: string; // User ID
iss?: string; // Issuer
aud?: string | string[]; // Audience
iat?: number; // Issued at (unix timestamp)
exp?: number; // Expiration (unix timestamp)
email?: string; // User email
name?: string; // User name
amr?: string[]; // Authentication methods
mfa_method?: string; // MFA method used
}clearCache(): void
Clear the JWKS cache (useful for testing or key rotation).
oauth.clearCache();Errors
import {
HawcxOAuthError, // Base error class
TokenExchangeError, // Code exchange failed
TokenVerificationError // JWT verification failed
} from '@hawcx/oauth-client';
try {
await oauth.exchangeCode(code, verifier);
} catch (error) {
if (error instanceof TokenExchangeError) {
console.log(error.statusCode); // HTTP status code
}
if (error instanceof TokenVerificationError) {
console.log(error.message); // "Token expired", "Invalid signature", etc.
}
}Delegation Module
Server-to-server API for managing users, MFA, and devices. Requires a Hawcx secret key.
DelegationClient
import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
const client = DelegationClient.fromSecretKey({
baseUrl: 'https://api.hawcx.com',
secretKey: process.env.HAWCX_SECRET_KEY!,
apiKey: 'optional-api-key', // Optional
timeoutSeconds: 15, // Optional (default: 15)
clockSkewSeconds: 300, // Optional (default: 300)
});MFA Operations
client.mfa.initiate(options): Promise<object>
Initiate MFA setup or change for a user.
// Email MFA
const result = await client.mfa.initiate({
userId: '[email protected]',
mfaMethod: MfaMethod.EMAIL,
});
// SMS MFA (requires phone number)
const result = await client.mfa.initiate({
userId: '[email protected]',
mfaMethod: MfaMethod.SMS,
phoneNumber: '+1234567890',
});
// TOTP MFA
const result = await client.mfa.initiate({
userId: '[email protected]',
mfaMethod: MfaMethod.TOTP,
});
// Change MFA method
const result = await client.mfa.initiate({
userId: '[email protected]',
mfaMethod: MfaMethod.EMAIL, // Current method for verification
mfaChangeTo: MfaMethod.TOTP, // New method to switch to
});MFA Methods:
enum MfaMethod {
EMAIL = 'email',
SMS = 'sms',
TOTP = 'totp',
}client.mfa.verify(options): Promise<object>
Verify MFA setup with OTP.
const result = await client.mfa.verify({
userId: '[email protected]',
sessionId: 'session-from-initiate',
otp: '123456', // 6-digit code
});User Operations
client.users.getCredentials(userId): Promise<object>
Get user credentials/metadata.
const creds = await client.users.getCredentials('[email protected]');
console.log(creds.mfa_method);Device Operations
client.devices.list(userId): Promise<object>
List all devices for a user.
const { devices } = await client.devices.list('[email protected]');client.devices.revoke(options): Promise<object>
Revoke a device (block it from authenticating).
await client.devices.revoke({
userId: '[email protected]',
deviceId: 'device-h2index',
});client.devices.unrevoke(options): Promise<object>
Unrevoke a previously revoked device.
await client.devices.unrevoke({
userId: '[email protected]',
deviceId: 'device-h2index',
});client.devices.delete(options): Promise<object>
Permanently delete a device.
await client.devices.delete({
userId: '[email protected]',
deviceId: 'device-h2index',
});Generic Request
client.request<TReq, TRes>(options): Promise<TRes>
Send a custom encrypted request to any delegation endpoint.
const result = await client.request({
endpoint: '/hc_auth/v5/custom/endpoint',
payload: { userid: '[email protected]', custom_field: 'value' },
headers: { 'X-Custom-Header': 'value' },
includeBaseUrl: true, // Optional (default: true)
});Credential Utilities
parseHawcxSecretKey(secretKey): ParsedCredentials
Parse a secret key into usable key material (advanced usage).
import { parseHawcxSecretKey } from '@hawcx/oauth-client';
const creds = parseHawcxSecretKey(process.env.HAWCX_SECRET_KEY!);
// Returns: { kid, hkid, signingKeyPem, verifyKeyPem, encryptKeyPem, decryptKeyPem }generateCredentialBlob(options): string
Generate a new credential blob (for admin tooling).
import { generateCredentialBlob } from '@hawcx/oauth-client';
const secretKey = generateCredentialBlob({
kid: 'customer-key-id',
hkid: 'hawcx-key-id',
edPrivate: Buffer.alloc(32), // Ed25519 private key (32 bytes)
xPrivate: Buffer.alloc(32), // X25519 private key (32 bytes)
hawcxEdPublic: Buffer.alloc(32), // Hawcx Ed25519 public key (32 bytes)
hawcxXPublic: Buffer.alloc(32), // Hawcx X25519 public key (32 bytes)
});
// Returns: "hwx_sk_v1_<base64url>"Delegation Errors
import {
DelegationError, // Base delegation error
DelegationCryptoError, // Encryption/signing failed
DelegationRequestError, // Request to IDP failed
DelegationResponseError, // Invalid response from IDP
} from '@hawcx/oauth-client';
try {
await client.mfa.initiate({ userId: '[email protected]', mfaMethod: MfaMethod.EMAIL });
} catch (error) {
if (error instanceof DelegationRequestError) {
console.log(error.statusCode); // HTTP status
console.log(error.responseBody); // Response body
}
}Step-Up Module
For step-up authentication flows (e.g., changing MFA method requires re-authentication).
StepUpClient
import { StepUpClient } from '@hawcx/oauth-client';
const stepUp = StepUpClient.fromSecretKey({
baseUrl: 'https://api.hawcx.com',
secretKey: process.env.HAWCX_SECRET_KEY!,
relyingParty: 'your-app.com', // Required
apiKey: 'optional-api-key', // Optional
apiPrefix: '/v1', // Optional (default: '/v1')
timeoutSeconds: 15, // Optional
clockSkewSeconds: 300, // Optional
});Methods
stepUp.startToken(options): Promise<StepUpStartTokenResponse>
Start a step-up authentication flow.
const { start_token, expires_in } = await stepUp.startToken({
userId: '[email protected]',
purpose: 'change_mfa_method',
newMfaMethod: 'totp', // 'email_otp' | 'sms_otp' | 'totp'
});
// Send start_token to client for step-up authenticationstepUp.consumeReceipt(options): Promise<StepUpConsumeResponse>
Consume a step-up receipt after user completes authentication.
const { ok } = await stepUp.consumeReceipt({
receipt: 'step-up-receipt-from-client',
});
if (ok) {
// Step-up verified, proceed with sensitive operation
}Types
type StepUpPurpose = 'change_mfa_method';
type HxAuthMfaMethod = 'sms_otp' | 'email_otp' | 'totp';
interface StepUpStartTokenResponse {
start_token: string;
expires_in: number;
}
interface StepUpConsumeResponse {
ok: boolean;
}Examples
Next.js Auth Callback
// app/api/auth/callback/route.ts
import { HawcxOAuth, TokenExchangeError } from '@hawcx/oauth-client';
import { cookies } from 'next/headers';
const oauth = new HawcxOAuth({
configId: process.env.HAWCX_CONFIG_ID!,
});
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const cookieStore = cookies();
const codeVerifier = cookieStore.get('code_verifier')?.value;
if (!code || !codeVerifier) {
return Response.redirect('/login?error=missing_params');
}
try {
const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
// Create session with claims.sub, claims.email, etc.
return Response.redirect('/dashboard');
} catch (error) {
if (error instanceof TokenExchangeError) {
console.error('Token exchange failed:', error.message);
}
return Response.redirect('/login?error=auth_failed');
}
}Admin MFA Management
import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
const admin = DelegationClient.fromSecretKey({
baseUrl: process.env.HAWCX_API_URL!,
secretKey: process.env.HAWCX_SECRET_KEY!,
});
// Reset user MFA to email
async function resetUserMfa(userId: string, otp: string) {
const initResult = await admin.mfa.initiate({
userId,
mfaMethod: MfaMethod.EMAIL,
});
const verifyResult = await admin.mfa.verify({
userId,
sessionId: initResult.session_id,
otp,
});
return verifyResult;
}
// Revoke all user devices
async function revokeAllDevices(userId: string) {
const { devices } = await admin.devices.list(userId);
for (const device of devices) {
await admin.devices.revoke({
userId,
deviceId: device.h2index,
});
}
}Step-Up for MFA Change
import { StepUpClient, DelegationClient, MfaMethod } from '@hawcx/oauth-client';
const stepUp = StepUpClient.fromSecretKey({
baseUrl: process.env.HAWCX_API_URL!,
secretKey: process.env.HAWCX_SECRET_KEY!,
relyingParty: 'myapp.com',
});
const delegation = DelegationClient.fromSecretKey({
baseUrl: process.env.HAWCX_API_URL!,
secretKey: process.env.HAWCX_SECRET_KEY!,
});
async function changeMfaMethod(userId: string, newMethod: 'totp' | 'sms_otp' | 'email_otp') {
// 1. Start step-up flow
const { start_token } = await stepUp.startToken({
userId,
purpose: 'change_mfa_method',
newMfaMethod: newMethod,
});
// 2. Send start_token to client, user completes step-up auth
// 3. Client sends back receipt
// 4. Verify step-up receipt
const { ok } = await stepUp.consumeReceipt({ receipt: 'receipt-from-client' });
if (!ok) {
throw new Error('Step-up verification failed');
}
// 5. Now safe to change MFA
await delegation.mfa.initiate({
userId,
mfaMethod: MfaMethod.EMAIL,
mfaChangeTo: newMethod,
});
}Environment Variables
| Variable | Description |
|----------|-------------|
| HAWCX_CONFIG_ID | Your Hawcx OAuth config ID (for token verification) |
| HAWCX_SECRET_KEY | Your Hawcx secret key (for delegation/step-up APIs) |
| HAWCX_API_URL | API base URL (default: https://api.hawcx.com) |
Secret Key Format
The HAWCX_SECRET_KEY is a compact credential blob:
hwx_sk_v1_<base64url-encoded-json>It contains:
- Your Ed25519 signing key (for request authentication)
- Your X25519 decryption key (for response decryption)
- Hawcx's Ed25519 public key (for response verification)
- Hawcx's X25519 public key (for request encryption)
- Key IDs for rotation tracking
Generate via the Hawcx Dashboard or admin tools.
JWKS Caching
The SDK automatically caches JWKS (JSON Web Key Set) for JWT verification:
- Keys are fetched lazily on first
verifyToken()call - Cached in memory for the lifetime of the
HawcxOAuthinstance - Auto-refreshes if a key ID isn't found (handles key rotation)
- Call
clearCache()to force a refresh
// Keys fetched once, cached thereafter
const claims1 = await oauth.verifyToken(token1); // Fetches JWKS
const claims2 = await oauth.verifyToken(token2); // Uses cache
const claims3 = await oauth.verifyToken(token3); // Uses cache
// Force refresh if needed
oauth.clearCache();
const claims4 = await oauth.verifyToken(token4); // Fetches JWKS againRequirements
- Node.js >= 18.0.0
License
MIT
