@@ -2,6 +2,7 @@ use std::io::{IsTerminal, Read};
22use std:: time:: Duration ;
33
44use chrono:: Utc ;
5+ use tracing:: warn;
56use zeroize:: Zeroizing ;
67
78use awsenc_core:: cache:: {
@@ -53,8 +54,15 @@ pub async fn run_auth(
5354 } ;
5455
5556 let creds = obtain_credentials ( & okta, & session_token, & resolved) . await ?;
57+ let okta_session = match okta. create_session ( & session_token) . await {
58+ Ok ( session) => Some ( session) ,
59+ Err ( err) => {
60+ warn ! ( "failed to create reusable Okta session: {err}" ) ;
61+ None
62+ }
63+ } ;
5664
57- encrypt_and_cache ( profile, storage, & creds, & session_token ) ?;
65+ encrypt_and_cache ( profile, storage, & creds, okta_session . as_ref ( ) ) ?;
5866 usage:: record_usage ( profile) ;
5967
6068 let remaining = creds
@@ -104,6 +112,7 @@ async fn obtain_credentials(
104112 session_token : & Zeroizing < String > ,
105113 resolved : & config:: ResolvedConfig ,
106114) -> Result < awsenc_core:: credential:: AwsCredentials > {
115+ ensure_supported_secondary_role ( resolved) ?;
107116 eprintln ! ( "Getting SAML assertion..." ) ;
108117 let saml_assertion = okta
109118 . get_saml_assertion ( session_token, & resolved. okta_application )
@@ -136,33 +145,45 @@ async fn obtain_credentials(
136145 Ok ( creds)
137146}
138147
148+ fn ensure_supported_secondary_role ( resolved : & config:: ResolvedConfig ) -> Result < ( ) > {
149+ if let Some ( role) = resolved. secondary_role . as_deref ( ) {
150+ return Err ( format ! (
151+ "secondary_role '{role}' is configured but chained role assumption is not supported yet"
152+ )
153+ . into ( ) ) ;
154+ }
155+ Ok ( ( ) )
156+ }
157+
139158#[ allow( clippy:: cast_sign_loss) ]
140159fn encrypt_and_cache (
141160 profile : & str ,
142161 storage : & dyn EncryptionStorage ,
143162 creds : & awsenc_core:: credential:: AwsCredentials ,
144- session_token : & Zeroizing < String > ,
163+ okta_session : Option < & OktaSession > ,
145164) -> Result < ( ) > {
146165 let creds_json = serde_json:: to_vec ( creds) ?;
147166 let aws_ciphertext = storage. encrypt ( & creds_json) ?;
148167
149- let okta_session = OktaSession {
150- session_id : session_token. as_str ( ) . to_owned ( ) ,
151- expiration : Utc :: now ( ) + chrono:: Duration :: hours ( 2 ) ,
152- } ;
153- let okta_json = serde_json:: to_vec ( & okta_session) ?;
154- let okta_ciphertext = storage. encrypt ( & okta_json) ?;
155-
156168 let cache_file = CacheFile {
157169 header : CacheHeader {
158170 magic : MAGIC ,
159171 version : FORMAT_VERSION ,
160- flags : FLAG_HAS_OKTA_SESSION ,
172+ flags : if okta_session. is_some ( ) {
173+ FLAG_HAS_OKTA_SESSION
174+ } else {
175+ 0
176+ } ,
161177 credential_expiration : creds. expiration . timestamp ( ) as u64 ,
162- okta_session_expiration : okta_session. expiration . timestamp ( ) as u64 ,
178+ okta_session_expiration : okta_session
179+ . map_or ( 0 , |session| session. expiration . timestamp ( ) as u64 ) ,
163180 } ,
164181 aws_ciphertext,
165- okta_session_ciphertext : Some ( okta_ciphertext) ,
182+ okta_session_ciphertext : okta_session
183+ . map ( serde_json:: to_vec)
184+ . transpose ( ) ?
185+ . map ( |json| storage. encrypt ( & json) )
186+ . transpose ( ) ?,
166187 } ;
167188
168189 cache:: write_cache ( profile, & cache_file) ?;
@@ -290,17 +311,16 @@ mod tests {
290311 session_token : Zeroizing :: new ( "sessiontoken456" . to_string ( ) ) ,
291312 expiration : Utc :: now ( ) + chrono:: Duration :: hours ( 1 ) ,
292313 } ;
293- let session_token = Zeroizing :: new ( "okta-session-token" . to_string ( ) ) ;
314+ let okta_session = OktaSession {
315+ session_id : "okta-session-id" . to_string ( ) ,
316+ expiration : Utc :: now ( ) + chrono:: Duration :: hours ( 2 ) ,
317+ } ;
294318
295319 // Test encrypt_and_cache by verifying it constructs correct structures
296320 // without relying on file I/O (which depends on HOME env var)
297321 let creds_json = serde_json:: to_vec ( & creds) . unwrap ( ) ;
298322 let aws_ciphertext = storage. encrypt ( & creds_json) . unwrap ( ) ;
299323
300- let okta_session = OktaSession {
301- session_id : session_token. as_str ( ) . to_owned ( ) ,
302- expiration : Utc :: now ( ) + chrono:: Duration :: hours ( 2 ) ,
303- } ;
304324 let okta_json = serde_json:: to_vec ( & okta_session) . unwrap ( ) ;
305325 let okta_ciphertext = storage. encrypt ( & okta_json) . unwrap ( ) ;
306326
@@ -313,7 +333,7 @@ mod tests {
313333 // Verify Okta session can be decrypted
314334 let okta_plaintext = storage. decrypt ( & okta_ciphertext) . unwrap ( ) ;
315335 let recovered_session: OktaSession = serde_json:: from_slice ( & okta_plaintext) . unwrap ( ) ;
316- assert_eq ! ( recovered_session. session_id, "okta-session-token " ) ;
336+ assert_eq ! ( recovered_session. session_id, "okta-session-id " ) ;
317337
318338 // Verify CacheFile structure
319339 #[ allow( clippy:: cast_sign_loss) ]
@@ -333,6 +353,36 @@ mod tests {
333353 assert ! ( cache_file. header. has_okta_session( ) ) ;
334354 }
335355
356+ #[ test]
357+ fn encrypt_and_cache_without_okta_session_clears_session_flag ( ) {
358+ use enclaveapp_app_storage:: mock:: MockEncryptionStorage as MockStorage ;
359+
360+ let storage = MockStorage :: new ( ) ;
361+ let creds = awsenc_core:: credential:: AwsCredentials {
362+ access_key_id : "AKIATEST" . to_string ( ) ,
363+ secret_access_key : Zeroizing :: new ( "secretkey123" . to_string ( ) ) ,
364+ session_token : Zeroizing :: new ( "sessiontoken456" . to_string ( ) ) ,
365+ expiration : Utc :: now ( ) + chrono:: Duration :: hours ( 1 ) ,
366+ } ;
367+
368+ let creds_json = serde_json:: to_vec ( & creds) . unwrap ( ) ;
369+ let aws_ciphertext = storage. encrypt ( & creds_json) . unwrap ( ) ;
370+ let cache_file = CacheFile {
371+ header : CacheHeader {
372+ magic : MAGIC ,
373+ version : FORMAT_VERSION ,
374+ flags : 0 ,
375+ credential_expiration : creds. expiration . timestamp ( ) as u64 ,
376+ okta_session_expiration : 0 ,
377+ } ,
378+ aws_ciphertext,
379+ okta_session_ciphertext : None ,
380+ } ;
381+
382+ assert_eq ! ( cache_file. header. flags, 0 ) ;
383+ assert ! ( !cache_file. header. has_okta_session( ) ) ;
384+ }
385+
336386 #[ test]
337387 fn resolved_profile_positional_preferred ( ) {
338388 let args = AuthArgs {
@@ -358,4 +408,23 @@ mod tests {
358408 let args = default_auth_args ( ) ;
359409 assert_eq ! ( args. resolved_profile( ) , None ) ;
360410 }
411+
412+ #[ test]
413+ fn secondary_role_configuration_fails_fast ( ) {
414+ let resolved = config:: ResolvedConfig {
415+ okta_organization : "org.okta.com" . into ( ) ,
416+ okta_user : "user@example.com" . into ( ) ,
417+ okta_application : "https://org.okta.com/app" . into ( ) ,
418+ okta_role : "arn:aws:iam::123:role/Primary" . into ( ) ,
419+ okta_factor : "push" . into ( ) ,
420+ okta_duration : 3600 ,
421+ biometric : false ,
422+ refresh_window_seconds : 600 ,
423+ secondary_role : Some ( "arn:aws:iam::456:role/Secondary" . into ( ) ) ,
424+ region : None ,
425+ } ;
426+
427+ let err = ensure_supported_secondary_role ( & resolved) . unwrap_err ( ) ;
428+ assert ! ( err. to_string( ) . contains( "secondary_role" ) ) ;
429+ }
361430}
0 commit comments