mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 05:35:40 +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>
108 lines
1.7 KiB
SCSS
Vendored
108 lines
1.7 KiB
SCSS
Vendored
.authorize-api-key {
|
|
max-width: 42rem;
|
|
margin: 0 auto;
|
|
padding: 3rem 1rem;
|
|
|
|
h1 {
|
|
margin: 0 0 0.75rem;
|
|
font-size: var(--font-up-5);
|
|
line-height: var(--line-height-small);
|
|
}
|
|
|
|
.alert {
|
|
border: 0;
|
|
border-radius: var(--d-border-radius-large);
|
|
margin: 1rem 0;
|
|
padding: 1rem 1.25rem;
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
&__user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
color: var(--primary-medium);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
&__username {
|
|
color: var(--primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
&__summary {
|
|
background: var(--primary-very-low);
|
|
border: 1px solid var(--primary-low);
|
|
border-radius: var(--d-border-radius-large);
|
|
margin-bottom: 1.5rem;
|
|
padding: 1.25rem;
|
|
}
|
|
|
|
&__permissions,
|
|
&__summary-detail,
|
|
&__summary-notice {
|
|
padding: 0;
|
|
}
|
|
|
|
&__permissions {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
&__permissions-header {
|
|
margin: 0 0 0.75rem;
|
|
}
|
|
|
|
&__scopes {
|
|
margin: 0;
|
|
padding-left: 1.25rem;
|
|
}
|
|
|
|
&__summary-detail,
|
|
&__summary-notice {
|
|
color: var(--primary-high);
|
|
font-size: var(--font-down-1-rem);
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
&__summary-notice {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
&__redirect {
|
|
background: var(--highlight-bg);
|
|
border-radius: var(--d-border-radius-large);
|
|
padding: 1rem 1.25rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
&__redirect-url {
|
|
margin: 0;
|
|
word-break: break-word;
|
|
}
|
|
|
|
&__buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 1.25rem;
|
|
}
|
|
|
|
&__code-form {
|
|
margin-top: 1.5rem;
|
|
|
|
.d-otp-slot {
|
|
font-size: var(--font-up-1);
|
|
padding: 0.625em 0.8em;
|
|
}
|
|
}
|
|
|
|
&__code-input {
|
|
margin-top: 0.75rem;
|
|
}
|
|
}
|