From dd67f4171d74975e69be6e960492077ca1f3ba2f Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 8 Apr 2026 14:36:34 -0400 Subject: [PATCH 1/3] Add regression tests for bearer-authenticated blob downloads --- .../active_storage_authorization_test.rb | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/integration/active_storage_authorization_test.rb b/test/integration/active_storage_authorization_test.rb index 605bc109d..045d08e4b 100644 --- a/test/integration/active_storage_authorization_test.rb +++ b/test/integration/active_storage_authorization_test.rb @@ -17,6 +17,13 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest assert_match %r{rails/active_storage}, response.location end + test "bearer token with board access can view blob" do + get rails_blob_path(@blob, disposition: :inline), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + + assert_response :redirect + assert_match %r{rails/active_storage}, response.location + end + test "authenticated user without board access cannot view blob" do sign_in_as :mike @@ -38,6 +45,13 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest assert_match %r{rails/active_storage/}, response.location end + test "bearer token with board access can view representation" do + get rails_representation_path(@blob.representation(resize_to_limit: [ 100, 100 ])), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + + assert_response :redirect + assert_match %r{rails/active_storage/}, response.location + end + test "authenticated user without board access cannot view representation" do sign_in_as :mike @@ -194,6 +208,15 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest assert_match %r{rails/active_storage}, response.location end + test "export owner can download their export with bearer token" do + blob = create_export_blob_for(users(:david)) + + get rails_blob_path(blob, disposition: :attachment), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + + assert_response :redirect + assert_match %r{rails/active_storage}, response.location + end + test "non-owner cannot download another user's export" do sign_in_as :jz @@ -249,4 +272,8 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest user.avatar.blob end end + + def bearer_token_env(token) + { "HTTP_AUTHORIZATION" => "Bearer #{token}" } + end end From 5e154a767f087eeac6afb14b25b3b3038a15d912 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 8 Apr 2026 14:37:24 -0400 Subject: [PATCH 2/3] Allow bearer auth on Active Storage downloads --- app/controllers/concerns/authentication.rb | 6 +++++- lib/rails_ext/active_storage_authorization.rb | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index a269fc2d6..95240f11e 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -57,7 +57,7 @@ module Authentication def authenticate_by_bearer_token if request.authorization.to_s.include?("Bearer") - if request.format.json? + if bearer_token_authenticatable_request? authenticate_or_request_with_http_token do |token| if identity = Identity.find_by_permissable_access_token(token, method: request.method) Current.identity = identity @@ -69,6 +69,10 @@ module Authentication end end + def bearer_token_authenticatable_request? + request.format.json? + end + def request_authentication if Current.account.present? session[:return_to_after_authenticating] = request.url diff --git a/lib/rails_ext/active_storage_authorization.rb b/lib/rails_ext/active_storage_authorization.rb index 7d6fa8510..33a2db43e 100644 --- a/lib/rails_ext/active_storage_authorization.rb +++ b/lib/rails_ext/active_storage_authorization.rb @@ -31,6 +31,10 @@ Rails.application.config.to_prepare do end private + def bearer_token_authenticatable_request? + true + end + def publicly_accessible_blob? @blob.publicly_accessible? end From 4a91e45aa510801c5b2f27217099021178d0f8f9 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 8 Apr 2026 14:41:32 -0400 Subject: [PATCH 3/3] Cover bearer-authenticated export download URLs --- .../accounts/exports_controller_test.rb | 19 +++++++++++++++++++ .../active_storage_authorization_test.rb | 15 ++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/test/controllers/accounts/exports_controller_test.rb b/test/controllers/accounts/exports_controller_test.rb index fd950f989..188858355 100644 --- a/test/controllers/accounts/exports_controller_test.rb +++ b/test/controllers/accounts/exports_controller_test.rb @@ -103,6 +103,25 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest assert body["download_url"].present? end + test "show as JSON with bearer token returns a download URL that can be fetched" do + export = Account::Export.create!(account: Current.account, user: users(:jason)) + export.build + sign_out + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:jasons_api_token).token}" } + + get account_export_path(export), as: :json, env: bearer_token + assert_response :success + + body = @response.parsed_body + assert_equal export.id, body["id"] + assert_equal "completed", body["status"] + assert body["download_url"].present? + + get URI(body["download_url"]).request_uri, env: bearer_token + assert_response :redirect + assert_match %r{rails/active_storage}, response.location + end + test "show as JSON with pending export" do export = Account::Export.create!(account: Current.account, user: users(:jason)) diff --git a/test/integration/active_storage_authorization_test.rb b/test/integration/active_storage_authorization_test.rb index 045d08e4b..3fed18267 100644 --- a/test/integration/active_storage_authorization_test.rb +++ b/test/integration/active_storage_authorization_test.rb @@ -18,7 +18,9 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest end test "bearer token with board access can view blob" do - get rails_blob_path(@blob, disposition: :inline), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" } + + get rails_blob_path(@blob, disposition: :inline), env: bearer_token assert_response :redirect assert_match %r{rails/active_storage}, response.location @@ -46,7 +48,9 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest end test "bearer token with board access can view representation" do - get rails_representation_path(@blob.representation(resize_to_limit: [ 100, 100 ])), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" } + + get rails_representation_path(@blob.representation(resize_to_limit: [ 100, 100 ])), env: bearer_token assert_response :redirect assert_match %r{rails/active_storage/}, response.location @@ -210,8 +214,9 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest test "export owner can download their export with bearer token" do blob = create_export_blob_for(users(:david)) + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" } - get rails_blob_path(blob, disposition: :attachment), env: bearer_token_env(identity_access_tokens(:davids_api_token).token) + get rails_blob_path(blob, disposition: :attachment), env: bearer_token assert_response :redirect assert_match %r{rails/active_storage}, response.location @@ -272,8 +277,4 @@ class ActiveStorageAuthorizationTest < ActionDispatch::IntegrationTest user.avatar.blob end end - - def bearer_token_env(token) - { "HTTP_AUTHORIZATION" => "Bearer #{token}" } - end end