Skip to content

Commit 305ebed

Browse files
authored
Allow passing a redirect path to a param on the /login route (#32711)
Detects the `redirect` queryParam on the /login route. If the user is already logged in, navigates to that page. Otherwise, sets the "destination_url" cookie so that the user will be redirected after logging in. If the param doesn't start with a single slash, ignore it and follow previous behavior (navigate to "/" or log in then navigate there) Respects subfolder configs.
1 parent 2e4076f commit 305ebed

File tree

4 files changed

+120
-27
lines changed

4 files changed

+120
-27
lines changed

app/controllers/static_controller.rb

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,36 @@ class StaticController < ApplicationController
2727
}
2828
CUSTOM_PAGES = {} # Add via `#add_topic_static_page` in plugin API
2929

30+
def extract_redirect_param
31+
redirect_path = params[:redirect]
32+
if redirect_path.present?
33+
begin
34+
forum_host = URI(Discourse.base_url).host
35+
uri = URI(redirect_path)
36+
37+
if uri.path.present? && !uri.path.starts_with?(login_path) &&
38+
(uri.host.blank? || uri.host == forum_host) && uri.path =~ %r{\A\/{1}[^\.\s]*\z}
39+
return "#{uri.path}#{uri.query ? "?#{uri.query}" : ""}"
40+
end
41+
rescue URI::Error, ArgumentError
42+
# If the URI is invalid, return "/" below
43+
end
44+
end
45+
46+
"/"
47+
end
48+
3049
def show
31-
if current_user && (params[:id] == "login" || params[:id] == "signup")
32-
return redirect_to(path "/")
50+
if params[:id] == "login"
51+
destination = extract_redirect_param
52+
53+
if current_user
54+
return redirect_to(path(destination), allow_other_host: false)
55+
elsif destination != "/"
56+
cookies[:destination_url] = path(destination)
57+
end
58+
elsif params[:id] == "signup" && current_user
59+
return redirect_to path("/")
3360
end
3461

3562
if SiteSetting.login_required? && current_user.nil? && %w[faq guidelines].include?(params[:id])
@@ -123,26 +150,7 @@ def enter
123150
params.delete(:username)
124151
params.delete(:password)
125152

126-
destination = path("/")
127-
128-
redirect_location = params[:redirect]
129-
if redirect_location.present? && !redirect_location.is_a?(String)
130-
raise Discourse::InvalidParameters.new(:redirect)
131-
elsif redirect_location.present? &&
132-
begin
133-
forum_uri = URI(Discourse.base_url)
134-
uri = URI(redirect_location)
135-
136-
if uri.path.present? && !uri.path.starts_with?(login_path) &&
137-
(uri.host.blank? || uri.host == forum_uri.host) &&
138-
uri.path =~ %r{\A\/{1}[^\.\s]*\z}
139-
destination = "#{uri.path}#{uri.query ? "?#{uri.query}" : ""}"
140-
end
141-
rescue URI::Error
142-
# Do nothing if the URI is invalid
143-
end
144-
end
145-
153+
destination = extract_redirect_param
146154
redirect_to(destination, allow_other_host: false)
147155
end
148156

spec/requests/static_controller_spec.rb

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
end
151151
end
152152

153-
it "should redirect to / when logged in and path is /login" do
153+
it "should redirect to / when logged in and path is /login without redirect" do
154154
sign_in(Fabricate(:user))
155155
get "/login"
156156
expect(response).to redirect_to("/")
@@ -252,6 +252,58 @@
252252
get "/login"
253253
end.to_not change { SiteSetting.title }
254254
end
255+
256+
context "without a subfolder" do
257+
it "redirects as requested when logged in and path is /login with valid redirect param" do
258+
sign_in(Fabricate(:user))
259+
get "/login", params: { redirect: "/foo" }
260+
expect(response).to redirect_to("/foo")
261+
end
262+
263+
it "redirects to / when logged in and path is /login with invalid redirect param" do
264+
sign_in(Fabricate(:user))
265+
get "/login", params: { redirect: "//foo" }
266+
expect(response).to redirect_to("/")
267+
get "/login", params: { redirect: "foo" }
268+
expect(response).to redirect_to("/")
269+
get "/login", params: { redirect: "http://foo" }
270+
expect(response).to redirect_to("/")
271+
get "/login", params: { redirect: "www.foo.bar" }
272+
expect(response).to redirect_to("/")
273+
end
274+
275+
it "sets the destination_url cookie when not logged in and path is /login with valid redirect param" do
276+
get "/login", params: { redirect: "/foo" }
277+
expect(response.cookies["destination_url"]).to eq("/foo")
278+
end
279+
end
280+
281+
context "with a subfolder" do
282+
before { set_subfolder "/sub_test" }
283+
284+
it "redirects as requested when logged in and path is /login with valid redirect param" do
285+
sign_in(Fabricate(:user))
286+
get "/login", params: { redirect: "/foo" }
287+
expect(response).to redirect_to("/sub_test/foo")
288+
end
289+
290+
it "redirects to / when logged in and path is /login with invalid redirect param" do
291+
sign_in(Fabricate(:user))
292+
get "/login", params: { redirect: "//foo" }
293+
expect(response).to redirect_to("/sub_test/")
294+
get "/login", params: { redirect: "foo" }
295+
expect(response).to redirect_to("/sub_test/")
296+
get "/login", params: { redirect: "http://foo" }
297+
expect(response).to redirect_to("/sub_test/")
298+
get "/login", params: { redirect: "www.foo.bar" }
299+
expect(response).to redirect_to("/sub_test/")
300+
end
301+
302+
it "sets the destination_url cookie when not logged in and path is /login with valid redirect param" do
303+
get "/login", params: { redirect: "/foo" }
304+
expect(response.cookies["destination_url"]).to eq("/sub_test/foo")
305+
end
306+
end
255307
end
256308

257309
describe "#enter" do
@@ -307,10 +359,7 @@
307359
context "with an array" do
308360
it "redirects to the root" do
309361
post "/login.json", params: { redirect: ["/foo"] }
310-
expect(response.status).to eq(400)
311-
json = response.parsed_body
312-
expect(json["errors"]).to be_present
313-
expect(json["errors"]).to include(I18n.t("invalid_params", message: "redirect"))
362+
expect(response).to redirect_to("/")
314363
end
315364
end
316365

spec/system/login_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ def wait_for_email_link(user, type)
3939
expect(page).to have_css(".header-dropdown-toggle.current-user")
4040
end
4141

42+
it "can login with redirect" do
43+
EmailToken.confirm(Fabricate(:email_token, user: user).token)
44+
45+
login_form
46+
.open_with_redirect("/about")
47+
.fill(username: "john", password: "supersecurepassword")
48+
.click_login
49+
expect(page).to have_current_path("/about")
50+
end
51+
4252
it "can login and activate account" do
4353
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
4454
expect(page).to have_css(".not-activated-modal")
@@ -258,6 +268,12 @@ def wait_for_email_link(user, type)
258268

259269
expect(page).to have_css(".authorize-api-key .scopes")
260270
end
271+
272+
it "redirects when navigating to login with redirect param" do
273+
mock_google_auth
274+
login_form.open_with_redirect("/about")
275+
expect(page).to have_current_path("/about")
276+
end
261277
end
262278
end
263279

@@ -294,6 +310,21 @@ def wait_for_email_link(user, type)
294310
expect(page).to have_css(".header-dropdown-toggle.current-user")
295311
end
296312

313+
it "can login with totp and redirect" do
314+
login_form
315+
.open_with_redirect("/about")
316+
.fill(username: "john", password: "supersecurepassword")
317+
.click_login
318+
319+
expect(page).to have_css(".second-factor")
320+
321+
totp = ROTP::TOTP.new(user_second_factor.data).now
322+
find("#login-second-factor").fill_in(with: totp)
323+
login_form.click_login
324+
325+
expect(page).to have_current_path("/about")
326+
end
327+
297328
it "can login with backup code" do
298329
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
299330

spec/system/page_objects/pages/login.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ def open
1616
self
1717
end
1818

19+
def open_with_redirect(redirect_path)
20+
visit("/login?redirect=#{redirect_path}")
21+
self
22+
end
23+
1924
def open_from_header
2025
find(".login-button").click
2126
end

0 commit comments

Comments
 (0)