Skip to content

Commit 791b942

Browse files
committed
Consolidate auth callbacks and fix Slack controller issues
Replaces #66, #67, #68, #70 with a single cohesive fix: - Merge check_session_expiry + check_user_token into one authenticate! callback, eliminating the double-render risk by design - SessionsController and Slack::ApplicationController now each skip a single authenticate! instead of managing multiple callbacks - Rename valid_slack_request? to verify_slack_request! and make it halt with head :unauthorized when signature verification fails - Replace head :unprocessable_entity in send_message with logging and Sentry.capture_exception so notification failures never affect the HTTP response back to Slack
1 parent 794e150 commit 791b942

File tree

6 files changed

+105
-22
lines changed

6 files changed

+105
-22
lines changed
Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
class ApplicationController < ActionController::Base
22
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
33
allow_browser versions: :modern
4-
before_action :check_session_expiry
5-
before_action :check_user_token
4+
before_action :authenticate!
65

76
private
87

9-
def check_user_token
10-
unless session[:user_token]
11-
render "puzzles/login"
12-
end
13-
end
14-
15-
def check_session_expiry
8+
def authenticate!
169
if session[:expires_at].present? && Time.current > session[:expires_at]
1710
reset_session
18-
render "puzzles/login"
19-
else
20-
session[:expires_at] = 1.hour.from_now
11+
render "puzzles/login" and return
2112
end
13+
session[:expires_at] = 1.hour.from_now
14+
render "puzzles/login" unless session[:user_token]
2215
end
2316
end

app/controllers/sessions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class SessionsController < ApplicationController
2-
skip_before_action :check_user_token
2+
skip_before_action :authenticate!
33

44
def create
55
auth = request.env["omniauth.auth"]

app/controllers/slack/application_controller.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
class Slack::ApplicationController < ApplicationController
22
skip_before_action :verify_authenticity_token
3-
skip_before_action :check_user_token
3+
skip_before_action :authenticate!
44

5-
before_action :valid_slack_request?
5+
before_action :verify_slack_request!
66

77
private
88

9-
def valid_slack_request?
10-
@verified ||= verify_slack_signature
9+
def verify_slack_request!
10+
head :unauthorized unless verify_slack_signature
1111
end
1212

1313
def verify_slack_signature
@@ -43,7 +43,8 @@ def open_view(view, trigger_id:)
4343

4444
def send_message(message, channel_id:)
4545
SlackClient::Client.instance.chat_postMessage(channel: channel_id, blocks: message)
46-
rescue Slack::Web::Api::Errors::SlackError
47-
head :unprocessable_entity
46+
rescue Slack::Web::Api::Errors::SlackError => e
47+
Rails.logger.error "Failed to send Slack message: #{e.message}"
48+
Sentry.capture_exception(e)
4849
end
4950
end

test/controllers/puzzles_controller_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ class PuzzlesControllerTest < ActionDispatch::IntegrationTest
77
assert_response :success
88
end
99

10+
test "unauthenticated request renders login" do
11+
get puzzles_path
12+
assert_response :success
13+
assert_dom "p", "Log in to access the Ruby or Rails admin panel."
14+
end
15+
16+
test "expired session renders login and requires re-authentication" do
17+
sign_in
18+
get puzzles_path # authenticate! sets session[:expires_at]
19+
travel_to 2.hours.from_now do
20+
get puzzles_path
21+
assert_response :success
22+
assert_dom "p", "Log in to access the Ruby or Rails admin panel."
23+
end
24+
end
25+
1026
test "should show error message when editing puzzle with invalid data" do
1127
puzzle = puzzles(:one)
1228

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
require "test_helper"
22

33
class SessionsControllerTest < ActionDispatch::IntegrationTest
4-
# test "the truth" do
5-
# assert true
6-
# end
4+
test "completes OAuth even when session has expired" do
5+
sign_in
6+
7+
travel_to 2.hours.from_now do
8+
sign_in
9+
assert_redirected_to root_path
10+
end
11+
end
712
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require "test_helper"
2+
3+
class Slack::PuzzlesControllerTest < ActionDispatch::IntegrationTest
4+
setup do
5+
ENV["SLACK_SIGNING_SECRET"] = "test_signing_secret"
6+
end
7+
8+
test "rejects request with invalid Slack signature" do
9+
post slack_puzzle_path, params: { payload: puzzle_payload },
10+
headers: slack_headers(secret: "wrong_secret")
11+
12+
assert_response :unauthorized
13+
end
14+
15+
test "rejects request with expired Slack timestamp" do
16+
post slack_puzzle_path, params: { payload: puzzle_payload },
17+
headers: slack_headers(timestamp: Time.now.to_i - 400)
18+
19+
assert_response :unauthorized
20+
end
21+
22+
test "renders ok when puzzle fails validation" do
23+
params = { payload: puzzle_payload(question: "") }
24+
25+
post slack_puzzle_path, params: params,
26+
headers: slack_headers(body: params.to_query)
27+
28+
assert_response :ok
29+
end
30+
31+
test "renders ok even when Slack notification fails after puzzle is saved" do
32+
original = Slack::ApplicationController.instance_method(:send_message)
33+
Slack::ApplicationController.define_method(:send_message) { |*| nil }
34+
35+
params = { payload: puzzle_payload(question: "What is unique about Ruby's blocks?") }
36+
37+
post slack_puzzle_path, params: params,
38+
headers: slack_headers(body: params.to_query)
39+
40+
assert_response :ok
41+
ensure
42+
Slack::ApplicationController.define_method(:send_message, original)
43+
end
44+
45+
private
46+
47+
def puzzle_payload(question: "What is Ruby?")
48+
{
49+
user: { id: "U123" },
50+
view: {
51+
state: {
52+
values: {
53+
question: { question: { value: question } },
54+
answer: { answer: { selected_option: { value: "ruby" } } },
55+
explanation: { explanation: { value: "It is a programming language." } },
56+
link: { link: { value: nil } }
57+
}
58+
}
59+
}
60+
}.to_json
61+
end
62+
63+
def slack_headers(secret: ENV["SLACK_SIGNING_SECRET"], timestamp: Time.now.to_i, body: "")
64+
ts = timestamp.to_s
65+
sig = "v0=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "v0:#{ts}:#{body}")
66+
{ "X-Slack-Request-Timestamp" => ts, "X-Slack-Signature" => sig }
67+
end
68+
end

0 commit comments

Comments
 (0)