-
Notifications
You must be signed in to change notification settings - Fork 170
Expand file tree
/
Copy pathsessions_controller.rb
More file actions
311 lines (245 loc) · 11.9 KB
/
Copy pathsessions_controller.rb
File metadata and controls
311 lines (245 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
class SessionsController < ApplicationController
def hca_new
session[:return_data] = build_return_data(params[:continue]) if params[:continue].present?
Rails.logger.info("Sessions return data: #{session[:return_data]}")
redirect_uri = url_for(action: :hca_create, only_path: false)
redirect_to User.hca_authorize_url(redirect_uri),
host: "https://auth.hackclub.com",
allow_other_host: "https://auth.hackclub.com"
end
def hca_create
return if handle_oauth_error("HCA", redirect_path: root_path, alert_label: "Hack Club Auth")
redirect_uri = url_for(action: :hca_create, only_path: false)
@user = User.from_hca_token(params[:code], redirect_uri, client_ip)
if @user&.persisted?
preserved_return_data = session[:return_data]
reset_session
session[:user_id] = @user.id
session[:return_data] = preserved_return_data if preserved_return_data
notice = "Successfully signed in with Hack Club Auth! Welcome!"
if @user.previously_new_record?
redirect_to setup_path, notice: notice
elsif session[:return_data]&.dig("url").present?
redirect_to session[:return_data].delete("url"), notice: notice
else
redirect_to root_path, notice: notice
end
else
redirect_to root_path, alert: "Failed to authenticate with Hack Club Auth!"
end
end
def slack_new
redirect_uri = url_for(action: :slack_create, only_path: false)
oauth_nonce = SecureRandom.hex(24)
session[:slack_oauth_state_nonce] = oauth_nonce
state_payload = {
token: oauth_nonce,
close_window: params[:close_window].present?,
continue: params[:continue]
}.to_json
Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.slack_authorize_url(redirect_uri, state: state_payload),
host: "https://slack.com",
allow_other_host: "https://slack.com"
end
def slack_create
return if handle_oauth_error("Slack", redirect_path: root_path, alert_label: "Slack")
redirect_uri = url_for(action: :slack_create, only_path: false)
slack_state = parse_slack_state(params[:state])
unless valid_oauth_state?(provider: "Slack", session_key: :slack_oauth_state_nonce, received_nonce: slack_state&.dig("token"))
return redirect_to(root_path, alert: "Failed to authenticate with Slack")
end
@user = User.from_slack_token(params[:code], redirect_uri, client_ip)
if @user&.persisted?
reset_session
session[:user_id] = @user.id
notice = "Successfully signed in with Slack! Welcome!"
continue_url = safe_return_url(slack_state&.dig("continue").presence)
if slack_state&.dig("close_window")
redirect_to close_window_path
elsif @user.previously_new_record?
session[:return_data] = build_return_data(continue_url)
redirect_to setup_path, notice: notice
elsif continue_url.present?
redirect_to continue_url, notice: notice # codeql[rb/url-redirection]
else
redirect_to root_path, notice: notice
end
else
report_message("Failed to create/update user from Slack data")
redirect_to root_path, alert: "Failed to sign in with Slack"
end
end
def close_window = render(inertia: "Auth/CloseWindow", layout: "inertia")
def github_new
return unless require_signed_in!("Please sign in first to link your GitHub account")
redirect_uri = url_for(action: :github_create, only_path: false)
oauth_nonce = SecureRandom.hex(24)
session[:github_oauth_state_nonce] = oauth_nonce
Rails.logger.info "Starting GitHub OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.github_authorize_url(redirect_uri, state: oauth_nonce),
allow_other_host: "https://github.com"
end
def github_create
return unless require_signed_in!("Please sign in first to link your GitHub account")
redirect_uri = url_for(action: :github_create, only_path: false)
if params[:error].present?
report_message("GitHub OAuth error: #{params[:error]}")
return redirect_to(my_settings_path, alert: "Failed to authenticate with GitHub. Error ID: #{Sentry.last_event_id}")
end
unless valid_oauth_state?(provider: "GitHub", session_key: :github_oauth_state_nonce, received_nonce: params[:state])
return redirect_to(my_settings_path, alert: "Failed to link GitHub account")
end
@user = User.from_github_token(params[:code], redirect_uri, current_user)
if @user&.persisted?
redirect_to my_settings_path, notice: "Successfully linked GitHub account!"
else
report_message("Failed to link GitHub account")
redirect_to my_settings_path, alert: "Failed to link GitHub account"
end
end
def github_unlink
return unless require_signed_in!("Please sign in first")
current_user.update!(github_access_token: nil, github_uid: nil, github_username: nil)
Rails.logger.info "GitHub account unlinked for User ##{current_user.id}"
redirect_to my_settings_path, notice: "GitHub account unlinked successfully"
end
def email
email = params[:email].downcase
continue_param = params[:continue]
if Rails.env.production?
HandleEmailSigninJob.perform_later(email, continue_param, client_ip)
else
token = HandleEmailSigninJob.perform_now(email, continue_param, client_ip)
public_url = ENV["PUBLIC_URL"].presence || root_url
session[:dev_magic_link] = URI.join(public_url, auth_token_path(token)).to_s
end
redirect_path = params[:redirect_to] == "signin" ? signin_path(sign_in_email: true) : root_path(sign_in_email: true)
redirect_to redirect_path, notice: "Check your email for a sign-in link!"
end
def add_email
return unless require_signed_in!("Please sign in first to add an email")
email = params[:email].downcase
conflict =
("#{email} is already linked to an account." if EmailAddress.exists?(email: email)) ||
("#{email} already has a pending verification — check your inbox, or use \"Resend\" to get a new link." if EmailVerificationRequest.kept.exists?(email: email))
return redirect_to(my_settings_path, alert: conflict) if conflict
verification_request = current_user.email_verification_requests.create!(email: email)
mailer = EmailVerificationMailer.verify_email(verification_request)
Rails.env.production? ? mailer.deliver_later : mailer.deliver_now
redirect_to my_settings_path, notice: "Verification email sent — check #{email} to confirm it."
rescue ActiveRecord::RecordInvalid => e
redirect_to my_settings_path, alert: "Couldn't add #{email}: #{e.record.errors.full_messages.join(', ')}."
end
def resend_email_verification
return unless require_signed_in!("Please sign in first to resend a verification email.")
email = params[:email].to_s.downcase
verification_request = current_user.email_verification_requests.kept.find_by(email: email)
unless verification_request
redirect_to my_settings_path, alert: "There's no pending verification for #{email}. Try adding the email again."
return
end
unless verification_request.resend_available?
cooldown_minutes = (verification_request.resend_cooldown_seconds / 60.0).ceil
redirect_to my_settings_path,
alert: "We just sent a verification email — you can resend it in #{cooldown_minutes} minute#{'s' unless cooldown_minutes == 1}."
return
end
verification_request.refresh_for_resend!
mailer = EmailVerificationMailer.verify_email(verification_request)
Rails.env.production? ? mailer.deliver_later : mailer.deliver_now
redirect_to my_settings_path, notice: "Verification email resent — check #{email} to confirm it."
rescue ActiveRecord::RecordInvalid => e
redirect_to my_settings_path, alert: "Couldn't resend the verification email: #{e.record.errors.full_messages.join(', ')}."
end
def unlink_email
return unless require_signed_in!("Please sign in first to unlink an email")
email = params[:email].downcase
email_record = current_user.email_addresses.find_by(email: email)
unless email_record
pending_request = current_user.email_verification_requests.kept.find_by(email: email)
return redirect_to(my_settings_path, alert: "#{email} isn't linked to your account.") unless pending_request
pending_request.soft_delete!
return redirect_to(my_settings_path, notice: "Removed the pending verification for #{email}.")
end
unless current_user.can_delete_email_address?(email_record)
return redirect_to(my_settings_path, alert: "You can only unlink emails that are used for signing in.")
end
email_verification_request = current_user.email_verification_requests.find_by(email: email)
email_record.destroy!
email_verification_request&.soft_delete!
redirect_to my_settings_path, notice: "Unlinked #{email} from your account."
rescue ActiveRecord::RecordNotDestroyed => e
redirect_to my_settings_path, alert: "Couldn't unlink #{email}: #{e.record.errors.full_messages.join(', ')}."
end
def token
verification_request = EmailVerificationRequest.valid.find_by(token: params[:token])
if verification_request
verification_request.verify!
redirect_to my_settings_path, notice: "Successfully verified your email address!"
return
end
valid_token = SignInToken.where(token: params[:token], used_at: nil)
.where("expires_at > ?", Time.current).first
if valid_token
valid_token.mark_used!
reset_session
session[:user_id] = valid_token.user_id
continue_url = safe_return_url(valid_token.continue_param)
session[:return_data] = (valid_token.return_data || {}).merge(build_return_data(continue_url))
if continue_url.present?
redirect_to continue_url, notice: "Successfully signed in!" # codeql[rb/url-redirection]
else
redirect_to root_path, notice: "Successfully signed in!"
end
else
redirect_to root_path, alert: "Invalid or expired link"
end
end
def impersonate
return unless require_admin!(alert: "You are not authorized to impersonate users")
user = User.find_by(id: params[:id])
return redirect_to(root_path, alert: "who?") unless user
return redirect_to(root_path, alert: "nice try, you cant do that") unless current_user.can_impersonate?(user)
session[:impersonater_user_id] ||= current_user.id
session[:user_id] = user.id
redirect_to root_path, notice: "Impersonating #{user.display_name}"
end
def stop_impersonating
session[:user_id] = session[:impersonater_user_id]
session[:impersonater_user_id] = nil
redirect_to root_path, notice: "Stopped impersonating"
end
def destroy
reset_session
redirect_to root_path, notice: "Signed out!"
end
private
def client_ip = request.headers["CF-Connecting-IP"].presence || request.remote_ip
def parse_slack_state(raw_state)
JSON.parse(raw_state)
rescue JSON::ParserError, TypeError
nil
end
def valid_oauth_state?(provider:, session_key:, received_nonce:)
expected_nonce = session.delete(session_key)
if expected_nonce.blank? || received_nonce.blank?
report_message("#{provider} OAuth state missing expected=#{expected_nonce.present?} received=#{received_nonce.present?}")
return false
end
return true if ActiveSupport::SecurityUtils.secure_compare(received_nonce.to_s, expected_nonce.to_s)
report_message("#{provider} OAuth state mismatch")
false
end
# Handles OAuth callback errors. Returns true if a redirect was performed.
def handle_oauth_error(provider, redirect_path:, alert_label:)
return false if params[:error].blank?
if params[:error] == "access_denied"
redirect_to redirect_path, alert: "Sign in cancelled"
return true
end
report_message("#{provider} OAuth error: #{params[:error]}")
redirect_to redirect_path, alert: "Failed to authenticate with #{alert_label}. Error ID: #{Sentry.last_event_id}"
true
end
end