HelloJohn / docs
Authentication

Authentication Hooks

Extend HelloJohn's authentication flow with server-side hooks — run custom logic on sign-up, sign-in, and token issuance.

Authentication Hooks

Hooks let you extend HelloJohn's authentication lifecycle with your own server-side logic. HelloJohn calls your HTTP endpoint at specific events, and your response can modify or block the flow.


Available Hooks

HookTriggerCan Block
pre_sign_upBefore a new user is created
post_sign_upAfter a new user is created
pre_sign_inBefore a user is authenticated
post_sign_inAfter a user signs in
jwtWhen issuing an access token— (adds claims)
pre_password_resetBefore sending a reset email

Configuring Hooks

curl -X PATCH "https://api.hellojohn.dev/v1/admin/config" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{
    "hooks": {
      "pre_sign_up": "https://your-app.com/hooks/pre-sign-up",
      "post_sign_up": "https://your-app.com/hooks/post-sign-up",
      "pre_sign_in": "https://your-app.com/hooks/pre-sign-in",
      "jwt": "https://your-app.com/hooks/jwt"
    }
  }'

Verifying Hook Requests

HelloJohn signs all hook requests with HMAC-SHA256. Verify the signature before processing:

import crypto from "crypto";

function verifyHookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

// Express middleware
function hookAuth(req: Request, res: Response, next: NextFunction) {
  const sig = req.headers["hellojohn-hook-signature"] as string;
  if (!verifyHookSignature(req.rawBody, sig, process.env.HOOK_SECRET!)) {
    return res.status(401).json({ error: "invalid_signature" });
  }
  next();
}

Your hook signing secret is available in Tenant Settings → Hooks → Signing Secret.


pre_sign_up Hook

Called before a new user is created. Reject sign-ups based on email domain, invitation list, or any other criteria.

Request payload:

{
  "email": "alice@example.com",
  "name": "Alice Smith",
  "provider": "email",
  "tenant_id": "tnt_01HABCDEF654321"
}

Allow sign-up — return 200 with {"allow": true}:

{ "allow": true }

Block sign-up — return 200 with {"allow": false}:

{
  "allow": false,
  "reason": "Sign-ups are restricted to @acme.com email addresses."
}

Implementation example:

app.post("/hooks/pre-sign-up", hookAuth, (req, res) => {
  const { email } = req.body;

  // Restrict to company domain
  if (!email.endsWith("@acme.com")) {
    return res.json({
      allow: false,
      reason: "Only @acme.com email addresses are allowed.",
    });
  }

  res.json({ allow: true });
});

post_sign_up Hook

Called after a new user is created. Use for provisioning, welcome emails, analytics, or creating records in your database.

Request payload:

{
  "user": {
    "id": "usr_01HABCDEF123456",
    "email": "alice@example.com",
    "name": "Alice Smith",
    "created_at": "2024-01-15T10:00:00Z"
  },
  "tenant_id": "tnt_01HABCDEF654321"
}

Response: Return 200 with any body — HelloJohn ignores the response content.

app.post("/hooks/post-sign-up", hookAuth, async (req, res) => {
  const { user } = req.body;

  // Create a record in your own DB
  await db.profiles.create({
    hj_user_id: user.id,
    email: user.email,
    plan: "free",
  });

  // Send to analytics
  analytics.track({ event: "user_signed_up", userId: user.id });

  res.json({ ok: true });
});

pre_sign_in Hook

Called before a user is authenticated. Block sign-ins based on IP address, device, or custom rules.

Request payload:

{
  "user": {
    "id": "usr_01HABCDEF123456",
    "email": "alice@example.com",
    "disabled": false
  },
  "ip_address": "203.0.113.42",
  "user_agent": "Mozilla/5.0...",
  "tenant_id": "tnt_01HABCDEF654321"
}

Allow:

{ "allow": true }

Block:

{
  "allow": false,
  "reason": "Sign-in blocked from your region."
}
app.post("/hooks/pre-sign-in", hookAuth, async (req, res) => {
  const { user, ip_address } = req.body;

  // Check if user is banned in your system
  const banned = await db.bans.findOne({ user_id: user.id });
  if (banned) {
    return res.json({ allow: false, reason: "Account suspended." });
  }

  // Block certain IP ranges
  if (isBlockedIP(ip_address)) {
    return res.json({ allow: false, reason: "Sign-in not allowed from this location." });
  }

  res.json({ allow: true });
});

jwt Hook

Called when issuing an access token. Return extra claims to include in the JWT. See Custom JWT Claims for full details.

Request payload:

{
  "user": { "id": "usr_01HABCDEF123456", ... },
  "session": { "id": "ses_01HABCDEF999888", "org_id": "org_..." },
  "tenant_id": "tnt_01HABCDEF654321"
}

Response:

{
  "claims": {
    "plan": "pro",
    "team_id": "team_42"
  }
}

Hook Timeouts and Errors

ScenarioBehavior
Hook responds within 3sNormal flow
Hook times outFlow continues (sign-up/in allowed)
Hook returns 5xxFlow continues (logged as error)
Hook returns 401 (signature invalid)Flow continues (logged as error)
Hook returns allow: falseRequest blocked

Always monitor hook errors in Tenant Settings → Hooks → Logs.


Hook Logs

View recent hook executions and errors:

curl "https://api.hellojohn.dev/v1/admin/hooks/logs?hook=pre_sign_up&limit=20" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321"

On this page