Skip to main content

DeviceFlow

Struct DeviceFlow 

Source
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

Source

pub fn new( app: AppRegistration, endpoints: Endpoints, ) -> Result<Self, DeviceFlowError>

§Errors

The HTTP client failing to build.

Source

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.

Source

pub fn with_http_client( http: Client, app: AppRegistration, endpoints: Endpoints, ) -> Self

Source

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.

Source

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.

Source

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.

Source

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

Source§

fn clone(&self) -> DeviceFlow

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for DeviceFlow

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more