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 } ;
@@ -390,72 +390,68 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
390390 let testctx = & cptestctx. external_client ;
391391
392392 // Set silo max TTL to 10 seconds
393- let _settings: views:: SiloSettings = object_put (
394- testctx,
395- "/v1/settings" ,
396- & params:: SiloSettingsUpdate {
397- device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
398- } ,
399- )
400- . await ;
401-
402- let client_id = Uuid :: new_v4 ( ) ;
393+ let settings = params:: SiloSettingsUpdate {
394+ device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
395+ } ;
396+ let _: views:: SiloSettings =
397+ object_put ( testctx, "/v1/settings" , & settings) . await ;
403398
404399 // Test 1: Request TTL above the max should fail at verification time
405- let authn_params_invalid = DeviceAuthRequest {
406- client_id,
407- ttl_seconds : Some ( 20 ) // Above the 10 second max
400+ let invalid_ttl = DeviceAuthRequest {
401+ client_id : Uuid :: new_v4 ( ) ,
402+ ttl_seconds : Some ( 20 ) , // Above the 10 second max
408403 } ;
409404
410- let auth_response: DeviceAuthResponse =
405+ let auth_response = NexusRequest :: new (
411406 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
412- . allow_non_dropshot_errors ( )
413- . body_urlencoded ( Some ( & authn_params_invalid) )
414- . expect_status ( Some ( StatusCode :: OK ) )
415- . execute ( )
416- . await
417- . expect ( "failed to start client authentication flow" )
418- . parsed_body ( )
419- . expect ( "client authentication response" ) ;
407+ . body_urlencoded ( Some ( & invalid_ttl) )
408+ . expect_status ( Some ( StatusCode :: OK ) ) ,
409+ )
410+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
411+ . await ;
420412
421- let confirm_params = DeviceAuthVerify { user_code : auth_response. user_code } ;
413+ let confirm_params =
414+ DeviceAuthVerify { user_code : auth_response. user_code } ;
422415
423- // Confirmation should fail because requested TTL exceeds max
424- let confirm_response = NexusRequest :: new (
416+ // Confirmation fails because requested TTL exceeds max
417+ let confirm_error = NexusRequest :: new (
425418 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
426419 . body ( Some ( & confirm_params) )
427420 . expect_status ( Some ( StatusCode :: BAD_REQUEST ) ) ,
428421 )
429422 . authn_as ( AuthnMode :: PrivilegedUser )
430- . execute ( )
431- . await
432- . expect ( "confirmation should fail for TTL above max" ) ;
423+ . execute_and_parse_unwrap :: < HttpErrorResponseBody > ( )
424+ . await ;
433425
434426 // Check that the error message mentions TTL
435- let error_body = String :: from_utf8_lossy ( & confirm_response. body ) ;
436- assert ! ( error_body. contains( "TTL" ) || error_body. contains( "ttl" ) ) ;
427+ assert_eq ! ( confirm_error. error_code, Some ( "InvalidRequest" . to_string( ) ) ) ;
428+ assert_eq ! (
429+ confirm_error. message,
430+ "Requested TTL 20 seconds exceeds maximum allowed TTL 10 seconds for this silo"
431+ ) ;
437432
438433 // Test 2: Request TTL below the max should succeed and be used
439- let authn_params_valid = DeviceAuthRequest {
440- client_id : Uuid :: new_v4 ( ) , // New client ID for new flow
441- ttl_seconds : Some ( 5 ) // Below the 10 second max
434+ let valid_ttl = DeviceAuthRequest {
435+ client_id : Uuid :: new_v4 ( ) ,
436+ ttl_seconds : Some ( 3 ) , // Below the 10 second max
442437 } ;
443438
444- let auth_response: DeviceAuthResponse =
439+ let auth_response = NexusRequest :: new (
445440 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
446- . allow_non_dropshot_errors ( )
447- . body_urlencoded ( Some ( & authn_params_valid) )
448- . expect_status ( Some ( StatusCode :: OK ) )
449- . execute ( )
450- . await
451- . expect ( "failed to start client authentication flow" )
452- . parsed_body ( )
453- . expect ( "client authentication response" ) ;
441+ . body_urlencoded ( Some ( & valid_ttl) )
442+ . expect_status ( Some ( StatusCode :: OK ) ) ,
443+ )
444+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
445+ . await ;
454446
455447 let device_code = auth_response. device_code ;
456448 let user_code = auth_response. user_code ;
457449 let confirm_params = DeviceAuthVerify { user_code } ;
458450
451+ // this time will be pretty close to the now() used on the server when
452+ // calculating expiration time
453+ let t0 = Utc :: now ( ) ;
454+
459455 // Confirmation should succeed
460456 NexusRequest :: new (
461457 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
@@ -470,44 +466,39 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
470466 let token_params = DeviceAccessTokenRequest {
471467 grant_type : "urn:ietf:params:oauth:grant-type:device_code" . to_string ( ) ,
472468 device_code,
473- client_id : authn_params_valid . client_id ,
469+ client_id : valid_ttl . client_id ,
474470 } ;
475471
476472 // Get the token
477- let token : DeviceAccessTokenGrant = NexusRequest :: new (
473+ let token_grant = NexusRequest :: new (
478474 RequestBuilder :: new ( testctx, Method :: POST , "/device/token" )
479475 . allow_non_dropshot_errors ( )
480476 . body_urlencoded ( Some ( & token_params) )
481477 . expect_status ( Some ( StatusCode :: OK ) ) ,
482478 )
483479 . authn_as ( AuthnMode :: PrivilegedUser )
484- . execute ( )
485- . await
486- . expect ( "failed to get token" )
487- . parsed_body ( )
488- . expect ( "failed to deserialize token response" ) ;
480+ . execute_and_parse_unwrap :: < DeviceAccessTokenGrant > ( )
481+ . await ;
489482
490- // Verify the token has the correct expiration (5 seconds from now)
483+ // Verify the token has roughly the correct expiration time. One second
484+ // threshold is sufficient to confirm it's not getting the silo max of 10
485+ // seconds. Locally, I saw diffs as low as 14ms.
491486 let tokens = get_tokens_priv ( testctx) . await ;
492- let our_token = tokens. iter ( ) . find ( |t| t. time_expires . is_some ( ) ) . unwrap ( ) ;
493- let expires_at = our_token. time_expires . unwrap ( ) ;
494- let now = Utc :: now ( ) ;
495-
496- // Should expire approximately 5 seconds from now (allow some tolerance for test timing)
497- let expected_expiry = now + chrono:: Duration :: seconds ( 5 ) ;
498- let time_diff = ( expires_at - expected_expiry) . num_seconds ( ) . abs ( ) ;
499- assert ! ( time_diff <= 2 , "Token expiry should be close to requested TTL" ) ;
487+ let time_expires = tokens[ 0 ] . time_expires . unwrap ( ) ;
488+ let expected_expires = t0 + Duration :: from_secs ( 3 ) ;
489+ let diff_ms = ( time_expires - expected_expires) . num_milliseconds ( ) . abs ( ) ;
490+ assert ! ( diff_ms <= 1000 , "time diff was {diff_ms} ms. should be near zero" ) ;
500491
501492 // Token should work initially
502- project_list ( & testctx, & token . access_token , StatusCode :: OK )
493+ project_list ( & testctx, & token_grant . access_token , StatusCode :: OK )
503494 . await
504495 . expect ( "token should work initially" ) ;
505496
506497 // Wait for token to expire
507- sleep ( Duration :: from_secs ( 6 ) ) . await ;
498+ sleep ( Duration :: from_secs ( 4 ) ) . await ;
508499
509- // Token should be expired now
510- project_list ( & testctx, & token . access_token , StatusCode :: UNAUTHORIZED )
500+ // Token is expired
501+ project_list ( & testctx, & token_grant . access_token , StatusCode :: UNAUTHORIZED )
511502 . await
512503 . expect ( "token should be expired" ) ;
513504}
0 commit comments