Skip to content

Commit 1483475

Browse files
committed
Fix Oauth forgetting data on registration
1 parent b47042f commit 1483475

7 files changed

Lines changed: 236 additions & 26 deletions

File tree

decidim-core/app/commands/decidim/create_omniauth_registration.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def call
2929
return broadcast(:ok, @user)
3030
end
3131
return broadcast(:invalid) if form.invalid?
32+
return broadcast(:add_tos_errors) if requires_tos_acceptance?
3233

3334
transaction do
3435
create_or_find_user
@@ -37,8 +38,6 @@ def call
3738
trigger_omniauth_event
3839

3940
broadcast(:ok, @user)
40-
rescue NeedTosAcceptance
41-
broadcast(:add_tos_errors, @user)
4241
rescue ActiveRecord::RecordInvalid => e
4342
broadcast(:error, e.record)
4443
end
@@ -75,14 +74,19 @@ def create_or_find_user
7574
attach_avatar(form.avatar_url) if form.avatar_url.present?
7675
@user.tos_agreement = form.tos_agreement
7776
@user.accepted_tos_version = Time.current
78-
raise NeedTosAcceptance if @user.tos_agreement.blank?
7977

8078
@user.skip_confirmation! if verified_email
8179
@user.save!
8280
@user.after_confirmation if verified_email
8381
end
8482
end
8583

84+
def requires_tos_acceptance?
85+
return false if form.tos_agreement.present?
86+
87+
verified_email.blank? || User.find_by(email: verified_email, organization:).blank?
88+
end
89+
8690
def attach_avatar(avatar_url)
8791
url = URI.parse(avatar_url)
8892
filename = File.basename(url.path)
@@ -148,9 +152,6 @@ def trigger_omniauth_event(event_name = "decidim.user.omniauth_registration")
148152
end
149153
end
150154

151-
class NeedTosAcceptance < StandardError
152-
end
153-
154155
class InvalidOauthSignature < StandardError
155156
end
156157
end

decidim-core/app/controllers/decidim/devise/omniauth_registrations_controller.rb

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ def new
1414
end
1515

1616
def create
17-
form_params = user_params_from_oauth_hash || params[:user]
17+
return handle_missing_oauth_data! if pending_oauth_data.blank?
1818

19-
@form = form(OmniauthRegistrationForm).from_params(form_params)
20-
@form.email ||= verified_email
19+
form_params = form_params_from_pending_oauth
2120

21+
@form = form(OmniauthRegistrationForm).from_params(form_params)
2222
CreateOmniauthRegistration.call(@form, verified_email) do
2323
on(:ok) do |user|
24+
clear_pending_oauth_data!
25+
2426
if user.active_for_authentication?
2527
sign_in_and_redirect user, event: :authentication
2628
set_flash_message :notice, :success, kind: @form.provider.capitalize
@@ -39,7 +41,6 @@ def create
3941

4042
on(:add_tos_errors) do
4143
set_flash_message :alert, :add_tos_errors if @form.valid_tos?
42-
session[:verified_email] = verified_email
4344
render :new_tos_fields
4445
end
4546

@@ -61,28 +62,90 @@ def action_missing(action_name)
6162

6263
private
6364

65+
def form_params_from_pending_oauth
66+
submitted_params = params.fetch(:user, {}).permit(:name, :nickname, :email, :tos_agreement, :newsletter).to_h.symbolize_keys
67+
oauth_params = pending_oauth_form_params
68+
69+
oauth_params.merge(
70+
submitted_params
71+
)
72+
end
73+
6474
def oauth_data
6575
@oauth_data ||= oauth_hash.slice(:provider, :uid, :info)
6676
end
6777

68-
# Private: Create form params from omniauth hash
69-
# Since we are using trusted omniauth data we are generating a valid signature.
70-
def user_params_from_oauth_hash
71-
return nil if oauth_data.empty?
78+
# Private: Create trusted form params from oauth data stored server-side.
79+
# Since we are using trusted oauth data we are generating a valid signature.
80+
def pending_oauth_form_params
81+
return {} if pending_oauth_data.blank?
7282

7383
{
84+
provider: pending_oauth_data[:provider],
85+
uid: pending_oauth_data[:uid],
86+
email: pending_oauth_data[:verified_email],
87+
name: pending_oauth_data[:name],
88+
nickname: pending_oauth_data[:nickname],
89+
oauth_signature: OmniauthRegistrationForm.create_signature(pending_oauth_data[:provider], pending_oauth_data[:uid]),
90+
avatar_url: pending_oauth_data[:avatar_url],
91+
raw_data: pending_oauth_data[:raw_data],
92+
pending_oauth_token:
93+
}
94+
end
95+
96+
def verified_email
97+
@verified_email ||= pending_oauth_data&.dig(:verified_email)
98+
end
99+
100+
def pending_oauth_data
101+
@pending_oauth_data ||= if oauth_hash.present?
102+
store_pending_oauth_data!
103+
else
104+
restore_pending_oauth_data
105+
end
106+
end
107+
108+
def pending_oauth_token
109+
@pending_oauth_token ||= params.dig(:user, :pending_oauth_token).presence || session.dig(:omniauth_registration, :token)
110+
end
111+
112+
def store_pending_oauth_data!
113+
data = {
74114
provider: oauth_data[:provider],
75115
uid: oauth_data[:uid],
76-
name: oauth_data[:info][:name],
77-
nickname: oauth_data[:info][:nickname],
78-
oauth_signature: OmniauthRegistrationForm.create_signature(oauth_data[:provider], oauth_data[:uid]),
79-
avatar_url: oauth_data[:info][:image],
116+
name: oauth_data.dig(:info, :name),
117+
nickname: oauth_data.dig(:info, :nickname),
118+
avatar_url: oauth_data.dig(:info, :image),
119+
verified_email: oauth_data.dig(:info, :email).presence,
80120
raw_data: oauth_hash
81121
}
122+
123+
session[:omniauth_registration] = {
124+
token: SecureRandom.hex(16),
125+
data:
126+
}
127+
128+
@pending_oauth_token = session[:omniauth_registration][:token]
129+
data
82130
end
83131

84-
def verified_email
85-
@verified_email ||= oauth_data.dig(:info, :email).presence || session[:verified_email]
132+
def restore_pending_oauth_data
133+
token = params.dig(:user, :pending_oauth_token)
134+
return {} if token.blank?
135+
136+
state = session[:omniauth_registration]&.with_indifferent_access
137+
return {} if state.blank?
138+
139+
stored_token = state[:token].to_s
140+
return {} unless stored_token.bytesize == token.to_s.bytesize
141+
return {} unless ActiveSupport::SecurityUtils.secure_compare(stored_token, token.to_s)
142+
143+
@pending_oauth_token = stored_token
144+
state[:data] || {}
145+
end
146+
147+
def clear_pending_oauth_data!
148+
session.delete(:omniauth_registration)
86149
end
87150

88151
def oauth_hash
@@ -91,6 +154,11 @@ def oauth_hash
91154

92155
raw_hash.deep_symbolize_keys
93156
end
157+
158+
def handle_missing_oauth_data!
159+
flash[:alert] = t("devise.failure.timeout")
160+
redirect_back_or_to(decidim.root_path)
161+
end
94162
end
95163
end
96164
end

decidim-core/app/forms/decidim/omniauth_registration_form.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class OmniauthRegistrationForm < Form
1515
attribute :oauth_signature, String
1616
attribute :avatar_url, String
1717
attribute :raw_data, Hash
18+
attribute :pending_oauth_token, String
1819

1920
validates :email, presence: true
2021
validates :name, presence: true

decidim-core/app/views/decidim/devise/omniauth_registrations/new.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424

2525
<%= render partial: "decidim/devise/shared/tos_fields", locals: { form: f, user: :user } %>
2626

27-
<%= f.hidden_field :uid %>
28-
<%= f.hidden_field :provider %>
29-
<%= f.hidden_field :oauth_signature %>
27+
<%= f.hidden_field :pending_oauth_token %>
3028
</div>
3129

3230
<div class="form__wrapper-block">

decidim-core/app/views/decidim/devise/omniauth_registrations/new_tos_fields.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212

1313
<%= render partial: "decidim/devise/shared/tos_fields", locals: { form: f, user: :user } %>
1414

15-
<%= f.hidden_field :uid %>
16-
<%= f.hidden_field :provider %>
17-
<%= f.hidden_field :oauth_signature %>
15+
<%= f.hidden_field :pending_oauth_token %>
1816
</div>
1917

2018
<div class="form__wrapper-block">

decidim-core/spec/commands/decidim/create_omniauth_registration_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ module Comments
107107
end
108108
end
109109

110+
context "when tos agreement is missing for a new user" do
111+
let(:tos_agreement) { nil }
112+
113+
it "broadcasts add_tos_errors" do
114+
expect { command.call }.to broadcast(:add_tos_errors)
115+
end
116+
117+
it "does not create a new user" do
118+
expect do
119+
command.call
120+
end.not_to change(User, :count)
121+
end
122+
end
123+
124+
context "when tos agreement is missing for an existing verified user" do
125+
let(:tos_agreement) { nil }
126+
127+
before do
128+
create(:user, email:, organization:)
129+
end
130+
131+
it "broadcasts ok" do
132+
expect { command.call }.to broadcast(:ok)
133+
end
134+
135+
it "does not create a new user" do
136+
expect do
137+
command.call
138+
end.not_to change(User, :count)
139+
end
140+
end
141+
110142
context "when the form is valid" do
111143
it "broadcasts ok" do
112144
expect { command.call }.to broadcast(:ok)

decidim-core/spec/controllers/omniauth_registrations_controller_spec.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,118 @@ module Decidim
219219
end
220220
end
221221
end
222+
223+
context "when ToS is missing and user retries without omniauth.auth" do
224+
let!(:user) { create(:user, organization:, email: "other@example.org") }
225+
let(:raw_data) do
226+
{
227+
"provider" => provider,
228+
"uid" => uid,
229+
"info" => {
230+
"name" => "Facebook User",
231+
"nickname" => "facebook_user",
232+
"email" => email
233+
},
234+
"extra" => {
235+
"raw_info" => {
236+
"id" => uid,
237+
"name" => "Facebook User"
238+
}
239+
}
240+
}
241+
end
242+
243+
before do
244+
request.env["omniauth.auth"] = raw_data
245+
end
246+
247+
it "preserves oauth data in pending state and creates the account on retry" do
248+
expect do
249+
post :create, params: { locale: I18n.locale, user: { tos_agreement: nil } }
250+
end.not_to change(User, :count)
251+
252+
expect(response).to render_template(:new_tos_fields)
253+
254+
token = session.dig(:omniauth_registration, :token)
255+
expect(token).to be_present
256+
expect(session.dig(:omniauth_registration, :data, :raw_data)).to eq(raw_data.deep_symbolize_keys)
257+
258+
request.env["omniauth.auth"] = nil
259+
allow(ActiveSupport::Notifications).to receive(:publish).and_call_original
260+
261+
expect do
262+
post :create, params: { locale: I18n.locale, user: { pending_oauth_token: token, tos_agreement: "1" } }
263+
end.to change(User, :count).by(1)
264+
265+
expect(controller).to be_user_signed_in
266+
expect(User.find_by(email:)).to be_present
267+
expect(ActiveSupport::Notifications).to have_received(:publish).with(
268+
"decidim.user.omniauth_registration",
269+
hash_including(
270+
provider:,
271+
uid:,
272+
email:,
273+
raw_data: raw_data.deep_symbolize_keys
274+
)
275+
)
276+
end
277+
end
278+
279+
context "when oauth data is missing and there is no pending state" do
280+
before do
281+
request.env["omniauth.auth"] = nil
282+
end
283+
284+
it "redirects to root with a timeout alert" do
285+
expect do
286+
post :create, params: { locale: I18n.locale }
287+
end.not_to raise_error
288+
289+
expect(controller).not_to be_user_signed_in
290+
expect(controller).to redirect_to(root_path)
291+
expect(flash[:alert]).to eq(I18n.t("devise.failure.timeout"))
292+
end
293+
end
294+
295+
context "when pending oauth state session keys are strings" do
296+
let(:pending_token) { SecureRandom.hex(16) }
297+
let(:pending_raw_data) do
298+
{
299+
provider:,
300+
uid:,
301+
info: {
302+
name: "Facebook User",
303+
nickname: "facebook_user",
304+
email:
305+
}
306+
}
307+
end
308+
309+
before do
310+
request.env["omniauth.auth"] = nil
311+
session[:omniauth_registration] = {
312+
"token" => pending_token,
313+
"data" => {
314+
"provider" => provider,
315+
"uid" => uid,
316+
"name" => "Facebook User",
317+
"nickname" => "facebook_user",
318+
"avatar_url" => nil,
319+
"verified_email" => email,
320+
"raw_data" => pending_raw_data
321+
}
322+
}
323+
end
324+
325+
it "restores the pending state and signs in" do
326+
expect do
327+
post :create, params: { locale: I18n.locale, user: { pending_oauth_token: pending_token, tos_agreement: "1" } }
328+
end.to change(Identity, :count).by(1)
329+
330+
expect(controller).to be_user_signed_in
331+
expect(User.find_by(email:)).to be_present
332+
end
333+
end
222334
end
223335
end
224336
end

0 commit comments

Comments
 (0)