reqsign_aws_core/provide_credential/
cognito.rs1use crate::Credential;
19use http::{Method, Request, StatusCode};
20use log::debug;
21use reqsign_core::time::Timestamp;
22use reqsign_core::{Context, Error, ProvideCredential, Result};
23use serde::Deserialize;
24use serde_json::json;
25
26#[derive(Debug, Clone)]
51pub struct CognitoIdentityCredentialProvider {
52 identity_pool_id: Option<String>,
53 region: Option<String>,
54 identity_id: Option<String>,
55 logins: Option<std::collections::HashMap<String, String>>,
56}
57
58impl Default for CognitoIdentityCredentialProvider {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl CognitoIdentityCredentialProvider {
65 pub fn new() -> Self {
67 Self {
68 identity_pool_id: None,
69 region: None,
70 identity_id: None,
71 logins: None,
72 }
73 }
74
75 pub fn with_identity_pool_id(mut self, pool_id: impl Into<String>) -> Self {
77 self.identity_pool_id = Some(pool_id.into());
78 self
79 }
80
81 pub fn with_region(mut self, region: impl Into<String>) -> Self {
83 self.region = Some(region.into());
84 self
85 }
86
87 pub fn with_identity_id(mut self, identity_id: impl Into<String>) -> Self {
89 self.identity_id = Some(identity_id.into());
90 self
91 }
92
93 pub fn with_logins(mut self, logins: std::collections::HashMap<String, String>) -> Self {
95 self.logins = Some(logins);
96 self
97 }
98
99 async fn get_identity_id(&self, ctx: &Context) -> Result<String> {
101 if let Some(id) = &self.identity_id {
103 return Ok(id.clone());
104 }
105 if let Some(id) = ctx.env_var("AWS_COGNITO_IDENTITY_ID") {
106 return Ok(id);
107 }
108
109 let pool_id = self
110 .identity_pool_id
111 .clone()
112 .or_else(|| ctx.env_var("AWS_COGNITO_IDENTITY_POOL_ID"))
113 .ok_or_else(|| Error::config_invalid("identity_pool_id is required".to_string()))?;
114
115 let region = self
116 .region
117 .clone()
118 .or_else(|| ctx.env_var("AWS_REGION"))
119 .or_else(|| ctx.env_var("AWS_DEFAULT_REGION"))
120 .ok_or_else(|| Error::config_invalid("region is required".to_string()))?;
121
122 let endpoint = ctx
124 .env_var("AWS_COGNITO_ENDPOINT")
125 .unwrap_or_else(|| format!("https://cognito-identity.{region}.amazonaws.com/"));
126
127 let body = if let Some(logins) = &self.logins {
128 json!({
129 "IdentityPoolId": pool_id,
130 "Logins": logins
131 })
132 } else {
133 json!({
134 "IdentityPoolId": pool_id
135 })
136 };
137
138 let req = Request::builder()
139 .method(Method::POST)
140 .uri(&endpoint)
141 .header("x-amz-target", "AWSCognitoIdentityService.GetId")
142 .header("content-type", "application/x-amz-json-1.1")
143 .body(bytes::Bytes::from(serde_json::to_vec(&body).map_err(
144 |e| Error::unexpected(format!("failed to serialize request body: {e}")),
145 )?))
146 .map_err(|e| Error::unexpected(format!("failed to build request: {e}")))?;
147
148 let resp = ctx
149 .http_send(req)
150 .await
151 .map_err(|e| Error::unexpected(format!("failed to get identity ID: {e}")))?;
152
153 if resp.status() != StatusCode::OK {
154 return Err(Error::unexpected(format!(
155 "Cognito GetId returned status: {}",
156 resp.status()
157 )));
158 }
159
160 let body = resp.into_body();
161 let result: GetIdResponse = serde_json::from_slice(&body)
162 .map_err(|e| Error::unexpected(format!("failed to parse GetId response: {e}")))?;
163
164 Ok(result.identity_id)
165 }
166
167 async fn get_credentials_for_identity(
169 &self,
170 ctx: &Context,
171 identity_id: &str,
172 ) -> Result<Credential> {
173 let region = self
174 .region
175 .clone()
176 .or_else(|| ctx.env_var("AWS_REGION"))
177 .or_else(|| ctx.env_var("AWS_DEFAULT_REGION"))
178 .ok_or_else(|| Error::config_invalid("region is required".to_string()))?;
179
180 let endpoint = ctx
182 .env_var("AWS_COGNITO_ENDPOINT")
183 .unwrap_or_else(|| format!("https://cognito-identity.{region}.amazonaws.com/"));
184
185 let body = if let Some(logins) = &self.logins {
186 json!({
187 "IdentityId": identity_id,
188 "Logins": logins
189 })
190 } else {
191 json!({
192 "IdentityId": identity_id
193 })
194 };
195
196 let req = Request::builder()
197 .method(Method::POST)
198 .uri(&endpoint)
199 .header(
200 "x-amz-target",
201 "AWSCognitoIdentityService.GetCredentialsForIdentity",
202 )
203 .header("content-type", "application/x-amz-json-1.1")
204 .body(bytes::Bytes::from(serde_json::to_vec(&body).map_err(
205 |e| Error::unexpected(format!("failed to serialize request body: {e}")),
206 )?))
207 .map_err(|e| Error::unexpected(format!("failed to build request: {e}")))?;
208
209 let resp = ctx
210 .http_send(req)
211 .await
212 .map_err(|e| Error::unexpected(format!("failed to get credentials: {e}")))?;
213
214 if resp.status() != StatusCode::OK {
215 return Err(Error::unexpected(format!(
216 "Cognito GetCredentialsForIdentity returned status: {}",
217 resp.status()
218 )));
219 }
220
221 let body = resp.into_body();
222 let result: GetCredentialsResponse = serde_json::from_slice(&body)
223 .map_err(|e| Error::unexpected(format!("failed to parse credentials response: {e}")))?;
224
225 let creds = result.credentials;
226 let expires_in = Timestamp::from_second(creds.expiration)
227 .map_err(|e| Error::unexpected(format!("invalid expiration date: {e}")))?;
228
229 Ok(Credential {
230 access_key_id: creds.access_key_id,
231 secret_access_key: creds.secret_key,
232 session_token: Some(creds.session_token),
233 expires_in: Some(expires_in),
234 })
235 }
236}
237
238#[derive(Debug, Deserialize)]
239#[serde(rename_all = "PascalCase")]
240struct GetIdResponse {
241 identity_id: String,
242}
243
244#[derive(Debug, Deserialize)]
245#[serde(rename_all = "PascalCase")]
246struct GetCredentialsResponse {
247 credentials: CognitoCredentials,
248 #[allow(dead_code)]
249 identity_id: String,
250}
251
252#[derive(Debug, Deserialize)]
253#[serde(rename_all = "PascalCase")]
254struct CognitoCredentials {
255 access_key_id: String,
256 secret_key: String,
257 session_token: String,
258 expiration: i64,
259}
260impl ProvideCredential for CognitoIdentityCredentialProvider {
261 type Credential = Credential;
262
263 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
264 let has_pool_id = self.identity_pool_id.is_some()
266 || ctx.env_var("AWS_COGNITO_IDENTITY_POOL_ID").is_some();
267
268 if !has_pool_id {
269 debug!("Cognito Identity: no identity pool ID configured");
270 return Ok(None);
271 }
272
273 let identity_id = self.get_identity_id(ctx).await?;
275 debug!("Cognito Identity: using identity ID: {identity_id}");
276
277 let creds = self.get_credentials_for_identity(ctx, &identity_id).await?;
279
280 Ok(Some(creds))
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use reqsign_file_read_tokio::TokioFileRead;
288 use reqsign_http_send_reqwest::ReqwestHttpSend;
289
290 #[tokio::test]
291 async fn test_cognito_provider_no_config() {
292 let ctx = Context::new()
293 .with_file_read(TokioFileRead)
294 .with_http_send(ReqwestHttpSend::default());
295 let provider = CognitoIdentityCredentialProvider::new();
296 let result = provider.provide_credential(&ctx).await.unwrap();
297 assert!(result.is_none());
298 }
299
300 #[test]
301 fn test_cognito_provider_builder() {
302 let provider = CognitoIdentityCredentialProvider::new()
303 .with_identity_pool_id("us-east-1:12345678-1234-1234-1234-123456789012")
304 .with_region("us-east-1");
305
306 assert_eq!(
307 provider.identity_pool_id,
308 Some("us-east-1:12345678-1234-1234-1234-123456789012".to_string())
309 );
310 assert_eq!(provider.region, Some("us-east-1".to_string()));
311 }
312}