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
| Hook | Trigger | Can Block |
|---|---|---|
pre_sign_up | Before a new user is created | ✅ |
post_sign_up | After a new user is created | — |
pre_sign_in | Before a user is authenticated | ✅ |
post_sign_in | After a user signs in | — |
jwt | When issuing an access token | — (adds claims) |
pre_password_reset | Before 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
| Scenario | Behavior |
|---|---|
| Hook responds within 3s | Normal flow |
| Hook times out | Flow continues (sign-up/in allowed) |
| Hook returns 5xx | Flow continues (logged as error) |
| Hook returns 401 (signature invalid) | Flow continues (logged as error) |
Hook returns allow: false | Request 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"