aws_sdk_rds/
auth_token.rs1use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider};
12use aws_sigv4::http_request;
13use aws_sigv4::http_request::{SignableBody, SignableRequest, SigningSettings};
14use aws_sigv4::sign::v4;
15use aws_smithy_runtime_api::box_error::BoxError;
16use aws_smithy_runtime_api::client::identity::Identity;
17use aws_types::region::Region;
18use std::fmt;
19use std::fmt::Debug;
20use std::time::Duration;
21
22const ACTION: &str = "connect";
23const SERVICE: &str = "rds-db";
24
25#[derive(Debug)]
48pub struct AuthTokenGenerator {
49 config: Config,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct AuthToken {
57 inner: String,
58}
59
60impl AuthToken {
61 #[must_use]
63 pub fn as_str(&self) -> &str {
64 &self.inner
65 }
66}
67
68impl fmt::Display for AuthToken {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}", self.inner)
71 }
72}
73
74impl AuthTokenGenerator {
75 pub fn new(config: Config) -> Self {
77 Self { config }
78 }
79
80 pub async fn auth_token(&self, config: &aws_types::sdk_config::SdkConfig) -> Result<AuthToken, BoxError> {
82 let credentials = self
83 .config
84 .credentials()
85 .or(config.credentials_provider())
86 .ok_or("credentials are required to create a signed URL for RDS")?
87 .provide_credentials()
88 .await?;
89 let identity: Identity = credentials.into();
90 let region = self
91 .config
92 .region()
93 .or(config.region())
94 .cloned()
95 .unwrap_or_else(|| Region::new("us-east-1"));
96 let time = config.time_source().ok_or("a time source is required")?;
97
98 let mut signing_settings = SigningSettings::default();
99 signing_settings.expires_in = Some(Duration::from_secs(self.config.expires_in().unwrap_or(900).min(900)));
100 signing_settings.signature_location = http_request::SignatureLocation::QueryParams;
101
102 let signing_params = v4::SigningParams::builder()
103 .identity(&identity)
104 .region(region.as_ref())
105 .name(SERVICE)
106 .time(time.now())
107 .settings(signing_settings)
108 .build()?;
109
110 let url = format!(
111 "https://{}:{}/?Action={}&DBUser={}",
112 self.config.hostname(),
113 self.config.port(),
114 ACTION,
115 self.config.username()
116 );
117 let signable_request = SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::empty()).expect("signable request");
118
119 let (signing_instructions, _signature) = http_request::sign(signable_request, &signing_params.into())?.into_parts();
120
121 let mut url = url::Url::parse(&url).unwrap();
122 for (name, value) in signing_instructions.params() {
123 url.query_pairs_mut().append_pair(name, value);
124 }
125 let inner = url.to_string().split_off("https://".len());
126
127 Ok(AuthToken { inner })
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct Config {
134 credentials: Option<SharedCredentialsProvider>,
138
139 hostname: String,
141
142 port: u64,
144
145 region: Option<Region>,
147
148 username: String,
150
151 expires_in: Option<u64>,
155}
156
157impl Config {
158 pub fn builder() -> ConfigBuilder {
160 ConfigBuilder::default()
161 }
162
163 pub fn credentials(&self) -> Option<SharedCredentialsProvider> {
165 self.credentials.clone()
166 }
167
168 pub fn hostname(&self) -> &str {
170 &self.hostname
171 }
172
173 pub fn port(&self) -> u64 {
175 self.port
176 }
177
178 pub fn region(&self) -> Option<&Region> {
180 self.region.as_ref()
181 }
182
183 pub fn username(&self) -> &str {
185 &self.username
186 }
187
188 pub fn expires_in(&self) -> Option<u64> {
192 self.expires_in
193 }
194}
195
196#[derive(Debug, Default)]
198pub struct ConfigBuilder {
199 credentials: Option<SharedCredentialsProvider>,
203
204 hostname: Option<String>,
206
207 port: Option<u64>,
209
210 region: Option<Region>,
212
213 username: Option<String>,
215
216 expires_in: Option<u64>,
218}
219
220impl ConfigBuilder {
221 pub fn credentials(mut self, credentials: impl ProvideCredentials + 'static) -> Self {
225 self.credentials = Some(SharedCredentialsProvider::new(credentials));
226 self
227 }
228
229 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
231 self.hostname = Some(hostname.into());
232 self
233 }
234
235 pub fn port(mut self, port: u64) -> Self {
237 self.port = Some(port);
238 self
239 }
240
241 pub fn region(mut self, region: Region) -> Self {
243 self.region = Some(region);
244 self
245 }
246
247 pub fn username(mut self, username: impl Into<String>) -> Self {
249 self.username = Some(username.into());
250 self
251 }
252
253 pub fn expires_in(mut self, expires_in: u64) -> Self {
257 self.expires_in = Some(expires_in);
258 self
259 }
260
261 pub fn build(self) -> Result<Config, BoxError> {
264 Ok(Config {
265 credentials: self.credentials,
266 hostname: self.hostname.ok_or("A hostname is required")?,
267 port: self.port.ok_or("a port is required")?,
268 region: self.region,
269 username: self.username.ok_or("a username is required")?,
270 expires_in: self.expires_in,
271 })
272 }
273}
274
275#[cfg(test)]
276mod test {
277 use super::{AuthTokenGenerator, Config};
278 use aws_credential_types::provider::SharedCredentialsProvider;
279 use aws_credential_types::Credentials;
280 use aws_smithy_async::test_util::ManualTimeSource;
281 use aws_types::region::Region;
282 use aws_types::SdkConfig;
283 use std::time::{Duration, UNIX_EPOCH};
284
285 #[tokio::test]
286 async fn signing_works() {
287 let time_source = ManualTimeSource::new(UNIX_EPOCH + Duration::from_secs(1724709600));
288 let sdk_config = SdkConfig::builder()
289 .credentials_provider(SharedCredentialsProvider::new(Credentials::new("akid", "secret", None, None, "test")))
290 .time_source(time_source)
291 .build();
292 let signer = AuthTokenGenerator::new(
293 Config::builder()
294 .hostname("prod-instance.us-east-1.rds.amazonaws.com")
295 .port(3306)
296 .region(Region::new("us-east-1"))
297 .username("peccy")
298 .build()
299 .unwrap(),
300 );
301
302 let signed_url = signer.auth_token(&sdk_config).await.unwrap();
303 assert_eq!(signed_url.as_str(), "prod-instance.us-east-1.rds.amazonaws.com:3306/?Action=connect&DBUser=peccy&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=akid%2F20240826%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20240826T220000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=dd0cba843009474347af724090233265628ace491ea17ce3eb3da098b983ad89");
304 }
305}