pub struct DeviceFlow { /* private fields */ }Expand description
The device-flow client.
Holds no credential of its own — only the public client_id — which is what
makes it constructible before any login has ever happened.
Implementations§
Source§impl DeviceFlow
impl DeviceFlow
Sourcepub fn new(
app: AppRegistration,
endpoints: Endpoints,
) -> Result<Self, DeviceFlowError>
pub fn new( app: AppRegistration, endpoints: Endpoints, ) -> Result<Self, DeviceFlowError>
§Errors
The HTTP client failing to build.
Sourcepub async fn refresh(
&self,
refresh_token: &SecretString,
) -> Result<UserAccessToken, DeviceFlowError>
pub async fn refresh( &self, refresh_token: &SecretString, ) -> Result<UserAccessToken, DeviceFlowError>
Exchange a refresh token for a fresh pair.
§No client secret, and why that is not an oversight
GitHub requires a confidential client credential for this exchange
“unless the user access token was generated using the device flow”,
and this product’s tokens always are. That exemption is what makes
renewal possible here at all: a published binary cannot carry such a
credential, because anyone who downloads it has it – and with it can
call the App’s token-management endpoints and revoke every user’s
access. Verified against live GitHub before this was written: the call
below returns 200 with client_id alone.
§The old pair is dead the instant this succeeds
GitHub rotates: “Once you use a refresh token, that refresh token and
the old user access token will no longer work.” Measured, not assumed
– the previous access token answered 401 immediately after.
So there is no retry here and there must not be one. A caller that
re-sends a spent refresh token gets incorrect_client_credentials,
which names the client id and the client secret and is about neither:
the credential is simply gone, and the machine needs an interactive
sign-in. Persist what this returns before using it, or a response
lost in flight takes the host’s access with it.
§Errors
DeviceFlowError, as the access-token request.
pub fn with_http_client( http: Client, app: AppRegistration, endpoints: Endpoints, ) -> Self
Sourcepub fn verification_url(&self) -> Url
pub fn verification_url(&self) -> Url
The canonical page this login must be approved on, and the only device-flow URL the product ever prints.
Sourcepub async fn start(&self) -> Result<DeviceAuthorization, DeviceFlowError>
pub async fn start(&self) -> Result<DeviceAuthorization, DeviceFlowError>
Begin a login.
The request carries the public client_id and nothing else — no secret,
no scope (a GitHub App’s scopes come from its declared permissions, not
from the grant), and no redirect URI.
§Errors
DeviceFlowError::Transport, DeviceFlowError::Status,
DeviceFlowError::Decode, DeviceFlowError::Malformed, or
DeviceFlowError::UntrustedVerificationUri.
Sourcepub async fn poll_once(
&self,
authorization: &DeviceAuthorization,
) -> Result<PollOutcome, DeviceFlowError>
pub async fn poll_once( &self, authorization: &DeviceAuthorization, ) -> Result<PollOutcome, DeviceFlowError>
Ask once whether the login has been approved.
§Errors
The four terminal members of the error matrix, plus transport and decode
failures. authorization_pending and slow_down are not errors —
they are PollOutcomes.
Sourcepub async fn complete(
&self,
authorization: &DeviceAuthorization,
sleeper: &dyn Sleeper,
) -> Result<UserAccessToken, DeviceFlowError>
pub async fn complete( &self, authorization: &DeviceAuthorization, sleeper: &dyn Sleeper, ) -> Result<UserAccessToken, DeviceFlowError>
Poll until the login is approved, refused, or expires.
Waiting goes through Sleeper rather than tokio::time::sleep, so a
test can assert on the sequence of intervals this produces instead of
waiting them out. That is what makes “slow_down demonstrably increases
the poll interval” an equality assertion rather than a stopwatch reading.
The elapsed budget is accumulated from the intervals actually waited, so
the local expiry backstop is as deterministic as the rest. GitHub’s own
expired_token remains authoritative and is checked first every round;
this only catches a server that never sends it.
§One dropped packet does not kill a login
A failure that says nothing about the login —
DeviceFlowError::is_retryable, meaning a transport error or a 5xx —
is absorbed here rather than propagated, up to
MAX_TRANSPORT_RETRIES consecutive times. This loop is the right place
for it and the caller is not: the poll interval, the expiry budget and
the device code all live here, so a retry costs one more scheduled poll
and nothing else, while a caller retrying complete would restart the
budget and re-derive the back-off. The counter resets on every successful
poll, so it bounds a burst, not the whole login.
The four terminal states of the error matrix are unaffected: they are not retryable, and a login the user declined is still refused on the first answer.
§Errors
Every terminal member of the error matrix.
Trait Implementations§
Source§impl Clone for DeviceFlow
impl Clone for DeviceFlow
Source§fn clone(&self) -> DeviceFlow
fn clone(&self) -> DeviceFlow
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more