---
title: Login protection
---

# 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 `/login` again. 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: allow` and 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_id` whose challenge already completed — to mint a session without passing a challenge of their own.

## The flow

<!-- prettier-ignore-start -->
::MermaidDiagram
---
code: |
  sequenceDiagram
    actor U as User
    participant C as Login form
    participant S as Your server
    participant R as Rupt
    participant X as Challenge UI

    U->>C: Submits login form (email, password)
    C->>R: evaluate.login({ user, email })
    R-->>C: { evaluation_id, redirect? }
    C->>S: POST /login { credentials, evaluation_id }
    S->>R: GET /v3/evaluations/{evaluation_id}
    R-->>S: { verdict, user, challenge }
    Note over S: Check password, run integrity check

    alt verdict = deny
      S-->>C: Reject (401)
      C-->>U: Show error
    else verdict = allow
      S->>S: Start a session
      S-->>U: Logged in
    else verdict = challenge
      S-->>C: { redirect } (no session yet)
      C-->>U: Navigate to challenge URL
      U->>X: Complete challenge
      X->>S: Redirect to success_url?evaluation=…
      S->>R: POST /v3/evaluations/{evaluation_id}/consume
      R-->>S: { challenge.status, createdAt } — or 409 if already used
      Note over S: Confirm the consume succeeded,<br/>challenge.status = completed, and the evaluation is fresh
      S->>S: Start a session
      S-->>U: Logged in
    end
---
::
<!-- prettier-ignore-end -->

## Step 1: Call evaluate at login

Pass the `user` id and `email` (and `phone` if you have it).

::ClientPlatform

#web

```js
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,
  }),
});
```

#ios

```swift
let response = try await rupt.evaluate(
  action: "login",
  user: user.id,
  email: form.email
)

// POST evaluation.evaluationId to your server with the credentials
```

#android

```kotlin
val response = rupt.evaluate(
  action = "login",
  user = user.id,
  email = form.email,
)

// POST response.evaluationId to your server with the credentials
```

::

## 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.

```ts
// 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.

```ts
// 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](/docs/v3/fundamentals/signup-protection) and you've covered both ends of authentication. Every other guide builds on one of the two.
