mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 08:23: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>
109 lines
1.9 KiB
SCSS
Vendored
109 lines
1.9 KiB
SCSS
Vendored
.d-otp {
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
width: min-content;
|
|
|
|
.email-login & {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.d-otp-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.d-otp-slot {
|
|
padding: var(--d-otp-slot-padding, 0.5em 0.65em);
|
|
font-size: var(--d-otp-slot-font-size, inherit);
|
|
line-height: normal;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: var(--d-input-border);
|
|
border-radius: var(--d-border-radius);
|
|
position: relative;
|
|
transition: all 0.3s ease-out;
|
|
width: 1em;
|
|
|
|
&.--is-focused {
|
|
@include default-focus;
|
|
}
|
|
|
|
&.--show-cursor {
|
|
// fake input blinking cursor
|
|
&::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translateX(-50%) translateY(-50%);
|
|
width: 1px;
|
|
height: 50%;
|
|
background-color: currentcolor;
|
|
animation: otp-cursor-blink 1s infinite;
|
|
}
|
|
}
|
|
|
|
&.--placeholder {
|
|
font-size: var(--font-up-0);
|
|
}
|
|
|
|
.form-kit__field.has-error & {
|
|
border-color: var(--danger);
|
|
}
|
|
}
|
|
|
|
.d-otp-separator {
|
|
color: var(--primary-medium);
|
|
font-size: var(--font-up-1);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.d-otp-input-wrapper {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.d-otp-input {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: calc(100% + 40px);
|
|
height: 100%;
|
|
display: flex;
|
|
text-align: left;
|
|
opacity: 1;
|
|
color: transparent;
|
|
pointer-events: all;
|
|
background: transparent;
|
|
caret-color: transparent;
|
|
border: 0 solid transparent;
|
|
outline: transparent solid 0;
|
|
box-shadow: none;
|
|
line-height: 1;
|
|
letter-spacing: -0.5em;
|
|
font-size: var(--root-height);
|
|
font-family: monospace;
|
|
font-variant-numeric: tabular-nums;
|
|
clip-path: inset(0 40px 0 0);
|
|
|
|
&[data-com-onepassword-filled] {
|
|
background-clip: text;
|
|
}
|
|
}
|
|
|
|
@keyframes otp-cursor-blink {
|
|
0%,
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
|
|
51%,
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|