mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 02:33:45 +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>
79 lines
1.8 KiB
Ruby
Vendored
79 lines
1.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module PageObjects
|
|
module Pages
|
|
class UserApiKeyShow < PageObjects::Pages::Base
|
|
def visit_authorization(public_key:)
|
|
page.visit(
|
|
"/user-api-key/new?#{
|
|
URI.encode_www_form(
|
|
scopes: "read",
|
|
client_id: "x" * 32,
|
|
application_name: "Test Application",
|
|
public_key: public_key,
|
|
nonce: SecureRandom.hex,
|
|
)
|
|
}",
|
|
)
|
|
|
|
self
|
|
end
|
|
|
|
def visit_otp(public_key:)
|
|
page.visit(
|
|
"/user-api-key/otp?#{
|
|
URI.encode_www_form(
|
|
application_name: "Test Application",
|
|
public_key: public_key,
|
|
auth_redirect: "discourse://auth_redirect",
|
|
)
|
|
}",
|
|
)
|
|
|
|
self
|
|
end
|
|
|
|
def click_authorize
|
|
click_button(I18n.t("user_api_key.authorize"))
|
|
self
|
|
end
|
|
|
|
def click_copy_key
|
|
find("#copy-api-key-btn").click
|
|
self
|
|
end
|
|
|
|
def payload
|
|
find("#user-api-key-payload").text
|
|
end
|
|
|
|
def has_authorization_form?
|
|
has_css?(".authorize-api-key h1", text: "Test Application") &&
|
|
has_button?(I18n.t("user_api_key.authorize"))
|
|
end
|
|
|
|
def has_payload?
|
|
has_css?("#user-api-key-payload") && has_css?("#copy-api-key-btn")
|
|
end
|
|
|
|
def has_copied_button?
|
|
has_button?(I18n.t("user_api_key.copied"))
|
|
end
|
|
|
|
def has_otp_form?
|
|
has_css?(
|
|
".authorize-api-key h1",
|
|
text: I18n.t("user_api_key.otp_description", application_name: "Test Application"),
|
|
) && has_button?(I18n.t("user_api_key.authorize"))
|
|
end
|
|
|
|
def has_no_sidebar?
|
|
has_no_css?("#d-sidebar")
|
|
end
|
|
|
|
def has_no_powered_by_discourse?
|
|
has_no_css?(".powered-by-discourse")
|
|
end
|
|
end
|
|
end
|
|
end
|