Intercept authentication flows
Authentication interceptors let you run custom validation at key points in the authentication flow. Use them to enforce security policies, apply business rules, or tailor the user experience during sign-up and login.
Scalekit exposes trigger points where you attach interceptors. For example, you can block sign-ups from suspicious IPs, restrict domains, or require approval for new registrations.
Trigger point | When it runs |
---|---|
Pre-signup | Before a user creates a new organization |
Pre-session creation | Before session tokens are issued for a user |
Pre-user invitation | Before an invitation is created or sent for a new organization member |
Pre-M2M token creation | Before issuing a machine-to-machine access token |
Register an interceptor
Section titled “Register an interceptor”In the Scalekit dashboard, register an interceptor at a trigger point from the Interceptors tab.
Interceptors page
- 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.
- Choose the fallback behavior if your app fails or times out (allow or block the flow).
- Click Create.
- Toggle Enable to activate the interceptor.
Trigger points
Section titled “Trigger points”PRE_SIGNUP
Section titled “PRE_SIGNUP”{ "display_name": "Pre-Signup", "trigger_point": "PRE_SIGNUP", "interceptor_context": { "environment_id": "env_92561807201272162", "user_id": "usr_93418238346728951", // Present when the user already exists (for example, member of another organization) "user_email": "john.doe@example.com", "connection_details": [6 collapsed lines
{ "id": "conn_92561808744978132", "type": "OAUTH", "provider": "GOOGLE" } ], "device_type": "Desktop", "ip_address": "203.0.113.24", "region": "IN", "city": "Bengaluru", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "triggered_at": "2025-10-09T09:48:02.875Z" }, "data": { // Present when the user already exists (for example, member of another organization) "user": { "id": "usr_93418238346728951", "name": "John Doe",5 collapsed lines
"email": "john.doe@example.com", "email_verified": true, "created_at": "2025-10-06T11:06:49.120Z", "updated_at": "2025-10-06T13:33:06.479Z", "first_name": "John", "last_name": "Doe", "memberships": [ { "organization_id": "org_93418204671239864", "status": "ACTIVE" } ] } }}
{ // Required: choose ALLOW or DENY "decision": "DENY", // ALLOW | DENY // Optional (with DENY) "error": { "message": "Email domain not allowed" }}
PRE_SESSION_CREATION
Section titled “PRE_SESSION_CREATION”{ "display_name": "Add custom claims to tokens", "trigger_point": "PRE_SESSION_CREATION", "interceptor_context": { "environment_id": "env_92561807204567213", "user_id": "usr_93418238346728951", "user_email": "john.doe@example.com", "organization_id": "org_93418204671239864", "connection_details": [ {6 collapsed lines
"id": "conn_92561808744978132", "type": "OAUTH", "provider": "GOOGLE" } ], "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", "device_type": "Desktop", "ip_address": "203.0.113.24", "region": "US", "city": "San Francisco", "triggered_at": "2025-10-08T15:22:42.381Z" }, "data": { "user": { "id": "usr_93418238346728951", "name": "John Doe", "email": "john.doe@example.com", "email_verified": true, "created_at": "2025-10-06T11:06:49.120Z", "updated_at": "2025-10-06T13:33:06.479Z", "first_name": "John", "last_name": "Doe", "memberships": [ {5 collapsed lines
"organization_id": "org_93418204671239864", "status": "ACTIVE" } ] } }}
{ // Required: choose ALLOW or DENY "decision": "ALLOW", // ALLOW | DENY // Optional (with ALLOW) "response": { "claims": { "plan": "pro", "region": "eu-west-1", "entitlements": ["dashboards", "advanced-exports"] } }}
PRE_USER_INVITATION
Section titled “PRE_USER_INVITATION”{ "display_name": "Block invites to suspicious emails", "trigger_point": "PRE_USER_INVITATION", "interceptor_context": { "environment_id": "env_92561807201272162", "user_id": "usr_93418238346728951", // Present when the invited user already exists (for example, member of another organization) "user_email": "john.doe@example.com", "organization_id": "org_93731871904672153", "city": "Bengaluru", "device_type": "Desktop", "ip_address": "182.156.5.2", "region": "IN", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", "triggered_at": "2025-10-09T12:50:41.803Z" }, "data": { "organization": { "id": "org_93731871904672153", "name": "Acme Corp" } }}
{ // Required: choose ALLOW or DENY "decision": "DENY", // ALLOW | DENY // Optional (with DENY) "error": { "message": "Invites to disposable email domains are blocked" }}
PRE_M2M_TOKEN_CREATION
Section titled “PRE_M2M_TOKEN_CREATION”{ "display_name": "Add custom claim", "trigger_point": "PRE_M2M_TOKEN_CREATION", "interceptor_context": { "environment_id": "env_17002334043308132", "client_id": "m2morg_93710427703245914", "user_agent": "PostmanRuntime/7.48.0", "device_type": "Unknown", "triggered_at": "2025-10-08T21:22:20.173Z" }, "data": { "m2m_token_claims": { "client_id": "m2morg_93710427703245914", "claims": {4 collapsed lines
"custom_claims": { "c1": "v1", "c2": "v2" }, "oid": "org_89669394174574792", "scope": "deploy:applications read:deployments", "scopes": [3 collapsed lines
"deploy:applications", "read:deployments" ] } } }}
{ // Required: choose ALLOW or DENY "decision": "ALLOW", // ALLOW | DENY // Optional (with ALLOW) "response": { "claims": { "scope": "deploy:applications read:deployments", "aud": "https://api.example.com" } }}
Audit logs
Section titled “Audit logs”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.
Requests and responses generated by the “Test” button are not logged. This keeps production logs free of test data.
Verify interceptor payloads
Section titled “Verify interceptor payloads”Scalekit sends POST requests to your registered interceptor endpoint. To ensure the request is coming from Scalekit and not a malicious actor, you should verify the request using the signing secret found in the Scalekit dashboard > Interceptors.
Here’s how to verify interceptor requests using the Scalekit SDK:
app.post('/auth/interceptors/pre-signup', async (req, res) => { // Parse the JSON body of the request const event = await req.json();
// Get headers from the request const headers = req.headers;
// Secret from Scalekit dashboard > Interceptors const secret = process.env.SCALEKIT_INTERCEPTOR_SECRET;
try { // Verify the interceptor payload await scalekit.verifyInterceptorPayload(secret, headers, event); } catch (error) { return res.status(400).json({ error: 'Invalid signature', }); }});
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/auth/interceptors/pre-signup")async def api_interceptor(request: Request): # Get request data body = await request.body()
# Extract interceptor headers headers = { 'interceptor-id': request.headers.get('interceptor-id'), 'interceptor-signature': request.headers.get('interceptor-signature'), 'interceptor-timestamp': request.headers.get('interceptor-timestamp') }
# Verify interceptor signature is_valid = scalekit.verify_interceptor_payload( secret='<secret>', headers=headers, payload=body ) print(is_valid)
return JSONResponse( status_code=201, content='' )
mux.HandleFunc("POST /auth/interceptors/pre-signup", func(w http.ResponseWriter, r *http.Request) { interceptorSecret := os.Getenv("SCALEKIT_INTERCEPTOR_SECRET")
// Read request body bodyBytes, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return }
// Prepare headers for verification headers := map[string]string{ "interceptor-id": r.Header.Get("interceptor-id"), "interceptor-signature": r.Header.Get("interceptor-signature"), "interceptor-timestamp": r.Header.Get("interceptor-timestamp"), }
// Verify interceptor signature _, err = sc.VerifyInterceptorPayload( interceptorSecret, headers, bodyBytes ) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return }})
@PostMapping("/auth/interceptors/pre-signup")public String interceptor(@RequestBody String body, @RequestHeader Map<String, String> headers) { String secret = "<INTERCEPTOR SECRET>";
// Verify interceptor signature boolean valid = scalekit.interceptor().verifyInterceptorPayload(secret, headers, body.getBytes());
if (!valid) { return "error"; }
ObjectMapper mapper = new ObjectMapper();
try { // Parse interceptor data JsonNode node = mapper.readTree(body); String interceptorType = node.get("type").asText(); JsonNode data = node.get("data");
// Handle pre-signup interceptor if ("PRE_SIGNUP".equals(interceptorType)) { handlePreSignup(data); } } catch (IOException e) { return "error"; }
return "ok";}
Testing interceptors
Section titled “Testing interceptors”Open the Test tab on the Interceptors page. The left panel shows the request body sent to your endpoint, and the right panel shows your application’s response.
Based on your application’s response, you can decide to allow or deny the user auth flow to continue to sign up or log into your application. This allows for you to test and take the interceptors live in production.
For quick testing without building or deploying an endpoint, use a request bin service like Beeceptor or RequestBin. These services provide temporary endpoints that capture incoming requests and let you configure responses, making them ideal for interceptor development and validation.