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());
                    }
                },
            }
        }
    }
}