1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
#![deny(missing_docs)]
//! Ways to authenticate with the Webex API
use crate::{AuthorizationType, RequestBody, RestClient};
use hyper::StatusCode;
use serde::Deserialize;
use tokio::time::{self, Duration, Instant};
const SCOPE: &str = "spark:all";
const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code";
#[allow(dead_code)]
/// Authenticates a device based on a Webex Integration
/// "client id" and a "client secret".
///
/// More information can be found on <https://developer.webex.com/docs/login-with-webex#device-grant-flow>.
pub struct DeviceAuthenticator {
client_id: String,
client_secret: String,
client: RestClient,
}
/// This struct contains the codes and URIs necessary
/// to complete the "device grant flow" log in.
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
pub struct VerificationToken {
/// Unique user verification code.
pub user_code: String,
device_code: String,
/// A verification URL the user can navigate
/// to on a different device and provide the unique
/// user verification code.
pub verification_uri: String,
/// A verification URL containing the embedded
/// hashed user verification code.
pub verification_uri_complete: String,
interval: u64,
}
#[derive(Deserialize, Debug)]
struct TokenResponse {
access_token: String,
}
/// Type alias for the bearer token.
pub type Bearer = String;
impl DeviceAuthenticator {
/// Creates a new [`DeviceAuthenticator`] using the "client ID" and
/// "client secret" provided by a Webex Integration.
///
/// For more details: <https://developer.webex.com/docs/integrations>.
#[must_use]
pub fn new(id: &str, secret: &str) -> Self {
let client = RestClient::new();
Self {
client_id: id.to_string(),
client_secret: secret.to_string(),
client,
}
}
/// First step of device authentication. Returns a [`VerificationToken`]
/// containing the codes and URLs that can be entered and navigated to
/// on a different device.
pub async fn verify(&self) -> Result<VerificationToken, crate::Error> {
let params = &[("client_id", self.client_id.as_str()), ("scope", SCOPE)];
let verification_token = self
.client
.api_post::<VerificationToken, _>(
"device/authorize",
RequestBody {
media_type: "application/x-www-form-urlencoded; charset=utf-8",
content: serde_html_form::to_string(params)?,
},
AuthorizationType::None,
)
.await?;
Ok(verification_token)
}
/// Second and final step of device authentication. Receives a [`VerificationToken`]
/// provided by [`verify`](DeviceAuthenticator::verify) and blocks until the user enters their crendentials using
/// the provided codes/links from [`VerificationToken`]. Returns a [`Bearer`] if successful.
pub async fn wait_for_authentication(
&self,
verification_token: &VerificationToken,
) -> Result<Bearer, crate::Error> {
let params = [
("grant_type", GRANT_TYPE),
("device_code", &verification_token.device_code),
("client_id", &self.client_id),
];
let mut interval = time::interval_at(
Instant::now() + Duration::from_secs(verification_token.interval),
Duration::from_secs(verification_token.interval + 1),
);
loop {
interval.tick().await;
match self
.client
.api_post::<TokenResponse, String>(
"device/token",
RequestBody {
media_type: "application/x-www-form-urlencoded; charset=utf-8",
content: serde_html_form::to_string(params)?,
},
AuthorizationType::Basic {
username: &self.client_id,
password: &self.client_secret,
},
)
.await
{
Ok(token) => return Ok(token.access_token),
Err(e) => match e.kind() {
crate::error::ErrorKind::StatusText(http_status, _) => {
if *http_status != StatusCode::PRECONDITION_REQUIRED {
return Err(crate::ErrorKind::Authentication.into());
}
}
_ => {
return Err(crate::ErrorKind::Authentication.into());
}
},
}
}
}
}