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.
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,
});
For access, the SDK navigates to the challenge UI automatically when one is required. Every other action (login, signup, and custom events) doesn't auto-navigate by default, so the form your user is filling isn't abandoned mid-submit. The redirect URL comes back in the response and your server decides what to do with it. See Signup protection. You can force either behavior per call with auto_challenge: true | false.
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
This step is optional for account-sharing prevention.
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
Make the final step a consumption. Consuming reads the evaluation and claims it in one atomic, single-use step, so a passed challenge can't be replayed into a second honored action. Confirm the consume succeeded, check that the challenge completed and the evaluation's action and identifiers match what you expected, and only then honor the action. (Flows with no server step, like self-managed account sharing, skip this.)
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.user?.metadata !== YOUR_EXPECTED_METADATA // Metadata mismatch
) {
// Integrity mismatch. Block the action.
return reject("Integrity mismatch");
}
// Honor the action
honorTheAction();
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.