duo_auth/
client.rs

1use std::{future::Future, sync::Arc, time::Duration};
2
3use reqwest::{Client, Method, Request, Url};
4use serde::{de::DeserializeOwned, Deserialize};
5
6use super::{
7    errors::Error,
8    request::{DuoRequest, Parameters},
9    response::DuoResponse,
10    types::PreauthResponse,
11    types::{
12        AuthRequest, AuthStatusResponse, EnrollResponse, EnrollStatusResponse, PreauthRequest,
13    },
14    StdError,
15};
16
17pub struct DuoClient(Arc<DuoClientInner>);
18
19struct DuoClientInner {
20    base_url: Url,
21    ikey: String,
22    skey: String,
23
24    client: reqwest::Client,
25}
26
27impl DuoClient {
28    pub fn new<D, I, S>(api_domain: D, ikey: I, skey: S) -> Result<DuoClient, Error>
29    where
30        D: Into<String>,
31        I: Into<String>,
32        S: Into<String>,
33    {
34        let client = reqwest::Client::builder()
35            .user_agent(concat!(
36                env!("CARGO_PKG_NAME"),
37                "/",
38                env!("CARGO_PKG_VERSION")
39            ))
40            .build()
41            .map_err(Error::unspecified)?;
42
43        Self::new_with_client(client, api_domain, ikey, skey)
44    }
45
46    pub fn new_with_client<C, D, I, S>(
47        client: C,
48        api_domain: D,
49        ikey: I,
50        skey: S,
51    ) -> Result<DuoClient, Error>
52    where
53        C: Into<Client>,
54        D: Into<String>,
55        I: Into<String>,
56        S: Into<String>,
57    {
58        let api_domain = api_domain.into();
59
60        let base_url = match Url::parse(&api_domain) {
61            Ok(url) => url,
62            Err(err) => {
63                return Err(Error::InvalidApiDomain {
64                    domain: api_domain,
65                    cause: err.into(),
66                })
67            }
68        };
69
70        // Fail fast when there's no domain
71        let _ = base_url
72            .host_str()
73            .ok_or_else(|| Error::InvalidApiDomain {
74                domain: api_domain,
75                cause: "no domain in url".into(),
76            })?
77            .to_string();
78
79        Ok(DuoClient(Arc::new(DuoClientInner {
80            base_url,
81            ikey: ikey.into(),
82            skey: skey.into(),
83            client: client.into(),
84        })))
85    }
86
87    pub fn auth(&self, data: AuthRequest) -> impl Future<Output = Result<String, Error>> {
88        let this = Arc::clone(&self.0);
89
90        async move { Self::request_auth(this, data).await }
91    }
92
93    pub fn auth_status<S: Into<String>>(
94        &self,
95        tx_id: S,
96    ) -> impl Future<Output = Result<AuthStatusResponse, Error>> {
97        let this = Arc::clone(&self.0);
98
99        async move {
100            let txid: String = tx_id.into();
101            Self::request_auth_status(this, &txid).await
102        }
103    }
104
105    pub fn auth_wait(&self, data: AuthRequest) -> impl Future<Output = Result<bool, StdError>> {
106        let this = Arc::clone(&self.0);
107
108        async move {
109            let txid = Self::request_auth(this.clone(), data).await?;
110            let mut status: Option<bool>;
111
112            loop {
113                status = Self::request_auth_status(this.clone(), &txid)
114                    .await?
115                    .ready();
116                match status {
117                    None => tokio::time::sleep(Duration::from_secs(2)).await,
118                    Some(v) => return Ok(v),
119                }
120            }
121        }
122    }
123
124    pub fn check(&self) -> impl Future<Output = Result<u64, Error>> {
125        let this = Arc::clone(&self.0);
126
127        async move {
128            #[derive(Deserialize, Debug)]
129            struct CheckResponse {
130                time: u64,
131            }
132
133            let request =
134                Self::new_request(&this, Method::GET, "/auth/v2/check", Parameters::default())?;
135            Self::send_request_json::<CheckResponse>(&this.client, request)
136                .await
137                .map(|r| r.time)
138        }
139    }
140
141    pub fn enroll<U: Into<String>>(
142        &self,
143        username: Option<U>,
144        valid_secs: Option<u64>,
145    ) -> impl Future<Output = Result<EnrollResponse, Error>> {
146        let this = Arc::clone(&self.0);
147
148        async move { Self::request_enroll(this, username, valid_secs).await }
149    }
150
151    pub fn enroll_status<U: Into<String>, A: Into<String>>(
152        &self,
153        user_id: U,
154        activation_code: A,
155    ) -> impl Future<Output = Result<EnrollStatusResponse, Error>> {
156        let this = Arc::clone(&self.0);
157
158        async move { Self::request_enroll_status(this, user_id, activation_code).await }
159    }
160
161    pub fn ping(&self) -> impl Future<Output = Result<u64, Error>> {
162        let this = Arc::clone(&self.0);
163
164        async move {
165            #[derive(Deserialize, Debug)]
166            struct PingResponse {
167                time: u64,
168            }
169
170            let request =
171                Self::new_request(&this, Method::GET, "/auth/v2/ping", Parameters::default())?;
172            Self::send_request_json::<PingResponse>(&this.client, request)
173                .await
174                .map(|r| r.time)
175        }
176    }
177
178    pub fn preauth(
179        &self,
180        data: PreauthRequest,
181    ) -> impl Future<Output = Result<PreauthResponse, Error>> {
182        let this = Arc::clone(&self.0);
183
184        async move { Self::request_preauth(this, data).await }
185    }
186
187    async fn request_auth(this: Arc<DuoClientInner>, data: AuthRequest) -> Result<String, Error> {
188        let mut parameters = Parameters::default();
189        parameters.set("async", "1");
190        data.apply(&mut parameters);
191
192        #[derive(Deserialize, Debug)]
193        struct AuthResponse {
194            txid: String,
195        }
196
197        let request = Self::new_request(&this, Method::POST, "/auth/v2/auth", parameters)?;
198        Self::send_request_json::<AuthResponse>(&this.client, request)
199            .await
200            .map(|r| r.txid)
201    }
202
203    async fn request_auth_status(
204        this: Arc<DuoClientInner>,
205        tx_id: &str,
206    ) -> Result<AuthStatusResponse, Error> {
207        let mut parameters = Parameters::default();
208        parameters.set("txid", tx_id);
209
210        let request = Self::new_request(&this, Method::GET, "/auth/v2/auth_status", parameters)?;
211        Self::send_request_json(&this.client, request).await
212    }
213
214    async fn request_enroll<U: Into<String>>(
215        this: Arc<DuoClientInner>,
216        username: Option<U>,
217        valid_secs: Option<u64>,
218    ) -> Result<EnrollResponse, Error> {
219        let mut parameters = Parameters::default();
220        parameters.set_opt("username", username);
221        parameters.set_opt("valid_secs", valid_secs.map(|v| v.to_string()));
222
223        let request = Self::new_request(&this, Method::POST, "/auth/v2/enroll", parameters)?;
224        Self::send_request_json(&this.client, request).await
225    }
226
227    async fn request_enroll_status<U: Into<String>, A: Into<String>>(
228        this: Arc<DuoClientInner>,
229        user_id: U,
230        activation_code: A,
231    ) -> Result<EnrollStatusResponse, Error> {
232        let mut parameters = Parameters::default();
233        parameters.set("user_id", user_id);
234        parameters.set("activation_code", activation_code);
235
236        let request = Self::new_request(&this, Method::POST, "/auth/v2/enroll_status", parameters)?;
237        Self::send_request_json(&this.client, request).await
238    }
239
240    async fn request_preauth(
241        this: Arc<DuoClientInner>,
242        data: PreauthRequest,
243    ) -> Result<PreauthResponse, Error> {
244        let mut parameters = Parameters::default();
245        data.apply(&mut parameters);
246
247        let request = Self::new_request(&this, Method::POST, "/auth/v2/preauth", parameters)?;
248        Self::send_request_json(&this.client, request).await
249    }
250
251    fn new_request<P: Into<String>>(
252        this: &Arc<DuoClientInner>,
253        method: Method,
254        path: P,
255        parameters: Parameters,
256    ) -> Result<Request, Error> {
257        DuoRequest::new(this.base_url.clone(), method, path, parameters)
258            .build(&this.client, &this.ikey, &this.skey)
259            .map_err(Error::unspecified)
260    }
261
262    async fn send_request_json<T>(client: &Client, request: Request) -> Result<T, Error>
263    where
264        T: DeserializeOwned + std::fmt::Debug,
265    {
266        let response = client.execute(request).await.map_err(Error::unspecified)?;
267
268        let body = response
269            .json::<DuoResponse<T>>()
270            .await
271            .map_err(Error::unspecified)?;
272
273        body.ok()
274    }
275}