From 4521e2f246e6e3013bc3ae6d633a8d9d83a98763 Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Fri, 22 Nov 2019 10:29:55 -0800 Subject: [PATCH] Rename PollInformation DeviceAuthResponse. Have it correctly handle either verification_uri or verification_url and deserialize into a struct that has the data types desired. --- src/authenticator_delegate.rs | 65 +++++++++++++++++++++++++++++++---- src/device.rs | 62 +++++++++++---------------------- 2 files changed, 78 insertions(+), 49 deletions(-) diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 9374c1d..7537bc2 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -1,4 +1,5 @@ //! Module containing types related to delegates. +use crate::error::{AuthErrorOr, Error}; use std::pin::Pin; use std::time::Duration; @@ -8,12 +9,13 @@ use futures::prelude::*; /// Contains state of pending authentication requests #[derive(Clone, Debug, PartialEq)] -pub struct PollInformation { +pub struct DeviceAuthResponse { + /// The device verification code. + pub device_code: String, /// Code the user must enter ... pub user_code: String, - /// ... at the verification URL - pub verification_url: String, - + /// ... at the verification URI + pub verification_uri: String, /// The `user_code` expires at the given time /// It's the time the user has left to authenticate your application pub expires_at: DateTime, @@ -22,17 +24,66 @@ pub struct PollInformation { pub interval: Duration, } +impl DeviceAuthResponse { + pub(crate) fn from_json(json_data: &[u8]) -> Result { + Ok(serde_json::from_slice::>(json_data)?.into_result()?) + } +} + +impl<'de> serde::Deserialize<'de> for DeviceAuthResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct RawDeviceAuthResponse { + device_code: String, + user_code: String, + // The standard dictates that verification_uri is required, but + // sadly google uses verification_url currently. One of these two + // fields need to be set and verification_uri takes precedence if + // they both are set. + verification_uri: Option, + verification_url: Option, + expires_in: i64, + interval: Option, + } + + let RawDeviceAuthResponse { + device_code, + user_code, + verification_uri, + verification_url, + expires_in, + interval, + } = RawDeviceAuthResponse::deserialize(deserializer)?; + + let verification_uri = verification_uri.or(verification_url).ok_or_else(|| { + serde::de::Error::custom("neither verification_uri nor verification_url specified") + })?; + let expires_at = Utc::now() + chrono::Duration::seconds(expires_in); + let interval = Duration::from_secs(interval.unwrap_or(5)); + Ok(DeviceAuthResponse { + device_code, + user_code, + verification_uri, + expires_at, + interval, + }) + } +} + /// DeviceFlowDelegate methods are called when a device flow needs to ask the /// application what to do in certain cases. pub trait DeviceFlowDelegate: Send + Sync { /// The server has returned a `user_code` which must be shown to the user, - /// along with the `verification_url`. + /// along with the `verification_uri`. /// # Notes /// * Will be called exactly once, provided we didn't abort during `request_code` phase. - fn present_user_code(&self, pi: &PollInformation) { + fn present_user_code(&self, pi: &DeviceAuthResponse) { println!( "Please enter {} at {} and grant access to this application", - pi.user_code, pi.verification_url + pi.user_code, pi.verification_uri ); println!("Do not close this application until you either denied or granted access."); println!( diff --git a/src/device.rs b/src/device.rs index 31ff37f..e32e6fa 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,16 +1,14 @@ use crate::authenticator_delegate::{ - DefaultDeviceFlowDelegate, DeviceFlowDelegate, PollInformation, + DefaultDeviceFlowDelegate, DeviceAuthResponse, DeviceFlowDelegate, }; -use crate::error::{AuthError, AuthErrorOr, Error}; +use crate::error::{AuthError, Error}; use crate::types::{ApplicationSecret, Token}; use std::borrow::Cow; use std::time::Duration; -use chrono::Utc; use futures::prelude::*; use hyper::header; -use serde::Deserialize; use url::form_urlencoded; pub const GOOGLE_DEVICE_CODE_URL: &str = "https://accounts.google.com/o/oauth2/device/code"; @@ -50,19 +48,18 @@ impl DeviceFlow { T: AsRef, C: hyper::client::connect::Connect + 'static, { - let (pollinf, device_code) = Self::request_code( + let device_auth_resp = Self::request_code( &self.app_secret, hyper_client, &self.device_code_url, scopes, ) .await?; - self.flow_delegate.present_user_code(&pollinf); + self.flow_delegate.present_user_code(&device_auth_resp); self.wait_for_device_token( hyper_client, &self.app_secret, - &pollinf, - &device_code, + &device_auth_resp, &self.grant_type, ) .await @@ -72,18 +69,22 @@ impl DeviceFlow { &self, hyper_client: &hyper::Client, app_secret: &ApplicationSecret, - pollinf: &PollInformation, - device_code: &str, + device_auth_resp: &DeviceAuthResponse, grant_type: &str, ) -> Result where C: hyper::client::connect::Connect + 'static, { - let mut interval = pollinf.interval; + let mut interval = device_auth_resp.interval; loop { tokio::timer::delay_for(interval).await; - interval = match Self::poll_token(&app_secret, hyper_client, device_code, grant_type) - .await + interval = match Self::poll_token( + &app_secret, + hyper_client, + &device_auth_resp.device_code, + grant_type, + ) + .await { Ok(token) => return Ok(token), Err(Error::AuthError(AuthError { error, .. })) @@ -119,7 +120,7 @@ impl DeviceFlow { client: &hyper::Client, device_code_url: &str, scopes: &[T], - ) -> Result<(PollInformation, String), Error> + ) -> Result where T: AsRef, C: hyper::client::connect::Connect + 'static, @@ -138,37 +139,14 @@ impl DeviceFlow { .body(hyper::Body::from(req)) .unwrap(); let resp = client.request(req).await?; - // This return type is defined in https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.2 - // The alias is present as Google use a non-standard name for verification_uri. - // According to the standard interval is optional, however, all tested implementations provide it. - // verification_uri_complete is optional in the standard but not provided in tested implementations. - #[derive(Deserialize)] - struct JsonData { - device_code: String, - user_code: String, - #[serde(alias = "verification_url")] - verification_uri: String, - expires_in: Option, - interval: i64, - } - - let json_bytes = resp.into_body().try_concat().await?; - let decoded: JsonData = - serde_json::from_slice::>(&json_bytes)?.into_result()?; - let expires_in = decoded.expires_in.unwrap_or(60 * 60); - let pi = PollInformation { - user_code: decoded.user_code, - verification_url: decoded.verification_uri, - expires_at: Utc::now() + chrono::Duration::seconds(expires_in), - interval: Duration::from_secs(i64::abs(decoded.interval) as u64), - }; - Ok((pi, decoded.device_code)) + let body = resp.into_body().try_concat().await?; + DeviceAuthResponse::from_json(&body) } /// If the first call is successful, this method may be called. /// As long as we are waiting for authentication, it will return `Ok(None)`. /// You should call it within the interval given the previously returned - /// `PollInformation.interval` field. + /// `DeviceAuthResponse.interval` field. /// /// The operation was successful once you receive an Ok(Some(Token)) for the first time. /// Subsequent calls will return the previous result, which may also be an error state. @@ -223,8 +201,8 @@ mod tests { #[derive(Clone)] struct FD; impl DeviceFlowDelegate for FD { - fn present_user_code(&self, pi: &PollInformation) { - assert_eq!("https://example.com/verify", pi.verification_url); + fn present_user_code(&self, pi: &DeviceAuthResponse) { + assert_eq!("https://example.com/verify", pi.verification_uri); } }