ibmcloud_core/authenticators/
token_api.rs

1use anyhow::Result;
2use chrono::Local;
3use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT};
4use serde::Deserialize;
5use std::env;
6
7const GRANT_TYPE: &str = "urn:ibm:params:oauth:grant-type:apikey";
8const IAM_CLOUD_URL_AUTH: &str = "https://iam.cloud.ibm.com/identity/token";
9
10#[derive(Deserialize, Debug, Clone)]
11pub struct Configs {
12    IAM_IDENTITY_URL: String,
13}
14
15impl Configs {
16    fn new() -> Configs {
17        let key = "IAM_IDENTITY_URL";
18        match env::var(key) {
19            Ok(val) => Configs {
20                IAM_IDENTITY_URL: val,
21            },
22            Err(..) => Configs {
23                IAM_IDENTITY_URL: IAM_CLOUD_URL_AUTH.to_string(),
24            },
25        }
26    }
27}
28
29#[derive(Deserialize, Debug, Clone)]
30pub struct AuthenticatorApiClient {
31    pub(crate) url: String,
32    pub(crate) token: TokenResponse,
33    pub(crate) options: Options,
34}
35
36impl AuthenticatorApiClient {
37    pub fn new(apikey: String) -> AuthenticatorApiClient {
38        let config = Configs::new();
39        AuthenticatorApiClient {
40            url: config.IAM_IDENTITY_URL,
41            token: TokenResponse {
42                access_token: "".to_string(),
43                refresh_token: None,
44                delegated_refresh_token: None,
45                token_type: "".to_string(),
46                expires_in: 0,
47                expiration: 0,
48            },
49            options: Options::new(apikey),
50        }
51    }
52    fn set_token(&mut self, token: TokenResponse) {
53        self.token = token
54    }
55
56    pub async fn get_token(&mut self) -> TokenResponse {
57        if self.token.validate_token() {
58            self.token.clone()
59        } else {
60            self.authenticate().await;
61            self.token.clone()
62        }
63    }
64
65    pub async fn authenticate(&mut self) -> Result<()> {
66        let response = request_token(self.options.clone(), String::from(&self.url.clone())).await?;
67
68        match response.clone() {
69            ResponseType::Ok(TokenResponse) => {
70                self.set_token(TokenResponse);
71                Ok(())
72            }
73            _ => Ok(()),
74        }
75    }
76}
77
78async fn request_token(req: Options, url: String) -> Result<ResponseType> {
79    let params = urlencoded_parameter(req);
80    let response: ResponseType = reqwest::Client::new()
81        .post(url)
82        .form(&params)
83        .headers(construct_headers())
84        .send()
85        .await?
86        .json()
87        .await?;
88    Ok(response)
89}
90
91#[derive(Deserialize, Debug, Clone, PartialEq)]
92pub struct Options {
93    //Grant type for this API call. You must set the
94    // grant type to urn:ibm:params:oauth:grant-type:apikey.
95    pub(crate) grant_type: String,
96
97    //The value of the api key.
98    pub(crate) apikey: String,
99}
100
101impl Options {
102    pub fn new(apikey: String) -> Options {
103        Options {
104            grant_type: GRANT_TYPE.to_string(),
105            apikey,
106        }
107    }
108}
109
110#[derive(Deserialize, Debug, Clone)]
111#[serde(untagged)]
112pub enum ResponseType {
113    Ok(TokenResponse),
114    Err(OidcExceptionResponse),
115}
116
117#[derive(Deserialize, Debug, Clone)]
118pub struct TokenResponse {
119    //Response body for POST /identity/token.
120
121    //The IAM access token that can be used to invoke various IBM Cloud APIs.
122    // Use this token with the prefix Bearer in the HTTP header Authorization
123    // for invocations of IAM compatible APIs.
124    access_token: String,
125
126    //(optional) A refresh token that can be used to get a new IAM access
127    // token if that token is expired. When using the default client
128    // (no basic authorization header) as described in this documentation,
129    // this refresh_token cannot be used to retrieve a new IAM access token.
130    // When the IAM access token is about to be expired, use the API key to
131    // create a new access token.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    refresh_token: Option<String>,
134
135    //(optional) A delegated refresh token that can only be consumed by
136    // the clients that have been specified in the API call as 'receiver_client_ids
137    #[serde(skip_serializing_if = "Option::is_none")]
138    delegated_refresh_token: Option<String>,
139
140    //The type of the token. Currently, only Bearer is returned.
141    token_type: String,
142
143    // Number of seconds until the IAM access token will expire.
144    expires_in: i32,
145
146    // Number of seconds counted since January 1st, 1970, until the IAM access token will expire.
147    expiration: i32,
148}
149
150impl TokenResponse {
151    pub fn get_access_token(&self) -> String {
152        self.access_token.clone()
153    }
154    pub fn get_expiration(&self) -> i32 {
155        self.expiration.clone()
156    }
157    fn validate_token(&self) -> bool {
158        let local_time = Local::now().timestamp();
159        let near_ex = self.get_expiration() as i64 - 5;
160        if local_time >= near_ex {
161            false
162        } else {
163            true
164        }
165    }
166}
167
168#[derive(Deserialize, Debug, Clone)]
169pub struct ExceptionResponseContext {
170    //Context fill with key properties for problem determination.
171
172    //The request ID of the inbound REST request.
173    #[serde(rename = "requestId")]
174    request_id: String,
175
176    //The request type of the inbound REST request.
177    #[serde(rename = "requestType")]
178    request_type: String,
179
180    //The user agent of the inbound REST request.
181    #[serde(rename = "userAgent")]
182    user_agent: String,
183
184    // The URL of that cluster.
185    url: String,
186
187    //The instance ID of the server instance processing the request.
188    #[serde(rename = "instanceId")]
189    instance_id: String,
190
191    //The thread ID of the server instance processing the request.
192    #[serde(rename = "threadId")]
193    thread_id: String,
194
195    //The host of the server instance processing the request.
196    host: String,
197
198    //The start time of the request.
199    #[serde(rename = "startTime")]
200    start_time: String,
201
202    //The finish time of the request.
203    #[serde(rename = "endTime")]
204    end_time: String,
205
206    //The elapsed time in msec.
207    #[serde(rename = "elapsedTime")]
208    elapsed_time: String,
209
210    //The language used to present the error message.
211    locale: String,
212
213    //The cluster name.
214    #[serde(rename = "clusterName")]
215    cluster_name: String,
216}
217
218#[derive(Deserialize, Debug, Clone)]
219pub struct MFARequirementsResponse {
220    //Response properties for MFA requirements.
221
222    //MFA error.
223    error: String,
224
225    //MFA Code.
226    code: String,
227
228    //MFA AuthorizationToken.
229    authorizationToken: String,
230}
231
232#[derive(Deserialize, Debug, Clone)]
233pub struct OidcExceptionResponse {
234    // Response body parameters in case of oidc error situations.
235
236    // Context fill with key properties for problem determination.
237    context: ExceptionResponseContext,
238
239    //Error message code of the REST Exception.
240    #[serde(rename = "errorCode")]
241    error_code: String,
242
243    //Error message of the REST Exception. Error messages are derived base on the input locale
244    // of the REST request and the available Message catalogs. Dynamic fallback to 'us-english'
245    // is happening if no message catalog is available for the provided input locale.
246    #[serde(rename = "errorMessage")]
247    error_message: String,
248    //Error details of the REST Exception.
249
250    //#[serde(rename = "errorDetails")]
251    //error_details: String,
252
253    //Response properties for MFA requirements.
254    //#[serde(skip_deserializing)]
255    //requirements: MFARequirementsResponse,
256}
257
258/// Helpers------------------------------------------------
259
260fn construct_headers() -> HeaderMap {
261    let mut headers = HeaderMap::new();
262    headers.insert(USER_AGENT, HeaderValue::from_static("reqwest"));
263    headers.insert(
264        CONTENT_TYPE,
265        HeaderValue::from_static("application/x-www-form-urlencoded"),
266    );
267    headers
268}
269
270fn urlencoded_parameter(token: Options) -> [(String, String); 2] {
271    let params: [(String, String); 2] = [
272        ("grant_type".to_string(), token.grant_type),
273        ("apikey".to_string(), token.apikey),
274    ];
275    params
276}
277
278#[cfg(test)]
279mod TokenApiTests {
280    use crate::authenticators::token_api::{
281        construct_headers, request_token, urlencoded_parameter, Options, ResponseType,
282        TokenResponse,
283    };
284    use chrono::Local;
285    use mockito::mock;
286    use reqwest::header::{HeaderMap, CONTENT_TYPE, USER_AGENT};
287
288    const ibm_cloud_iam_url: &str = "ibm_cloud_iam_url";
289    const api_key: &str = "apikey";
290    const GRANT_TYPE: &str = "urn:ibm:params:oauth:grant-type:apikey";
291
292    #[test]
293    fn set_urlencoded_parameter() {
294        let token: Options = Options {
295            grant_type: GRANT_TYPE.to_string(),
296            apikey: api_key.to_string(),
297        };
298        let param = urlencoded_parameter(token);
299
300        assert_eq!(param[0].0, "grant_type".to_string());
301        assert_eq!(param[0].1, GRANT_TYPE.to_string());
302        assert_eq!(param[1].0, "apikey".to_string());
303        assert_eq!(param[1].0, api_key.to_string());
304    }
305
306    #[test]
307    fn set_headers_map() {
308        let headers = construct_headers();
309
310        assert_eq!(headers.get(USER_AGENT).unwrap(), &"reqwest");
311        assert_eq!(
312            headers.get(CONTENT_TYPE).unwrap(),
313            &"application/x-www-form-urlencoded"
314        )
315    }
316
317    #[test]
318    fn tokenresponse_get_access_token() {
319        let tokenresponse = TokenResponse {
320            access_token: "Token".to_string(),
321            refresh_token: None,
322            delegated_refresh_token: None,
323            token_type: "".to_string(),
324            expires_in: 0,
325            expiration: 0,
326        };
327        assert_eq!(tokenresponse.get_access_token(), "Token".to_string())
328    }
329
330    #[test]
331    fn tokenresponse_get_expiration() {
332        let tokenresponse = TokenResponse {
333            access_token: "".to_string(),
334            refresh_token: None,
335            delegated_refresh_token: None,
336            token_type: "".to_string(),
337            expires_in: 0,
338            expiration: 169900991,
339        };
340        assert_eq!(tokenresponse.get_expiration(), 169900991)
341    }
342    #[test]
343    fn tokenresponse_get_validate_token() {
344        let invalid_token = TokenResponse {
345            access_token: "".to_string(),
346            refresh_token: None,
347            delegated_refresh_token: None,
348            token_type: "".to_string(),
349            expires_in: 0,
350            expiration: (Local::now().timestamp() - 3000) as i32,
351        };
352
353        let valid_token = TokenResponse {
354            access_token: "".to_string(),
355            refresh_token: None,
356            delegated_refresh_token: None,
357            token_type: "".to_string(),
358            expires_in: 0,
359            expiration: (Local::now().timestamp() + 3000) as i32,
360        };
361
362        assert_eq!(invalid_token.validate_token(), false);
363        assert_eq!(valid_token.validate_token(), true)
364    }
365}