Challenge flow
This picks up where the quick start leaves off. You've wired evaluate on the client and the server confirmation. Now a policy returns a challenge verdict and Rupt runs an interactive challenge: it sends the user to a hosted verification page, and when they pass, sends them back to you. This guide is the generic wiring for that round trip, and every other guide that challenges a user reuses it.
The shape never changes: evaluate on the client, let Rupt run the challenge, and confirm the outcome on your server before you honor the action. The final server step consumes the evaluation, so a passed challenge is single-use and can't be replayed. Nothing trusts the client.
Account sharing prevention is a special case and doesn't follow this flow. See Access protection instead!.
The flow
Before you start: the policy and its challenge config
A challenge only happens because a policy told it to. In the dashboard, create a policy whose action is challenge for the action you're protecting (login, signup, or access) and the checks you want to gate on.
Every challenge policy points at a challenge config, which holds the verification channels and the URLs Rupt uses for the round trip:
- Success URL: where Rupt sends the user after they pass. This is the page on your site that finishes the action.
- Primary, secondary, and logout URLs: the links shown inside the challenge UI (for example, "back to app" or "log out").
You set these once on the challenge config, and they apply to every challenge that policy issues.
Step 1: Evaluate on the client
Call evaluate at the moment the user takes the action. Pass whatever identifiers you have: a user id, email, phone. The response carries an evaluation_id and, when a challenge is required, a redirect.
import Rupt from "@ruptjs/client";
const rupt = new Rupt({ clientId: "your_client_id" });
const evaluation = await rupt.evaluate.login({
user: "USER_ID",
email: form.email,
});
You will then hand the evaluation ID to your server. The server will get the evaluation details (including the verdict and redirect URL) and hand them back to the client if needed.
Step 2: Set the success URL
In the dashboard, on the challenge config your policy uses, set Success URL to the page that completes the action, for example https://yourapp.com/verified.
When the user passes, Rupt redirects there with the evaluation ID appended:
https://yourapp.com/verified?evaluation=68f…
Step 3: Hand the evaluation ID to your server
Your success page reads the evaluation ID off the URL and posts it to your backend. The client never decides the outcome. It only carries the evaluation ID across.
const params = new URLSearchParams(window.location.search);
await fetch("/verify-challenge", {
method: "POST",
body: JSON.stringify({
evaluation_id: params.get("evaluation"),
}),
});
Step 4: Consume the evaluation on your server
When a client in your success page hands you an evaluation ID, you should consume it. This gives you the evaluation state and marks it as consumed. This is done to prevent someone from taking a successful challenge and using it again to honor other actions. Check that the challenge completed and the evaluation's action and identifiers match what you expected, and only then honor the action.
import { RuptAPI } from "@ruptjs/api";
const rupt = new RuptAPI("API_SECRET");
let evaluation;
try {
// Consume reads and claims the evaluation in one shot. A replay of the
// success URL gets 409 because the evaluation was already used.
evaluation = await rupt.consumeEvaluation(evaluation_id);
} catch (err) {
if (err.status === 409) {
// Already used. Reject and have the user start the action again.
return reject("This challenge was already used. Start the action again.");
}
return reject("Could not verify the challenge");
}
if (evaluation.challenge?.status !== "completed") {
// Challenge not completed. Send the user back to the challenge UI.
return redirect(evaluation.redirect);
}
if (
evaluation.user?.email !== expectedEmail || // Identity mismatch
evaluation.action !== "YOUR_EXPECTED_ACTION" || // Action mismatch
evaluation.metadata !== YOUR_EXPECTED_METADATA // Metadata mismatch
) {
// Integrity mismatch. Block the action.
return reject("Integrity mismatch");
}
// Honor the action
honorTheAction();
If you consume via the raw API, check challenge.status, action, and the user identifiers in the response before you honor the action, exactly as the SDK examples do. Treat any challenge status other than completed as a block.
Where to go next
- Signup protection: the signup variant. The user is new with no ID yet, so you store a little state to bind the pending signup to its challenge.
- Login protection: the login variant. Nothing to store, just hold off issuing the session until the challenge completes.
- Account sharing prevention: a self-managed variant on
access, with no server step.
- Need help? Contact support.
- Want to see Rupt in action? Request a demo.
- Questions? Talk to sales.
- Check out our changelog.
- Check our status page.
- LLM? Read llms.txt.