From 8a095e4939ad9f7a2cec2d519aede35622d6788d Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 11 Jun 2026 07:54:48 +1000 Subject: [PATCH] Enforce group access on Bearer API key forward-auth at use-time The ApiKey model only validates group access on creation (user_must_have_access runs on create). The bearer path in /api/verify never re-checked, so a user removed from an application's allowed groups kept access via an existing key until it was manually revoked. Add an app.user_allowed?(user) check to authenticate_bearer_token, matching the session path, returning 401 when the user no longer has group access. Adds a regression test that revokes membership after key creation. Co-Authored-By: Claude Fable 5 --- .../api/forward_auth_controller.rb | 8 +++++ .../api/forward_auth_bearer_test.rb | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/app/controllers/api/forward_auth_controller.rb b/app/controllers/api/forward_auth_controller.rb index 3cf659a..1165358 100644 --- a/app/controllers/api/forward_auth_controller.rb +++ b/app/controllers/api/forward_auth_controller.rb @@ -138,6 +138,14 @@ module Api return render_bearer_error("Application is inactive") end + # Re-check group membership at use-time. The ApiKey model only validates + # access on creation, so a user removed from the app's allowed groups + # afterwards must not keep access via an existing key. + unless app.user_allowed?(user) + Rails.logger.info "ForwardAuth: API key '#{api_key.name}' denied - user #{user.email_address} lacks group access to #{app.domain_pattern}" + return render_bearer_error("Access denied: insufficient group membership") + end + api_key.touch_last_used! headers = app.headers_for_user(user) diff --git a/test/controllers/api/forward_auth_bearer_test.rb b/test/controllers/api/forward_auth_bearer_test.rb index af60597..1980d59 100644 --- a/test/controllers/api/forward_auth_bearer_test.rb +++ b/test/controllers/api/forward_auth_bearer_test.rb @@ -113,6 +113,42 @@ module Api assert_equal "Application is inactive", json["error"] end + test "bearer token returns 401 once user is removed from allowed groups" do + # App restricted to a specific group; user is a member when the key is made. + group = Group.create!(name: "webdav-users") + restricted_app = Application.create!( + name: "Restricted WebDAV", + slug: "restricted-webdav", + app_type: "forward_auth", + domain_pattern: "restricted.example.com", + active: true + ) + restricted_app.allowed_groups << group + @user.groups << group + + key = @user.api_keys.create!(name: "Restricted Key", application: restricted_app) + token = key.plaintext_token + + # Sanity: access works while membership stands. + get "/api/verify", headers: { + "Authorization" => "Bearer #{token}", + "X-Forwarded-Host" => "restricted.example.com" + } + assert_response :ok + + # Revoke group membership; the existing key must stop working. + @user.groups.destroy(group) + + get "/api/verify", headers: { + "Authorization" => "Bearer #{token}", + "X-Forwarded-Host" => "restricted.example.com" + } + + assert_response :unauthorized + json = JSON.parse(response.body) + assert_equal "Access denied: insufficient group membership", json["error"] + end + test "no bearer token falls through to cookie auth" do # No auth header, no session -> should redirect (cookie flow) get "/api/verify", headers: {