oauth-device-flows 0.1.0

A specialized Rust library implementing OAuth 2.0 Device Authorization Grant (RFC 8628)
Documentation
//! Core types for OAuth device flows

use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use url::Url;

/// Response from the device authorization endpoint
#[derive(Debug, Clone)]
pub struct AuthorizationResponse {
    /// The device code used for polling
    pub device_code: Secret<String>,

    /// The user code to display to the user
    pub user_code: String,

    /// The verification URI where the user should go
    pub verification_uri: Url,

    /// Optional complete verification URI with user code embedded
    pub verification_uri_complete: Option<Url>,

    /// How long the device code is valid for
    pub expires_in: u64,

    /// Recommended polling interval in seconds
    pub interval: u64,
}

impl AuthorizationResponse {
    /// Get the device code (secret)
    pub fn device_code(&self) -> &str {
        self.device_code.expose_secret()
    }

    /// Get the user code
    pub fn user_code(&self) -> &str {
        &self.user_code
    }

    /// Get the verification URI
    pub fn verification_uri(&self) -> &Url {
        &self.verification_uri
    }

    /// Get the complete verification URI if available
    pub fn verification_uri_complete(&self) -> Option<&Url> {
        self.verification_uri_complete.as_ref()
    }

    /// Get the expiration time as Duration
    pub fn expires_in(&self) -> Duration {
        Duration::from_secs(self.expires_in)
    }

    /// Get the polling interval as Duration
    pub fn poll_interval(&self) -> Duration {
        Duration::from_secs(self.interval)
    }

    /// Generate a QR code for the verification URI (requires qr-codes feature)
    #[cfg(feature = "qr-codes")]
    pub fn generate_qr_code(&self) -> Result<String, crate::error::DeviceFlowError> {
        use qrcode::{render::unicode, QrCode};

        let uri = self
            .verification_uri_complete
            .as_ref()
            .unwrap_or(&self.verification_uri);

        let code = QrCode::new(uri.as_str())?;
        Ok(code
            .render::<unicode::Dense1x2>()
            .dark_color(unicode::Dense1x2::Light)
            .light_color(unicode::Dense1x2::Dark)
            .build())
    }
}

/// Response from the token endpoint
#[derive(Debug, Clone)]
pub struct TokenResponse {
    /// The access token
    pub access_token: Secret<String>,

    /// The token type (usually "Bearer")
    pub token_type: String,

    /// How long the access token is valid for in seconds
    pub expires_in: Option<u64>,

    /// The refresh token (if available)
    pub refresh_token: Option<Secret<String>>,

    /// The scopes granted
    pub scope: Option<String>,

    /// When this token was issued (set by the library)
    pub issued_at: OffsetDateTime,
}

impl TokenResponse {
    /// Get the access token (secret)
    pub fn access_token(&self) -> &str {
        self.access_token.expose_secret()
    }

    /// Get the refresh token (secret) if available
    pub fn refresh_token(&self) -> Option<&str> {
        self.refresh_token
            .as_ref()
            .map(|t| t.expose_secret().as_str())
    }

    /// Check if the token is expired
    pub fn is_expired(&self) -> bool {
        if let Some(expires_in) = self.expires_in {
            let expiry = self.issued_at + time::Duration::seconds(expires_in as i64);
            OffsetDateTime::now_utc() >= expiry
        } else {
            false
        }
    }

    /// Get the expiration time
    pub fn expires_at(&self) -> Option<OffsetDateTime> {
        self.expires_in
            .map(|expires_in| self.issued_at + time::Duration::seconds(expires_in as i64))
    }

    /// Get the remaining lifetime of the token
    pub fn remaining_lifetime(&self) -> Option<Duration> {
        self.expires_at().map(|expires_at| {
            let remaining = expires_at - OffsetDateTime::now_utc();
            Duration::from_secs(remaining.whole_seconds().max(0) as u64)
        })
    }

    /// Check if the token will expire within the given duration
    pub fn expires_within(&self, duration: Duration) -> bool {
        if let Some(remaining) = self.remaining_lifetime() {
            remaining <= duration
        } else {
            false
        }
    }
}

/// Error response from OAuth endpoints
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
    /// Error code
    pub error: String,

    /// Human-readable error description
    pub error_description: Option<String>,

    /// URI to a human-readable web page with error information
    pub error_uri: Option<Url>,
}

/// Device authorization request payload
#[derive(Debug, Clone, Serialize)]
pub struct DeviceAuthorizationRequest {
    /// Client identifier
    pub client_id: String,

    /// Requested scopes
    pub scope: Option<String>,
}

/// Token request payload for device flow
#[derive(Debug, Clone, Serialize)]
pub struct DeviceTokenRequest {
    /// Grant type (always "urn:ietf:params:oauth:grant-type:device_code")
    pub grant_type: String,

    /// Device code from authorization response
    pub device_code: String,

    /// Client identifier
    pub client_id: String,
}

/// Token refresh request payload
#[derive(Debug, Clone, Serialize)]
pub struct RefreshTokenRequest {
    /// Grant type (always "refresh_token")
    pub grant_type: String,

    /// Refresh token
    pub refresh_token: String,

    /// Client identifier
    pub client_id: String,

    /// Optional scope (should not exceed originally granted scope)
    pub scope: Option<String>,
}

// Manual deserialization for AuthorizationResponse
impl<'de> Deserialize<'de> for AuthorizationResponse {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct AuthorizationResponseHelper {
            device_code: String,
            user_code: String,
            verification_uri: Url,
            verification_uri_complete: Option<Url>,
            expires_in: u64,
            interval: u64,
        }

        let helper = AuthorizationResponseHelper::deserialize(deserializer)?;
        Ok(AuthorizationResponse {
            device_code: Secret::new(helper.device_code),
            user_code: helper.user_code,
            verification_uri: helper.verification_uri,
            verification_uri_complete: helper.verification_uri_complete,
            expires_in: helper.expires_in,
            interval: helper.interval,
        })
    }
}

// Manual deserialization for TokenResponse
impl<'de> Deserialize<'de> for TokenResponse {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct TokenResponseHelper {
            access_token: String,
            token_type: String,
            expires_in: Option<u64>,
            refresh_token: Option<String>,
            scope: Option<String>,
        }

        let helper = TokenResponseHelper::deserialize(deserializer)?;
        Ok(TokenResponse {
            access_token: Secret::new(helper.access_token),
            token_type: helper.token_type,
            expires_in: helper.expires_in,
            refresh_token: helper.refresh_token.map(Secret::new),
            scope: helper.scope,
            issued_at: OffsetDateTime::now_utc(),
        })
    }
}