diff --git a/nexus/auth/src/authn/external/session_cookie.rs b/nexus/auth/src/authn/external/session_cookie.rs index 2b9cd7c325b..c096b7ab294 100644 --- a/nexus/auth/src/authn/external/session_cookie.rs +++ b/nexus/auth/src/authn/external/session_cookie.rs @@ -396,13 +396,7 @@ mod test { }]), }; let result = authn_with_cookie(&context, Some("session=abc")).await; - assert!(matches!( - result, - SchemeResult::Authenticated(Details { - actor: _, - device_token_expiration: _ - }) - )); + assert!(matches!(result, SchemeResult::Authenticated(Details { .. }))); // valid cookie should have updated time_last_used let sessions = context.sessions.lock().unwrap(); diff --git a/nexus/src/app/device_auth.rs b/nexus/src/app/device_auth.rs index 138121675c9..3fc6bf627cf 100644 --- a/nexus/src/app/device_auth.rs +++ b/nexus/src/app/device_auth.rs @@ -142,32 +142,32 @@ impl super::Nexus { // token is being used) // Validate the requested TTL against the silo's max TTL - if let Some(max) = silo_max_ttl { - if requested_ttl > max.0.into() { - return Err(Error::invalid_request(&format!( - "Requested TTL {} seconds exceeds maximum allowed \ - TTL for this silo of {} seconds", - requested_ttl, max - ))); - } + if let Some(max) = silo_max_ttl + && requested_ttl > max.0.into() + { + return Err(Error::invalid_request(&format!( + "Requested TTL {} seconds exceeds maximum allowed \ + TTL for this silo of {} seconds", + requested_ttl, max + ))); }; let requested_exp = Utc::now() + Duration::seconds(requested_ttl.0.into()); // If currently authenticated via token, error if requested exceeds it - if let Some(auth_exp) = opctx.authn.device_token_expiration() { - if requested_exp > auth_exp { - return Err(Error::invalid_request( - "Requested token TTL would exceed the expiration time \ - of the token being used to authenticate the confirm \ - request. To get the full requested TTL, confirm \ - this token using a web console session. Alternatively, \ - omit requested TTL to get a token with the longest \ - allowed lifetime, determined by the lesser of the silo \ - max and the current token's expiration time.", - )); - } + if let Some(auth_exp) = opctx.authn.device_token_expiration() + && requested_exp > auth_exp + { + return Err(Error::invalid_request( + "Requested token TTL would exceed the expiration time \ + of the token being used to authenticate the confirm \ + request. To get the full requested TTL, confirm \ + this token using a web console session. Alternatively, \ + omit requested TTL to get a token with the longest \ + allowed lifetime, determined by the lesser of the silo \ + max and the current token's expiration time.", + )); } Some(requested_exp) diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index 7544d30b941..61bacf99fe3 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -549,6 +549,7 @@ pub enum AuthnMode { PrivilegedUser, SiloUser(SiloUserUuid), Session(String), + DeviceToken(String), } impl AuthnMode { @@ -579,6 +580,10 @@ impl AuthnMode { let header_value = format!("session={}", session_token); parse_header_pair(http::header::COOKIE, header_value) } + AuthnMode::DeviceToken(token) => { + let header_value = format!("Bearer {}", token); + parse_header_pair(http::header::AUTHORIZATION, header_value) + } } } } diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 54ce471d07c..b40b2aa1e46 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -1316,20 +1316,17 @@ pub async fn create_session_for_user( .to_string() } -/// Log in with test suite password, return session cookie (formatted for Cookie -/// header) +/// Log in with test suite password. Returns session token. pub async fn create_console_session( cptestctx: &ControlPlaneTestContext, ) -> String { - let token = create_session_for_user( + create_session_for_user( &cptestctx.external_client, cptestctx.silo_name.as_str(), cptestctx.user_name.as_ref(), TEST_SUITE_PASSWORD, ) - .await; - - format!("session={}", token) + .await } #[derive(Debug)] diff --git a/nexus/tests/integration_tests/audit_log.rs b/nexus/tests/integration_tests/audit_log.rs index 4577f75827c..72ed115cf1a 100644 --- a/nexus/tests/integration_tests/audit_log.rs +++ b/nexus/tests/integration_tests/audit_log.rs @@ -61,7 +61,8 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) { assert_eq!(audit_log.items.len(), 1); // this this creates its own entry - let session_cookie = create_console_session(ctx).await; + let session_cookie = + format!("session={}", create_console_session(ctx).await); let t3 = Utc::now(); // after second entry diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index 8e3375b1a8b..760af15717e 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -52,7 +52,8 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { .expect("failed to clear cookie and 204 on logout"); // log in and pull the token out of the header so we can use it for authed requests - let session_token = create_console_session(cptestctx).await; + let session_cookie = + format!("session={}", create_console_session(cptestctx).await); let project_params = ProjectCreate { identity: IdentityMetadataCreateParams { @@ -101,7 +102,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { // now make same requests with cookie RequestBuilder::new(&testctx, Method::POST, "/v1/projects") - .header(header::COOKIE, &session_token) + .header(header::COOKIE, &session_cookie) .body(Some(&project_params)) // TODO: explicit expect_status not needed. decide whether to keep it anyway .expect_status(Some(StatusCode::CREATED)) @@ -110,7 +111,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { .expect("failed to create org with session cookie"); RequestBuilder::new(&testctx, Method::GET, "/projects/whatever") - .header(header::COOKIE, &session_token) + .header(header::COOKIE, &session_cookie) .expect_console_asset() .execute() .await @@ -124,7 +125,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { // logout with an actual session should delete the session in the db RequestBuilder::new(&testctx, Method::POST, "/v1/logout") - .header(header::COOKIE, &session_token) + .header(header::COOKIE, &session_cookie) .expect_status(Some(StatusCode::NO_CONTENT)) // logout also clears the cookie client-side .expect_response_header( @@ -151,7 +152,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { // now the same requests with the same session cookie should 401/302 because // logout also deletes the session server-side RequestBuilder::new(&testctx, Method::POST, "/v1/projects") - .header(header::COOKIE, &session_token) + .header(header::COOKIE, &session_cookie) .body(Some(&project_params)) .expect_status(Some(StatusCode::UNAUTHORIZED)) .execute() @@ -159,7 +160,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) { .expect("failed to get 401 for unauthed API request"); RequestBuilder::new(&testctx, Method::GET, "/projects/whatever") - .header(header::COOKIE, &session_token) + .header(header::COOKIE, &session_cookie) .expect_status(Some(StatusCode::FOUND)) .execute() .await @@ -173,8 +174,9 @@ async fn expect_console_page( ) { let mut builder = RequestBuilder::new(testctx, Method::GET, path); - if let Some(session_token) = session_token { - builder = builder.header(http::header::COOKIE, &session_token) + if let Some(token) = session_token { + builder = + builder.header(http::header::COOKIE, &format!("session={token}")) } let console_page = builder @@ -954,13 +956,13 @@ async fn test_session_idle_timeout_deletes_session() { let testctx = &cptestctx.external_client; // Start session - let session_cookie = create_console_session(&cptestctx).await; + let session_token = create_console_session(&cptestctx).await; // sleep here not necessary given TTL of 0 // Make a request with the expired session cookie let me_response = RequestBuilder::new(testctx, Method::GET, "/v1/me") - .header(header::COOKIE, &session_cookie) + .header(header::COOKIE, &format!("session={}", session_token)) .expect_status(Some(StatusCode::UNAUTHORIZED)) .execute() .await @@ -977,10 +979,9 @@ async fn test_session_idle_timeout_deletes_session() { let opctx = OpContext::for_tests(cptestctx.logctx.log.new(o!()), datastore.clone()); - let token = session_cookie.strip_prefix("session=").unwrap(); let db_token_error = nexus .datastore() - .session_lookup_by_token(&opctx, token.to_string()) + .session_lookup_by_token(&opctx, session_token) .await .expect_err("session should be deleted"); assert_matches::assert_matches!( diff --git a/nexus/tests/integration_tests/device_auth.rs b/nexus/tests/integration_tests/device_auth.rs index 5e8f2230e59..99d38fec1a5 100644 --- a/nexus/tests/integration_tests/device_auth.rs +++ b/nexus/tests/integration_tests/device_auth.rs @@ -633,199 +633,130 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) { .expect("token should be expired"); } +/// Verifies that device tokens created without specifying TTL are automatically +/// clamped to the authenticating token's expiration time. #[nexus_test] -async fn test_device_token_cannot_extend_expiration( +async fn test_device_token_clamps_to_auth_token_when_no_ttl_specified( cptestctx: &ControlPlaneTestContext, ) { let testctx = &cptestctx.external_client; - // get silo belonging to privileged user to make sure local user for session - // testing is created in the same silo - let me = object_get::(testctx, "/v1/me").await; - let silo_name = me.silo_name.as_str(); - // Set silo max TTL to 15 seconds - let settings = params::SiloAuthSettingsUpdate { - device_token_max_ttl_seconds: NonZeroU32::new(15).into(), - }; - let _: views::SiloAuthSettings = - object_put(testctx, "/v1/auth-settings", &settings).await; - - // First, test that session auth does NOT clamp token TTL - // Create a local user and get a session token - let silo_url = format!("/v1/system/silos/{silo_name}"); - let test_silo: views::Silo = object_get(testctx, &silo_url).await; - let _test_user = create_local_user( - testctx, - &test_silo, - &"session-test-user".parse().unwrap(), - test_params::UserPassword::Password("test-password".to_string()), - ) - .await; - - let session_token = create_session_for_user( + let _: views::SiloAuthSettings = object_put( testctx, - silo_name, - "session-test-user", - "test-password", + "/v1/auth-settings", + ¶ms::SiloAuthSettingsUpdate { + device_token_max_ttl_seconds: NonZeroU32::new(15).into(), + }, ) .await; - // we can use the same client_id for everything because the device codes differentiate let client_id = Uuid::new_v4(); - // Test session auth with explicit TTL = silo max (15 seconds) - let session_request_with_ttl = - DeviceAuthRequest { client_id, ttl_seconds: NonZeroU32::new(15) }; - - let session_auth_response = NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/auth") - .body_urlencoded(Some(&session_request_with_ttl)) - .expect_status(Some(StatusCode::OK)), - ) - .execute_and_parse_unwrap::() - .await; - - let session_time = Utc::now(); - - NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/confirm") - .body(Some(&DeviceAuthVerify { - user_code: session_auth_response.user_code, - })) - .expect_status(Some(StatusCode::NO_CONTENT)), - ) - .authn_as(AuthnMode::Session(session_token.clone())) - .execute() - .await - .expect("failed to confirm with session auth"); + // Create an initial token with 8 second TTL + let (initial_token_grant, initial_confirm_time) = + create_and_confirm_device_auth( + testctx, + client_id, + NonZeroU32::new(8), + AuthnMode::PrivilegedUser, + ) + .await; - let session_token_grant = fetch_device_token( - testctx, - session_auth_response.device_code, - client_id, - AuthnMode::Session(session_token.clone()), - ) - .await; + let initial_token = initial_token_grant.access_token; + let initial_expiration = initial_token_grant.time_expires.unwrap(); - // Verify session-authenticated token gets full silo max (15 seconds) - let session_expiration = session_token_grant.time_expires.unwrap(); - let session_ttl_secs = (session_expiration - session_time).num_seconds(); + // Verify initial token expires in roughly 8 seconds + let initial_ttl_secs = + (initial_expiration - initial_confirm_time).num_seconds(); assert!( - 14 <= session_ttl_secs && session_ttl_secs <= 16, - "should be full silo max (~15 seconds), got {session_ttl_secs}" + 7 <= initial_ttl_secs && initial_ttl_secs <= 9, + "initial token should expire in ~8 seconds, got {initial_ttl_secs}" ); - // Test session auth with no TTL specified, gets silo max - let session_request_no_ttl = - DeviceAuthRequest { client_id, ttl_seconds: None }; - - let session_auth_response2 = NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/auth") - .body_urlencoded(Some(&session_request_no_ttl)) - .expect_status(Some(StatusCode::OK)), - ) - .execute_and_parse_unwrap::() - .await; - - let session_time2 = Utc::now(); - - NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/confirm") - .body(Some(&DeviceAuthVerify { - user_code: session_auth_response2.user_code, - })) - .expect_status(Some(StatusCode::NO_CONTENT)), - ) - .authn_as(AuthnMode::Session(session_token.clone())) - .execute() - .await - .expect("failed to confirm with session auth (no TTL)"); - - let session_token_grant2 = fetch_device_token( + // Create a new token WITHOUT specifying TTL, using the initial token as auth. + // This should succeed with the new token's expiration clamped to the auth token's expiration. + let (clamped_token_grant, _) = create_and_confirm_device_auth( testctx, - session_auth_response2.device_code, client_id, - AuthnMode::Session(session_token), + None, // No TTL specified + AuthnMode::DeviceToken(initial_token), ) .await; - // When no TTL is specified, token still gets silo max expiration - let session_expiration2 = session_token_grant2.time_expires.unwrap(); - let session_ttl_secs2 = (session_expiration2 - session_time2).num_seconds(); + let clamped_expiration = clamped_token_grant.time_expires.unwrap(); + + // The new token should be clamped to the initial token's expiration + let time_diff_ms = + (clamped_expiration - initial_expiration).num_milliseconds().abs(); + assert!( - 14 <= session_ttl_secs2 && session_ttl_secs2 <= 16, - "should be silo max (~15 seconds), got {session_ttl_secs2}" + time_diff_ms <= 1000, + "clamped token expiration should match initial token expiration. \ + Initial: {initial_expiration}, Clamped: {clamped_expiration}, \ + Diff: {time_diff_ms}ms" ); +} - // Now test device token auth with clamping - // Create an initial token with 8 second TTL - let client_id = Uuid::new_v4(); - let initial_request = - DeviceAuthRequest { client_id, ttl_seconds: NonZeroU32::new(8) }; +/// Verifies that device tokens created with explicit TTL exceeding the authenticating +/// token's remaining lifetime are rejected with a 400 error. +#[nexus_test] +async fn test_device_token_cannot_exceed_auth_token_expiration( + cptestctx: &ControlPlaneTestContext, +) { + let testctx = &cptestctx.external_client; - let auth_response_1 = NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/auth") - .body_urlencoded(Some(&initial_request)) - .expect_status(Some(StatusCode::OK)), + // Set silo max TTL to 15 seconds + let _: views::SiloAuthSettings = object_put( + testctx, + "/v1/auth-settings", + ¶ms::SiloAuthSettingsUpdate { + device_token_max_ttl_seconds: NonZeroU32::new(15).into(), + }, ) - .execute_and_parse_unwrap::() .await; - let initial_time = Utc::now(); - - // Confirm with privileged user to create the token - NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/confirm") - .body(Some(&DeviceAuthVerify { - user_code: auth_response_1.user_code, - })) - .expect_status(Some(StatusCode::NO_CONTENT)), - ) - .authn_as(AuthnMode::PrivilegedUser) - .execute() - .await - .expect("failed to confirm initial token"); + let client_id = Uuid::new_v4(); - // Fetch the initial token - let initial_token_grant = fetch_device_token( - testctx, - auth_response_1.device_code, - client_id, - AuthnMode::PrivilegedUser, - ) - .await; + // Create an initial token with 8 second TTL + let (initial_token_grant, initial_confirm_time) = + create_and_confirm_device_auth( + testctx, + client_id, + NonZeroU32::new(8), + AuthnMode::PrivilegedUser, + ) + .await; let initial_token = initial_token_grant.access_token; let initial_expiration = initial_token_grant.time_expires.unwrap(); // Verify initial token expires in roughly 8 seconds - let initial_ttl_secs = (initial_expiration - initial_time).num_seconds(); + let initial_ttl_secs = + (initial_expiration - initial_confirm_time).num_seconds(); assert!( 7 <= initial_ttl_secs && initial_ttl_secs <= 9, "initial token should expire in ~8 seconds, got {initial_ttl_secs}" ); - // Now use the initial token to authenticate and start a NEW device auth flow - // Request a token with 12 second TTL (which is less than silo max of 15) - // This should ERROR because 12 seconds exceeds the ~8 seconds remaining on the auth token - let second_request = + // Try to create a new token with 12 second TTL using the initial token as auth. + // This should fail because 12 seconds exceeds the ~8 seconds remaining on the auth token. + let exceeding_ttl_request = DeviceAuthRequest { client_id, ttl_seconds: NonZeroU32::new(12) }; - let auth_response_2 = NexusRequest::new( + let auth_response = NexusRequest::new( RequestBuilder::new(testctx, Method::POST, "/device/auth") - .body_urlencoded(Some(&second_request)) + .body_urlencoded(Some(&exceeding_ttl_request)) .expect_status(Some(StatusCode::OK)), ) .execute_and_parse_unwrap::() .await; - // Attempting to confirm with the initial token should FAIL because the requested - // TTL exceeds the auth token's expiration + // Attempting to confirm with the initial token should fail let error = NexusRequest::new( RequestBuilder::new(testctx, Method::POST, "/device/confirm") .body(Some(&DeviceAuthVerify { - user_code: auth_response_2.user_code, + user_code: auth_response.user_code, })) .header(header::AUTHORIZATION, format!("Bearer {initial_token}")) .expect_status(Some(StatusCode::BAD_REQUEST)), @@ -834,58 +765,86 @@ async fn test_device_token_cannot_extend_expiration( .await; assert!(error.message.contains("Requested token TTL would exceed")); +} - // Now start a third flow WITHOUT specifying a TTL - // This should succeed with clamping to the auth token expiration - let third_request = DeviceAuthRequest { client_id, ttl_seconds: None }; +/// Verifies that tokens confirmed with session-based authentication do NOT +/// have clamped TTLs. Session tokens allow the full silo max TTL to be used +/// regardless of session expiration. +#[nexus_test] +async fn test_session_auth_does_not_clamp_device_token_ttl( + cptestctx: &ControlPlaneTestContext, +) { + let testctx = &cptestctx.external_client; - let auth_response_3 = NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/auth") - .body_urlencoded(Some(&third_request)) - .expect_status(Some(StatusCode::OK)), + // Get the silo for the privileged user + let me = object_get::(testctx, "/v1/me").await; + let silo_name = me.silo_name.as_str(); + + // Set silo max TTL to 15 seconds + let _: views::SiloAuthSettings = object_put( + testctx, + "/v1/auth-settings", + ¶ms::SiloAuthSettingsUpdate { + device_token_max_ttl_seconds: NonZeroU32::new(15).into(), + }, ) - .execute_and_parse_unwrap::() .await; - // Confirm using the initial token - NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/confirm") - .body(Some(&DeviceAuthVerify { - user_code: auth_response_3.user_code, - })) - .header(header::AUTHORIZATION, format!("Bearer {initial_token}")) - .expect_status(Some(StatusCode::NO_CONTENT)), + // Create a local user and get a session token + let silo_url = format!("/v1/system/silos/{silo_name}"); + let test_silo: views::Silo = object_get(testctx, &silo_url).await; + let _test_user = create_local_user( + testctx, + &test_silo, + &"session-test-user".parse().unwrap(), + test_params::UserPassword::Password("test-password".to_string()), ) - .execute() - .await - .expect("failed to confirm third token"); + .await; - // Fetch the third token (no auth needed, just retrieving what was created at confirm time) - let third_token_grant = NexusRequest::new( - RequestBuilder::new(testctx, Method::POST, "/device/token") - .allow_non_dropshot_errors() - .body_urlencoded(Some(&DeviceAccessTokenRequest { - grant_type: "urn:ietf:params:oauth:grant-type:device_code" - .to_string(), - device_code: auth_response_3.device_code, - client_id, - })) - .expect_status(Some(StatusCode::OK)), + let session_token = create_session_for_user( + testctx, + silo_name, + "session-test-user", + "test-password", ) - .execute_and_parse_unwrap::() .await; - let third_expiration = third_token_grant.time_expires.unwrap(); + let client_id = Uuid::new_v4(); - // The third token should be clamped to the initial token's expiration - let time_diff_ms = - (third_expiration - initial_expiration).num_milliseconds().abs(); + // Test 1: Session auth with explicit TTL = silo max (15 seconds) + let (token_with_ttl, confirm_time_with_ttl) = + create_and_confirm_device_auth( + testctx, + client_id, + NonZeroU32::new(15), + AuthnMode::Session(session_token.clone()), + ) + .await; + // Verify token gets full silo max (15 seconds), not clamped by session + let expiration_with_ttl = token_with_ttl.time_expires.unwrap(); + let ttl_secs = (expiration_with_ttl - confirm_time_with_ttl).num_seconds(); assert!( - time_diff_ms <= 1000, - "third token expiration should be clamped to initial token expiration. \ - Initial: {initial_expiration}, Third: {third_expiration}, \ - Diff: {time_diff_ms}ms" + 14 <= ttl_secs && ttl_secs <= 16, + "should be full silo max (~15 seconds), got {ttl_secs}" + ); + + // Test 2: Session auth with no TTL specified - should also get silo max + let (token_no_ttl, confirm_time_no_ttl) = create_and_confirm_device_auth( + testctx, + client_id, + None, + AuthnMode::Session(session_token), + ) + .await; + + // When no TTL is specified with session auth, token still gets full silo max + let expiration_no_ttl = token_no_ttl.time_expires.unwrap(); + let ttl_secs_no_ttl = + (expiration_no_ttl - confirm_time_no_ttl).num_seconds(); + assert!( + 14 <= ttl_secs_no_ttl && ttl_secs_no_ttl <= 16, + "should be silo max (~15 seconds), got {ttl_secs_no_ttl}" ); } @@ -1126,11 +1085,49 @@ async fn list_user_sessions( .items } +/// Create a device auth request, confirm it with the given auth, and fetch the +/// resulting token. Returns the token grant and confirmation time. +async fn create_and_confirm_device_auth( + testctx: &ClientTestContext, + client_id: Uuid, + ttl_seconds: Option, + confirm_auth: AuthnMode, +) -> (DeviceAccessTokenGrant, chrono::DateTime) { + let auth_request = DeviceAuthRequest { client_id, ttl_seconds }; + + // note only confirm requires auth. initial request and token fetch don't + let auth_response = NexusRequest::new( + RequestBuilder::new(testctx, Method::POST, "/device/auth") + .body_urlencoded(Some(&auth_request)) + .expect_status(Some(StatusCode::OK)), + ) + .execute_and_parse_unwrap::() + .await; + + let confirmation_time = Utc::now(); + + NexusRequest::new( + RequestBuilder::new(testctx, Method::POST, "/device/confirm") + .body(Some(&DeviceAuthVerify { + user_code: auth_response.user_code, + })) + .expect_status(Some(StatusCode::NO_CONTENT)), + ) + .authn_as(confirm_auth) + .execute() + .await + .expect("failed to confirm device auth"); + + let token_grant = + fetch_device_token(testctx, auth_response.device_code, client_id).await; + + (token_grant, confirmation_time) +} + async fn fetch_device_token( testctx: &ClientTestContext, device_code: String, client_id: Uuid, - authn_mode: AuthnMode, ) -> DeviceAccessTokenGrant { NexusRequest::new( RequestBuilder::new(testctx, Method::POST, "/device/token") @@ -1143,7 +1140,6 @@ async fn fetch_device_token( })) .expect_status(Some(StatusCode::OK)), ) - .authn_as(authn_mode) .execute_and_parse_unwrap::() .await }