discourse/app/assets/stylesheets/common/base/authorize-api-key.scss
Sam 5458a5f150
FEATURE: User API key device authorization flow (#40189)
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>
2026-06-10 16:09:44 -04:00

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;
}
}