PasswordsController
Supabase::Rails::PasswordsController — the two-step password reset flow.
Supabase::Rails::PasswordsController handles the two-step password reset flow: request a reset email, then set the new password via the recovery deep-link. The install generator writes a top-level PasswordsController that inherits from it.
Under the hood, create wraps the supabase-rb auth.reset_password_for_email call (via supabase_reset_password); update wraps auth.update_user (via supabase_update_user).
# app/controllers/passwords_controller.rb (written by `bin/rails generate supabase:install`)
class PasswordsController < Supabase::Rails::PasswordsController
endSource
# app/controllers/supabase/rails/passwords_controller.rb (in the gem)
class PasswordsController < BaseController
allow_unauthenticated_access only: %i[new create edit update]
def new; end
def create
result = supabase_reset_password(email: params[:email])
if result.success?
redirect_to new_session_path,
notice: I18n.t("supabase.rails.passwords.reset_sent")
else
flash.now[:alert] = result.error.message
render :new, status: :unprocessable_entity
end
end
def edit; end
def update
result = supabase_update_user(password: params[:password])
if result.success?
redirect_to new_session_path,
notice: I18n.t("supabase.rails.passwords.updated")
else
flash.now[:alert] = result.error.message
render :edit, status: :unprocessable_entity
end
end
endRoutes
| Helper | Verb | URL | Action |
|---|---|---|---|
new_password_path | GET | /passwords/new | new |
passwords_path | POST | /passwords | create |
edit_password_path(token) | GET | /passwords/:token/edit | edit |
password_path(token) | PATCH/PUT | /passwords/:token | update |
Mounted by resources :passwords, only: %i[new create edit update], param: :token inside supabase_authentication_routes. The param: :token makes the dynamic segment :token instead of the default :id.
The `:token` segment is opaque to the gem
The :token route segment exists so the URL the recovery email points the user at is shaped like /passwords/<recovery_token>/edit. The action body never reads params[:token] — Supabase Auth uses a session cookie (set by the recovery deep-link redirect) as the credential. The path param is presentational only; you can leave it any value the recovery email puts there.
Actions
new
Renders the "Forgot your password?" form at /passwords/new. Action body is empty. Allowed without authentication.
The shipped view at app/views/supabase/rails/passwords/new.html.erb submits to passwords_path with one field (email).
create
Triggers the password-reset email. Allowed without authentication.
Params
| Param | Source | Notes |
|---|---|---|
email | POST body | Required. Forwarded to Supabase Auth verbatim. |
Outcome dispatch
| Branch | Redirect / render | Status | Flash |
|---|---|---|---|
| Success | redirect_to new_session_path | 302 | notice: I18n.t("supabase.rails.passwords.reset_sent") |
| Failure — 4xx | render :new | 422 | flash.now[:alert] = result.error.message |
| Failure — 5xx | render :new | 422 | flash.now[:alert] = result.error.message |
The success branch fires whether or not the email address exists in Supabase Auth — the upstream call returns success either way, preventing user enumeration. The Supabase project's email templates control the recovery email content and link.
The recovery deep-link must point back at `edit_password_path`
By default Supabase Auth sends the user to <SUPABASE_URL>/auth/v1/verify?... which redirects to the Site URL configured for the project (Authentication → URL Configuration in the dashboard). Set that Site URL to your app's origin so the user lands on <your-app>/passwords/<token>/edit with a recovery session cookie already set. Add additional callback URLs under Redirect URLs for staging / preview environments.
edit
Renders the "Update your password" form at /passwords/:token/edit. Action body is empty — Rails renders passwords/edit.html.erb. Allowed without authentication.
The user arrives here after clicking the recovery email link. Supabase Auth's verify endpoint redirects to this URL with a recovery session cookie already set on the host's domain, so update can talk to Supabase as the authenticated user even though no manual sign-in has happened.
The shipped view at app/views/supabase/rails/passwords/edit.html.erb submits to password_path(params[:token]) with two fields (password, password_confirmation).
update
Sets the new password and redirects to sign-in. Allowed without authentication (the recovery session cookie is what's verifying the user).
Params
| Param | Source | Notes |
|---|---|---|
password | PATCH/PUT body | Required. Max 72 bytes (bcrypt limit). |
token | URL path | Ignored by the action body. Presentational — Supabase Auth uses the recovery session cookie. |
Outcome dispatch
| Branch | Redirect / render | Status | Flash |
|---|---|---|---|
| Success | redirect_to new_session_path | 302 | notice: I18n.t("supabase.rails.passwords.updated") |
| Failure — weak password | render :edit | 422 | flash.now[:alert] = result.error.message (WEAK_PASSWORD mapper message) |
| Failure — missing session | render :edit | 422 | flash.now[:alert] = result.error.message (SESSION_MISSING — recovery cookie expired or never set) |
| Failure — other 4xx/5xx | render :edit | 422 | flash.now[:alert] = result.error.message |
The success branch deliberately redirects to new_session_path, not after_authentication_url — Supabase Auth invalidates the recovery session as soon as the password update lands, so the user must sign in again with the new password.
The SESSION_MISSING branch fires when the user lands on the page without a valid recovery cookie (link expired, opened in a different browser, etc.). The flash surfaces the mapper's "Session missing" message; restart the flow from new_password_path.
allow_unauthenticated_access
allow_unauthenticated_access only: %i[new create edit update]All four actions are exempt. Even edit / update — the recovery session cookie is what Supabase Auth reads, not the standard authenticated-user cookie.
Hookable callbacks
| Hook | Default | Where to override |
|---|---|---|
after_authentication_url | stored_location_for_redirect || root_url | Not used by this controller — both success redirects target new_session_path directly because Supabase invalidates the recovery session on update. |
For a different post-update destination (e.g. an in-app banner instead of forcing a re-sign-in flow), override update in the host subclass:
class PasswordsController < Supabase::Rails::PasswordsController
def update
result = supabase_update_user(password: params[:password])
if result.success?
redirect_to login_help_path, notice: t(".updated")
else
flash.now[:alert] = result.error.message
render :edit, status: :unprocessable_entity
end
end
endOverride patterns
Custom redirect_to: on the reset email. supabase_reset_password accepts a redirect_to: keyword that becomes the URL the recovery email's "Reset password" button points at — useful for routing to a /auth/recovery page that performs additional verification before exposing the password form. The URL is validated against config.supabase.allowed_redirect_origins by RedirectValidator before the upstream call:
class PasswordsController < Supabase::Rails::PasswordsController
def create
result = supabase_reset_password(
email: params[:email],
redirect_to: edit_password_url(token: "recovery"),
)
if result.success?
redirect_to new_session_path, notice: t(".sent")
else
flash.now[:alert] = result.error.message
render :new, status: :unprocessable_entity
end
end
endA redirect_to: outside the allowlist fails fast with Result.failure(AuthError(INVALID_REDIRECT, 400)) without ever bothering the upstream — an attacker who forges a sign-up form with redirect_to=https://evil.example.com cannot redirect users via this controller.
Password-confirmation parity check. The shipped view sends a password_confirmation field; the action body ignores it because Supabase Auth handles the canonical password validation. To enforce same-value before calling the upstream, override update:
class PasswordsController < Supabase::Rails::PasswordsController
def update
if params[:password] != params[:password_confirmation]
flash.now[:alert] = t(".confirmation_mismatch")
return render :edit, status: :unprocessable_entity
end
super
end
endReplacing with a from-scratch controller — point the host subclass at Supabase::Rails::BaseController and call supabase_update_user (or any of the other low-level helpers) directly. The helper requires a valid encrypted session cookie — when the cookie is missing or carries no access token, it fast-fails with Result.failure(AuthError.session_missing) (401, SESSION_MISSING) before reaching the upstream.
See also
- Controllers overview — the full route table and the override pattern.
BaseController— the common parent class.Authenticationconcern —supabase_reset_password,supabase_update_user.RedirectValidator— theredirect_to:allowlist.AuthErrorMapper—WEAK_PASSWORD,SESSION_MISSING,AUTH_UPSTREAM_ERROR.supabase:viewsgenerator — copypasswords/new.html.erb+passwords/edit.html.erbinto the host app.- supabase-rb:
auth.reset_password_for_emailandauth.update_user— the underlying Ruby methods these actions delegate to.