From ccbe4857a1475571e1c72ccdc93b53aa99cbce9f Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 8 Oct 2026 18:00:17 +0200 Subject: [PATCH] docs(auth): document the desktop handoff deep link and deny (#3303) --- .../content/docs/http-api/authentication.mdx | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/fluxer_docs/src/content/docs/http-api/authentication.mdx b/fluxer_docs/src/content/docs/http-api/authentication.mdx index e5f343663..1e6b13d7b 100644 --- a/fluxer_docs/src/content/docs/http-api/authentication.mdx +++ b/fluxer_docs/src/content/docs/http-api/authentication.mdx @@ -289,6 +289,7 @@ The code that identifies one pending desktop handoff. | code1 | string | The handoff code to present to the approving device | | expires_at | ISO8601 timestamp | The time at which the handoff expires, five minutes after creation | | poll_secret? | string | The secret the initiating device presents to read the issued token | +| return_method? | string | How the approving browser hands the sign-in back, `deep_link` when the request registered a `return_uri` and `code` otherwise | 1 The code is 12 characters drawn from the alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789`, rendered as two groups of six separated by a hyphen @@ -306,6 +307,7 @@ The device metadata shown to the approving device before it transfers a session. | --- | --- | --- | | status | string | The state, either `pending` or `expired` | | client_info?1 | ?[client info](#client-info-object) object | The initiating device metadata | +| return_method? | string | How the approving browser hands the sign-in back, either `deep_link` or `code`, present only when the status is `pending` | 1 The value is null whenever the status is `expired` @@ -317,7 +319,7 @@ The state of one desktop handoff as observed by the device that initiated it. | Field | Type | Description | | --- | --- | --- | -| status1 | string | The state, one of `pending`, `expired`, or `completed` | +| status1 | string | The state, one of `pending`, `denied`, `expired`, or `completed` | | token?2 | ?string | The newly issued user session token | | user_id?2 | ?snowflake | The authenticated user ID | | user?2 | ?[partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account | @@ -326,6 +328,18 @@ The state of one desktop handoff as observed by the device that initiated it. 2 The fields are present together only when the status is `completed` +## Handoff completion object + +The deep link that returns a completed handoff to the initiating app. + +### Structure + +| Field | Type | Description | +| --- | --- | --- | +| return_url1 | string | The registered `return_uri` with the handoff `code` and a one-time `grant` in its query string | + +1 The grant is 32 random bytes as 43 base64url characters, and only the initiating app that receives the link can present it + ## Origin handoff object The identifier of one stored origin handoff. @@ -1474,7 +1488,15 @@ This read reserves no username and changes no account state. No Gateway Dispatch Creates a pending handoff and returns the code the initiating device shows to an already signed-in device. Authentication is not required. Returns a [handoff initiation](#handoff-initiation-object) object. -The request has no body. Fluxer derives the device metadata shown to the approving device from the request itself. +The body is optional. Fluxer derives the device metadata shown to the approving device from the request itself. + +### JSON body + +| Field | Type | Description | +| --- | --- | --- | +| return_uri?1 | string | The deep link the approving browser opens to hand the sign-in back to the initiating desktop app | + +1 The value must be exactly `fluxer://handoff`, `fluxer-canary://handoff` or `fluxer-development://handoff`, and anything else returns 400 with a validation error ### Request headers @@ -1488,6 +1510,7 @@ The request has no body. Fluxer derives the device metadata shown to the approvi | Status | Body | Condition | | --- | --- | --- | | 200 | [handoff initiation](#handoff-initiation-object) object | A handoff request was created | +| 400 | [error response](/http-api/#error-response) | The `return_uri` is not an accepted deep link | | 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request | | 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs | | 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling | @@ -1546,7 +1569,7 @@ A successful lookup consumes one of the code's three lookups and permits [comple -Approves a pending handoff by issuing a new session to the initiating device on the authority of a live one. Requires the approving device's own user session token. Returns 204 with no body. +Approves a pending handoff by issuing a new session to the initiating device on the authority of a live one. Requires the approving device's own user session token. Returns a [handoff completion](#handoff-completion-object) object for the `deep_link` return method, otherwise 204 with no body. Fluxer reads that token from the `Authorization` header, or from the body `token` field when the body supplies one. @@ -1557,6 +1580,7 @@ Fluxer reads that token from the `Authorization` header, or from the body `token | code1 | string | The handoff code shown by the initiating device | | token?2 | string | The approving device's own user session token | | user_id3 | snowflake | The account the approving session belongs to | +| return_method?4 | string | Either `deep_link` or `code`, defaulting to `code` | 1 The code uses the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information), and only a code that operation has already inspected is accepted @@ -1564,6 +1588,8 @@ Fluxer reads that token from the `Authorization` header, or from the body `token 3 A supplied token that resolves to a different account returns `SESSION_TOKEN_MISMATCH` +4 `deep_link` returns 400 `INVALID_HANDOFF_CODE` when the handoff registered no `return_uri` + :::caution[The approving token may travel in the body] Sending the token in `token` puts a live credential into a JSON payload. A client SHOULD use the `Authorization` header and omit `token`. ::: @@ -1576,8 +1602,9 @@ Session creation can also return 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED` for | Status | Body | Condition | | --- | --- | --- | -| 204 | empty | The handoff was completed | -| 400 | [error response](/http-api/#error-response) | The body, code, expiry state, or session ownership is invalid | +| 200 | [handoff completion](#handoff-completion-object) object | The handoff was completed with the `deep_link` return method | +| 204 | empty | The handoff was completed with the `code` return method | +| 400 | [error response](/http-api/#error-response) | The body, code, expiry state, return method, or session ownership is invalid | | 401 | [error response](/http-api/#error-response) | The approving credential is missing, invalid, or revoked | | 403 | [error response](/http-api/#error-response) | The approving session belongs to a bot or to an account awaiting approval or rejected | | 404 | [error response](/http-api/#error-response) | The approving session resolves to an account that no longer exists | @@ -1587,7 +1614,7 @@ Session creation can also return 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED` for ### Side effects -Completion creates one new authentication session for the initiating device and publishes its token against the code. The token stays readable through [get desktop handoff status](#get-desktop-handoff-status) for whatever remains of the handoff's five minutes. Completion also discards the pending handoff, so the code cannot be completed a second time and a later [get desktop handoff information](#get-desktop-handoff-information) reports `expired`. +Completion creates one new authentication session for the initiating device and publishes its token against the code. With the `deep_link` return method, the token also requires the grant from `return_url`. The token stays readable through [get desktop handoff status](#get-desktop-handoff-status) for whatever remains of the handoff's five minutes. Completion also discards the pending handoff, so the code cannot be completed a second time and a later [get desktop handoff information](#get-desktop-handoff-information) reports `expired`. The approving session remains valid. The new session takes the initiating device's client IP address, user agent, and reported operating system from the handoff record. No Gateway Dispatch is emitted. @@ -1595,6 +1622,38 @@ The approving session remains valid. The new session takes the initiating device 10 requests per minute, on the `auth:handoff:complete` bucket, and a client IP address is blocked after 5 failed code attempts in 15 minutes. +## Deny a desktop handoff + + + +Declines a pending handoff after the approving device has looked it up. Authentication is not required. Returns 204 with no body. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) | + +Only a code that [get desktop handoff information](#get-desktop-handoff-information) has already inspected can be denied. An unknown, expired, or uninspected code returns 400 `INVALID_HANDOFF_CODE` and records a failed attempt. A malformed code and a request from a client IP address that has exhausted its failed-attempt allowance return the same error without recording one. + +### Response + +| Status | Body | Condition | +| --- | --- | --- | +| 204 | empty | The handoff was denied | +| 400 | [error response](/http-api/#error-response) | The code is malformed, uninspected or unknown, or the client IP attempt allowance is exhausted | +| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request | +| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs | +| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling | + +### Side effects + +The handoff stays stored until it expires, marked denied, and the inspected state is cleared. The initiating device reads the `denied` status, [complete desktop handoff](#complete-desktop-handoff) rejects the code, and [get desktop handoff information](#get-desktop-handoff-information) reports `expired`. No session is created and no Gateway Dispatch is emitted. + +### Rate limit + +10 requests per minute, on the `auth:handoff:deny` bucket, and a client IP address is blocked after 5 failed code attempts in 15 minutes. + ## Get desktop handoff status @@ -1648,6 +1707,9 @@ Reports the state of a handoff and delivers the new session token once, to a cal | Field | Type | Description | | --- | --- | --- | | poll_secret | string | The secret returned by [initiate desktop handoff](#initiate-desktop-handoff) | +| grant?1 | string | The one-time grant from the [handoff completion](#handoff-completion-object) deep link | + +1 A handoff completed with the `deep_link` return method reports `pending` until the grant is presented, and a wrong grant also records a failed attempt :::caution[The completed status is delivered exactly once] The token is returned once. Later polls report `expired`, so the initiating device MUST retain it when first received.