oauth_device_flows/
types.rs

1//! Core types for OAuth device flows
2
3use secrecy::{ExposeSecret, Secret};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6use time::OffsetDateTime;
7use url::Url;
8
9/// Response from the device authorization endpoint
10#[derive(Debug, Clone)]
11pub struct AuthorizationResponse {
12    /// The device code used for polling
13    pub device_code: Secret<String>,
14
15    /// The user code to display to the user
16    pub user_code: String,
17
18    /// The verification URI where the user should go
19    pub verification_uri: Url,
20
21    /// Optional complete verification URI with user code embedded
22    pub verification_uri_complete: Option<Url>,
23
24    /// How long the device code is valid for
25    pub expires_in: u64,
26
27    /// Recommended polling interval in seconds
28    pub interval: u64,
29}
30
31impl AuthorizationResponse {
32    /// Get the device code (secret)
33    pub fn device_code(&self) -> &str {
34        self.device_code.expose_secret()
35    }
36
37    /// Get the user code
38    pub fn user_code(&self) -> &str {
39        &self.user_code
40    }
41
42    /// Get the verification URI
43    pub fn verification_uri(&self) -> &Url {
44        &self.verification_uri
45    }
46
47    /// Get the complete verification URI if available
48    pub fn verification_uri_complete(&self) -> Option<&Url> {
49        self.verification_uri_complete.as_ref()
50    }
51
52    /// Get the expiration time as Duration
53    pub fn expires_in(&self) -> Duration {
54        Duration::from_secs(self.expires_in)
55    }
56
57    /// Get the polling interval as Duration
58    pub fn poll_interval(&self) -> Duration {
59        Duration::from_secs(self.interval)
60    }
61
62    /// Generate a QR code for the verification URI (requires qr-codes feature)
63    #[cfg(feature = "qr-codes")]
64    pub fn generate_qr_code(&self) -> Result<String, crate::error::DeviceFlowError> {
65        use qrcode::{render::unicode, QrCode};
66
67        let uri = self
68            .verification_uri_complete
69            .as_ref()
70            .unwrap_or(&self.verification_uri);
71
72        let code = QrCode::new(uri.as_str())?;
73        Ok(code
74            .render::<unicode::Dense1x2>()
75            .dark_color(unicode::Dense1x2::Light)
76            .light_color(unicode::Dense1x2::Dark)
77            .build())
78    }
79}
80
81/// Response from the token endpoint
82#[derive(Debug, Clone)]
83pub struct TokenResponse {
84    /// The access token
85    pub access_token: Secret<String>,
86
87    /// The token type (usually "Bearer")
88    pub token_type: String,
89
90    /// How long the access token is valid for in seconds
91    pub expires_in: Option<u64>,
92
93    /// The refresh token (if available)
94    pub refresh_token: Option<Secret<String>>,
95
96    /// The scopes granted
97    pub scope: Option<String>,
98
99    /// When this token was issued (set by the library)
100    pub issued_at: OffsetDateTime,
101}
102
103impl TokenResponse {
104    /// Get the access token (secret)
105    pub fn access_token(&self) -> &str {
106        self.access_token.expose_secret()
107    }
108
109    /// Get the refresh token (secret) if available
110    pub fn refresh_token(&self) -> Option<&str> {
111        self.refresh_token
112            .as_ref()
113            .map(|t| t.expose_secret().as_str())
114    }
115
116    /// Check if the token is expired
117    pub fn is_expired(&self) -> bool {
118        if let Some(expires_in) = self.expires_in {
119            let expiry = self.issued_at + time::Duration::seconds(expires_in as i64);
120            OffsetDateTime::now_utc() >= expiry
121        } else {
122            false
123        }
124    }
125
126    /// Get the expiration time
127    pub fn expires_at(&self) -> Option<OffsetDateTime> {
128        self.expires_in
129            .map(|expires_in| self.issued_at + time::Duration::seconds(expires_in as i64))
130    }
131
132    /// Get the remaining lifetime of the token
133    pub fn remaining_lifetime(&self) -> Option<Duration> {
134        self.expires_at().map(|expires_at| {
135            let remaining = expires_at - OffsetDateTime::now_utc();
136            Duration::from_secs(remaining.whole_seconds().max(0) as u64)
137        })
138    }
139
140    /// Check if the token will expire within the given duration
141    pub fn expires_within(&self, duration: Duration) -> bool {
142        if let Some(remaining) = self.remaining_lifetime() {
143            remaining <= duration
144        } else {
145            false
146        }
147    }
148}
149
150/// Error response from OAuth endpoints
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ErrorResponse {
153    /// Error code
154    pub error: String,
155
156    /// Human-readable error description
157    pub error_description: Option<String>,
158
159    /// URI to a human-readable web page with error information
160    pub error_uri: Option<Url>,
161}
162
163/// Device authorization request payload
164#[derive(Debug, Clone, Serialize)]
165pub struct DeviceAuthorizationRequest {
166    /// Client identifier
167    pub client_id: String,
168
169    /// Requested scopes
170    pub scope: Option<String>,
171}
172
173/// Token request payload for device flow
174#[derive(Debug, Clone, Serialize)]
175pub struct DeviceTokenRequest {
176    /// Grant type (always "urn:ietf:params:oauth:grant-type:device_code")
177    pub grant_type: String,
178
179    /// Device code from authorization response
180    pub device_code: String,
181
182    /// Client identifier
183    pub client_id: String,
184}
185
186/// Token refresh request payload
187#[derive(Debug, Clone, Serialize)]
188pub struct RefreshTokenRequest {
189    /// Grant type (always "refresh_token")
190    pub grant_type: String,
191
192    /// Refresh token
193    pub refresh_token: String,
194
195    /// Client identifier
196    pub client_id: String,
197
198    /// Optional scope (should not exceed originally granted scope)
199    pub scope: Option<String>,
200}
201
202// Manual deserialization for AuthorizationResponse
203impl<'de> Deserialize<'de> for AuthorizationResponse {
204    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205    where
206        D: serde::Deserializer<'de>,
207    {
208        #[derive(Deserialize)]
209        struct AuthorizationResponseHelper {
210            device_code: String,
211            user_code: String,
212            verification_uri: Url,
213            verification_uri_complete: Option<Url>,
214            expires_in: u64,
215            interval: u64,
216        }
217
218        let helper = AuthorizationResponseHelper::deserialize(deserializer)?;
219        Ok(AuthorizationResponse {
220            device_code: Secret::new(helper.device_code),
221            user_code: helper.user_code,
222            verification_uri: helper.verification_uri,
223            verification_uri_complete: helper.verification_uri_complete,
224            expires_in: helper.expires_in,
225            interval: helper.interval,
226        })
227    }
228}
229
230// Manual deserialization for TokenResponse
231impl<'de> Deserialize<'de> for TokenResponse {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: serde::Deserializer<'de>,
235    {
236        #[derive(Deserialize)]
237        struct TokenResponseHelper {
238            access_token: String,
239            token_type: String,
240            expires_in: Option<u64>,
241            refresh_token: Option<String>,
242            scope: Option<String>,
243        }
244
245        let helper = TokenResponseHelper::deserialize(deserializer)?;
246        Ok(TokenResponse {
247            access_token: Secret::new(helper.access_token),
248            token_type: helper.token_type,
249            expires_in: helper.expires_in,
250            refresh_token: helper.refresh_token.map(Secret::new),
251            scope: helper.scope,
252            issued_at: OffsetDateTime::now_utc(),
253        })
254    }
255}