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 } ;
@@ -426,72 +426,68 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
426426 let testctx = & cptestctx. external_client ;
427427
428428 // Set silo max TTL to 10 seconds
429- let _settings: views:: SiloSettings = object_put (
430- testctx,
431- "/v1/settings" ,
432- & params:: SiloSettingsUpdate {
433- device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
434- } ,
435- )
436- . await ;
437-
438- let client_id = Uuid :: new_v4 ( ) ;
429+ let settings = params:: SiloSettingsUpdate {
430+ device_token_max_ttl_seconds : Nullable ( NonZeroU32 :: new ( 10 ) ) ,
431+ } ;
432+ let _: views:: SiloSettings =
433+ object_put ( testctx, "/v1/settings" , & settings) . await ;
439434
440435 // Test 1: Request TTL above the max should fail at verification time
441- let authn_params_invalid = DeviceAuthRequest {
442- client_id,
443- ttl_seconds : Some ( 20 ) // Above the 10 second max
436+ let invalid_ttl = DeviceAuthRequest {
437+ client_id : Uuid :: new_v4 ( ) ,
438+ ttl_seconds : Some ( 20 ) , // Above the 10 second max
444439 } ;
445440
446- let auth_response: DeviceAuthResponse =
441+ let auth_response = NexusRequest :: new (
447442 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
448- . allow_non_dropshot_errors ( )
449- . body_urlencoded ( Some ( & authn_params_invalid) )
450- . expect_status ( Some ( StatusCode :: OK ) )
451- . execute ( )
452- . await
453- . expect ( "failed to start client authentication flow" )
454- . parsed_body ( )
455- . expect ( "client authentication response" ) ;
443+ . body_urlencoded ( Some ( & invalid_ttl) )
444+ . expect_status ( Some ( StatusCode :: OK ) ) ,
445+ )
446+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
447+ . await ;
456448
457- let confirm_params = DeviceAuthVerify { user_code : auth_response. user_code } ;
449+ let confirm_params =
450+ DeviceAuthVerify { user_code : auth_response. user_code } ;
458451
459- // Confirmation should fail because requested TTL exceeds max
460- let confirm_response = NexusRequest :: new (
452+ // Confirmation fails because requested TTL exceeds max
453+ let confirm_error = NexusRequest :: new (
461454 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
462455 . body ( Some ( & confirm_params) )
463456 . expect_status ( Some ( StatusCode :: BAD_REQUEST ) ) ,
464457 )
465458 . authn_as ( AuthnMode :: PrivilegedUser )
466- . execute ( )
467- . await
468- . expect ( "confirmation should fail for TTL above max" ) ;
459+ . execute_and_parse_unwrap :: < HttpErrorResponseBody > ( )
460+ . await ;
469461
470462 // Check that the error message mentions TTL
471- let error_body = String :: from_utf8_lossy ( & confirm_response. body ) ;
472- assert ! ( error_body. contains( "TTL" ) || error_body. contains( "ttl" ) ) ;
463+ assert_eq ! ( confirm_error. error_code, Some ( "InvalidRequest" . to_string( ) ) ) ;
464+ assert_eq ! (
465+ confirm_error. message,
466+ "Requested TTL 20 seconds exceeds maximum allowed TTL 10 seconds for this silo"
467+ ) ;
473468
474469 // Test 2: Request TTL below the max should succeed and be used
475- let authn_params_valid = DeviceAuthRequest {
476- client_id : Uuid :: new_v4 ( ) , // New client ID for new flow
477- ttl_seconds : Some ( 5 ) // Below the 10 second max
470+ let valid_ttl = DeviceAuthRequest {
471+ client_id : Uuid :: new_v4 ( ) ,
472+ ttl_seconds : Some ( 3 ) , // Below the 10 second max
478473 } ;
479474
480- let auth_response: DeviceAuthResponse =
475+ let auth_response = NexusRequest :: new (
481476 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
482- . allow_non_dropshot_errors ( )
483- . body_urlencoded ( Some ( & authn_params_valid) )
484- . expect_status ( Some ( StatusCode :: OK ) )
485- . execute ( )
486- . await
487- . expect ( "failed to start client authentication flow" )
488- . parsed_body ( )
489- . expect ( "client authentication response" ) ;
477+ . body_urlencoded ( Some ( & valid_ttl) )
478+ . expect_status ( Some ( StatusCode :: OK ) ) ,
479+ )
480+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
481+ . await ;
490482
491483 let device_code = auth_response. device_code ;
492484 let user_code = auth_response. user_code ;
493485 let confirm_params = DeviceAuthVerify { user_code } ;
494486
487+ // this time will be pretty close to the now() used on the server when
488+ // calculating expiration time
489+ let t0 = Utc :: now ( ) ;
490+
495491 // Confirmation should succeed
496492 NexusRequest :: new (
497493 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
@@ -506,44 +502,39 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
506502 let token_params = DeviceAccessTokenRequest {
507503 grant_type : "urn:ietf:params:oauth:grant-type:device_code" . to_string ( ) ,
508504 device_code,
509- client_id : authn_params_valid . client_id ,
505+ client_id : valid_ttl . client_id ,
510506 } ;
511507
512508 // Get the token
513- let token : DeviceAccessTokenGrant = NexusRequest :: new (
509+ let token_grant = NexusRequest :: new (
514510 RequestBuilder :: new ( testctx, Method :: POST , "/device/token" )
515511 . allow_non_dropshot_errors ( )
516512 . body_urlencoded ( Some ( & token_params) )
517513 . expect_status ( Some ( StatusCode :: OK ) ) ,
518514 )
519515 . authn_as ( AuthnMode :: PrivilegedUser )
520- . execute ( )
521- . await
522- . expect ( "failed to get token" )
523- . parsed_body ( )
524- . expect ( "failed to deserialize token response" ) ;
516+ . execute_and_parse_unwrap :: < DeviceAccessTokenGrant > ( )
517+ . await ;
525518
526- // Verify the token has the correct expiration (5 seconds from now)
519+ // Verify the token has roughly the correct expiration time. One second
520+ // threshold is sufficient to confirm it's not getting the silo max of 10
521+ // seconds. Locally, I saw diffs as low as 14ms.
527522 let tokens = get_tokens_priv ( testctx) . await ;
528- let our_token = tokens. iter ( ) . find ( |t| t. time_expires . is_some ( ) ) . unwrap ( ) ;
529- let expires_at = our_token. time_expires . unwrap ( ) ;
530- let now = Utc :: now ( ) ;
531-
532- // Should expire approximately 5 seconds from now (allow some tolerance for test timing)
533- let expected_expiry = now + chrono:: Duration :: seconds ( 5 ) ;
534- let time_diff = ( expires_at - expected_expiry) . num_seconds ( ) . abs ( ) ;
535- assert ! ( time_diff <= 2 , "Token expiry should be close to requested TTL" ) ;
523+ let time_expires = tokens[ 0 ] . time_expires . unwrap ( ) ;
524+ let expected_expires = t0 + Duration :: from_secs ( 3 ) ;
525+ let diff_ms = ( time_expires - expected_expires) . num_milliseconds ( ) . abs ( ) ;
526+ assert ! ( diff_ms <= 1000 , "time diff was {diff_ms} ms. should be near zero" ) ;
536527
537528 // Token should work initially
538- project_list ( & testctx, & token . access_token , StatusCode :: OK )
529+ project_list ( & testctx, & token_grant . access_token , StatusCode :: OK )
539530 . await
540531 . expect ( "token should work initially" ) ;
541532
542533 // Wait for token to expire
543- sleep ( Duration :: from_secs ( 6 ) ) . await ;
534+ sleep ( Duration :: from_secs ( 4 ) ) . await ;
544535
545- // Token should be expired now
546- project_list ( & testctx, & token . access_token , StatusCode :: UNAUTHORIZED )
536+ // Token is expired
537+ project_list ( & testctx, & token_grant . access_token , StatusCode :: UNAUTHORIZED )
547538 . await
548539 . expect ( "token should be expired" ) ;
549540}
0 commit comments