Login protection
The login evaluation has three verdicts: allow, deny, and challenge. The allow and deny verdicts are simple cases.
The challenge verdict requires a bit more work to ensure it's not bypassed. The one rule is: don't issue a session or token while a challenge is outstanding. If the verdict is challenge, the user gets nothing until your server confirms the challenge completed.
Login and signup protection are the foundation of every other guide. Once you have those two down, you can build on them with different policies and checks for any other use case.
What this protects against
- A user triggers a login challenge, closes the tab, and hits
/loginagain. If a challenge never blocks the session, the second attempt just works. - The client sends one user to Rupt and authenticates as a different one. Your server sees
verdict: allowand trusts it. - A stolen-credential login that should have been challenged sails through because the server never confirmed the outcome.
- An attacker replays the post-challenge success URL — or guesses an
evaluation_idwhose challenge already completed — to mint a session without passing a challenge of their own.
The flow
Step 1: Call evaluate at login
Pass the user id and email (and phone if you have it).
import Rupt from "@ruptjs/client";
const rupt = new Rupt({ clientId: "your_client_id" });
const loginEval = await rupt.evaluate.login({
user: user.id,
email: form.email,
});
// POST /login to your server with the credentials and the evaluation ID
await fetch("/login", {
method: "POST",
body: JSON.stringify({
...credentials,
evaluation_id: loginEval?.evaluation_id,
}),
});
Step 2: Handle the verdict on your server
Your server checks the password, fetches the evaluation, runs the integrity check (the action and user match what you expected), then branches on the verdict. On a challenge it issues nothing and hands back the redirect.
// POST /login
if (!checkPassword(credentials)) return reject("Invalid credentials");
const evaluation = await rupt.getEvaluation(evaluation_id);
// Integrity check — block tampering before anything else
if (evaluation.action !== "login") return reject("Action mismatch");
if (evaluation.user?.id !== user.id) return reject("Identity mismatch");
if (evaluation.verdict === "deny") {
return reject("Login denied");
}
if (evaluation.verdict === "allow") {
return { session: startSession(user) };
}
if (evaluation.verdict === "challenge") {
// Don't start a session. Send the user to the challenge first.
return { redirect: evaluation.redirect };
}
Step 3: Configure the challenge success URL
In the Rupt dashboard, on the relevant Challenge Config (Policies -> Edit -> Challenge Config), set Success URL to the page that finishes login. For example: https://yourapp.com/login/complete.
When the user passes, Rupt redirects there with the evaluation ID appended:
https://yourapp.com/login/complete?evaluation=68f…
Step 4: Consume the evaluation and start the session
Your /login/complete route takes the evaluation ID from the URL and consumes it. Consuming is a single-use, atomic claim: Rupt marks the evaluation spent and returns it in one step, so the same success URL can never start a second session. The first call wins; a replay throws 409. Start the session only if the consume succeeds, the challenge completed, and the evaluation is still fresh.
// POST /login/complete
const { evaluation_id } = req.body;
let evaluation;
try {
// Single-use: the first call wins, a replay throws 409.
evaluation = await rupt.consumeEvaluation(evaluation_id);
} catch (err) {
if (err.status === 409) return reject("This login link was already used");
// Network or 5xx — fail open, allow the login to proceed. Worth logging.
return { session: startSession(evaluation.user) };
}
if (evaluation.action !== "login") return reject("Action mismatch");
// Cap time on challenge to 10 mins (as an example).
if (Date.now() - new Date(evaluation.createdAt).getTime() >= 10 * 60 * 1000) {
return reject("Login session expired");
}
if (evaluation.challenge?.status !== "completed") {
return reject("Challenge not completed");
}
return { session: startSession(evaluation.user) };
The session starts here, never at /login when a challenge was issued. Consuming rather than just reading is what makes it safe: an attacker who captures the success URL — or guesses an evaluation_id — finds it already spent, never completed, or expired.
Pair this with Signup protection and you've covered both ends of authentication. Every other guide builds on one of the two.
- 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.