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>
84 lines
2.5 KiB
Ruby
Vendored
84 lines
2.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class UserApiKey::DeviceAuth::CreateRequest
|
|
include Service::Base
|
|
|
|
params do
|
|
attribute :nonce, :string
|
|
attribute :scopes, :string
|
|
attribute :client_id, :string
|
|
attribute :application_name, :string
|
|
attribute :public_key, :string
|
|
attribute :push_url, :string
|
|
attribute :padding, :string
|
|
attribute :expires_in_seconds, :string
|
|
|
|
validates :nonce, presence: true
|
|
validates :scopes, presence: true
|
|
validates :client_id, presence: true
|
|
end
|
|
|
|
options { attribute :request_id, :string }
|
|
|
|
model :client, optional: true
|
|
|
|
try Discourse::InvalidParameters, Discourse::InvalidAccess do
|
|
step :validate_request
|
|
step :build_grant
|
|
step :validate_grant_size
|
|
step :reserve_codes
|
|
step :store_grant
|
|
end
|
|
|
|
private
|
|
|
|
def fetch_client(params:)
|
|
UserApiKeyClient.find_by(client_id: params.client_id)
|
|
end
|
|
|
|
def validate_request(params:, client:)
|
|
UserApiKey::DeviceAuth::RequestValidator.validate!(params.attributes.symbolize_keys, client)
|
|
end
|
|
|
|
def build_grant(params:, client:)
|
|
request_params = params.attributes.symbolize_keys
|
|
scopes = request_params[:scopes].split(",")
|
|
expires_in_seconds = UserApiKey::Expiry.parse_seconds!(request_params[:expires_in_seconds])
|
|
device_code = SecureRandom.hex(32)
|
|
|
|
context[:device_request] = { device_code: device_code }
|
|
context[:grant] = UserApiKey::DeviceAuth::Grant.build(
|
|
request_params,
|
|
client,
|
|
scopes,
|
|
expires_in_seconds,
|
|
device_code,
|
|
)
|
|
UserApiKey::DeviceAuth::PayloadBuilder.validate_size!(context[:grant])
|
|
end
|
|
|
|
def validate_grant_size(grant:)
|
|
UserApiKey::DeviceAuth::KeyCreator.validate_grant_size!(grant)
|
|
end
|
|
|
|
def reserve_codes(device_request:, grant:)
|
|
codes = UserApiKey::DeviceAuth::CodeRegistry.reserve_for(device_request[:device_code])
|
|
|
|
grant.assign_codes!(user_code: codes.user_code, request_token: codes.request_token)
|
|
device_request[:user_code] = codes.user_code
|
|
device_request[:request_token] = codes.request_token
|
|
end
|
|
|
|
def store_grant(device_request:, grant:, options:)
|
|
UserApiKey::DeviceAuth::KeyCreator.validate_grant_size!(grant)
|
|
UserApiKey::DeviceAuth::GrantStore.save!(grant, ttl: UserApiKey::DeviceAuth::DEVICE_AUTH_TTL)
|
|
UserApiKey::DeviceAuth.trace(
|
|
"device_auth.create.succeeded",
|
|
request_id: options.request_id,
|
|
client_id: grant.client_id,
|
|
device_code: grant.device_code,
|
|
request_token: grant.request_token,
|
|
user_code: grant.user_code,
|
|
)
|
|
end
|
|
end
|