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 } ;
@@ -55,6 +55,8 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {
5555 . expect ( "failed to reject device auth start without client_id" ) ;
5656
5757 let client_id = Uuid :: new_v4 ( ) ;
58+ // note that this exercises ttl_seconds being omitted from the body because
59+ // it's URL encoded, so None means it's omitted
5860 let authn_params = DeviceAuthRequest { client_id, ttl_seconds : None } ;
5961
6062 // Using a JSON encoded body fails.
@@ -436,72 +438,68 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
436438 let testctx = & cptestctx. external_client ;
437439
438440 // Set silo max TTL to 10 seconds
439- let _settings: views:: SiloSettings = object_put (
440- testctx,
441- "/v1/settings" ,
442- & params:: SiloSettingsUpdate {
443- device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) ,
444- } ,
445- )
446- . await ;
447-
448- let client_id = Uuid :: new_v4 ( ) ;
441+ let settings = params:: SiloAuthSettingsUpdate {
442+ device_token_max_ttl_seconds : NonZeroU32 :: new ( 10 ) . into ( ) ,
443+ } ;
444+ let _: views:: SiloAuthSettings =
445+ object_put ( testctx, "/v1/auth-settings" , & settings) . await ;
449446
450447 // Test 1: Request TTL above the max should fail at verification time
451- let authn_params_invalid = DeviceAuthRequest {
452- client_id,
453- ttl_seconds : Some ( 20 ) // Above the 10 second max
448+ let invalid_ttl = DeviceAuthRequest {
449+ client_id : Uuid :: new_v4 ( ) ,
450+ ttl_seconds : Some ( 20 ) , // Above the 10 second max
454451 } ;
455452
456- let auth_response: DeviceAuthResponse =
453+ let auth_response = NexusRequest :: new (
457454 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
458- . allow_non_dropshot_errors ( )
459- . body_urlencoded ( Some ( & authn_params_invalid) )
460- . expect_status ( Some ( StatusCode :: OK ) )
461- . execute ( )
462- . await
463- . expect ( "failed to start client authentication flow" )
464- . parsed_body ( )
465- . expect ( "client authentication response" ) ;
455+ . body_urlencoded ( Some ( & invalid_ttl) )
456+ . expect_status ( Some ( StatusCode :: OK ) ) ,
457+ )
458+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
459+ . await ;
466460
467- let confirm_params = DeviceAuthVerify { user_code : auth_response. user_code } ;
461+ let confirm_params =
462+ DeviceAuthVerify { user_code : auth_response. user_code } ;
468463
469- // Confirmation should fail because requested TTL exceeds max
470- let confirm_response = NexusRequest :: new (
464+ // Confirmation fails because requested TTL exceeds max
465+ let confirm_error = NexusRequest :: new (
471466 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
472467 . body ( Some ( & confirm_params) )
473468 . expect_status ( Some ( StatusCode :: BAD_REQUEST ) ) ,
474469 )
475470 . authn_as ( AuthnMode :: PrivilegedUser )
476- . execute ( )
477- . await
478- . expect ( "confirmation should fail for TTL above max" ) ;
471+ . execute_and_parse_unwrap :: < HttpErrorResponseBody > ( )
472+ . await ;
479473
480474 // Check that the error message mentions TTL
481- let error_body = String :: from_utf8_lossy ( & confirm_response. body ) ;
482- assert ! ( error_body. contains( "TTL" ) || error_body. contains( "ttl" ) ) ;
475+ assert_eq ! ( confirm_error. error_code, Some ( "InvalidRequest" . to_string( ) ) ) ;
476+ assert_eq ! (
477+ confirm_error. message,
478+ "Requested TTL 20 seconds exceeds maximum allowed TTL 10 seconds for this silo"
479+ ) ;
483480
484481 // Test 2: Request TTL below the max should succeed and be used
485- let authn_params_valid = DeviceAuthRequest {
486- client_id : Uuid :: new_v4 ( ) , // New client ID for new flow
487- ttl_seconds : Some ( 5 ) // Below the 10 second max
482+ let valid_ttl = DeviceAuthRequest {
483+ client_id : Uuid :: new_v4 ( ) ,
484+ ttl_seconds : Some ( 3 ) , // Below the 10 second max
488485 } ;
489486
490- let auth_response: DeviceAuthResponse =
487+ let auth_response = NexusRequest :: new (
491488 RequestBuilder :: new ( testctx, Method :: POST , "/device/auth" )
492- . allow_non_dropshot_errors ( )
493- . body_urlencoded ( Some ( & authn_params_valid) )
494- . expect_status ( Some ( StatusCode :: OK ) )
495- . execute ( )
496- . await
497- . expect ( "failed to start client authentication flow" )
498- . parsed_body ( )
499- . expect ( "client authentication response" ) ;
489+ . body_urlencoded ( Some ( & valid_ttl) )
490+ . expect_status ( Some ( StatusCode :: OK ) ) ,
491+ )
492+ . execute_and_parse_unwrap :: < DeviceAuthResponse > ( )
493+ . await ;
500494
501495 let device_code = auth_response. device_code ;
502496 let user_code = auth_response. user_code ;
503497 let confirm_params = DeviceAuthVerify { user_code } ;
504498
499+ // this time will be pretty close to the now() used on the server when
500+ // calculating expiration time
501+ let t0 = Utc :: now ( ) ;
502+
505503 // Confirmation should succeed
506504 NexusRequest :: new (
507505 RequestBuilder :: new ( testctx, Method :: POST , "/device/confirm" )
@@ -516,44 +514,39 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
516514 let token_params = DeviceAccessTokenRequest {
517515 grant_type : "urn:ietf:params:oauth:grant-type:device_code" . to_string ( ) ,
518516 device_code,
519- client_id : authn_params_valid . client_id ,
517+ client_id : valid_ttl . client_id ,
520518 } ;
521519
522520 // Get the token
523- let token : DeviceAccessTokenGrant = NexusRequest :: new (
521+ let token_grant = NexusRequest :: new (
524522 RequestBuilder :: new ( testctx, Method :: POST , "/device/token" )
525523 . allow_non_dropshot_errors ( )
526524 . body_urlencoded ( Some ( & token_params) )
527525 . expect_status ( Some ( StatusCode :: OK ) ) ,
528526 )
529527 . authn_as ( AuthnMode :: PrivilegedUser )
530- . execute ( )
531- . await
532- . expect ( "failed to get token" )
533- . parsed_body ( )
534- . expect ( "failed to deserialize token response" ) ;
528+ . execute_and_parse_unwrap :: < DeviceAccessTokenGrant > ( )
529+ . await ;
535530
536- // Verify the token has the correct expiration (5 seconds from now)
531+ // Verify the token has roughly the correct expiration time. One second
532+ // threshold is sufficient to confirm it's not getting the silo max of 10
533+ // seconds. Locally, I saw diffs as low as 14ms.
537534 let tokens = get_tokens_priv ( testctx) . await ;
538- let our_token = tokens. iter ( ) . find ( |t| t. time_expires . is_some ( ) ) . unwrap ( ) ;
539- let expires_at = our_token. time_expires . unwrap ( ) ;
540- let now = Utc :: now ( ) ;
541-
542- // Should expire approximately 5 seconds from now (allow some tolerance for test timing)
543- let expected_expiry = now + chrono:: Duration :: seconds ( 5 ) ;
544- let time_diff = ( expires_at - expected_expiry) . num_seconds ( ) . abs ( ) ;
545- assert ! ( time_diff <= 2 , "Token expiry should be close to requested TTL" ) ;
535+ let time_expires = tokens[ 0 ] . time_expires . unwrap ( ) ;
536+ let expected_expires = t0 + Duration :: from_secs ( 3 ) ;
537+ let diff_ms = ( time_expires - expected_expires) . num_milliseconds ( ) . abs ( ) ;
538+ assert ! ( diff_ms <= 1000 , "time diff was {diff_ms} ms. should be near zero" ) ;
546539
547540 // Token should work initially
548- project_list ( & testctx, & token . access_token , StatusCode :: OK )
541+ project_list ( & testctx, & token_grant . access_token , StatusCode :: OK )
549542 . await
550543 . expect ( "token should work initially" ) ;
551544
552545 // Wait for token to expire
553- sleep ( Duration :: from_secs ( 6 ) ) . await ;
546+ sleep ( Duration :: from_secs ( 4 ) ) . await ;
554547
555- // Token should be expired now
556- project_list ( & testctx, & token . access_token , StatusCode :: UNAUTHORIZED )
548+ // Token is expired
549+ project_list ( & testctx, & token_grant . access_token , StatusCode :: UNAUTHORIZED )
557550 . await
558551 . expect ( "token should be expired" ) ;
559552}
0 commit comments