From 5212b869ad0a94b18ffecd288f0e98735e8e6911 Mon Sep 17 00:00:00 2001 From: "workos-tars[bot]" <269013284+workos-tars[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:49:55 +0000 Subject: [PATCH 1/2] feat: expose authentication method on action contexts --- src/helpers/actions.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/helpers/actions.rs b/src/helpers/actions.rs index 065efb89..16280ffe 100644 --- a/src/helpers/actions.rs +++ b/src/helpers/actions.rs @@ -6,6 +6,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; +use crate::enums::AuthenticateResponseAuthenticationMethod; use crate::error::Error; use crate::helpers::webhook_verification::{ compute_webhook_signature, parse_webhook_signature_header, @@ -70,6 +71,8 @@ pub struct ActionContext { pub object: String, /// Unique identifier for the action context. pub id: String, + /// The authentication method used to initiate the action. + pub authentication_method: AuthenticateResponseAuthenticationMethod, /// Caller IP address (both context types). pub ip_address: Option, /// Caller user agent (both context types). @@ -303,12 +306,16 @@ mod tests { #[test] fn construct_action_authentication() { let secret = "shh"; - let payload = r#"{"object":"authentication_action_context","id":"action_01","user":{"object":"user","id":"user_01","email":"test@example.com","email_verified":true,"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"},"organization":{"object":"organization","id":"org_01","name":"Acme","domains":[],"metadata":{},"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"},"ip_address":"1.2.3.4","user_agent":"curl/8","device_fingerprint":"fp_123","issuer":"https://auth.example.com"}"#; + let payload = r#"{"object":"authentication_action_context","id":"action_01","authentication_method":"Password","user":{"object":"user","id":"user_01","email":"test@example.com","email_verified":true,"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"},"organization":{"object":"organization","id":"org_01","name":"Acme","domains":[],"metadata":{},"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"},"ip_address":"1.2.3.4","user_agent":"curl/8","device_fingerprint":"fp_123","issuer":"https://auth.example.com"}"#; let header = action_sig_header(secret, payload); let helper = ActionsHelper::new().with_clock(|| fixed_now(1_700_000_000_000)); let action = helper.construct_action(payload, &header, secret).unwrap(); assert_eq!(action.object, "authentication_action_context"); assert_eq!(action.id, "action_01"); + assert_eq!( + action.authentication_method, + AuthenticateResponseAuthenticationMethod::Password + ); assert_eq!(action.ip_address.as_deref(), Some("1.2.3.4")); assert_eq!(action.user_agent.as_deref(), Some("curl/8")); assert_eq!(action.device_fingerprint.as_deref(), Some("fp_123")); @@ -323,12 +330,16 @@ mod tests { #[test] fn construct_action_user_registration() { let secret = "shh"; - let payload = r#"{"object":"user_registration_action_context","id":"action_02","user_data":{"object":"user_data","email":"new@example.com","first_name":"New","last_name":"User","name":null},"ip_address":"5.6.7.8","user_agent":"Mozilla/5.0","device_fingerprint":"fp_456"}"#; + let payload = r#"{"object":"user_registration_action_context","id":"action_02","authentication_method":"GoogleOAuth","user_data":{"object":"user_data","email":"new@example.com","first_name":"New","last_name":"User","name":null},"ip_address":"5.6.7.8","user_agent":"Mozilla/5.0","device_fingerprint":"fp_456"}"#; let header = action_sig_header(secret, payload); let helper = ActionsHelper::new().with_clock(|| fixed_now(1_700_000_000_000)); let action = helper.construct_action(payload, &header, secret).unwrap(); assert_eq!(action.object, "user_registration_action_context"); assert_eq!(action.id, "action_02"); + assert_eq!( + action.authentication_method, + AuthenticateResponseAuthenticationMethod::GoogleOAuth + ); assert_eq!(action.ip_address.as_deref(), Some("5.6.7.8")); let user_data = action.user_data.as_ref().unwrap(); assert_eq!(user_data.email, "new@example.com"); From 89cc3530f5a47184c8ddb25ff87499fd7900cdac Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Thu, 13 Aug 2026 09:26:06 -0400 Subject: [PATCH 2/2] fix: make authentication_method optional on ActionContext A missing or null authentication_method made construct_action return a decode error, failing a customer's entire Actions endpoint instead of returning an Allow/Deny verdict. AuthenticateResponse already models the same field as an Option, and the Go and Node SDKs degrade to a zero value rather than erroring. Restores coverage for the field-free payload, which the updated tests no longer exercised, and pins the Unknown forward-compat fallback. --- src/helpers/actions.rs | 44 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/helpers/actions.rs b/src/helpers/actions.rs index 16280ffe..629704a4 100644 --- a/src/helpers/actions.rs +++ b/src/helpers/actions.rs @@ -63,16 +63,20 @@ pub struct ActionUserData { /// `organization_membership`, `issuer` /// - `user_registration_action_context`: `user_data`, `invitation` /// -/// `ip_address`, `user_agent`, and `device_fingerprint` are shared by both -/// context types; fields specific to the other variant are `None`. +/// `authentication_method`, `ip_address`, `user_agent`, and +/// `device_fingerprint` are shared by both context types; fields specific to +/// the other variant are `None`. #[derive(Debug, Clone, Deserialize)] pub struct ActionContext { /// Discriminates the action context type. pub object: String, /// Unique identifier for the action context. pub id: String, - /// The authentication method used to initiate the action. - pub authentication_method: AuthenticateResponseAuthenticationMethod, + /// The authentication method used to initiate the action. `None` when the + /// payload omits it, so an unrecognized or absent method never fails the + /// whole action. + #[serde(default)] + pub authentication_method: Option, /// Caller IP address (both context types). pub ip_address: Option, /// Caller user agent (both context types). @@ -314,7 +318,7 @@ mod tests { assert_eq!(action.id, "action_01"); assert_eq!( action.authentication_method, - AuthenticateResponseAuthenticationMethod::Password + Some(AuthenticateResponseAuthenticationMethod::Password) ); assert_eq!(action.ip_address.as_deref(), Some("1.2.3.4")); assert_eq!(action.user_agent.as_deref(), Some("curl/8")); @@ -338,7 +342,7 @@ mod tests { assert_eq!(action.id, "action_02"); assert_eq!( action.authentication_method, - AuthenticateResponseAuthenticationMethod::GoogleOAuth + Some(AuthenticateResponseAuthenticationMethod::GoogleOAuth) ); assert_eq!(action.ip_address.as_deref(), Some("5.6.7.8")); let user_data = action.user_data.as_ref().unwrap(); @@ -350,4 +354,32 @@ mod tests { assert!(action.organization.is_none()); assert!(action.invitation.is_none()); } + + #[test] + fn construct_action_without_authentication_method() { + let secret = "shh"; + let payload = r#"{"object":"user_registration_action_context","id":"action_02","user_data":{"object":"user_data","email":"new@example.com","first_name":"New","last_name":"User","name":null},"ip_address":"5.6.7.8","user_agent":"Mozilla/5.0","device_fingerprint":"fp_456"}"#; + let header = action_sig_header(secret, payload); + let helper = ActionsHelper::new().with_clock(|| fixed_now(1_700_000_000_000)); + let action = helper.construct_action(payload, &header, secret).unwrap(); + assert_eq!(action.id, "action_02"); + assert!(action.authentication_method.is_none()); + assert_eq!(action.user_data.as_ref().unwrap().email, "new@example.com"); + } + + #[test] + fn construct_action_unrecognized_authentication_method() { + let secret = "shh"; + let payload = r#"{"object":"authentication_action_context","id":"action_03","authentication_method":"SomeFutureMethod","user":{"object":"user","id":"user_01","email":"test@example.com","email_verified":true,"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"},"ip_address":"1.2.3.4"}"#; + let header = action_sig_header(secret, payload); + let helper = ActionsHelper::new().with_clock(|| fixed_now(1_700_000_000_000)); + let action = helper.construct_action(payload, &header, secret).unwrap(); + assert_eq!(action.id, "action_03"); + assert_eq!( + action.authentication_method, + Some(AuthenticateResponseAuthenticationMethod::Unknown( + "SomeFutureMethod".to_string() + )) + ); + } }