Authentication
Documentation / Authentication / Password Reset

Password Reset

What the framework provides

Runique includes a complete, ready-to-use system:

  • 2 auto-registered routes (forgot form + reset form)
  • 2 built-in forms (ForgotPasswordForm, PasswordResetForm)
  • Token generation and validation (UUID, 1h TTL, single-use)
  • Automatic email sending if the mailer is configured
  • Built-in rate limiting (5 requests / 5 min by default)
  • Included i18n messages
  • Once the reset succeeds: every session of the account closed, and the one of the browser it ran in (even signed in as another account) — in the same transaction as the password change. Showing the form signs nobody out
  • Depending on the account's state: activation link (pending account), reset link (active account), "account blocked" email with no link (deactivated account) — the page always answers the same. See Account states

Activation

RuniqueApp::builder(config)
    .with_public_url("https://mysite.com")
    .with_mailer_from_env()
    .with_password_reset(|pr| pr)
    .build()
    .await?

The links sent by email are built on the site's public URL, set in the builder with .with_public_url("https://mysite.com") (see Builder).

In production, the app refuses to boot without it: otherwise the link would be built from the request's Host header, which an attacker chooses (Host: evil.com → the victim's token goes to them). In debug, the Host stays the fallback, with a warning.

Accounts are those of eihwaz_users, the framework's user table — extend it with extend!{ table: "eihwaz_users", ... }.


Configuration

All options are optional — the defaults work without any changes.

.with_password_reset(|pr| pr
    .forgot_route("/forgot-password")           // default: /forgot-password
    .reset_route("/reset-password")             // default: /reset-password
    .forgot_template("auth/forgot.html")        // default: auth/forgot_password.html
    .reset_template("auth/reset.html")          // default: auth/reset_password.html
    .email_template("emails/reset.html")        // default: built-in template
    .success_redirect("/login")                 // default: /
    .token_ttl(Duration::from_secs(1800))       // default: 1h (std::time::Duration)
)

Templates

Two templates can be created in templates/. If absent, the framework uses its own default templates.

auth/forgot_password.html — "forgot email" form

{% extends "base.html" %}
{% block content %}
<form method="post">
    {{ form.html | safe }}
    <button type="submit">Send reset link</button>
</form>

{% if form.errors.email %}
    <p class="error">{{ form.errors.email }}</p>
{% endif %}
{% endblock %}

Available Tera context variables:

VariableDescription
formForm with email field
form.errorsValidation errors

auth/reset_password.html — reset form

{% extends "base.html" %}
{% block content %}
<form method="post">
    {{ form.html | safe }}
    <button type="submit">Change password</button>
</form>

{% if form.errors.password %}
    <p class="error">{{ form.errors.password }}</p>
{% endif %}
{% endblock %}

Available variables:

VariableDescription
formForm with a hidden token and email, password, and confirm fields
form.errorsValidation errors

Automatic validations: minimum 10-character password, matching confirmation, valid and non-expired token, and the typed email matching the account bound to the token.


Full flow

1. User clicks "Forgot password"
        ↓
2. GET /forgot-password → displays the email form
        ↓
3. POST /forgot-password → email submitted
   - If email exists: generates a token (1h), sends an email with the link
   - If email unknown: same response (security — does not reveal existing accounts)
        ↓
4. User clicks the link received by email
        ↓
5. GET /reset-password/{token} → displays the reset form
   - Invalid or expired token → error
        ↓
6. POST /reset-password/{token}
   - Validates the form and checks the typed email against the account bound to the token
   - Consumes the token (single-use)
   - Updates password in DB
   - Redirects to success_redirect (default: /)

Active sessions after reset: once the new password is saved, every session of the account is closed, on every device — a stolen one included —, and the session of the browser that did the reset. All of it happens in the password change's transaction: when the database refuses, nothing changes and the link stays usable. One link is live per account: a new request replaces the previous one. The user signs in again with the new password. Other accounts are left alone. Only Runique's stores are covered: a store plugged in with with_session_store() (Redis…) can't be searched by user — see Revoking sessions.


Admin integration

If the admin is activated with .user_resource(), the panel can send reset links directly from the user detail view.

The link is built on the public URL (.with_public_url()) and the configured reset route (reset_route, /reset-password by default): nothing to set on the admin side. Without a mailer, the link is shown in a flash message.

Optional admin email templates (framework defaults otherwise):

  • templates/admin/reset_password_email.html
  • templates/admin/user_created_email.html

Available variables in these templates: username, email, reset_url.


Without a configured mailer

The mailer is optional. If absent:

  • In development: the reset link appears in a flash message in the admin
  • On the forgot form: the success message is shown but no email is sent

To test in dev without an SMTP server: EMAIL_BACKEND=console prints the email to the terminal.


← Authentication | Mailer →