From 8acc304c3f5270758c0cca5363f2469e51adb336 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Tue, 7 May 2024 19:39:10 +0100
Subject: [PATCH] Add `authenticate_or_request_with_http_token`
---
lib/rage/controller/api.rb | 22 ++++
spec/controller/api/authenticate_spec.rb | 142 +++++++++++++++--------
2 files changed, 114 insertions(+), 50 deletions(-)
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index ff7edbb4..7d67756d 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -428,6 +428,28 @@ def authenticate_with_http_token
yield token
end
+ # Authenticate using an HTTP Bearer token, or otherwise render an HTTP header requesting the client to send a
+ # Bearer token. For the authentication to be considered successful, the block should return a non-nil value.
+ #
+ # @yield [token] token value extracted from the `Authorization` header
+ # @example
+ # before_action :authenticate
+ #
+ # def authenticate
+ # authenticate_or_request_with_http_token do |token|
+ # ApiToken.find_by(token: token)
+ # end
+ # end
+ def authenticate_or_request_with_http_token
+ authenticate_with_http_token { |token| yield(token) } || request_http_token_authentication
+ end
+
+ # Render an HTTP header requesting the client to send a Bearer token for authentication.
+ def request_http_token_authentication
+ headers["Www-Authenticate"] = "Token"
+ render plain: "HTTP Token: Access denied.", status: 401
+ end
+
if !defined?(::ActionController::Parameters)
# Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.
# You can also load Strong Params to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.
diff --git a/spec/controller/api/authenticate_spec.rb b/spec/controller/api/authenticate_spec.rb
index fa1839dd..fa33cc00 100644
--- a/spec/controller/api/authenticate_spec.rb
+++ b/spec/controller/api/authenticate_spec.rb
@@ -3,22 +3,9 @@
RSpec.describe RageController::API do
subject { described_class.new(env, nil) }
- context "with a Bearer token" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Bearer my_token" } }
-
- it "extracts the token" do
- subject.authenticate_with_http_token do |token|
- expect(token).to eq("my_token")
- end
- end
-
- it "returns the value of the login procedure" do
- value = subject.authenticate_with_http_token { :request_authenticated }
- expect(value).to eq(:request_authenticated)
- end
-
- context "with a token prefix" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Bearer token=my_token" } }
+ context "#authenticate_with_http_token" do
+ context "with a Bearer token" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Bearer my_token" } }
it "extracts the token" do
subject.authenticate_with_http_token do |token|
@@ -30,25 +17,25 @@
value = subject.authenticate_with_http_token { :request_authenticated }
expect(value).to eq(:request_authenticated)
end
- end
- end
- context "with a Token token" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Token my_token" } }
+ context "with a token prefix" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Bearer token=my_token" } }
- it "extracts the token" do
- subject.authenticate_with_http_token do |token|
- expect(token).to eq("my_token")
- end
- end
+ it "extracts the token" do
+ subject.authenticate_with_http_token do |token|
+ expect(token).to eq("my_token")
+ end
+ end
- it "returns the value of the login procedure" do
- value = subject.authenticate_with_http_token { :request_authenticated }
- expect(value).to eq(:request_authenticated)
+ it "returns the value of the login procedure" do
+ value = subject.authenticate_with_http_token { :request_authenticated }
+ expect(value).to eq(:request_authenticated)
+ end
+ end
end
- context "with a token prefix" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Token token=my_token" } }
+ context "with a Token token" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Token my_token" } }
it "extracts the token" do
subject.authenticate_with_http_token do |token|
@@ -61,45 +48,100 @@
expect(value).to eq(:request_authenticated)
end
- context "with quotes" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Token token=\"my_token\"" } }
+ context "with a token prefix" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Token token=my_token" } }
it "extracts the token" do
subject.authenticate_with_http_token do |token|
expect(token).to eq("my_token")
end
end
+
+ it "returns the value of the login procedure" do
+ value = subject.authenticate_with_http_token { :request_authenticated }
+ expect(value).to eq(:request_authenticated)
+ end
+
+ context "with quotes" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Token token=\"my_token\"" } }
+
+ it "extracts the token" do
+ subject.authenticate_with_http_token do |token|
+ expect(token).to eq("my_token")
+ end
+ end
+ end
end
end
- end
- context "with a Digest token" do
- let(:env) { { "HTTP_AUTHORIZATION" => "Digest my_token" } }
+ context "with a Digest token" do
+ let(:env) { { "HTTP_AUTHORIZATION" => "Digest my_token" } }
- it "doesn't extract the token" do
- value = subject.authenticate_with_http_token { :request_authenticated }
- expect(value).to be_nil
+ it "doesn't extract the token" do
+ value = subject.authenticate_with_http_token { :request_authenticated }
+ expect(value).to be_nil
+ end
+
+ it "doesn't call the login procedure" do
+ expect {
+ subject.authenticate_with_http_token { raise }
+ }.not_to raise_error
+ end
end
- it "doesn't call the login procedure" do
- expect {
- subject.authenticate_with_http_token { raise }
- }.not_to raise_error
+ context "with no token" do
+ let(:env) { {} }
+
+ it "returns nil" do
+ value = subject.authenticate_with_http_token { :request_authenticated }
+ expect(value).to be_nil
+ end
+
+ it "doesn't call the login procedure" do
+ expect {
+ subject.authenticate_with_http_token { raise }
+ }.not_to raise_error
+ end
end
end
- context "with no token" do
+ context "#authenticate_or_request_with_http_token" do
let(:env) { {} }
- it "returns nil" do
- value = subject.authenticate_with_http_token { :request_authenticated }
- expect(value).to be_nil
+ before do
+ expect(subject).to receive(:authenticate_with_http_token).and_yield("my_test_token")
end
- it "doesn't call the login procedure" do
- expect {
- subject.authenticate_with_http_token { raise }
- }.not_to raise_error
+ it "extracts the token" do
+ subject.authenticate_or_request_with_http_token do |token|
+ expect(token).to eq("my_test_token")
+ end
+ end
+
+ it "returns the value of the login procedure" do
+ value = subject.authenticate_or_request_with_http_token { :request_authenticated }
+ expect(value).to eq(:request_authenticated)
+ end
+
+ it "doesn't request authentication if login procedure returns non nil" do
+ subject.authenticate_or_request_with_http_token { :request_authenticated }
+ expect(subject.response.headers).not_to have_key("Www-Authenticate")
+ end
+
+ it "doesn't request authentication if login procedure returns nil" do
+ subject.authenticate_or_request_with_http_token {}
+ expect(subject.response.headers["Www-Authenticate"]).to eq("Token")
+ end
+ end
+
+ context "#request_http_token_authentication" do
+ let(:env) { {} }
+
+ it "requests token authentication" do
+ subject.request_http_token_authentication {}
+
+ expect(subject.response.body).to eq("HTTP Token: Access denied.")
+ expect(subject.response.headers["Www-Authenticate"]).to eq("Token")
end
end
end