mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 06:23:51 +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>
36 lines
1.2 KiB
Ruby
Vendored
36 lines
1.2 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class UserApiKey::DeviceAuth::PayloadBuilder
|
|
AUTH_API_VERSION = UserApiKey::DeviceAuth::AUTH_API_VERSION
|
|
DEVICE_KEY_PLACEHOLDER = UserApiKey::DeviceAuth::DEVICE_KEY_PLACEHOLDER
|
|
|
|
def self.validate_size!(grant)
|
|
payload = payload_json(grant, key_value: DEVICE_KEY_PLACEHOLDER, push: false)
|
|
UserApiKey::DeviceAuth::Crypto.validate_payload_size!(
|
|
payload,
|
|
UserApiKey::DeviceAuth::Crypto.parse_public_key!(grant.public_key),
|
|
padding: grant.padding,
|
|
)
|
|
end
|
|
|
|
def self.encrypted_payload!(grant, key)
|
|
public_key = UserApiKey::DeviceAuth::Crypto.parse_public_key!(grant.public_key)
|
|
payload =
|
|
payload_json(grant, key_value: key.key, push: key.has_push?, expires_at: key.expires_at)
|
|
|
|
UserApiKey::DeviceAuth::Crypto.validate_payload_size!(
|
|
payload,
|
|
public_key,
|
|
padding: grant.padding,
|
|
)
|
|
Base64.encode64(
|
|
UserApiKey::DeviceAuth::Crypto.encrypt!(public_key, payload, padding: grant.padding),
|
|
)
|
|
end
|
|
|
|
def self.payload_json(grant, key_value:, push:, expires_at: grant.expires_at)
|
|
payload = { key: key_value, nonce: grant.nonce, push: push, api: AUTH_API_VERSION }
|
|
payload[:expires_at] = expires_at.iso8601 if expires_at.present?
|
|
payload.to_json
|
|
end
|
|
end
|