mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 23:32:53 +00:00
* Several tests and fixes. * Harden safe_return_url to reject URLs containing colons Adds an extra guard in safe_return_url to reject paths containing ':' characters, preventing edge-case scheme-like redirects (e.g. /javascript:...). Addresses CodeQL URL redirection warning. * Oops!
236 lines
7 KiB
Ruby
236 lines
7 KiB
Ruby
require "test_helper"
|
|
require "uri"
|
|
|
|
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
ActiveRecord::FixtureSet.reset_cache
|
|
end
|
|
|
|
# -- HCA: hca_new stores continue in session --
|
|
|
|
test "hca_new stores continue path for oauth authorize" do
|
|
continue_query = {
|
|
client_id: "Ck47_6hihaBqZO7z3CLmJlCB-0NzHtZHGeDBwG4CqRs",
|
|
redirect_uri: "https://game.hackclub.com/hackatime/callback",
|
|
response_type: "code",
|
|
scope: "profile read",
|
|
state: "a254695483383bd70ee41424b75d638a869e5d6769e11b50"
|
|
}
|
|
continue_path = "/oauth/authorize?#{Rack::Utils.build_query(continue_query)}"
|
|
|
|
get hca_auth_path(continue: continue_path)
|
|
|
|
assert_equal continue_path, session.dig(:return_data, "url")
|
|
assert_response :redirect
|
|
assert_redirected_to %r{/oauth/authorize}
|
|
end
|
|
|
|
test "hca_new rejects external continue URL" do
|
|
get hca_auth_path(continue: "https://evil.example.com/phish")
|
|
|
|
assert_nil session.dig(:return_data, "url")
|
|
assert_response :redirect
|
|
assert_redirected_to %r{/oauth/authorize}
|
|
end
|
|
|
|
test "hca_new rejects javascript continue URL" do
|
|
get hca_auth_path(continue: "javascript:alert(1)")
|
|
|
|
assert_nil session.dig(:return_data, "url")
|
|
assert_response :redirect
|
|
assert_redirected_to %r{/oauth/authorize}
|
|
end
|
|
|
|
test "hca_new rejects protocol-relative continue URL" do
|
|
get hca_auth_path(continue: "//evil.example.com/phish")
|
|
|
|
assert_nil session.dig(:return_data, "url")
|
|
assert_response :redirect
|
|
assert_redirected_to %r{/oauth/authorize}
|
|
end
|
|
|
|
# -- Signin: preserves continue param --
|
|
|
|
test "signin renders with continue param in inertia props" do
|
|
oauth_path = "/oauth/authorize?client_id=test&response_type=code"
|
|
|
|
get signin_path(continue: oauth_path)
|
|
|
|
assert_response :success
|
|
assert_inertia_component "Auth/SignIn"
|
|
assert_inertia_prop "continue_param", oauth_path
|
|
end
|
|
|
|
test "signin renders without continue param when not provided" do
|
|
get signin_path
|
|
|
|
assert_response :success
|
|
assert_inertia_component "Auth/SignIn"
|
|
assert_inertia_prop "continue_param", nil
|
|
end
|
|
|
|
# -- Email auth: persists continue into sign-in token --
|
|
|
|
test "email auth stores continue param in sign-in token" do
|
|
user = User.create!
|
|
email = "continue-test-#{SecureRandom.hex(4)}@example.com"
|
|
user.email_addresses.create!(email: email)
|
|
|
|
oauth_path = "/oauth/authorize?client_id=test&response_type=code"
|
|
|
|
# LoopsMailer forces SMTP delivery even in test; temporarily override
|
|
original_delivery_method = LoopsMailer.delivery_method
|
|
LoopsMailer.delivery_method = :test
|
|
post email_auth_path, params: { email: email, continue: oauth_path }
|
|
LoopsMailer.delivery_method = original_delivery_method
|
|
|
|
assert_response :redirect
|
|
|
|
token = SignInToken.last
|
|
assert_not_nil token
|
|
assert_equal oauth_path, token.continue_param
|
|
end
|
|
|
|
test "email token redirects to continue param after sign in" do
|
|
user = User.create!
|
|
oauth_path = "/oauth/authorize?client_id=test&response_type=code"
|
|
sign_in_token = user.sign_in_tokens.create!(
|
|
auth_type: :email,
|
|
continue_param: oauth_path
|
|
)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to oauth_path
|
|
assert_equal user.id, session[:user_id]
|
|
end
|
|
|
|
test "email token falls back to root when no continue param" do
|
|
user = User.create!
|
|
sign_in_token = user.sign_in_tokens.create!(auth_type: :email)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
assert_equal user.id, session[:user_id]
|
|
end
|
|
|
|
test "email token rejects external continue URL" do
|
|
user = User.create!
|
|
sign_in_token = user.sign_in_tokens.create!(
|
|
auth_type: :email,
|
|
continue_param: "https://evil.example.com/phish"
|
|
)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
assert_equal user.id, session[:user_id]
|
|
end
|
|
|
|
test "email token rejects protocol-relative continue URL" do
|
|
user = User.create!
|
|
sign_in_token = user.sign_in_tokens.create!(
|
|
auth_type: :email,
|
|
continue_param: "//evil.example.com/phish"
|
|
)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
end
|
|
|
|
test "slack_new stores oauth nonce and embeds it in state" do
|
|
get slack_auth_path(close_window: true, continue: "/projects")
|
|
|
|
assert_response :redirect
|
|
assert_not_nil session[:slack_oauth_state_nonce]
|
|
|
|
redirect_query = Rack::Utils.parse_nested_query(URI.parse(response.redirect_url).query)
|
|
state = JSON.parse(redirect_query["state"])
|
|
|
|
assert_equal session[:slack_oauth_state_nonce], state["token"]
|
|
assert_equal true, state["close_window"]
|
|
assert_equal "/projects", state["continue"]
|
|
end
|
|
|
|
test "slack_create rejects oauth callback with mismatched state nonce" do
|
|
get slack_auth_path
|
|
expected_nonce = session[:slack_oauth_state_nonce]
|
|
|
|
get "/auth/slack/callback", params: { code: "oauth-code", state: { token: "wrong-#{expected_nonce}" }.to_json }
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
assert_nil session[:slack_oauth_state_nonce]
|
|
end
|
|
|
|
test "github_new stores oauth nonce and passes it in redirect state" do
|
|
user = User.create!
|
|
sign_in_as(user)
|
|
|
|
get github_auth_path
|
|
|
|
assert_response :redirect
|
|
assert_not_nil session[:github_oauth_state_nonce]
|
|
|
|
redirect_query = Rack::Utils.parse_nested_query(URI.parse(response.redirect_url).query)
|
|
assert_equal session[:github_oauth_state_nonce], redirect_query["state"]
|
|
end
|
|
|
|
test "github_create rejects oauth callback with mismatched state nonce" do
|
|
user = User.create!
|
|
sign_in_as(user)
|
|
|
|
get github_auth_path
|
|
expected_nonce = session[:github_oauth_state_nonce]
|
|
|
|
get "/auth/github/callback", params: { code: "oauth-code", state: "wrong-#{expected_nonce}" }
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to my_settings_path
|
|
assert_nil session[:github_oauth_state_nonce]
|
|
end
|
|
|
|
test "expired token redirects to root with alert" do
|
|
user = User.create!
|
|
sign_in_token = user.sign_in_tokens.create!(
|
|
auth_type: :email,
|
|
continue_param: "/oauth/authorize?client_id=test",
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
assert_nil session[:user_id]
|
|
end
|
|
|
|
test "used token redirects to root with alert" do
|
|
user = User.create!
|
|
sign_in_token = user.sign_in_tokens.create!(
|
|
auth_type: :email,
|
|
continue_param: "/oauth/authorize?client_id=test",
|
|
used_at: 1.minute.ago
|
|
)
|
|
|
|
get auth_token_path(token: sign_in_token.token)
|
|
|
|
assert_response :redirect
|
|
assert_redirected_to root_path
|
|
assert_nil session[:user_id]
|
|
end
|
|
|
|
private
|
|
|
|
def sign_in_as(user)
|
|
token = user.sign_in_tokens.create!(auth_type: :email)
|
|
get auth_token_path(token: token.token)
|
|
assert_equal user.id, session[:user_id]
|
|
end
|
|
end
|