mirror of
https://github.com/discourse/discourse.git
synced 2026-08-12 05:37:26 +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>
48 lines
1.5 KiB
Ruby
Vendored
48 lines
1.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe UserApiKey::DeviceAuth::Crypto do
|
|
let(:key) { OpenSSL::PKey::RSA.new(2048) }
|
|
let(:public_key_pem) { key.public_key.to_pem }
|
|
|
|
describe ".parse_public_key!" do
|
|
it "parses RSA public keys" do
|
|
expect(described_class.parse_public_key!(public_key_pem)).to be_a(OpenSSL::PKey::RSA)
|
|
end
|
|
|
|
it "raises an invalid parameter error for invalid keys" do
|
|
expect { described_class.parse_public_key!("not a key") }.to raise_error(
|
|
Discourse::InvalidParameters,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe ".validate_payload_size!" do
|
|
it "allows payloads that fit the key and padding" do
|
|
expect { described_class.validate_payload_size!("short", key.public_key) }.not_to raise_error
|
|
end
|
|
|
|
it "raises when payloads are too large" do
|
|
expect { described_class.validate_payload_size!("x" * 300, key.public_key) }.to raise_error(
|
|
Discourse::InvalidParameters,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe ".encrypt!" do
|
|
it "encrypts using the requested padding" do
|
|
encrypted = described_class.encrypt!(key.public_key, "secret", padding: "oaep")
|
|
|
|
expect(encrypted).to be_present
|
|
expect(encrypted).not_to eq("secret")
|
|
end
|
|
|
|
it "raises an invalid parameter error when encryption fails" do
|
|
public_key = key.public_key
|
|
allow(public_key).to receive(:encrypt).and_raise(OpenSSL::PKey::PKeyError)
|
|
|
|
expect { described_class.encrypt!(public_key, "secret") }.to raise_error(
|
|
Discourse::InvalidParameters,
|
|
)
|
|
end
|
|
end
|
|
end
|