ibmcloud_core/authenticators/
token_api.rs1use 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(¶ms)
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 pub(crate) grant_type: String,
96
97 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 access_token: String,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
133 refresh_token: Option<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 delegated_refresh_token: Option<String>,
139
140 token_type: String,
142
143 expires_in: i32,
145
146 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 #[serde(rename = "requestId")]
174 request_id: String,
175
176 #[serde(rename = "requestType")]
178 request_type: String,
179
180 #[serde(rename = "userAgent")]
182 user_agent: String,
183
184 url: String,
186
187 #[serde(rename = "instanceId")]
189 instance_id: String,
190
191 #[serde(rename = "threadId")]
193 thread_id: String,
194
195 host: String,
197
198 #[serde(rename = "startTime")]
200 start_time: String,
201
202 #[serde(rename = "endTime")]
204 end_time: String,
205
206 #[serde(rename = "elapsedTime")]
208 elapsed_time: String,
209
210 locale: String,
212
213 #[serde(rename = "clusterName")]
215 cluster_name: String,
216}
217
218#[derive(Deserialize, Debug, Clone)]
219pub struct MFARequirementsResponse {
220 error: String,
224
225 code: String,
227
228 authorizationToken: String,
230}
231
232#[derive(Deserialize, Debug, Clone)]
233pub struct OidcExceptionResponse {
234 context: ExceptionResponseContext,
238
239 #[serde(rename = "errorCode")]
241 error_code: String,
242
243 #[serde(rename = "errorMessage")]
247 error_message: String,
248 }
257
258fn 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}