Skip to content

Intercept authentication flows

Apply decision checks at key points in the authentication flow

Execute custom business logic during sign-up or login processes. For example, you can integrate with external systems to validate user existence before allowing login, or prevent sign-ups originating from suspicious IP addresses.

Scalekit calls your application at key trigger points during authentication flows and waits for an ALLOW or DENY response to determine whether to continue with the authentication process. For example, one trigger point occurs immediately before a user signs up for your application. We’ll explore more trigger points throughout this guide.

Example of pre-signup interceptor UserYour appScalekitInterceptor Attempts sign-up Detects pre-signup trigger POST /auth/interceptors/pre-signup Process the request: allow IP address? Decision ALLOW or DENY Proceed or block sign-up

You can define interceptors at several trigger points during authentication flows.

Trigger pointWhen it runs
Pre-signupBefore a user creates a new organization
Pre-session creationBefore session tokens are issued for a user
Pre-user invitationBefore an invitation is created or sent for a new organization member
Pre-M2M token creationBefore issuing a machine-to-machine access token

At each trigger point, Scalekit sends a POST request to your interceptor endpoint with the relevant details needed to process the request.

  1. Create an HTTPS endpoint that receives and verifies POST requests from Scalekit. This critical security step ensures requests are authentic and haven’t been tampered with.

    Express.js - Verify request signature
    // Security: ALWAYS verify requests are from Scalekit before processing
    // This prevents unauthorized parties from triggering your interceptor logic
    app.post('/auth/interceptors/pre-signup', async (req, res) => {
    try {
    // Parse the request payload and headers
    const event = await req.json();
    const headers = req.headers;
    // Get the signing secret from Scalekit dashboard > Interceptors tab
    // Store this securely in environment variables
    const interceptorSecret = process.env.SCALEKIT_INTERCEPTOR_SECRET;
    // Initialize Scalekit client (reference installation guide for setup)
    const scalekit = new ScalekitClient(
    process.env.SCALEKIT_ENVIRONMENT_URL,
    process.env.SCALEKIT_CLIENT_ID,
    process.env.SCALEKIT_CLIENT_SECRET
    );
    // Verify the interceptor payload signature
    // This confirms the request is from Scalekit and hasn't been tampered with
    await scalekit.verifyInterceptorPayload(interceptorSecret, headers, event);
    // ✓ Request verified - proceed to business logic (next step)
    } catch (error) {
    console.error('Interceptor verification failed:', error);
    // DENY on verification failures to fail securely
    return res.status(200).json({
    decision: 'DENY',
    error: {
    message: 'Unable to process request. Please try again later.'
    }
    });
    }
    });
  2. After verification, extract data from the payload, apply your custom validation logic, and return either ALLOW or DENY to control the authentication flow.

    Express.js - Business logic and response
    // Use case: Apply custom validation rules before allowing authentication
    // Examples: email domain validation, IP filtering, database checks, etc.
    app.post('/auth/interceptors/pre-signup', async (req, res) => {
    try {
    // ... (verification code from Step 1)
    // Extract data from the verified payload
    const { interceptor_context, data } = event;
    const userEmail = interceptor_context?.user_email || data?.user?.email;
    // Implement your business logic
    // Example: Validate email domain against an allowlist
    const emailDomain = userEmail?.split('@')[1];
    const allowedDomains = ['company.com', 'example.com'];
    if (!allowedDomains.includes(emailDomain)) {
    // DENY: Block the authentication flow
    return res.status(200).json({
    decision: 'DENY',
    error: {
    message: 'Sign-ups from this email domain are not permitted.'
    }
    });
    }
    // Optional: Log successful validations for audit purposes
    console.log(`Allowed signup for ${userEmail}`);
    // ALLOW: Permit the authentication flow to continue
    return res.status(200).json({
    decision: 'ALLOW'
    });
    } catch (error) {
    console.error('Interceptor error:', error);
    return res.status(200).json({
    decision: 'DENY',
    error: {
    message: 'Unable to process request. Please try again later.'
    }
    });
    }
    });
  3. Register the interceptor in Scalekit dashboard

    Section titled “Register the interceptor in Scalekit dashboard”

    Configure your interceptor by specifying the trigger point, endpoint URL, timeout settings, and fallback behavior.

    In the Scalekit dashboard, navigate to the Interceptors tab to register your endpoint.

    Interceptors settings in the Scalekit dashboard

    • Enter a descriptive name, choose a trigger point, and provide the HTTPS endpoint that will receive POST requests
    • Set the timeout for your app’s response (recommended: 3-5 seconds)
    • Choose the fallback behavior if your app fails or times out (allow or block the flow)
    • Click Create
    • Toggle Enable to activate the interceptor
  4. Use the Test tab in the Scalekit dashboard to verify your implementation before enabling it in production.

    • Open the Test tab on the Interceptors page
    • The left panel shows the request body sent to your endpoint
    • Click Send request to test your interceptor
    • The right panel shows your application’s response
    • Verify your endpoint returns the expected ALLOW or DENY decision

    Interceptor test tab example

  5. Scalekit keeps a log of every interceptor request sent to your app and the response it returned. Use these logs to debug and troubleshoot issues.

    Interceptor logs in the dashboard

    Requests and responses generated by the “Test” button are not logged. This keeps production logs free of test data.

Block signups from restricted IP addresses

Section titled “Block signups from restricted IP addresses”

Prevent new user signups from specific IP addresses or geographic regions. The request includes ip_address and region (country code) in interceptor_context.

Express.js
app.post('/auth/interceptor/pre-signup', async (req, res) => {
const { interceptor_context } = req.body;
// Extract IP address and region from the request
const ipAddress = interceptor_context.ip_address;
const region = interceptor_context.region;
// Define your IP blocklist (you can also check against a database)
const blockedIPs = ['203.0.113.24', '198.51.100.42'];
const blockedRegions = ['XX', 'YY']; // Example: blocked region codes
// Check if IP is blocked
if (blockedIPs.includes(ipAddress)) {
return res.json({
decision: 'DENY',
error: {
message: 'Signups from your IP address are not allowed due to security policy'
}
});
}
// Check if region is blocked
if (blockedRegions.includes(region)) {
return res.json({
decision: 'DENY',
error: {
message: 'Signups from your location are restricted due to compliance requirements'
}
});
}
// Allow signup to proceed
return res.json({
decision: 'ALLOW'
});
});

Add custom claims to ID tokens issued by Scalekit. Fetch user metadata from your database and return claims in the response.claims object. Claims are automatically included in the ID token after authentication.

Express.js
app.post('/auth/interceptor/pre-session-creation', async (req, res) => {
const { interceptor_context } = req.body;
const userId = interceptor_context.user_id;
const organizationId = interceptor_context.organization_id;
// Fetch user subscription and permissions from your database
const userMetadata = await fetchUserMetadata(userId, organizationId);
// Build custom claims based on your business logic
const customClaims = {
plan: userMetadata.subscription.plan, // 'free', 'pro', 'enterprise'
plan_expires_at: userMetadata.subscription.expiresAt,
features: userMetadata.features, // ['analytics', 'api_access', 'advanced_reports']
org_role: userMetadata.organizationRole, // 'admin', 'member', 'viewer'
department: userMetadata.department,
cost_center: userMetadata.costCenter
};
// Return ALLOW decision with custom claims
return res.json({
decision: 'ALLOW',
response: {
claims: customClaims
}
});
});