Allow SSO sessions to set a password without the current one

Accounts provisioned by trusted-header SSO get a random password the user
never saw, so /change_password was unusable for them. When the trusted
header asserts the same email as the session user, waive the current
password check and hide the field, so native clients (Yattee) can be given
a password to log in with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
NeskireDK 2026-08-12 21:02:26 +02:00
parent c644d73def
commit 5025d06c00
2 changed files with 14 additions and 3 deletions

View File

@ -23,6 +23,8 @@ module Invidious::Routes::Account
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY) csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY)
sso_verified = Invidious::TrustedHeaderAuth.asserted_email(env) == user.email
templated "user/change_password" templated "user/change_password"
end end
@ -48,8 +50,13 @@ module Invidious::Routes::Account
return error_template(400, ex) return error_template(400, ex)
end end
# An SSO session proves the identity through the trusted header already, so
# the current password is waived. Accounts provisioned by SSO were given a
# random password the user never saw and could never type here.
sso_verified = Invidious::TrustedHeaderAuth.asserted_email(env) == user.email
password = env.params.body["password"]? password = env.params.body["password"]?
if password.nil? || password.empty? if !sso_verified && (password.nil? || password.empty?)
return error_template(401, "Password is a required field") return error_template(401, "Password is a required field")
end end
@ -68,8 +75,10 @@ module Invidious::Routes::Account
return error_template(400, "Password cannot be longer than 55 characters") return error_template(400, "Password cannot be longer than 55 characters")
end end
if !Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) if !sso_verified
return error_template(401, "Incorrect password") if !Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.not_nil!.byte_slice(0, 55))
return error_template(401, "Incorrect password")
end
end end
new_password = Crypto::Bcrypt::Password.create(new_password, cost: 10) new_password = Crypto::Bcrypt::Password.create(new_password, cost: 10)

View File

@ -10,8 +10,10 @@
<legend><%= I18n.translate(locale, "Change password") %></legend> <legend><%= I18n.translate(locale, "Change password") %></legend>
<fieldset> <fieldset>
<% if !sso_verified %>
<label for="password"><%= I18n.translate(locale, "Password") %> :</label> <label for="password"><%= I18n.translate(locale, "Password") %> :</label>
<input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>"> <input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>">
<% end %>
<label for="new_password[0]"><%= I18n.translate(locale, "New password") %> :</label> <label for="new_password[0]"><%= I18n.translate(locale, "New password") %> :</label>
<input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>"> <input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>">