mirror of
https://github.com/discourse/discourse.git
synced 2026-08-07 13:19:19 +08:00
Adds an OAuth-style device authorization flow for user API keys so applications that can't open a browser (CLIs, headless tools, IoT clients) can request a key by displaying a short user-facing code. The client POSTs to `/user-api-key/device` to obtain a device code, a user code, and a verification URL. The user visits the URL, authenticates, confirms the application and scopes, and either approves or denies the request. Meanwhile the client polls `/user-api-key/device/poll` until it receives the encrypted key payload, a denial, or expiry. The flow is implemented as a `UserApiKey::DeviceAuth` namespace of service objects (`CreateRequest`, `Authorize`, `Deny`, `Poll`, `Store`, `Crypto`, `ApprovalTokenStore`, `GrantPresenter`). Pending grants live in Redis with a short TTL and are rate limited per IP and per user code. Encrypted payload generation is shared with the existing redirect-based flow. Also adds first-class expiration for user API keys: - New `expires_at` column on `user_api_keys`. - New `max_user_api_key_expiry_days` site setting (default 365). - Clients can request a key lifetime via `expires_in_seconds`, which is surfaced to the user on the authorization screen and serialized back to the client. - A `user_api_key` rake task for listing, inspecting, expiring, and revoking keys from the console. --------- Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
44 lines
1.5 KiB
Ruby
Vendored
44 lines
1.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe UserApiKey::DeviceAuth do
|
|
describe ".trace" do
|
|
it "does not raise when an event subscriber raises" do
|
|
listener = ->(*) { raise "boom" }
|
|
DiscourseEvent.on(described_class::TRACE_EVENT, &listener)
|
|
|
|
expect {
|
|
described_class.trace("device_auth.test", device_code: SecureRandom.hex(32))
|
|
}.not_to raise_error
|
|
ensure
|
|
DiscourseEvent.off(described_class::TRACE_EVENT, &listener)
|
|
end
|
|
|
|
it "reports when verbose logging fails" do
|
|
SiteSetting.verbose_user_api_key_device_auth_logging = true
|
|
allow(Rails.logger).to receive(:info).and_raise("boom")
|
|
allow(Discourse).to receive(:warn_exception)
|
|
|
|
expect {
|
|
described_class.trace("device_auth.test", device_code: SecureRandom.hex(32))
|
|
}.not_to raise_error
|
|
expect(Discourse).to have_received(:warn_exception).with(
|
|
an_instance_of(RuntimeError),
|
|
message: "User API key device auth trace failed",
|
|
env: {
|
|
event: "device_auth.test",
|
|
},
|
|
)
|
|
end
|
|
|
|
it "truncates string payload values" do
|
|
SiteSetting.verbose_user_api_key_device_auth_logging = true
|
|
logged_message = nil
|
|
allow(Rails.logger).to receive(:info) { |message| logged_message = message }
|
|
|
|
described_class.trace("device_auth.test", client_id: "x" * 300)
|
|
|
|
logged_payload = JSON.parse(logged_message)
|
|
expect(logged_payload["client_id"]).to eq("x" * described_class::TRACE_MAX_VALUE_LENGTH)
|
|
end
|
|
end
|
|
end
|