Files
discourse/spec/requests/discourse_id_controller_spec.rb
Penar Musaraj d45ebd746c DEV: Add Discourse ID authenticator (#33186)
Adds a Discourse ID authenticator. Not available for use in production
just yet, but soon communities will be able to use this service to let
users authenticate using a central Discourse ID account.

Includes a support for a `/revoke` action, allowing users to log out of
multiple client instances from a central auth service.

Internal ticket: t/155397
---------

Co-authored-by: Loïc Guitaut <loic@discourse.org>
2025-06-17 09:47:00 -04:00

136 lines
4.4 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Users::DiscourseIdController do
let(:client_id) { SiteSetting.discourse_id_client_id }
let(:hashed_secret) { Digest::SHA256.hexdigest(SiteSetting.discourse_id_client_secret) }
let(:identifier) { SecureRandom.hex }
let(:provider_name) { "discourse_id" }
let!(:user) { Fabricate(:user) }
let!(:user_associated_account) do
Fabricate(:user_associated_account, user:, provider_name:, provider_uid: identifier)
end
before do
SiteSetting.enable_discourse_id = true
SiteSetting.discourse_id_client_id = SecureRandom.hex
SiteSetting.discourse_id_client_secret = SecureRandom.hex
end
describe "#revoke" do
context "with valid parameters" do
it "revokes all auth tokens for the user" do
UserAuthToken.generate!(user_id: user.id)
UserAuthToken.generate!(user_id: user.id)
expect(UserAuthToken.where(user_id: user.id).count).to eq(2)
timestamp = Time.now.to_i
signature =
OpenSSL::HMAC.hexdigest(
"sha256",
hashed_secret,
"#{client_id}:#{identifier}:#{timestamp}",
)
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(UserAuthToken.where(user_id: user.id).count).to eq(0)
end
end
context "with invalid parameters" do
it "returns 400 when signature is invalid" do
timestamp = Time.now.to_i
signature = SecureRandom.hex
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
expect(response.parsed_body["error"]).to match(/Signature is invalid/)
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
end
it "returns 400 when timestamp is too old" do
timestamp = Time.now.to_i - 6.minutes.to_i
signature =
OpenSSL::HMAC.hexdigest(
"sha256",
hashed_secret,
"#{client_id}:#{identifier}:#{timestamp}",
)
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
expect(response.parsed_body["error"]).to match(/Timestamp is expired/)
end
it "returns 400 when user_id is not found" do
identifier = "non_existent_user_id"
timestamp = Time.now.to_i
signature =
OpenSSL::HMAC.hexdigest(
"sha256",
hashed_secret,
"#{client_id}:#{identifier}:#{timestamp}",
)
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
expect(response.parsed_body["error"]).to eq("Invalid request")
end
it "returns 400 when client_id or client_secret is blank" do
SiteSetting.discourse_id_client_id = ""
timestamp = Time.now.to_i
signature =
OpenSSL::HMAC.hexdigest(
"sha256",
hashed_secret,
"#{client_id}:#{identifier}:#{timestamp}",
)
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
expect(response.parsed_body["error"]).to eq("Invalid request")
SiteSetting.discourse_id_client_id = SecureRandom.hex
SiteSetting.discourse_id_client_secret = ""
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
expect(response.parsed_body["error"]).to eq("Invalid request")
end
end
context "with rate limiting" do
before { RateLimiter.enable }
it "rate limits after 5 requests per minute for the same user_id" do
identifier = "non_existent_user_id"
timestamp = Time.now.to_i
signature = SecureRandom.hex
5.times do
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(400)
end
post "/auth/discourse_id/revoke.json", params: { signature:, identifier:, timestamp: }
expect(response.status).to eq(429)
end
end
end
end