55use std:: num:: NonZeroU32 ;
66
77use chrono:: Utc ;
8- use dropshot:: ResultsPage ;
98use dropshot:: test_util:: ClientTestContext ;
9+ use dropshot:: { HttpErrorResponseBody , ResultsPage } ;
1010use nexus_auth:: authn:: USER_TEST_UNPRIVILEGED ;
1111use nexus_db_queries:: db:: fixed_data:: silo:: DEFAULT_SILO ;
1212use nexus_db_queries:: db:: identity:: { Asset , Resource } ;
@@ -398,72 +398,68 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
398398 let testctx = & cptestctx. external_client ;
399399
400400 // Set silo max TTL to 10 seconds
401- let _settings: views:: SiloSettings = object_put (
402- testctx,
403- "/v1/settings" ,
404- & params:: SiloSettingsUpdate {
405- device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
406- } ,
407- )
408- . await ;
409-
410- let client_id = Uuid :: new_v4 ( ) ;
401+ let settings = params:: SiloSettingsUpdate {
402+ device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
403+ } ;
404+ let _: views:: SiloSettings =
405+ object_put ( testctx, "/v1/settings" , & settings) . await ;
411406
412407 // Test 1: Request TTL above the max should fail at verification time
413- let authn_params_invalid = DeviceAuthRequest {
414- client_id,
415- ttl_seconds : Some ( 20 ) // Above the 10 second max
408+ let invalid_ttl = DeviceAuthRequest {
409+ client_id : Uuid :: new_v4 ( ) ,
410+ ttl_seconds : Some ( 20 ) , // Above the 10 second max
416411 } ;
417412
418- let auth_response: DeviceAuthResponse =
413+ let auth_response = NexusRequest :: new (
419414 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
420- . allow_non_dropshot_errors ( )
421- . body_urlencoded ( Some ( & authn_params_invalid) )
422- . expect_status ( Some ( StatusCode :: OK ) )
423- . execute ( )
424- . await
425- . expect ( "failed to start client authentication flow" )
426- . parsed_body ( )
427- . expect ( "client authentication response" ) ;
415+ . body_urlencoded ( Some ( & invalid_ttl) )
416+ . expect_status ( Some ( StatusCode :: OK ) ) ,
417+ )
418+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
419+ . await ;
428420
429- let confirm_params = DeviceAuthVerify { user_code : auth_response. user_code } ;
421+ let confirm_params =
422+ DeviceAuthVerify { user_code : auth_response. user_code } ;
430423
431- // Confirmation should fail because requested TTL exceeds max
432- let confirm_response = NexusRequest :: new (
424+ // Confirmation fails because requested TTL exceeds max
425+ let confirm_error = NexusRequest :: new (
433426 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
434427 . body ( Some ( & confirm_params) )
435428 . expect_status ( Some ( StatusCode :: BAD_REQUEST ) ) ,
436429 )
437430 . authn_as ( AuthnMode :: PrivilegedUser )
438- . execute ( )
439- . await
440- . expect ( "confirmation should fail for TTL above max" ) ;
431+ . execute_and_parse_unwrap :: < HttpErrorResponseBody > ( )
432+ . await ;
441433
442434 // Check that the error message mentions TTL
443- let error_body = String :: from_utf8_lossy ( & confirm_response. body ) ;
444- assert ! ( error_body. contains( "TTL" ) || error_body. contains( "ttl" ) ) ;
435+ assert_eq ! ( confirm_error. error_code, Some ( "InvalidRequest" . to_string( ) ) ) ;
436+ assert_eq ! (
437+ confirm_error. message,
438+ "Requested TTL 20 seconds exceeds maximum allowed TTL 10 seconds for this silo"
439+ ) ;
445440
446441 // Test 2: Request TTL below the max should succeed and be used
447- let authn_params_valid = DeviceAuthRequest {
448- client_id : Uuid :: new_v4 ( ) , // New client ID for new flow
449- ttl_seconds : Some ( 5 ) // Below the 10 second max
442+ let valid_ttl = DeviceAuthRequest {
443+ client_id : Uuid :: new_v4 ( ) ,
444+ ttl_seconds : Some ( 3 ) , // Below the 10 second max
450445 } ;
451446
452- let auth_response: DeviceAuthResponse =
447+ let auth_response = NexusRequest :: new (
453448 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
454- . allow_non_dropshot_errors ( )
455- . body_urlencoded ( Some ( & authn_params_valid) )
456- . expect_status ( Some ( StatusCode :: OK ) )
457- . execute ( )
458- . await
459- . expect ( "failed to start client authentication flow" )
460- . parsed_body ( )
461- . expect ( "client authentication response" ) ;
449+ . body_urlencoded ( Some ( & valid_ttl) )
450+ . expect_status ( Some ( StatusCode :: OK ) ) ,
451+ )
452+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
453+ . await ;
462454
463455 let device_code = auth_response. device_code ;
464456 let user_code = auth_response. user_code ;
465457 let confirm_params = DeviceAuthVerify { user_code } ;
466458
459+ // this time will be pretty close to the now() used on the server when
460+ // calculating expiration time
461+ let t0 = Utc :: now ( ) ;
462+
467463 // Confirmation should succeed
468464 NexusRequest :: new (
469465 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
@@ -478,44 +474,39 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
478474 let token_params = DeviceAccessTokenRequest {
479475 grant_type : "urn:ietf:params:oauth:grant-type:device_code" . to_string ( ) ,
480476 device_code,
481- client_id : authn_params_valid . client_id ,
477+ client_id : valid_ttl . client_id ,
482478 } ;
483479
484480 // Get the token
485- let token : DeviceAccessTokenGrant = NexusRequest :: new (
481+ let token_grant = NexusRequest :: new (
486482 RequestBuilder :: new ( testctx, Method :: POST , "/device/token" )
487483 . allow_non_dropshot_errors ( )
488484 . body_urlencoded ( Some ( & token_params) )
489485 . expect_status ( Some ( StatusCode :: OK ) ) ,
490486 )
491487 . authn_as ( AuthnMode :: PrivilegedUser )
492- . execute ( )
493- . await
494- . expect ( "failed to get token" )
495- . parsed_body ( )
496- . expect ( "failed to deserialize token response" ) ;
488+ . execute_and_parse_unwrap :: < DeviceAccessTokenGrant > ( )
489+ . await ;
497490
498- // Verify the token has the correct expiration (5 seconds from now)
491+ // Verify the token has roughly the correct expiration time. One second
492+ // threshold is sufficient to confirm it's not getting the silo max of 10
493+ // seconds. Locally, I saw diffs as low as 14ms.
499494 let tokens = get_tokens_priv ( testctx) . await ;
500- let our_token = tokens. iter ( ) . find ( |t| t. time_expires . is_some ( ) ) . unwrap ( ) ;
501- let expires_at = our_token. time_expires . unwrap ( ) ;
502- let now = Utc :: now ( ) ;
503-
504- // Should expire approximately 5 seconds from now (allow some tolerance for test timing)
505- let expected_expiry = now + chrono:: Duration :: seconds ( 5 ) ;
506- let time_diff = ( expires_at - expected_expiry) . num_seconds ( ) . abs ( ) ;
507- assert ! ( time_diff <= 2 , "Token expiry should be close to requested TTL" ) ;
495+ let time_expires = tokens[ 0 ] . time_expires . unwrap ( ) ;
496+ let expected_expires = t0 + Duration :: from_secs ( 3 ) ;
497+ let diff_ms = ( time_expires - expected_expires) . num_milliseconds ( ) . abs ( ) ;
498+ assert ! ( diff_ms <= 1000 , "time diff was {diff_ms} ms. should be near zero" ) ;
508499
509500 // Token should work initially
510- project_list ( & testctx, & token . access_token , StatusCode :: OK )
501+ project_list ( & testctx, & token_grant . access_token , StatusCode :: OK )
511502 . await
512503 . expect ( "token should work initially" ) ;
513504
514505 // Wait for token to expire
515- sleep ( Duration :: from_secs ( 6 ) ) . await ;
506+ sleep ( Duration :: from_secs ( 4 ) ) . await ;
516507
517- // Token should be expired now
518- project_list ( & testctx, & token . access_token , StatusCode :: UNAUTHORIZED )
508+ // Token is expired
509+ project_list ( & testctx, & token_grant . access_token , StatusCode :: UNAUTHORIZED )
519510 . await
520511 . expect ( "token should be expired" ) ;
521512}
0 commit comments